diff --git a/tools/integration-tests/src/turn-differential/coreCommandTrace.ts b/tools/integration-tests/src/turn-differential/coreCommandTrace.ts index 31ab3149..e9543045 100644 --- a/tools/integration-tests/src/turn-differential/coreCommandTrace.ts +++ b/tools/integration-tests/src/turn-differential/coreCommandTrace.ts @@ -64,6 +64,8 @@ export interface TurnCommandFixtureRequest { cities?: Array>; troops?: Array>; diplomacy?: Array>; + generalTurns?: Array>; + nationTurns?: Array>; randomFoundingCandidateCityIds?: number[]; generalCooldowns?: Array; }; @@ -741,18 +743,35 @@ const projectWorld = ( }; }; -const emptyDatabaseClient = { +const buildSnapshotReservedTurnDatabaseClient = (snapshot: CanonicalTurnSnapshot) => ({ generalTurn: { - findMany: async () => [], + findMany: async () => + snapshot.generalTurns.map((turn, index) => ({ + id: index + 1, + generalId: readNumber(turn, 'generalId'), + turnIdx: readNumber(turn, 'turnIndex'), + actionCode: readString(turn, 'action', '휴식'), + arg: asRecord(turn.args), + createdAt: new Date(0), + })), deleteMany: async () => ({ count: 0 }), createMany: async () => ({ count: 0 }), }, nationTurn: { - findMany: async () => [], + findMany: async () => + snapshot.nationTurns.map((turn, index) => ({ + id: index + 1, + nationId: readNumber(turn, 'nationId'), + officerLevel: readNumber(turn, 'officerLevel'), + turnIdx: readNumber(turn, 'turnIndex'), + actionCode: readString(turn, 'action', '휴식'), + arg: asRecord(turn.args), + createdAt: new Date(0), + })), deleteMany: async () => ({ count: 0 }), createMany: async () => ({ count: 0 }), }, -}; +}); export const runCoreTurnCommandTrace = async ( request: TurnCommandFixtureRequest, @@ -776,10 +795,13 @@ export const runCoreTurnCommandTrace = async ( generalCooldowns: request.observe?.generalCooldowns ?? [], nationCooldowns: request.observe?.nationCooldowns ?? [], }; - const reservedTurns = new InMemoryReservedTurnStore(emptyDatabaseClient as never, { - maxGeneralTurns: 30, - maxNationTurns: 12, - }); + const reservedTurns = new InMemoryReservedTurnStore( + buildSnapshotReservedTurnDatabaseClient(referenceBefore) as never, + { + maxGeneralTurns: 30, + maxNationTurns: 12, + } + ); await reservedTurns.loadAll(); const actor = snapshot.generals.find((general) => general.id === request.actorGeneralId); if (!actor) { @@ -851,7 +873,9 @@ export const runCoreTurnCommandTrace = async ( }, }); world = new InMemoryTurnWorld(state, snapshot, { - schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] }, + schedule: { + entries: [{ startMinute: 0, tickMinutes: Math.max(1, Math.round(state.tickSeconds / 60)) }], + }, generalTurnHandler: handler, }); const before = projectWorld(world, reservedTurns, [], [], selector); diff --git a/tools/integration-tests/test/turnCommandCoreReference.integration.test.ts b/tools/integration-tests/test/turnCommandCoreReference.integration.test.ts index b332d0da..283f449b 100644 --- a/tools/integration-tests/test/turnCommandCoreReference.integration.test.ts +++ b/tools/integration-tests/test/turnCommandCoreReference.integration.test.ts @@ -212,6 +212,7 @@ integration('core ↔ legacy command-boundary differential', () => { const afterActor = reference.after.generals.find((general) => general.id === actorGeneralId); const coreBeforeActor = core.before.generals.find((general) => general.id === actorGeneralId); const coreAfterActor = core.after.generals.find((general) => general.id === actorGeneralId); + const expectedTurnMinutes = Number(reference.before.world.tickMinutes); expect( reference.before.generalTurns.find( (turn) => turn.generalId === actorGeneralId && turn.turnIndex === 0 @@ -223,10 +224,10 @@ integration('core ↔ legacy command-boundary differential', () => { )?.action ).toBe('휴식'); expect(timestampMillis(afterActor?.turnTime) - timestampMillis(beforeActor?.turnTime)).toBe( - 10 * 60_000 + expectedTurnMinutes * 60_000 ); expect(timestampMillis(coreAfterActor?.turnTime) - timestampMillis(coreBeforeActor?.turnTime)).toBe( - 10 * 60_000 + expectedTurnMinutes * 60_000 ); expect(timestampMillis(coreAfterActor?.turnTime)).toBe(timestampMillis(afterActor?.turnTime)); expect(afterActor?.mySet).toBe(Math.min(9, Number(beforeActor?.mySet) + 3)); diff --git a/tools/integration-tests/test/turnCommandNationMatrix.integration.test.ts b/tools/integration-tests/test/turnCommandNationMatrix.integration.test.ts index 26b705ed..d4f6e043 100644 --- a/tools/integration-tests/test/turnCommandNationMatrix.integration.test.ts +++ b/tools/integration-tests/test/turnCommandNationMatrix.integration.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from 'vitest'; +import { NATION_TURN_COMMAND_KEYS } from '@sammo-ts/logic'; + import { compareTurnSnapshotDeltas } from '../src/turn-differential/compare.js'; import { runCoreTurnCommandTrace, type TurnCommandFixtureRequest } from '../src/turn-differential/coreCommandTrace.js'; import { @@ -27,6 +29,12 @@ const readNationMeta = (row: { meta?: unknown } | undefined): Record { + const raw = String(value); + const normalized = raw.includes('T') ? raw : `${raw.replace(' ', 'T').replace(/\.(\d{3})\d*$/, '.$1')}Z`; + return new Date(normalized).getTime(); +}; + const normalizeStoredLogText = (value: unknown): string => String(value) .replace(/^(?:●<\/>|◆<\/>|★<\/>)(?:(?:\d+년 )?\d+월:|\d+년:)?/, '') @@ -345,6 +353,15 @@ const buildRequest = ( }; }), ], + nationTurns: [ + { + nationId: 1, + officerLevel: 12, + turnIndex: 1, + action: 'che_국호변경', + args: { nationName: '다음국' }, + }, + ], }, observe: { generalIds: [ @@ -628,11 +645,18 @@ const cases: NationMatrixCase[] = [ ], ]; +describe('nation command differential coverage manifest', () => { + it('keeps one successful Ref/Core case for every registered nation turn command', () => { + expect(new Set(cases.map(([action]) => action))).toEqual(new Set(NATION_TURN_COMMAND_KEYS)); + }); +}); + integration('nation command success matrix', () => { it.each(cases)( '%s matches the legacy state delta and command RNG', async (action, args, fixturePatches) => { const request = buildRequest(action, args, fixturePatches); + request.includeLifecycle = true; const reference = runReferenceTurnCommandTraceRequest( workspaceRoot!, request as unknown as Record @@ -667,6 +691,42 @@ integration('nation command success matrix', () => { expect(reference.execution.outcome).toMatchObject({ completed: true }); expect(core.execution.outcome).not.toHaveProperty('blockedReason'); expect(core.rng).toEqual(reference.rng); + + const actorGeneralId = request.actorGeneralId; + const actorNationId = 1; + const actorOfficerLevel = 12; + const referenceBeforeActor = reference.before.generals.find((general) => general.id === actorGeneralId); + const referenceAfterActor = reference.after.generals.find((general) => general.id === actorGeneralId); + const coreBeforeActor = core.before.generals.find((general) => general.id === actorGeneralId); + const coreAfterActor = core.after.generals.find((general) => general.id === actorGeneralId); + const expectedTurnMinutes = Number(reference.before.world.tickMinutes); + const findActorNationTurn = (turns: Array>, turnIndex: number) => + turns.find( + (turn) => + turn.nationId === actorNationId && + turn.officerLevel === actorOfficerLevel && + turn.turnIndex === turnIndex + ); + + expect(findActorNationTurn(reference.before.nationTurns, 0)?.action).toBe(action); + expect(findActorNationTurn(core.before.nationTurns, 0)?.action).toBe(action); + expect(findActorNationTurn(reference.after.nationTurns, 0)).toMatchObject({ + action: 'che_국호변경', + args: { nationName: '다음국' }, + }); + expect(findActorNationTurn(core.after.nationTurns, 0)).toMatchObject({ + action: 'che_국호변경', + args: { nationName: '다음국' }, + }); + expect( + timestampMillis(referenceAfterActor?.turnTime) - timestampMillis(referenceBeforeActor?.turnTime) + ).toBe(expectedTurnMinutes * 60_000); + expect(timestampMillis(coreAfterActor?.turnTime) - timestampMillis(coreBeforeActor?.turnTime)).toBe( + expectedTurnMinutes * 60_000 + ); + expect(timestampMillis(coreAfterActor?.turnTime)).toBe(timestampMillis(referenceAfterActor?.turnTime)); + expect(referenceAfterActor?.mySet).toBe(Math.min(9, Number(referenceBeforeActor?.mySet) + 3)); + expect(coreAfterActor?.mySet).toBe(referenceAfterActor?.mySet); expect( compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, { ignoredPathPatterns: ignoredLifecyclePaths, @@ -702,6 +762,31 @@ integration('nation command success matrix', () => { ); }); +integration('legacy nation lifecycle comparison guard', () => { + it('does not alter command effects or RNG beyond the explicit queue and turn lifecycle', () => { + const request = buildRequest('che_포상', { isGold: true, amount: 100, destGeneralID: 3 }); + const commandOnly = runReferenceTurnCommandTraceRequest( + workspaceRoot!, + request as unknown as Record + ); + const withLifecycle = runReferenceTurnCommandTraceRequest(workspaceRoot!, { + ...request, + includeLifecycle: true, + } as unknown as Record); + + expect(withLifecycle.rng).toEqual(commandOnly.rng); + expect( + compareTurnSnapshotDeltas( + commandOnly.before, + commandOnly.after, + withLifecycle.before, + withLifecycle.after, + { ignoredPathPatterns: ignoredLifecyclePaths } + ) + ).toEqual([]); + }, 180_000); +}); + type NationActiveActionInheritanceCase = { name: string; request: NationMatrixCase;