mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
0a4c6fbace
db/database.ts opened the connection as a side effect of being imported, so the moment anything reached it the database was open. index.ts had to defer loading the app until a first-boot restore was done, and nothing in the container decided when the connection opened or closed. The module now opens the connection on first use and opens nothing at import. A connection closed on purpose (a restore, the shutdown) stays closed until it is reopened, as before. The new DatabaseLifecycle provider owns the decisions: buildApp() opens through it, which also binds the ORM to later swaps (the hook attachOrm used to register) and runs the schema bootstrap in the same order as before, and the shutdown closes through it. Its file property is the one resolved database path. The module keeps every export, so the test suites that replace it with vi.mock keep working; buildDbMock gains the new openDb.
123 lines
6.4 KiB
TypeScript
123 lines
6.4 KiB
TypeScript
/**
|
|
* `runDemoSeed()` (`src/demo/demo-seed.ts`, called from `runSchemaBootstrap`
|
|
* in `db/orm.ts:44`) used to run before the first `withRequestContext`
|
|
* (`bootstrap.ts:108`) and on every restore (`db/orm.ts`'s reinitialize
|
|
* hook) — entirely raw `better-sqlite3`, so nothing needed a context yet,
|
|
* but the seed's own docstring pre-authorised the fix: "The domain phase
|
|
* that gives demo seeding a repository read must wrap it in
|
|
* `withRequestContext` then." `runSchemaBootstrap` (Plan 3c Task 0b) now
|
|
* wraps the call in `withRequestContext(orm, …)`, the one place it already
|
|
* has `orm` in scope — covering both of `runDemoSeed`'s real call sites
|
|
* (`DatabaseLifecycle.open()` at boot and its restore hook) with no
|
|
* second wrapper to keep in sync.
|
|
*
|
|
* This is a real `DEMO_MODE` boot proving that wrap doesn't change what the
|
|
* seed does and logs no missing-context error, so Plan 3i's demo
|
|
* conversion never meets a second C1 (task-6-review-parity.md's boot-sweep
|
|
* finding).
|
|
*
|
|
* Does NOT use `buildDbMock` the way most integration suites do — its
|
|
* `runDemoSeed` is a deliberate no-op stub (tests/helpers/db-mock.ts) so
|
|
* that suite's app boots don't carry demo-seed side effects, which would
|
|
* silence the very thing this file exists to exercise. `NODE_ENV=test`
|
|
* already gives `db/database.ts`'s own singleton an isolated, per-worker
|
|
* `:memory:` copy of the migrated schema snapshot (`readSchemaSnapshot()`),
|
|
* so nothing needs stubbing for the boot itself — the `vi.mock` below (Task
|
|
* 0b review M2) exists only to wrap `runDemoSeed` with a context-recording
|
|
* spy, not to replace its behaviour: `importOriginal` re-exports everything
|
|
* else from the real module unchanged.
|
|
*
|
|
* `seedDemoData`'s own `require('../demo/demo-seed')` (a deliberate dynamic
|
|
* require — see that file's docstring) cannot resolve under vitest's SWC
|
|
* transform (`Cannot find module '../demo/demo-seed'`), independent of this
|
|
* task and of request-context wrapping — `runDemoSeed`'s own try/catch
|
|
* already swallows that into a `[Demo] Seed error:` log line in every
|
|
* environment, wrapped or not, which is exactly why `buildDbMock` stubs the
|
|
* whole function out for every other suite rather than letting it run for
|
|
* real. This test cannot assert the demo user was actually created for that
|
|
* reason (a genuine environment gap, not a functional one — production runs
|
|
* compiled Node, not a vitest/SWC transform); what it CAN and does assert is
|
|
* the one thing Task 0b changed: no `cannotUseGlobalContext`/"global
|
|
* EntityManager" line reaches the log, and the pre-existing module-resolution
|
|
* failure is what actually gets logged instead, unchanged by the wrap.
|
|
*/
|
|
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
import type { INestApplication } from '@nestjs/common';
|
|
import { RequestContext } from '@mikro-orm/core';
|
|
|
|
/**
|
|
* Task 0b review M2: the pre-fix version of this file only asserted "no
|
|
* `cannotUseGlobalContext`/`global EntityManager` line reached the log",
|
|
* which cannot distinguish a wrapped call from an unwrapped one — `runDemoSeed`
|
|
* is raw `better-sqlite3` and never touches the ORM, so it produces neither
|
|
* line either way (verified: deleting the `withRequestContext` wrap in
|
|
* `db/orm.ts:59` left this ratchet green, 1/1). Fixed by asserting the
|
|
* context POSITIVELY: a thin spy around the real `runDemoSeed`, preserving
|
|
* everything else `../../src/db/database` exports (`importOriginal`),
|
|
* records whether `RequestContext.currentRequestContext()` is defined at the
|
|
* instant `runDemoSeed` is entered — this is exactly the ratchet MikroORM's
|
|
* own `allowGlobalContext: false` gate reads at query time, so it is the
|
|
* real signal, not a log-line proxy for it. Deleting the `withRequestContext`
|
|
* wrap now fails this test directly (verified).
|
|
*/
|
|
// `vi.hoisted`, not a plain top-level `const`: `vi.mock`'s factory is
|
|
// invoked when something up the import chain first resolves
|
|
// `../../src/db/database` (during `buildApp`'s own module graph, not at
|
|
// this file's textual position), and vitest hoists the `vi.mock` call
|
|
// itself above every import — a plain `const` here would still be in its
|
|
// temporal dead zone when the factory closes over it. Same hazard the
|
|
// legacy `trip-membership.service.test.ts` guarded against with its own
|
|
// `vi.hoisted(() => ({ testDb, dbMock }))`.
|
|
const { contextSeenOnEntry } = vi.hoisted(() => ({ contextSeenOnEntry: [] as boolean[] }));
|
|
vi.mock('../../src/db/database', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('../../src/db/database')>();
|
|
return {
|
|
...actual,
|
|
runDemoSeed: async (): Promise<void> => {
|
|
contextSeenOnEntry.push(RequestContext.currentRequestContext() !== undefined);
|
|
return await actual.runDemoSeed();
|
|
},
|
|
};
|
|
});
|
|
|
|
import { buildApp } from '../../src/bootstrap';
|
|
|
|
describe('runDemoSeed runs inside a request context (Plan 3c Task 0b)', () => {
|
|
let app: INestApplication | undefined;
|
|
const prevDemo = process.env.DEMO_MODE;
|
|
|
|
afterEach(async () => {
|
|
await app?.close();
|
|
app = undefined;
|
|
contextSeenOnEntry.length = 0;
|
|
if (prevDemo === undefined) delete process.env.DEMO_MODE;
|
|
else process.env.DEMO_MODE = prevDemo;
|
|
});
|
|
|
|
it('DEMO-SEED-001: a DEMO_MODE boot runs the seed inside a request context (no cannotUseGlobalContext)', async () => {
|
|
process.env.DEMO_MODE = 'true';
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
try {
|
|
app = await buildApp();
|
|
|
|
// runDemoSeed ran at all (proven by SOME [Demo] line reaching the log —
|
|
// in this environment always the pre-existing require() gap above, but
|
|
// ANY [Demo] line proves the function was invoked, not skipped).
|
|
const demoLines = errSpy.mock.calls.map((args) => args.map(String).join(' ')).filter((line) => line.includes('[Demo]'));
|
|
expect(demoLines.length).toBeGreaterThan(0);
|
|
|
|
// The one thing this task could regress: a missing request context.
|
|
const suspicious = demoLines.filter((line) => /cannotUseGlobalContext|global EntityManager/i.test(line));
|
|
expect(suspicious).toEqual([]);
|
|
|
|
// DEMO-SEED-002 (0b review M2): the positive ratchet. runDemoSeed was
|
|
// actually entered (the spy fired) AND every entry saw a defined
|
|
// request context.
|
|
expect(contextSeenOnEntry.length).toBeGreaterThan(0);
|
|
expect(contextSeenOnEntry.every(Boolean)).toBe(true);
|
|
} finally {
|
|
errSpy.mockRestore();
|
|
}
|
|
});
|
|
});
|