test: full precision 명령의 Ref 투영 경계를 고정한다
This commit is contained in:
@@ -104,6 +104,12 @@ DB auto ID, 생성 시각처럼 의미 없는 차이는 comparator에서 이름
|
|||||||
명시합니다. 의미 field를 ignore하거나 숫자 허용 범위를 넓혀 mismatch를
|
명시합니다. 의미 field를 ignore하거나 숫자 허용 범위를 넓혀 mismatch를
|
||||||
숨기지 않습니다.
|
숨기지 않습니다.
|
||||||
|
|
||||||
|
Core가 의도적으로 full precision을 유지하는 `nation.tech`와 `city.trust`를 Ref의
|
||||||
|
MariaDB `FLOAT` snapshot과 비교할 때는 raw 차이 경로를 fixture별로 먼저 고정합니다.
|
||||||
|
그 뒤 test-only binary32 저장·6자리 읽기 projection을 Core의 절대 before/after 값에
|
||||||
|
적용해 Ref delta와 정확히 일치하는지 다시 검사합니다. 제품 상태를 양자화하거나
|
||||||
|
전역 tolerance·전역 ignore로 다른 차이를 숨기지 않습니다.
|
||||||
|
|
||||||
JSON missing·`{}`·`[]`는 snapshot 원형에서 구분합니다. 일반 message option 부재의
|
JSON missing·`{}`·`[]`는 snapshot 원형에서 구분합니다. 일반 message option 부재의
|
||||||
Ref `[]`와 Core `{}`만 의미상 같게 보며, actionable diplomacy의 `option=null` sentinel은
|
Ref `[]`와 Core `{}`만 의미상 같게 보며, actionable diplomacy의 `option=null` sentinel은
|
||||||
부재로 합치지 않습니다. Ref log prefix와 Core format은 독립적으로 해석하고, 서로 다른
|
부재로 합치지 않습니다. Ref log prefix와 Core format은 독립적으로 해석하고, 서로 다른
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import type { CanonicalTurnSnapshot } from './canonical.js';
|
||||||
|
|
||||||
|
const roundHalfEven = (value: number): number => {
|
||||||
|
const lower = Math.floor(value);
|
||||||
|
const fraction = value - lower;
|
||||||
|
const tolerance = Number.EPSILON * Math.max(1, Math.abs(value)) * 4;
|
||||||
|
if (Math.abs(fraction - 0.5) <= tolerance) {
|
||||||
|
return lower % 2 === 0 ? lower : lower + 1;
|
||||||
|
}
|
||||||
|
return Math.round(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test-only projection for a MariaDB FLOAT read through the Ref PHP service.
|
||||||
|
* Core product state intentionally keeps JavaScript/PostgreSQL precision; this
|
||||||
|
* oracle is only used to prove that an explicitly enumerated raw difference is
|
||||||
|
* caused by Ref's binary32 write and six-significant-digit read boundary.
|
||||||
|
*/
|
||||||
|
export const projectRefFloatRead = (value: number): number => {
|
||||||
|
const stored = Math.fround(value);
|
||||||
|
if (!Number.isFinite(stored) || stored === 0) {
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
|
const sign = stored < 0 ? -1 : 1;
|
||||||
|
const absolute = Math.abs(stored);
|
||||||
|
const exponent = Math.floor(Math.log10(absolute));
|
||||||
|
const scale = 10 ** (5 - exponent);
|
||||||
|
return sign * (roundHalfEven(absolute * scale) / scale);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface RefFloatSnapshotProjection {
|
||||||
|
cityTrust?: boolean;
|
||||||
|
nationTech?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectField = (row: Record<string, unknown>, field: string): Record<string, unknown> => {
|
||||||
|
const value = row[field];
|
||||||
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
return { ...row, [field]: projectRefFloatRead(value) };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const projectSnapshotThroughRefFloatRead = (
|
||||||
|
snapshot: CanonicalTurnSnapshot,
|
||||||
|
projection: RefFloatSnapshotProjection
|
||||||
|
): CanonicalTurnSnapshot => ({
|
||||||
|
...snapshot,
|
||||||
|
cities: projection.cityTrust ? snapshot.cities.map((city) => projectField(city, 'trust')) : snapshot.cities,
|
||||||
|
nations: projection.nationTech ? snapshot.nations.map((nation) => projectField(nation, 'tech')) : snapshot.nations,
|
||||||
|
});
|
||||||
@@ -3,8 +3,10 @@ import path from 'node:path';
|
|||||||
|
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { compareTurnSnapshotDeltas } from '../src/turn-differential/compare.js';
|
import type { CanonicalTurnSnapshot } from '../src/turn-differential/canonical.js';
|
||||||
|
import { compareTurnSnapshotDeltas, type SnapshotDifference } from '../src/turn-differential/compare.js';
|
||||||
import { runCoreTurnCommandTrace, type TurnCommandFixtureRequest } from '../src/turn-differential/coreCommandTrace.js';
|
import { runCoreTurnCommandTrace, type TurnCommandFixtureRequest } from '../src/turn-differential/coreCommandTrace.js';
|
||||||
|
import { projectSnapshotThroughRefFloatRead } from '../src/turn-differential/legacyNumericProjection.js';
|
||||||
import { orderedSemanticLogStreams } from '../src/turn-differential/logProjection.js';
|
import { orderedSemanticLogStreams } from '../src/turn-differential/logProjection.js';
|
||||||
import {
|
import {
|
||||||
projectSemanticTurnMessages,
|
projectSemanticTurnMessages,
|
||||||
@@ -53,6 +55,89 @@ const timestampMillis = (value: unknown): number => {
|
|||||||
|
|
||||||
const semanticLogSignatures = (logs: Array<Record<string, unknown>>): string[] => orderedSemanticLogStreams(logs);
|
const semanticLogSignatures = (logs: Array<Record<string, unknown>>): string[] => orderedSemanticLogStreams(logs);
|
||||||
|
|
||||||
|
const conquestTechDifferences: SnapshotDifference[] = [
|
||||||
|
{
|
||||||
|
path: 'nations[1].tech',
|
||||||
|
reference: 0.009999999999990905,
|
||||||
|
core: 0.006600000000048567,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const defenderTechDifferences: SnapshotDifference[] = [
|
||||||
|
{
|
||||||
|
path: 'nations[1].tech',
|
||||||
|
reference: 3.6200000000000045,
|
||||||
|
core: 3.623399999999947,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'nations[2].tech',
|
||||||
|
reference: 5.190000000000055,
|
||||||
|
core: 5.19056999999998,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const multipleDefendersTechDifferences: SnapshotDifference[] = [
|
||||||
|
{
|
||||||
|
path: 'nations[1].tech',
|
||||||
|
reference: 3.2100000000000364,
|
||||||
|
core: 3.210239999999999,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'nations[2].tech',
|
||||||
|
reference: 5.110000000000014,
|
||||||
|
core: 5.1083999999999605,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const twoNationConquestTechDifferences: SnapshotDifference[] = [
|
||||||
|
...conquestTechDifferences,
|
||||||
|
{
|
||||||
|
path: 'nations[2].tech',
|
||||||
|
reference: 0.009999999999990905,
|
||||||
|
core: 0.009900000000016007,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const expectedSortieRawDifferences: Record<string, SnapshotDifference[]> = {
|
||||||
|
'live sortie conquest': conquestTechDifferences,
|
||||||
|
'live sortie collapsed nation conflict cleanup': conquestTechDifferences,
|
||||||
|
'live sortie against a defending general': defenderTechDifferences,
|
||||||
|
'live sortie against multiple defending generals': multipleDefendersTechDifferences,
|
||||||
|
'live sortie supply retreat': [],
|
||||||
|
'live sortie noncapital conquest': twoNationConquestTechDifferences,
|
||||||
|
'live sortie emergency capital': twoNationConquestTechDifferences,
|
||||||
|
'live sortie conflict arbitration': twoNationConquestTechDifferences,
|
||||||
|
'live sortie tied conflict': twoNationConquestTechDifferences,
|
||||||
|
'live sortie outer lifecycle: conquest': conquestTechDifferences,
|
||||||
|
'live sortie outer lifecycle: collapsed nation conflict cleanup': conquestTechDifferences,
|
||||||
|
'live sortie outer lifecycle: defender': defenderTechDifferences,
|
||||||
|
'live sortie outer lifecycle: multiple defenders': multipleDefendersTechDifferences,
|
||||||
|
'live sortie outer lifecycle: supply retreat': [],
|
||||||
|
'live sortie outer lifecycle: noncapital conquest': twoNationConquestTechDifferences,
|
||||||
|
'live sortie outer lifecycle: emergency capital': twoNationConquestTechDifferences,
|
||||||
|
'live sortie outer lifecycle: conflict arbitration': twoNationConquestTechDifferences,
|
||||||
|
'live sortie outer lifecycle: tied conflict': twoNationConquestTechDifferences,
|
||||||
|
'collapse scout positive': conquestTechDifferences,
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectSortieDeltaParity = (
|
||||||
|
reference: { before: CanonicalTurnSnapshot; after: CanonicalTurnSnapshot },
|
||||||
|
core: { before: CanonicalTurnSnapshot; after: CanonicalTurnSnapshot },
|
||||||
|
ignoredPathPatterns: RegExp[],
|
||||||
|
expectedRawDifferences: SnapshotDifference[]
|
||||||
|
): void => {
|
||||||
|
const rawDifferences = compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
||||||
|
ignoredPathPatterns,
|
||||||
|
});
|
||||||
|
expect(rawDifferences).toEqual(expectedRawDifferences);
|
||||||
|
expect(
|
||||||
|
compareTurnSnapshotDeltas(
|
||||||
|
reference.before,
|
||||||
|
reference.after,
|
||||||
|
projectSnapshotThroughRefFloatRead(core.before, { nationTech: true }),
|
||||||
|
projectSnapshotThroughRefFloatRead(core.after, { nationTech: true }),
|
||||||
|
{ ignoredPathPatterns }
|
||||||
|
)
|
||||||
|
).toEqual([]);
|
||||||
|
};
|
||||||
|
|
||||||
const readFixture = (relativePath: string): TurnCommandFixtureRequest => {
|
const readFixture = (relativePath: string): TurnCommandFixtureRequest => {
|
||||||
const stackRoot = path.join(workspaceRoot!, 'docker_compose_files/reference');
|
const stackRoot = path.join(workspaceRoot!, 'docker_compose_files/reference');
|
||||||
const fixture = JSON.parse(
|
const fixture = JSON.parse(
|
||||||
@@ -281,16 +366,23 @@ integration('core ↔ legacy command-boundary differential', () => {
|
|||||||
);
|
);
|
||||||
expect(semanticLogSignatures(core.after.logs)).toEqual(semanticLogSignatures(referenceAddedLogs));
|
expect(semanticLogSignatures(core.after.logs)).toEqual(semanticLogSignatures(referenceAddedLogs));
|
||||||
}
|
}
|
||||||
expect(
|
const ignoredPathPatterns =
|
||||||
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
|
||||||
ignoredPathPatterns:
|
|
||||||
request.action === 'che_출병'
|
request.action === 'che_출병'
|
||||||
? request.includeLifecycle
|
? request.includeLifecycle
|
||||||
? comparedLifecycleIgnoredPaths
|
? comparedLifecycleIgnoredPaths
|
||||||
: ignoredLifecyclePaths
|
: ignoredLifecyclePaths
|
||||||
: [...ignoredLifecyclePaths, /^generals\[[^\]]+\]\.killTurn(?:\.|$)/],
|
: [...ignoredLifecyclePaths, /^generals\[[^\]]+\]\.killTurn(?:\.|$)/];
|
||||||
|
if (request.action === 'che_출병') {
|
||||||
|
const expectedRawDifferences = expectedSortieRawDifferences[label];
|
||||||
|
expect(expectedRawDifferences, `missing raw numeric contract for ${label}`).toBeDefined();
|
||||||
|
expectSortieDeltaParity(reference, core, ignoredPathPatterns, expectedRawDifferences!);
|
||||||
|
} else {
|
||||||
|
expect(
|
||||||
|
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
||||||
|
ignoredPathPatterns,
|
||||||
})
|
})
|
||||||
).toEqual([]);
|
).toEqual([]);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
120_000
|
120_000
|
||||||
);
|
);
|
||||||
@@ -345,10 +437,11 @@ integration('core ↔ legacy command-boundary differential', () => {
|
|||||||
unreadPrivateDelta: 1,
|
unreadPrivateDelta: 1,
|
||||||
hasUnreadMessage: true,
|
hasUnreadMessage: true,
|
||||||
});
|
});
|
||||||
expect(
|
expectSortieDeltaParity(
|
||||||
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
reference,
|
||||||
ignoredPathPatterns: ignoredLifecyclePaths,
|
core,
|
||||||
})
|
ignoredLifecyclePaths,
|
||||||
).toEqual([]);
|
expectedSortieRawDifferences['collapse scout positive']!
|
||||||
|
);
|
||||||
}, 120_000);
|
}, 120_000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,12 +2,17 @@ import { describe, expect, it } from 'vitest';
|
|||||||
import { asRecord, GAME_TICKS_PER_TURN, LEGACY_RANK_DATA_TYPES } from '@sammo-ts/common';
|
import { asRecord, GAME_TICKS_PER_TURN, LEGACY_RANK_DATA_TYPES } from '@sammo-ts/common';
|
||||||
import { GENERAL_TURN_COMMAND_KEYS } from '@sammo-ts/logic';
|
import { GENERAL_TURN_COMMAND_KEYS } from '@sammo-ts/logic';
|
||||||
|
|
||||||
import { compareTurnSnapshotDeltas } from '../src/turn-differential/compare.js';
|
import type { CanonicalTurnSnapshot } from '../src/turn-differential/canonical.js';
|
||||||
|
import { compareTurnSnapshotDeltas, type SnapshotDifference } from '../src/turn-differential/compare.js';
|
||||||
import { runCoreTurnCommandTrace, type TurnCommandFixtureRequest } from '../src/turn-differential/coreCommandTrace.js';
|
import { runCoreTurnCommandTrace, type TurnCommandFixtureRequest } from '../src/turn-differential/coreCommandTrace.js';
|
||||||
import {
|
import {
|
||||||
normalizeStoredTurnLogText as normalizeStoredLogText,
|
normalizeStoredTurnLogText as normalizeStoredLogText,
|
||||||
orderedSemanticLogStreams,
|
orderedSemanticLogStreams,
|
||||||
} from '../src/turn-differential/logProjection.js';
|
} from '../src/turn-differential/logProjection.js';
|
||||||
|
import {
|
||||||
|
projectSnapshotThroughRefFloatRead,
|
||||||
|
type RefFloatSnapshotProjection,
|
||||||
|
} from '../src/turn-differential/legacyNumericProjection.js';
|
||||||
import {
|
import {
|
||||||
projectSemanticTurnMessages,
|
projectSemanticTurnMessages,
|
||||||
projectSemanticUnreadMessageDeltas,
|
projectSemanticUnreadMessageDeltas,
|
||||||
@@ -64,6 +69,28 @@ const successfulLifecycleIgnoredPaths = [
|
|||||||
/^nations\[[^\]]+\]\.meta(?:\.|$)/,
|
/^nations\[[^\]]+\]\.meta(?:\.|$)/,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const expectRefFloatProjectedDeltaParity = (
|
||||||
|
reference: { before: CanonicalTurnSnapshot; after: CanonicalTurnSnapshot },
|
||||||
|
core: { before: CanonicalTurnSnapshot; after: CanonicalTurnSnapshot },
|
||||||
|
ignoredPathPatterns: RegExp[],
|
||||||
|
expectedRawDifferences: SnapshotDifference[],
|
||||||
|
projection: RefFloatSnapshotProjection
|
||||||
|
): void => {
|
||||||
|
const rawDifferences = compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
||||||
|
ignoredPathPatterns,
|
||||||
|
});
|
||||||
|
expect(rawDifferences).toEqual(expectedRawDifferences);
|
||||||
|
expect(
|
||||||
|
compareTurnSnapshotDeltas(
|
||||||
|
reference.before,
|
||||||
|
reference.after,
|
||||||
|
projectSnapshotThroughRefFloatRead(core.before, projection),
|
||||||
|
projectSnapshotThroughRefFloatRead(core.after, projection),
|
||||||
|
{ ignoredPathPatterns }
|
||||||
|
)
|
||||||
|
).toEqual([]);
|
||||||
|
};
|
||||||
|
|
||||||
const general = (id: number, nationId: number, cityId: number, officerLevel: number): Record<string, unknown> => ({
|
const general = (id: number, nationId: number, cityId: number, officerLevel: number): Record<string, unknown> => ({
|
||||||
id,
|
id,
|
||||||
nationId,
|
nationId,
|
||||||
@@ -508,11 +535,26 @@ integration('general command success matrix', () => {
|
|||||||
expect(actorTurnAt(reference.after.generalTurns, 0)).toBeUndefined();
|
expect(actorTurnAt(reference.after.generalTurns, 0)).toBeUndefined();
|
||||||
expect(actorTurnAt(core.after.generalTurns, 0)).toBeUndefined();
|
expect(actorTurnAt(core.after.generalTurns, 0)).toBeUndefined();
|
||||||
}
|
}
|
||||||
expect(
|
expectRefFloatProjectedDeltaParity(
|
||||||
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
reference,
|
||||||
ignoredPathPatterns: successfulLifecycleIgnoredPaths,
|
core,
|
||||||
})
|
successfulLifecycleIgnoredPaths,
|
||||||
).toEqual([]);
|
action === 'che_출병'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
path: 'nations[1].tech',
|
||||||
|
reference: { $snapshotState: 'missing' },
|
||||||
|
core: 0.004800000000045657,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'nations[2].tech',
|
||||||
|
reference: 0.009999999999990905,
|
||||||
|
core: 0.009000000000014552,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
{ nationTech: action === 'che_출병' }
|
||||||
|
);
|
||||||
|
|
||||||
// Logs and messages live outside the generic state-delta graph.
|
// Logs and messages live outside the generic state-delta graph.
|
||||||
// Assert both for every registered success case so a command cannot
|
// Assert both for every registered success case so a command cannot
|
||||||
@@ -2647,11 +2689,21 @@ integration('general command in-action failure matrix', () => {
|
|||||||
failureLogTexts(reference.after.logs, failureText).map(legacyActionLogBody)
|
failureLogTexts(reference.after.logs, failureText).map(legacyActionLogBody)
|
||||||
);
|
);
|
||||||
expect(core.rng).toEqual(reference.rng);
|
expect(core.rng).toEqual(reference.rng);
|
||||||
expect(
|
expectRefFloatProjectedDeltaParity(
|
||||||
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
reference,
|
||||||
ignoredPathPatterns: ignoredLifecyclePaths,
|
core,
|
||||||
})
|
ignoredLifecyclePaths,
|
||||||
).toEqual([]);
|
action === 'che_주민선정'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
path: 'cities[3].trust',
|
||||||
|
reference: 2.9643999999999977,
|
||||||
|
core: 2.9644093559690674,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
{ cityTrust: action === 'che_주민선정' }
|
||||||
|
);
|
||||||
},
|
},
|
||||||
120_000
|
120_000
|
||||||
);
|
);
|
||||||
@@ -3047,11 +3099,21 @@ integration('general sabotage successful effect matrix', () => {
|
|||||||
if ('actor' in expected) {
|
if ('actor' in expected) {
|
||||||
expect(findById(reference.after.generals, 1)).toMatchObject(expected.actor);
|
expect(findById(reference.after.generals, 1)).toMatchObject(expected.actor);
|
||||||
}
|
}
|
||||||
expect(
|
expectRefFloatProjectedDeltaParity(
|
||||||
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
reference,
|
||||||
ignoredPathPatterns: ignoredLifecyclePaths,
|
core,
|
||||||
})
|
ignoredLifecyclePaths,
|
||||||
).toEqual([]);
|
action === 'che_선동'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
path: 'cities[70].trust',
|
||||||
|
reference: -9.8934,
|
||||||
|
core: -9.893404080791214,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
{ cityTrust: action === 'che_선동' }
|
||||||
|
);
|
||||||
|
|
||||||
if (process.env.TURN_DIFFERENTIAL_SABOTAGE_EVIDENCE === '1') {
|
if (process.env.TURN_DIFFERENTIAL_SABOTAGE_EVIDENCE === '1') {
|
||||||
process.stderr.write(
|
process.stderr.write(
|
||||||
@@ -3109,11 +3171,21 @@ integration('general sabotage stat progression matrix', () => {
|
|||||||
expect(core.rng).toEqual(reference.rng);
|
expect(core.rng).toEqual(reference.rng);
|
||||||
expect(reference.after.generals.find((entry) => entry.id === 1)?.[stat]).toBe(101);
|
expect(reference.after.generals.find((entry) => entry.id === 1)?.[stat]).toBe(101);
|
||||||
expect(core.after.generals.find((entry) => entry.id === 1)?.[stat]).toBe(101);
|
expect(core.after.generals.find((entry) => entry.id === 1)?.[stat]).toBe(101);
|
||||||
expect(
|
expectRefFloatProjectedDeltaParity(
|
||||||
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
reference,
|
||||||
ignoredPathPatterns: ignoredLifecyclePaths,
|
core,
|
||||||
})
|
ignoredLifecyclePaths,
|
||||||
).toEqual([]);
|
action === 'che_선동'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
path: 'cities[70].trust',
|
||||||
|
reference: -9.8934,
|
||||||
|
core: -9.893404080791214,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
{ cityTrust: action === 'che_선동' }
|
||||||
|
);
|
||||||
},
|
},
|
||||||
120_000
|
120_000
|
||||||
);
|
);
|
||||||
@@ -3171,11 +3243,21 @@ integration('general sabotage probability clamp matrix', () => {
|
|||||||
: { operation: 'nextBits', arguments: { bits: 1 } }
|
: { operation: 'nextBits', arguments: { bits: 1 } }
|
||||||
);
|
);
|
||||||
expect(core.rng).toEqual(reference.rng);
|
expect(core.rng).toEqual(reference.rng);
|
||||||
expect(
|
expectRefFloatProjectedDeltaParity(
|
||||||
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
reference,
|
||||||
ignoredPathPatterns: ignoredLifecyclePaths,
|
core,
|
||||||
})
|
ignoredLifecyclePaths,
|
||||||
).toEqual([]);
|
action === 'che_선동' && boundary === 'max'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
path: 'cities[70].trust',
|
||||||
|
reference: -11.155500000000004,
|
||||||
|
core: -11.15547143003728,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
{ cityTrust: action === 'che_선동' && boundary === 'max' }
|
||||||
|
);
|
||||||
},
|
},
|
||||||
120_000
|
120_000
|
||||||
);
|
);
|
||||||
@@ -3334,11 +3416,21 @@ integration('general sabotage injury boundary matrix', () => {
|
|||||||
injuryLogTexts(reference.after.logs).map(legacyInjuryLogBody)
|
injuryLogTexts(reference.after.logs).map(legacyInjuryLogBody)
|
||||||
);
|
);
|
||||||
expect(core.rng).toEqual(reference.rng);
|
expect(core.rng).toEqual(reference.rng);
|
||||||
expect(
|
expectRefFloatProjectedDeltaParity(
|
||||||
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
reference,
|
||||||
ignoredPathPatterns: ignoredLifecyclePaths,
|
core,
|
||||||
})
|
ignoredLifecyclePaths,
|
||||||
).toEqual([]);
|
action === 'che_선동'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
path: 'cities[70].trust',
|
||||||
|
reference: -4.810900000000004,
|
||||||
|
core: -4.810929741150531,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
{ cityTrust: action === 'che_선동' }
|
||||||
|
);
|
||||||
},
|
},
|
||||||
120_000
|
120_000
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import {
|
|||||||
compareTurnSnapshotDeltas,
|
compareTurnSnapshotDeltas,
|
||||||
compareTurnSnapshots,
|
compareTurnSnapshots,
|
||||||
} from '../src/turn-differential/compare.js';
|
} from '../src/turn-differential/compare.js';
|
||||||
|
import {
|
||||||
|
projectRefFloatRead,
|
||||||
|
projectSnapshotThroughRefFloatRead,
|
||||||
|
} from '../src/turn-differential/legacyNumericProjection.js';
|
||||||
|
|
||||||
const snapshot = (
|
const snapshot = (
|
||||||
engine: 'ref' | 'core2026',
|
engine: 'ref' | 'core2026',
|
||||||
@@ -29,6 +33,23 @@ const snapshot = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('turn snapshot differential comparator', () => {
|
describe('turn snapshot differential comparator', () => {
|
||||||
|
it('projects only explicitly selected Core numeric state through the Ref FLOAT boundary', () => {
|
||||||
|
expect(projectRefFloatRead(1_000.0048)).toBe(1_000);
|
||||||
|
expect(projectRefFloatRead(1_000.009)).toBe(1_000.01);
|
||||||
|
expect(projectRefFloatRead(70.10659591920879)).toBe(70.1066);
|
||||||
|
|
||||||
|
const core = snapshot('core2026', {
|
||||||
|
cities: [{ id: 1, trust: 70.10659591920879, agriculture: 123.456789 }],
|
||||||
|
nations: [{ id: 1, tech: 1_000.009, gold: 123.456789 }],
|
||||||
|
});
|
||||||
|
const projected = projectSnapshotThroughRefFloatRead(core, { cityTrust: true, nationTech: true });
|
||||||
|
|
||||||
|
expect(projected.cities[0]).toEqual({ id: 1, trust: 70.1066, agriculture: 123.456789 });
|
||||||
|
expect(projected.nations[0]).toEqual({ id: 1, tech: 1_000.01, gold: 123.456789 });
|
||||||
|
expect(core.cities[0]?.trust).toBe(70.10659591920879);
|
||||||
|
expect(core.nations[0]?.tech).toBe(1_000.009);
|
||||||
|
});
|
||||||
|
|
||||||
it('compares entity arrays by semantic identity instead of database row order', () => {
|
it('compares entity arrays by semantic identity instead of database row order', () => {
|
||||||
const reference = snapshot('ref', {
|
const reference = snapshot('ref', {
|
||||||
cities: [
|
cities: [
|
||||||
|
|||||||
Reference in New Issue
Block a user