test: Ref 명령 행렬 fixture의 결정성을 보강한다
This commit is contained in:
@@ -28,13 +28,24 @@ interface EntityIdentity {
|
||||
semantic: boolean;
|
||||
}
|
||||
|
||||
const entityIdentity = (value: Record<string, unknown>, index: number): EntityIdentity => {
|
||||
const entityIdentity = (value: Record<string, unknown>, index: number, path: string): EntityIdentity => {
|
||||
if (
|
||||
(typeof value.generalId === 'number' || typeof value.generalId === 'string') &&
|
||||
typeof value.type === 'string'
|
||||
) {
|
||||
return { key: `${String(value.generalId)}:${value.type}`, semantic: true };
|
||||
}
|
||||
if (
|
||||
(path === 'world.generalCooldowns' || path === 'world.nationCooldowns') &&
|
||||
typeof value.actionName === 'string'
|
||||
) {
|
||||
for (const key of ['generalId', 'nationId']) {
|
||||
const candidate = value[key];
|
||||
if (typeof candidate === 'number' || typeof candidate === 'string') {
|
||||
return { key: `${String(candidate)}:${value.actionName}`, semantic: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key of ['id', 'generalId', 'nationId', 'fromNationId']) {
|
||||
const candidate = value[key];
|
||||
if (typeof candidate === 'number' || typeof candidate === 'string') {
|
||||
@@ -62,7 +73,7 @@ const flatten = (value: unknown, path: string, output: FlatSnapshot): void => {
|
||||
path === 'logs' || path === 'messages'
|
||||
? { key: String(index), semantic: false }
|
||||
: typeof entry === 'object' && entry !== null && !Array.isArray(entry)
|
||||
? entityIdentity(entry as Record<string, unknown>, index)
|
||||
? entityIdentity(entry as Record<string, unknown>, index, path)
|
||||
: { key: String(index), semantic: false };
|
||||
if (identity.semantic) {
|
||||
const firstIndex = semanticKeys.get(identity.key);
|
||||
|
||||
@@ -72,6 +72,8 @@ export interface TurnCommandFixtureRequest {
|
||||
initMonth?: number;
|
||||
year?: number;
|
||||
month?: number;
|
||||
develCost?: number;
|
||||
isUnited?: 0 | 1 | 2 | 3;
|
||||
hiddenSeed?: string;
|
||||
scenarioEffect?: string | null;
|
||||
staticEventHandlers?: Record<string, string[]>;
|
||||
@@ -333,6 +335,9 @@ const buildGeneral = (row: Record<string, unknown>, fallbackTurnTime: Date): Tur
|
||||
: row.hasOwner === true
|
||||
? 'turn-differential-owner'
|
||||
: null,
|
||||
inheritancePoints: {
|
||||
active_action: readNumber(row, 'inheritActiveActionPoints'),
|
||||
},
|
||||
penalty: row.penalty,
|
||||
triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
|
||||
meta: {
|
||||
@@ -376,6 +381,7 @@ const buildGeneral = (row: Record<string, unknown>, fallbackTurnTime: Date): Tur
|
||||
belong: readNumber(row, 'belong', readNumber(meta, 'belong')),
|
||||
permission: readString(row, 'permission', readString(meta, 'permission', 'normal')),
|
||||
block: readNumber(row, 'blockState', readNumber(meta, 'block')),
|
||||
inherit_active_action: readNumber(row, 'inheritActiveActionPoints') / 3,
|
||||
},
|
||||
...(lastTurn ? { lastTurn } : {}),
|
||||
...(typeof row.turnTick === 'number' ? { turnTick: row.turnTick } : {}),
|
||||
|
||||
@@ -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 }],
|
||||
|
||||
Reference in New Issue
Block a user