fix invader lifecycle history parity

This commit is contained in:
2026-07-27 10:48:44 +00:00
parent d917160464
commit 1509a39d26
2 changed files with 115 additions and 7 deletions
@@ -443,7 +443,7 @@ export const createRaiseInvaderHandler = (options: {
scope: LogScope.SYSTEM,
category: LogCategory.HISTORY,
text,
format: LogFormat.NOTICE_YEAR_MONTH,
format: LogFormat.YEAR_MONTH,
year: environment.year,
month: environment.month,
});
@@ -524,7 +524,7 @@ export const createInvaderEndingHandler = (options: {
scope: LogScope.SYSTEM,
category: LogCategory.HISTORY,
text,
format: LogFormat.NOTICE_YEAR_MONTH,
format: LogFormat.YEAR_MONTH,
year: environment.year,
month: environment.month,
});
@@ -1,3 +1,7 @@
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { createGamePostgresConnector, type GamePrismaClient } from '@sammo-ts/infra';
import type { City, Nation } from '@sammo-ts/logic';
@@ -25,6 +29,60 @@ const invaderCityId = 991_101;
const sourceEventId = 991_100;
const createdEventIds = [991_101, 991_102];
type ReferenceInvaderLifecycleTrace = {
phases: {
afterRaise: {
createdNationCount: number;
generalCountsPerNation: number[];
diplomacyCountsPerNation: number[];
diplomacyStates: string[];
isunited: number;
blockChangeScout: boolean;
};
atWar: { results: string[]; autoDeleteEventCount: number };
atPeace: {
results: string[];
autoDeleteEventCount: number;
rulerWanderTurnCount: number;
endingEventPresent: boolean;
};
afterUserWin: {
result: string;
endingEventPresent: boolean;
isunited: number;
refreshLimit: number;
logs: string[];
};
};
};
const runReferenceInvaderLifecycle = (): ReferenceInvaderLifecycleTrace | null => {
const workspaceRoot = process.env.TURN_DIFFERENTIAL_WORKSPACE_ROOT;
if (process.env.TURN_DIFFERENTIAL_REFERENCE !== '1' || !workspaceRoot) {
return null;
}
const stackDirectory = path.join(workspaceRoot, 'docker_compose_files/reference');
const runnerScript =
process.env.MONTHLY_INVADER_LIFECYCLE_RUNNER_SCRIPT ??
path.join(workspaceRoot, 'ref/sam/hwe/compare/monthly_event_trace.php');
const fixturePath =
process.env.MONTHLY_INVADER_LIFECYCLE_FIXTURE ??
path.join(path.dirname(runnerScript), 'fixtures/monthly_invader_lifecycle.json');
const stdout = execFileSync('./scripts/run-turn-differential-case.sh', ['-'], {
cwd: stackDirectory,
input: fs.readFileSync(fixturePath, 'utf8'),
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
COMPOSE_PROJECT_NAME: 'sam-rebuild-reference',
TURN_DIFFERENTIAL_RUNNER_SCRIPT: runnerScript,
TURN_DIFFERENTIAL_COMPARE_DIR: path.dirname(runnerScript),
},
});
return JSON.parse(stdout) as ReferenceInvaderLifecycleTrace;
};
const buildCity = (id: number, nationId: number, level: number, name: string): City => ({
id,
name,
@@ -155,6 +213,7 @@ integration('RaiseInvader database persistence', () => {
});
it('commits invader entities, dynamic events, diplomacy, turns, ranks, city state, and logs atomically', async () => {
const referenceTrace = runReferenceInvaderLifecycle();
const createdGeneralIds = Array.from({ length: 10 }, (_, index) => firstCreatedGeneralId + index);
const createCity = (city: City) =>
db.city.create({
@@ -386,6 +445,15 @@ integration('RaiseInvader database persistence', () => {
expect(await db.worldState.findUniqueOrThrow({ where: { id: stateRow.id } })).toMatchObject({
meta: expect.objectContaining({ isunited: 1, block_change_scout: false }),
});
if (referenceTrace) {
expect(referenceTrace.phases.afterRaise).toMatchObject({
generalCountsPerNation: [10],
diplomacyCountsPerNation: [2],
diplomacyStates: ['1:24'],
isunited: 1,
blockChangeScout: false,
});
}
const sequenceProbe = await db.event.create({
data: {
targetCode: 'month',
@@ -399,6 +467,23 @@ integration('RaiseInvader database persistence', () => {
await db.event.delete({ where: { id: sequenceProbe.id } });
expect(world.removeEvent(sourceEventId)).toBe(true);
await world.advanceMonth(new Date('0200-02-01T00:00:00.000Z'));
await dbHooks.hooks.flushChanges?.({
lastTurnTime: world.getState().lastTurnTime.toISOString(),
processedGenerals: 0,
processedTurns: 1,
durationMs: 0,
partial: false,
});
expect(await db.event.findUnique({ where: { id: createdEventIds[0]! } })).not.toBeNull();
if (referenceTrace) {
expect(referenceTrace.phases.atWar).toEqual({
results: ['On War'],
autoDeleteEventCount: referenceTrace.phases.afterRaise.createdNationCount,
});
}
world.applyDiplomacyPatch({
srcNationId: createdNationId,
destNationId: existingNationId,
@@ -409,7 +494,7 @@ integration('RaiseInvader database persistence', () => {
destNationId: createdNationId,
patch: { state: 2, term: 0 },
});
await world.advanceMonth(new Date('0200-02-01T00:00:00.000Z'));
await world.advanceMonth(new Date('0200-03-01T00:00:00.000Z'));
await dbHooks.hooks.flushChanges?.({
lastTurnTime: world.getState().lastTurnTime.toISOString(),
processedGenerals: 0,
@@ -426,13 +511,24 @@ integration('RaiseInvader database persistence', () => {
})
).toEqual(Array.from({ length: 30 }, () => ({ actionCode: 'che_방랑' })));
expect(await db.event.findUnique({ where: { id: createdEventIds[1]! } })).not.toBeNull();
if (referenceTrace) {
expect(referenceTrace.phases.atPeace).toMatchObject({
results: ['Deleted'],
autoDeleteEventCount: 0,
endingEventPresent: true,
});
expect(
referenceTrace.phases.atPeace.rulerWanderTurnCount /
referenceTrace.phases.afterRaise.createdNationCount
).toBe(30);
}
for (const generalId of createdGeneralIds) {
expect(world.removeGeneral(generalId)).toBe(true);
}
expect(world.removeNation(createdNationId)).toBe(true);
world.updateCity(invaderCityId, { nationId: existingNationId });
await world.advanceMonth(new Date('0200-03-01T00:00:00.000Z'));
await world.advanceMonth(new Date('0200-04-01T00:00:00.000Z'));
await dbHooks.hooks.flushChanges?.({
lastTurnTime: world.getState().lastTurnTime.toISOString(),
processedGenerals: 0,
@@ -447,14 +543,26 @@ integration('RaiseInvader database persistence', () => {
});
expect(
await db.logEntry.findMany({
where: { year: 200, month: 3, text: { contains: '【이벤트】' } },
where: { year: 200, month: 4, text: { contains: '【이벤트】' } },
orderBy: { id: 'asc' },
select: { text: true },
})
).toEqual([
{ text: '<R>★</>200년 3월:<L><b>【이벤트】</b></>이민족을 모두 소탕했습니다!' },
{ text: '<R>★</>200년 3월:<L><b>【이벤트】</b></>중원은 당분간 태평성대를 누릴 것입니다.' },
{ text: '<C>●</>200년 4월:<L><b>【이벤트】</b></>이민족을 모두 소탕했습니다!' },
{ text: '<C>●</>200년 4월:<L><b>【이벤트】</b></>중원은 당분간 태평성대를 누릴 것입니다.' },
]);
if (referenceTrace) {
expect(referenceTrace.phases.afterUserWin).toEqual({
result: 'Deleted',
endingEventPresent: false,
isunited: 3,
refreshLimit: 300,
logs: [
'<C>●</>200년 4월:<L><b>【이벤트】</b></>이민족을 모두 소탕했습니다!',
'<C>●</>200년 4월:<L><b>【이벤트】</b></>중원은 당분간 태평성대를 누릴 것입니다.',
],
});
}
} finally {
await dbHooks.close();
}