diff --git a/app/game-api/test/inputEventBoundary.integration.test.ts b/app/game-api/test/inputEventBoundary.integration.test.ts index 205d782e..011ae27c 100644 --- a/app/game-api/test/inputEventBoundary.integration.test.ts +++ b/app/game-api/test/inputEventBoundary.integration.test.ts @@ -18,6 +18,7 @@ import { const databaseUrl = process.env.INPUT_EVENT_DATABASE_URL; const integration = describe.skipIf(!databaseUrl); const journalGeneralIds = [9_980_081, 9_980_082] as const; +const clockScenarioCode = 'input-event-boundary'; const journalBoundaryRouter = router({ mutate: procedure @@ -56,6 +57,21 @@ integration('API input event boundary', () => { await db.readModelRevision.deleteMany({ where: { domain: 'front.general', entityId: { in: [...journalGeneralIds] } }, }); + await db.worldState.deleteMany({ where: { scenarioCode: clockScenarioCode } }); + const base = new Date('2099-09-03T00:00:00.000Z'); + await db.worldState.create({ + data: { + scenarioCode: clockScenarioCode, + currentYear: 200, + currentMonth: 1, + tickSeconds: 600, + clockBaseTime: base, + clockTick: 0n, + clockMode: 'realtime', + clockWallAnchor: base, + clockPhase: 'RUNNING', + }, + }); }); afterAll(async () => { @@ -68,6 +84,7 @@ integration('API input event boundary', () => { await db.readModelRevision.deleteMany({ where: { domain: 'front.general', entityId: { in: [...journalGeneralIds] } }, }); + await db.worldState.deleteMany({ where: { scenarioCode: clockScenarioCode } }); await close?.(); }); diff --git a/app/game-engine/test/databaseCommandQueue.integration.test.ts b/app/game-engine/test/databaseCommandQueue.integration.test.ts index 540e386d..18854667 100644 --- a/app/game-engine/test/databaseCommandQueue.integration.test.ts +++ b/app/game-engine/test/databaseCommandQueue.integration.test.ts @@ -15,6 +15,20 @@ integration('database command queue', () => { let close: (() => Promise) | undefined; let db: GamePrismaClient; + const cleanupFixtures = async (): Promise => { + await db.inputEvent.deleteMany({ where: { requestId: { startsWith: 'integration:engine:' } } }); + await db.clockProjectionOutbox.deleteMany({ + where: { suspensionId: { in: ['integration-queue-revision-8-9', 'integration-unification-wait'] } }, + }); + await db.clockSuspension.deleteMany({ + where: { id: { in: ['integration-queue-revision-8-9', 'integration-unification-wait'] } }, + }); + await db.message.deleteMany({ where: { mailbox: 991_199 } }); + await db.worldState.deleteMany({ + where: { scenarioCode: { in: ['queue-clock-test', 'queue-unification-clock-test'] } }, + }); + }; + beforeAll(async () => { const connector = createGamePostgresConnector({ url: databaseUrl! }); await connector.connect(); @@ -25,22 +39,10 @@ integration('database command queue', () => { }); }); - beforeEach(async () => { - await db.inputEvent.deleteMany({ where: { requestId: { startsWith: 'integration:engine:' } } }); - await db.clockProjectionOutbox.deleteMany({ - where: { suspensionId: { in: ['integration-queue-revision-8-9', 'integration-unification-wait'] } }, - }); - await db.clockSuspension.deleteMany({ - where: { id: { in: ['integration-queue-revision-8-9', 'integration-unification-wait'] } }, - }); - await db.message.deleteMany({ where: { mailbox: 991_199 } }); - await db.worldState.updateMany({ data: { clockPhase: 'RUNNING' } }); - }); + beforeEach(cleanupFixtures); afterAll(async () => { - await db.inputEvent.deleteMany({ - where: { requestId: { startsWith: 'integration:engine:' } }, - }); + await cleanupFixtures(); await close?.(); }); diff --git a/app/game-engine/tsdown.config.ts b/app/game-engine/tsdown.config.ts index 3e102d24..ee9a7ce5 100644 --- a/app/game-engine/tsdown.config.ts +++ b/app/game-engine/tsdown.config.ts @@ -10,6 +10,8 @@ export default defineConfig({ 'scenario/gameCancellation': 'src/scenario/gameCancellation.ts', 'scenario/scenarioSeeder': 'src/scenario/scenarioSeeder.ts', 'scenario/unitSetLoader': 'src/scenario/unitSetLoader.ts', + 'turn/clockProjectionOutbox': 'src/turn/clockProjectionOutbox.ts', + 'turn/clockReconciliation': 'src/turn/clockReconciliation.ts', 'turn/databaseHooks': 'src/turn/databaseHooks.ts', 'turn/inMemoryWorld': 'src/turn/inMemoryWorld.ts', 'turn/monthlyDisasterAction': 'src/turn/monthlyDisasterAction.ts', diff --git a/docs/developer/game-clock-reconciliation-plan.md b/docs/developer/game-clock-reconciliation-plan.md index 7ab3ea5e..0de687f0 100644 --- a/docs/developer/game-clock-reconciliation-plan.md +++ b/docs/developer/game-clock-reconciliation-plan.md @@ -53,9 +53,9 @@ mean deployment or production validation. ## Milestone 5 - test-branch release gate -- [ ] Full typecheck, architecture, lint, unit, build, and non-conditional +- [x] Full typecheck, architecture, lint, unit, build, and non-conditional integration suites. -- [ ] Dedicated PostgreSQL/Redis conditional integration suite with skip count +- [x] Dedicated PostgreSQL/Redis conditional integration suite with skip count recorded. - [x] Recovery runbook exercised from each incomplete status. - [x] Admin status/readiness exposes revision, phase, participant checksums, and @@ -126,3 +126,16 @@ mean deployment or production validation. - The dedicated PostgreSQL/Redis fixture reached `RUNNING@2/2` only after the Redis projection. DB-wall versus a mocked 12-hour host drift and concurrent general-access lock acquisition completed without drift or deadlock. +- `CI=1 TURBO_CONCURRENCY=1 pnpm typecheck` passed 21/21 tasks, + `CI=1 TURBO_CONCURRENCY=1 pnpm test` passed 12/12 package tasks, + `CI=1 TURBO_CONCURRENCY=1 pnpm build` passed 26/26 tasks, and workspace lint + passed. `pnpm check:architecture` registered 22 authoritative clock fields + and 18 participants. +- Dedicated sequential PostgreSQL/Redis runs passed clock reconciliation 3/3, + atomic unification 1/1, command queue 7/7, runtime clock persistence 3/3, + API input-event boundary 13/13, and tournament revision 4/4: 31/31 enabled + tests with zero skips. The queue and API suites now create and remove their + own clock fixtures so a completed file cannot leak phase or initialization + state into the next file. +- User-test deployment and public runtime evidence remain deliberately open: + Git push is not deployment, and no deployment was authorized in this work.