test: Ref 명령 행렬 fixture의 결정성을 보강한다

This commit is contained in:
2026-08-24 19:45:32 +00:00
parent 7deb94549f
commit 04101ba862
5 changed files with 170 additions and 18 deletions
@@ -136,6 +136,8 @@ const buildRequest = (
startYear: 180,
year: 190,
month: 1,
develCost: 18,
isUnited: 0,
hiddenSeed: 'turn-command-general-matrix-v1',
freezeClock: true,
...fixturePatches.world,
@@ -575,6 +577,7 @@ type GeneralActiveActionInheritanceCase = {
name: string;
action: string;
args?: Record<string, unknown>;
initialPoint?: number;
expectedPointDelta?: number;
};
@@ -582,6 +585,7 @@ const generalActiveActionInheritanceCases: GeneralActiveActionInheritanceCase[]
{
name: 'ordinary training does not count as a legacy active action',
action: 'che_훈련',
initialPoint: 6,
expectedPointDelta: 0,
},
{
@@ -616,8 +620,11 @@ const readActiveActionPoints = (snapshot: { generals: Array<Record<string, unkno
integration('general active-action inheritance point parity', () => {
it.each(generalActiveActionInheritanceCases)(
'$name',
async ({ name, action, args, expectedPointDelta }) => {
const request = buildRequest(action, args);
async ({ name, action, args, initialPoint = 0, expectedPointDelta }) => {
const request = buildRequest(action, args, {
ownerId: 2_000_000_001,
inheritActiveActionPoints: initialPoint,
});
request.setup!.world!.hiddenSeed = `general-active-action-${name}`;
const reference = runReferenceTurnCommandTraceRequest(
workspaceRoot!,
@@ -628,6 +635,8 @@ integration('general active-action inheritance point parity', () => {
readActiveActionPoints(reference.after, 1) - readActiveActionPoints(reference.before, 1);
const corePointDelta = readActiveActionPoints(core.after, 1) - readActiveActionPoints(core.before, 1);
expect(readActiveActionPoints(reference.before, 1)).toBe(initialPoint);
expect(readActiveActionPoints(core.before, 1)).toBe(initialPoint);
expect(reference.execution.outcome).toMatchObject({ completed: true });
expect(core.execution.outcome).toMatchObject({
requestedAction: action,
@@ -205,6 +205,8 @@ const buildRequest = (
startYear: 180,
year: 190,
month: 1,
develCost: 18,
isUnited: 0,
hiddenSeed: 'turn-command-nation-matrix-v1',
freezeClock: true,
...fixturePatches.world,
@@ -819,6 +821,7 @@ integration('legacy nation lifecycle comparison guard', () => {
type NationActiveActionInheritanceCase = {
name: string;
request: NationMatrixCase;
initialPoint?: number;
expectedPointDelta: number;
};
@@ -826,6 +829,7 @@ const nationActiveActionInheritanceCases: NationActiveActionInheritanceCase[] =
{
name: 'ordinary award does not count as a legacy active action',
request: ['che_포상', { isGold: true, amount: 100, destGeneralID: 3 }],
initialPoint: 6,
expectedPointDelta: 0,
},
{
@@ -857,8 +861,18 @@ const readNationActorActiveActionPoints = (
integration('nation active-action inheritance point parity', () => {
it.each(nationActiveActionInheritanceCases)(
'$name',
async ({ name, request: [action, args, fixturePatches], expectedPointDelta }) => {
const request = buildRequest(action, args, fixturePatches);
async ({ name, request: [action, args, fixturePatches], initialPoint = 0, expectedPointDelta }) => {
const request = buildRequest(action, args, {
...fixturePatches,
generals: {
...fixturePatches?.generals,
1: {
...fixturePatches?.generals?.[1],
ownerId: 2_000_000_001,
inheritActiveActionPoints: initialPoint,
},
},
});
request.setup!.world!.hiddenSeed = `nation-active-action-${name}`;
const reference = runReferenceTurnCommandTraceRequest(
workspaceRoot!,
@@ -871,6 +885,8 @@ integration('nation active-action inheritance point parity', () => {
const corePointDelta =
readNationActorActiveActionPoints(core.after, 1) - readNationActorActiveActionPoints(core.before, 1);
expect(readNationActorActiveActionPoints(reference.before, 1)).toBe(initialPoint);
expect(readNationActorActiveActionPoints(core.before, 1)).toBe(initialPoint);
expect(reference.execution.outcome).toMatchObject({ completed: true });
expect(core.execution.outcome).toMatchObject({
requestedAction: action,
@@ -1902,7 +1918,7 @@ integration('nation volunteer-recruitment constraints, creation values, RNG, and
expect(readNumericField(created, 'killTurn')).toBeGreaterThanOrEqual(64);
expect(readNumericField(created, 'killTurn')).toBeLessThanOrEqual(70);
}
const addedLogs = snapshot.after.logs.slice(snapshot.before.logs.length);
const addedLogs = addedReferenceLogs(snapshot.before, snapshot.after.logs);
expect(addedLogs.map((entry) => entry.text)).toEqual(
expect.arrayContaining([expect.stringContaining('의병모집 발동')])
);
@@ -2965,7 +2981,7 @@ integration('nation seizure zero target balance parity', () => {
request as unknown as Record<string, unknown>
);
const core = await runCoreTurnCommandTrace(request, reference.before);
const referenceLogs = reference.after.logs.slice(reference.before.logs.length);
const referenceLogs = addedReferenceLogs(reference.before, reference.after.logs);
expect(reference.execution.outcome).toMatchObject({ completed: true });
expect(core.execution.outcome).toMatchObject({
@@ -3227,7 +3243,7 @@ integration('nation material aid resource boundaries', () => {
request as unknown as Record<string, unknown>
);
const core = await runCoreTurnCommandTrace(request, reference.before);
const referenceLogs = reference.after.logs.slice(reference.before.logs.length);
const referenceLogs = addedReferenceLogs(reference.before, reference.after.logs);
expect(referenceLogs.map((entry) => entry.text)).toEqual(
expect.arrayContaining([expect.stringContaining('쌀<C>0</>를 지원')])
@@ -3394,7 +3410,7 @@ integration('nation material aid accumulated assistance and officer logs', () =>
request as unknown as Record<string, unknown>
);
const core = await runCoreTurnCommandTrace(request, reference.before);
const referenceLogs = reference.after.logs.slice(reference.before.logs.length);
const referenceLogs = addedReferenceLogs(reference.before, reference.after.logs);
const sourceOfficerLog = {
generalId: 3,
text: expect.stringContaining('<D><b>타국</b></>으로 금<C>100</> 쌀<C>200</>을 지원했습니다.'),
@@ -3574,7 +3590,7 @@ integration('nation population move value and resource boundaries', () => {
}
if (amount === 0) {
const zeroMoveText = '인구 <C>0</>명을 옮겼습니다.';
expect(reference.after.logs.slice(reference.before.logs.length).map((entry) => entry.text)).toEqual(
expect(addedReferenceLogs(reference.before, reference.after.logs).map((entry) => entry.text)).toEqual(
expect.arrayContaining([expect.stringContaining(zeroMoveText)])
);
expect(core.after.logs.map((entry) => entry.text)).toEqual(
@@ -4629,7 +4645,7 @@ integration('nation random capital constraints, candidates, RNG, and city reset
readNumericField(readGeneralMeta(coreGeneralBefore), 'inherit_active_action')
);
const noCandidateText = '이동할 수 있는 도시가 없습니다.';
expect(reference.after.logs.slice(reference.before.logs.length).map((entry) => entry.text)).toEqual(
expect(addedReferenceLogs(reference.before, reference.after.logs).map((entry) => entry.text)).toEqual(
expect.arrayContaining([expect.stringContaining(noCandidateText)])
);
expect(core.after.logs.map((entry) => entry.text)).toEqual(
@@ -5055,7 +5071,7 @@ integration('nation mobilize-people target, delay, and city-effect boundaries',
if (expectedWall !== undefined) {
expect(readNumericField(cityAfter, 'wall')).toBe(expectedWall);
}
const addedLogs = snapshot.after.logs.slice(snapshot.before.logs.length);
const addedLogs = addedReferenceLogs(snapshot.before, snapshot.after.logs);
expect(addedLogs.map((entry) => entry.text)).toEqual(
expect.arrayContaining([expect.stringContaining('백성동원 발동')])
);
@@ -5349,7 +5365,7 @@ integration('nation degrade-relations target, diplomacy, front, and cooldown bou
expect(readNumericField(sourceCityAfter, 'frontState')).toBe(expectedSourceFront);
expect(readNumericField(destCityAfter, 'frontState')).toBe(expectedDestFront);
const addedLogs = snapshot.after.logs.slice(snapshot.before.logs.length);
const addedLogs = addedReferenceLogs(snapshot.before, snapshot.after.logs);
expect(addedLogs.map((entry) => entry.text)).toEqual(
expect.arrayContaining([expect.stringContaining('이호경식 발동')])
);
@@ -5656,7 +5672,7 @@ integration('nation surprise-attack target, diplomacy-term, front, and cooldown
expect(readNumericField(sourceCityAfter, 'frontState')).toBe(expectedSourceFront);
expect(readNumericField(destCityAfter, 'frontState')).toBe(expectedDestFront);
const addedLogs = snapshot.after.logs.slice(snapshot.before.logs.length);
const addedLogs = addedReferenceLogs(snapshot.before, snapshot.after.logs);
expect(addedLogs.map((entry) => entry.text)).toEqual(
expect.arrayContaining([expect.stringContaining('급습 발동')])
);
@@ -5964,7 +5980,7 @@ integration('nation desperate-survival multistep, diplomacy, effects, and cooldo
expect(readNumericField(foreignAfter, 'atmos')).toBe(readNumericField(foreignBefore, 'atmos'));
if (completed) {
const addedLogs = snapshot.after.logs.slice(snapshot.before.logs.length);
const addedLogs = addedReferenceLogs(snapshot.before, snapshot.after.logs);
expect(addedLogs.map((entry) => entry.text)).toEqual(
expect.arrayContaining([expect.stringContaining('필사즉생 발동')])
);
@@ -6284,7 +6300,7 @@ integration('nation deception target, multistep, movement, RNG, and cooldown bou
);
if (completed) {
const addedLogs = snapshot.after.logs.slice(snapshot.before.logs.length);
const addedLogs = addedReferenceLogs(snapshot.before, snapshot.after.logs);
expect(addedLogs.map((entry) => entry.text)).toEqual(
expect.arrayContaining([expect.stringContaining('허보 발동')])
);
@@ -6735,7 +6751,7 @@ integration('nation scorched-earth constraints, multistep, city values, officers
expect(
readNumericField(auxAfter, 'did_특성초토화') - readNumericField(auxBefore, 'did_특성초토화')
).toBe(readNumericField(cityBefore, 'level') >= 8 ? 1 : 0);
expect(snapshot.after.logs.slice(snapshot.before.logs.length).map((entry) => entry.text)).toEqual(
expect(addedReferenceLogs(snapshot.before, snapshot.after.logs).map((entry) => entry.text)).toEqual(
expect.arrayContaining([
expect.stringContaining('초토화했습니다'),
expect.stringContaining('초토화</> 명령'),
@@ -198,6 +198,116 @@ describe('turn snapshot differential comparator', () => {
);
});
it('keys multiple cooldowns for one owner by owner and action name', () => {
const referenceBefore = snapshot('ref', {
world: {
year: 183,
month: 1,
tickMinutes: 10,
turnTime: '0183-01-01T00:00:00.000Z',
isUnited: 0,
generalCooldowns: [
{ generalId: 1, actionName: '일반 행동', nextAvailableTurn: 0 },
{ generalId: 1, actionName: '특수 행동', nextAvailableTurn: 0 },
],
nationCooldowns: [
{ nationId: 1, actionName: '피장파장', nextAvailableTurn: 0 },
{ nationId: 1, actionName: '이호경식', nextAvailableTurn: 0 },
],
},
});
const coreBefore = snapshot('core2026', {
world: {
year: 183,
month: 1,
tickMinutes: 10,
turnTime: '0183-01-01T00:00:00.000Z',
isUnited: 0,
generalCooldowns: [
{ generalId: 1, actionName: '특수 행동', nextAvailableTurn: 0 },
{ generalId: 1, actionName: '일반 행동', nextAvailableTurn: 0 },
],
nationCooldowns: [
{ nationId: 1, actionName: '이호경식', nextAvailableTurn: 0 },
{ nationId: 1, actionName: '피장파장', nextAvailableTurn: 0 },
],
},
});
const reference = snapshot('ref', {
world: {
year: 183,
month: 1,
tickMinutes: 10,
turnTime: '0183-01-01T00:00:00.000Z',
isUnited: 0,
generalCooldowns: [
{ generalId: 1, actionName: '일반 행동', nextAvailableTurn: 3 },
{ generalId: 1, actionName: '특수 행동', nextAvailableTurn: 7 },
],
nationCooldowns: [
{ nationId: 1, actionName: '피장파장', nextAvailableTurn: 9 },
{ nationId: 1, actionName: '이호경식', nextAvailableTurn: 11 },
],
},
});
const core = snapshot('core2026', {
world: {
year: 183,
month: 1,
tickMinutes: 10,
turnTime: '0183-01-01T00:00:00.000Z',
isUnited: 0,
generalCooldowns: [
{ generalId: 1, actionName: '특수 행동', nextAvailableTurn: 7 },
{ generalId: 1, actionName: '일반 행동', nextAvailableTurn: 3 },
],
nationCooldowns: [
{ nationId: 1, actionName: '이호경식', nextAvailableTurn: 11 },
{ nationId: 1, actionName: '피장파장', nextAvailableTurn: 9 },
],
},
});
expect(compareTurnSnapshots(reference, core)).toEqual([]);
expect(compareTurnSnapshotDeltas(referenceBefore, reference, coreBefore, core)).toEqual([]);
const mutant = snapshot('core2026', {
...core,
world: {
...core.world,
nationCooldowns: [
{ nationId: 1, actionName: '이호경식', nextAvailableTurn: 11 },
{ nationId: 1, actionName: '피장파장', nextAvailableTurn: 10 },
],
},
});
expect(compareTurnSnapshots(reference, mutant)).toContainEqual({
path: 'world.nationCooldowns[1:피장파장].nextAvailableTurn',
reference: 9,
core: 10,
});
expect(compareTurnSnapshotDeltas(referenceBefore, reference, coreBefore, mutant)).toEqual([
{
path: 'world.nationCooldowns[1:피장파장].nextAvailableTurn',
reference: 9,
core: 10,
},
]);
const duplicate = snapshot('ref', {
world: {
...reference.world,
nationCooldowns: [
{ nationId: 1, actionName: '피장파장', nextAvailableTurn: 9 },
{ nationId: 1, actionName: '피장파장', nextAvailableTurn: 10 },
],
},
});
expect(() => compareTurnSnapshots(duplicate, core)).toThrowError(
'Duplicate semantic entity key "1:피장파장" at "world.nationCooldowns": indexes 0 and 1'
);
});
it('reports exact changed paths for general and nation command state', () => {
const reference = snapshot('ref', {
diplomacy: [{ fromNationId: 1, toNationId: 2, state: 1, term: 24 }],