import fs from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; import { compareTurnSnapshotDeltas } from '../src/turn-differential/compare.js'; import { runCoreTurnCommandTrace, type TurnCommandFixtureRequest } from '../src/turn-differential/coreCommandTrace.js'; import { findTurnDifferentialWorkspaceRoot, runReferenceTurnCommandTraceRequest, } from '../src/turn-differential/referenceSnapshot.js'; const configuredWorkspaceRoot = process.env.TURN_DIFFERENTIAL_WORKSPACE_ROOT; const workspaceRoot = configuredWorkspaceRoot ?? findTurnDifferentialWorkspaceRoot(process.cwd()); const integration = describe.skipIf(!workspaceRoot || process.env.TURN_DIFFERENTIAL_REFERENCE !== '1'); const ignoredLifecyclePaths = [ /^generalTurns/, /^nationTurns/, /^logs/, /^messages/, /^world\.turnTime$/, /^generals\[[^\]]+\]\.(?:turnTime|recentWarTime|lastTurn|mySet)(?:\.|$)/, /^generals\[[^\]]+\]\.meta(?:\.|$)/, /^nations\[[^\]]+\]\.meta(?:\.|$)/, ]; const normalizeStoredLogText = (value: unknown): string => String(value) .replace(/^(?:●<\/>|◆<\/>|★<\/>)(?:(?:\d+년 )?\d+월:|\d+년:)?/, '') .replace(/(.*?)<\/span>/g, '$1') .replace(/ <1>\d{2}:\d{2}<\/>$/, ''); const semanticLogSignatures = (logs: Array>): string[] => logs .map((entry) => JSON.stringify({ scope: String(entry.scope).toLowerCase(), category: String(entry.category).toLowerCase(), generalId: Number(entry.generalId) || null, nationId: Number(entry.nationId) || null, text: normalizeStoredLogText(entry.text), }) ) .sort(); const readFixture = (relativePath: string): TurnCommandFixtureRequest => { const stackRoot = path.join(workspaceRoot!, 'docker_compose_files/reference'); const fixture = JSON.parse( fs.readFileSync(path.join(stackRoot, relativePath), 'utf8') ) as TurnCommandFixtureRequest; return { ...fixture, setup: { ...fixture.setup, isolateWorld: true, world: { ...fixture.setup?.world, hiddenSeed: 'turn-command-differential-seed', }, generals: fixture.setup?.generals?.map((general) => ({ ...general, personality: 'None', specialDomestic: 'None', specialWar: 'None', itemHorse: 'None', itemWeapon: 'None', itemBook: 'None', itemExtra: 'None', })), }, }; }; integration('core ↔ legacy command-boundary differential', () => { it.each([ ['nation declaration', 'fixtures/turn-differential/nation-declaration.json'], ['live sortie conquest', 'fixtures/turn-differential/live-sortie-conquest.json'], ])( '%s matches command RNG and canonical state delta', async (_label, fixturePath) => { const request = readFixture(fixturePath); if (request.action === 'che_출병') { request.observe!.includeNationHistoryLogs = true; request.observe!.includeGlobalHistoryLogs = true; } const reference = runReferenceTurnCommandTraceRequest( workspaceRoot!, request as unknown as Record ); const core = await runCoreTurnCommandTrace(request, reference.before); expect(core.execution.outcome).toMatchObject({ requestedAction: request.action, actionKey: request.action, usedFallback: false, }); expect(reference.execution.outcome).toMatchObject({ completed: true }); expect(core.rng).toEqual(reference.rng); if (request.action === 'che_출병') { const generalLogWatermark = reference.before.watermarks.logId; const historyLogWatermark = reference.before.watermarks.historyLogId; const referenceAddedLogs = reference.after.logs.filter((entry) => String(entry.scope).toLowerCase() === 'nation' || (String(entry.scope).toLowerCase() === 'system' && String(entry.category).toLowerCase() === 'history') ? (Number(entry.id) || 0) > historyLogWatermark : (Number(entry.id) || 0) > generalLogWatermark ); expect(semanticLogSignatures(core.after.logs)).toEqual(semanticLogSignatures(referenceAddedLogs)); } expect( compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, { ignoredPathPatterns: request.action === 'che_출병' ? ignoredLifecyclePaths : [...ignoredLifecyclePaths, /^generals\[[^\]]+\]\.killTurn(?:\.|$)/], }) ).toEqual([]); }, 120_000 ); });