diff --git a/docs/integration-tests.md b/docs/integration-tests.md index f77dcff2..c10b1e27 100644 --- a/docs/integration-tests.md +++ b/docs/integration-tests.md @@ -134,3 +134,9 @@ PostgreSQL·Redis 경계를 증명하지 않습니다. Full suite 실패는 변 Ref 호환성 판정은 [차등 검증](architecture/turn-state-differential-testing.md), UI는 [프론트엔드 호환 검증](frontend-legacy-parity.md)을 함께 사용합니다. + +전투 아이템 전체 차등 검증은 공격자·방어자 각각 145개 fixture를 모두 Core와 +Ref에서 실행하고 event, RNG, outcome, 전체 log bucket을 항목별로 비교합니다. +Ref harness는 fixture마다 새 PHP process를 띄우지 않고 `--jsonl` 입력을 한 +process에서 일괄 처리합니다. `ITEM_PARITY_FILTER`는 특정 항목을 진단할 때만 +사용하며, 전체 검증의 fixture 수나 assertion을 줄이는 최적화로 사용하지 않습니다. diff --git a/tools/integration-tests/test/battleDifferential.test.ts b/tools/integration-tests/test/battleDifferential.test.ts index 8b0a918d..9f0cf569 100644 --- a/tools/integration-tests/test/battleDifferential.test.ts +++ b/tools/integration-tests/test/battleDifferential.test.ts @@ -582,7 +582,28 @@ const runReferenceTraceBatch = (workspaceRoot: string, fixtureLines: string[]): } const compareSourceRoot = process.env.REF_COMPARE_SOURCE_ROOT; if (!compareSourceRoot) { - throw new Error('BATTLE_CORPUS_PATH requires REF_COMPARE_SOURCE_ROOT with the JSONL-capable ref harness.'); + const stdout = execFileSync( + 'docker', + ['compose', 'exec', '-T', 'php', 'php', '/var/www/html/hwe/compare/battle_trace.php', '--jsonl'], + { + cwd: path.join(workspaceRoot, 'docker_compose_files/reference'), + input: normalizeJsonlForManifest(fixtureLines), + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + maxBuffer: 512 * 1024 * 1024, + } + ); + const traces = stdout + .split(/\r?\n/u) + .filter(Boolean) + .map((line) => JSON.parse(line) as ReferenceTrace); + if (traces.length !== fixtureLines.length) { + throw new Error(`ref batch returned ${traces.length} traces for ${fixtureLines.length} fixtures`); + } + traces.forEach((trace, index) => + assertReferenceFixtureIdentity(trace, fixtureLines[index]!, `compose trace[${index}]`) + ); + return traces; } const stdout = runReferenceSourceScript({ workspaceRoot, @@ -2370,6 +2391,14 @@ describeWithReference('ref ↔ core2026 battle differential', () => { }, }; const failures: string[] = []; + const cases: Array<{ + itemKey: string; + base: BattleSimRequestPayload & { startYear: number }; + coreEvents: WarBattleTraceEvent[]; + coreLogs: CoreLogCapture; + coreRng: TracingRng | null; + coreOutcome: WarBattleOutcome | null; + }> = []; const itemFilter = process.env['ITEM_PARITY_FILTER']; for (const [itemKey, slot] of [...itemSlots].sort(([lhs], [rhs]) => lhs.localeCompare(rhs))) { @@ -2423,7 +2452,16 @@ describeWithReference('ref ↔ core2026 battle differential', () => { return coreRng.createRandUtil(); }, }); - const reference = runReferenceTrace(workspaceRoot!, JSON.stringify(base)); + cases.push({ itemKey, base, coreEvents, coreLogs, coreRng, coreOutcome }); + } + + const references = runReferenceTraceBatch( + workspaceRoot!, + cases.map(({ base }) => JSON.stringify(base)) + ); + for (let index = 0; index < cases.length; index += 1) { + const { itemKey, base, coreEvents, coreLogs, coreRng, coreOutcome } = cases[index]!; + const reference = references[index]!; try { assertTraceParity(coreEvents, reference, coreRng, coreOutcome); assertAllLogBucketsParity(coreLogs, reference, base, `item.attacker.${itemKey}`); @@ -2487,6 +2525,14 @@ describeWithReference('ref ↔ core2026 battle differential', () => { }, }; const failures: string[] = []; + const cases: Array<{ + itemKey: string; + base: BattleSimRequestPayload & { startYear: number }; + coreEvents: WarBattleTraceEvent[]; + coreLogs: CoreLogCapture; + coreRng: TracingRng | null; + coreOutcome: WarBattleOutcome | null; + }> = []; const itemFilter = process.env['ITEM_PARITY_FILTER']; for (const [itemKey, slot] of [...itemSlots].sort(([lhs], [rhs]) => lhs.localeCompare(rhs))) { @@ -2538,7 +2584,16 @@ describeWithReference('ref ↔ core2026 battle differential', () => { return coreRng.createRandUtil(); }, }); - const reference = runReferenceTrace(workspaceRoot!, JSON.stringify(base)); + cases.push({ itemKey, base, coreEvents, coreLogs, coreRng, coreOutcome }); + } + + const references = runReferenceTraceBatch( + workspaceRoot!, + cases.map(({ base }) => JSON.stringify(base)) + ); + for (let index = 0; index < cases.length; index += 1) { + const { itemKey, base, coreEvents, coreLogs, coreRng, coreOutcome } = cases[index]!; + const reference = references[index]!; try { assertTraceParity(coreEvents, reference, coreRng, coreOutcome); assertAllLogBucketsParity(coreLogs, reference, base, `item.defender.${itemKey}`);