import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { createGamePostgresConnector, type GamePrismaClient } from '@sammo-ts/infra'; import { readCoreDatabaseSnapshot } from '../src/turn-differential/databaseSnapshot.js'; import { captureCoreDatabaseTurnTrace } from '../src/turn-differential/trace.js'; const databaseUrl = process.env.TURN_DIFFERENTIAL_DATABASE_URL ?? process.env.INPUT_EVENT_DATABASE_URL; const integration = describe.skipIf(!databaseUrl); const ids = { general: 2_147_000_101, city: 2_147_000_102, nation: 2_147_000_103, }; integration('core2026 turn state database snapshot adapter', () => { let db: GamePrismaClient; let disconnect: (() => Promise) | undefined; let createdWorldId: number | null = null; beforeAll(async () => { const connector = createGamePostgresConnector({ url: databaseUrl! }); await connector.connect(); db = connector.prisma; disconnect = () => connector.disconnect(); const world = await db.worldState.findFirst({ orderBy: { id: 'asc' } }); if (!world) { createdWorldId = ( await db.worldState.create({ data: { scenarioCode: 'turn-differential-adapter', currentYear: 183, currentMonth: 1, tickSeconds: 600, config: {}, meta: { lastTurnTime: '0183-01-01T00:00:00.000Z', isUnited: 0 }, }, }) ).id; } await db.nation.create({ data: { id: ids.nation, name: '비교국', color: '#123456', capitalCityId: ids.city, gold: 100, rice: 200, tech: 10, level: 1, typeCode: 'che_중립', meta: { gennum: 1, power: 300, war: 0 }, }, }); await db.city.create({ data: { id: ids.city, name: '비교도시', level: 5, nationId: ids.nation, population: 10_000, populationMax: 20_000, agriculture: 1_000, agricultureMax: 2_000, commerce: 1_000, commerceMax: 2_000, security: 1_000, securityMax: 2_000, trust: 80, trade: 100, defence: 500, defenceMax: 1_000, wall: 500, wallMax: 1_000, region: 1, meta: { state: 0, term: 0 }, }, }); await db.general.create({ data: { id: ids.general, name: '비교장수', nationId: ids.nation, cityId: ids.city, leadership: 70, strength: 60, intel: 80, gold: 1_000, rice: 1_000, crew: 500, crewTypeId: 1100, train: 90, atmos: 90, turnTime: new Date('0183-01-01T00:00:00.000Z'), lastTurn: { command: '휴식' }, meta: { killturn: 24, myset: 6, intel_exp: 3 }, }, }); }); afterAll(async () => { await db.general.deleteMany({ where: { id: ids.general } }); await db.city.deleteMany({ where: { id: ids.city } }); await db.nation.deleteMany({ where: { id: ids.nation } }); if (createdWorldId !== null) { await db.worldState.deleteMany({ where: { id: createdWorldId } }); } await disconnect?.(); }); it('projects selected PostgreSQL rows into the canonical comparison schema', async () => { const result = await readCoreDatabaseSnapshot(databaseUrl!, { generalIds: [ids.general], cityIds: [ids.city], nationIds: [ids.nation], }); expect(result.engine).toBe('core2026'); expect(result.generals).toContainEqual( expect.objectContaining({ id: ids.general, nationId: ids.nation, cityId: ids.city, intelligence: 80, killTurn: 24, mySet: 6, }) ); expect(result.cities).toContainEqual( expect.objectContaining({ id: ids.city, agriculture: 1_000, defence: 500, state: 0, term: 0, }) ); expect(result.nations).toContainEqual( expect.objectContaining({ id: ids.nation, generalCount: 1, power: 300, }) ); }); it('captures before/after state around a real database execution boundary', async () => { const trace = await captureCoreDatabaseTurnTrace( databaseUrl!, { kind: 'general', actorGeneralId: ids.general, action: 'che_농지개간', args: null, observe: { generalIds: [ids.general], cityIds: [ids.city], nationIds: [ids.nation], }, }, async () => { await db.$transaction([ db.general.update({ where: { id: ids.general }, data: { gold: { decrement: 10 } }, }), db.city.update({ where: { id: ids.city }, data: { agriculture: { increment: 42 } }, }), ]); return { outcome: { ok: true } }; } ); expect(trace.before.generals[0]?.gold).toBe(1_000); expect(trace.after.generals[0]?.gold).toBe(990); expect(trace.before.cities[0]?.agriculture).toBe(1_000); expect(trace.after.cities[0]?.agriculture).toBe(1_042); expect(trace.execution).toMatchObject({ kind: 'general', action: 'che_농지개간', seedDomain: 'generalCommand', }); }); });