fix faction disband state and log parity

This commit is contained in:
2026-07-26 20:13:10 +00:00
parent 0f9fa69e1f
commit a7de5778ed
10 changed files with 198 additions and 13 deletions
@@ -124,6 +124,9 @@ export const projectCoreDatabaseSnapshot = (rows: {
'officerCityId',
readNumber(meta, 'officer_city', readNumber(meta, 'officerCity', readNumber(meta, 'officerCityId')))
),
belong: readNumber(row, 'belong', readNumber(meta, 'belong')),
permission: readString(row, 'permission') ?? readString(meta, 'permission') ?? 'normal',
maxBelong: readNumber(meta, 'max_belong'),
betray: row.betray,
personality: row.personality ?? null,
specialDomestic: row.specialDomestic ?? null,
@@ -281,6 +281,8 @@ const buildGeneral = (row: Record<string, unknown>, fallbackTurnTime: Date): Tur
'officerCityId',
readNumber(meta, 'officer_city', readNumber(meta, 'officerCity'))
),
belong: readNumber(row, 'belong', readNumber(meta, 'belong')),
permission: readString(row, 'permission', readString(meta, 'permission', 'normal')),
block: readNumber(row, 'blockState', readNumber(meta, 'block')),
},
...(lastTurn ? { lastTurn } : {}),
@@ -543,6 +545,9 @@ const projectWorld = (
'officer_city',
readNumber(general.meta, 'officerCity', readNumber(general.meta, 'officerCityId'))
),
belong: readNumber(general.meta, 'belong'),
permission: readString(general.meta, 'permission', 'normal'),
maxBelong: readNumber(general.meta, 'max_belong'),
betray: readNumber(general.meta, 'betray'),
personality: general.role.personality,
specialDomestic: general.role.specialDomestic,
@@ -88,6 +88,8 @@ const general = (id: number, nationId: number, cityId: number, officerLevel: num
killTurn: 24,
npcState: 0,
blockState: 0,
belong: 10,
permission: 'normal',
personality: 'None',
specialDomestic: 'None',
specialWar: 'None',
@@ -487,6 +489,112 @@ integration('NPC active command boundary parity', () => {
);
});
interface DisbandFactionBoundaryCase {
name: string;
actorPatch?: Record<string, unknown>;
fixturePatches: FixturePatches;
completed: boolean;
compareLogs?: boolean;
}
const disbandFactionBoundaryCases: DisbandFactionBoundaryCase[] = [
{
name: 'a non-lord cannot disband the wandering nation',
actorPatch: { officerLevel: 11 },
fixturePatches: {
nations: { 1: { level: 0, capitalCityId: 0, typeCode: 'None' } },
cities: { 3: { nationId: 0, supplyState: 0, frontState: 0 } },
},
completed: false,
},
{
name: 'a settled nation cannot be disbanded',
fixturePatches: {
nations: { 1: { level: 1 } },
},
completed: false,
},
{
name: 'disbanding preserves the legacy member state, resources, and destruction logs',
actorPatch: {
troopId: 3,
belong: 5,
permission: 'ambassador',
gold: 2_000,
rice: 3_000,
meta: { max_belong: 2 },
},
fixturePatches: {
nations: { 1: { name: '방랑군', level: 0, capitalCityId: 0, typeCode: 'None' } },
cities: { 3: { nationId: 0, supplyState: 0, frontState: 0 } },
generals: {
3: {
troopId: 3,
belong: 7,
permission: 'auditor',
gold: 2_500,
rice: 4_000,
meta: { max_belong: 3 },
},
},
troops: [{ id: 3, nationId: 1, name: '방랑군 부대' }],
},
completed: true,
compareLogs: true,
},
];
integration('general faction disband boundary, state, and log parity', () => {
it.each(disbandFactionBoundaryCases)(
'$name',
async ({ name, actorPatch, fixturePatches, completed, compareLogs }) => {
const request = buildRequest('che_해산', undefined, actorPatch, fixturePatches);
request.setup!.world!.hiddenSeed = `general-disband-${name}`;
request.observe!.includeGlobalHistoryLogs = true;
const reference = runReferenceTurnCommandTraceRequest(
workspaceRoot!,
request as unknown as Record<string, unknown>
);
const core = await runCoreTurnCommandTrace(request, reference.before);
if (process.env.TURN_DIFFERENTIAL_DEBUG === '1') {
process.stderr.write(
`${JSON.stringify(
{
name,
referenceGenerals: reference.after.generals,
coreGenerals: core.after.generals,
referenceLogs: addedReferenceLogs(reference.before, reference.after.logs),
coreLogs: core.after.logs,
},
null,
2
)}\n`
);
}
expect(reference.execution.outcome).toMatchObject({ completed });
expect(core.execution.outcome).toMatchObject({
requestedAction: 'che_해산',
actionKey: completed ? 'che_해산' : '휴식',
usedFallback: !completed,
});
expect(core.rng).toEqual(reference.rng);
expect(
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
ignoredPathPatterns: ignoredLifecyclePaths,
})
).toEqual([]);
if (compareLogs) {
expect(semanticLogSignatures(core.after.logs)).toEqual(
semanticLogSignatures(addedReferenceLogs(reference.before, reference.after.logs))
);
}
},
120_000
);
});
interface SelfStateBoundaryCase {
name: string;
action: 'che_단련' | 'che_숙련전환' | 'che_사기진작' | 'che_요양';