test(turn): cover general pre-required turn boundaries

This commit is contained in:
2026-07-26 04:41:15 +00:00
parent 200d883b5b
commit 5c76a20bc9
3 changed files with 119 additions and 5 deletions
@@ -9,7 +9,8 @@
현재 ref MariaDB와 core memory를 잇는 공통 runner, 성공 경로 55개,
실행 중 확률 실패 9개, full constraint fallback 12개와 모략 확률 clamp
8개, 모략 결과값 경계 5개, 부상 경계 3개가 구현됐다.
8개, 모략 결과값 경계 5개, 부상 경계 3개, alternative 5개와 pre-required
turn 중간 경계 6개가 구현됐다.
실패 9개는 내정 critical
`주민선정/정착장려/상업투자/기술연구/물자조달`과 모략
`화계/선동/파괴/탈취`이며 RNG 전체 trace, semantic state delta와 실패
@@ -23,9 +24,12 @@ state delta를 비교한다. 결과값 5개는 화계 농업·상업, 선동 치
파괴 수비·성벽의 0 바닥과 탈취의 대상 국가 자원 상한·미보급 도시 최종
저장 상태를 비교한다. 부상 3개는 화계·선동·파괴의 대상 장수별 판정,
부상도 80 상한, 병력·훈련·사기 0.98 정수 저장과 대상 장수 로그를
비교한다. 나머지 명령별 제약 실패·값 경계·alternative와 전체 core
PostgreSQL 재조회가 완료 기준을 통과하기 전까지 55개 명령 전체의 동적
호환 상태를 `확인`으로 올리지 않는다.
비교한다. alternative 5개는 해산·랜덤임관·무작위건국·출병의 모든 대체
분기에서 최초 명령 RNG의 연속 소비와 최종 상태를 비교한다. pre-required
turn 6개는 전투태세 1/2/3턴, 내정·전투 특기 초기화 1턴, 은퇴 1턴의
`last_turn`과 진행 로그, RNG 무소비를 비교한다. 나머지 명령별 제약
실패·값 경계와 전체 core PostgreSQL 재조회가 완료 기준을 통과하기 전까지
55개 명령 전체의 동적 호환 상태를 `확인`으로 올리지 않는다.
## 결정 요약
@@ -199,8 +199,15 @@ of 80, integer persistence after multiplying crew/train/atmosphere by 0.98,
and the target-general injury log for fire attack, agitation and destruction.
They restore the legacy Korean particle in the log, add the two missing logs,
and round rather than floor the integer fields.
Five general alternative cases cover disband and random-appointment fallback
to talent search, both random-founding alternatives, and sortie fallback to
movement. The alternative command reuses the original requested-action RNG
instance and current consumption position. Six pre-required-turn cases cover
battle-preparation terms 1 through 3 plus the first intermediate turn of both
trait resets and retirement. They compare the exact intermediate `lastTurn`,
progress log, zero command-RNG consumption and semantic state delta.
This is not yet a claim that every command-specific
constraint, clamp, alternative and persistence boundary has been dynamically
constraint, clamp and persistence boundary has been dynamically
compared.
The fixture runner also reports whether the requested legacy command reached
@@ -828,6 +828,109 @@ integration('general command alternative matrix', () => {
}, 120_000);
});
type GeneralPreReqBoundaryCase = {
name: string;
action: string;
actorPatch?: Record<string, unknown>;
expectedCommand: string;
expectedTerm: number;
expectedProgressText: string;
};
const generalPreReqBoundaryCases: GeneralPreReqBoundaryCase[] = [
{
name: 'battle preparation starts at term 1',
action: 'che_전투태세',
expectedCommand: '전투태세',
expectedTerm: 1,
expectedProgressText: '전투태세 수행중... (1/4)',
},
{
name: 'battle preparation advances term 1 to 2',
action: 'che_전투태세',
actorPatch: { lastTurn: { command: '전투태세', term: 1 } },
expectedCommand: '전투태세',
expectedTerm: 2,
expectedProgressText: '전투태세 수행중... (2/4)',
},
{
name: 'battle preparation advances term 2 to 3',
action: 'che_전투태세',
actorPatch: { lastTurn: { command: '전투태세', term: 2 } },
expectedCommand: '전투태세',
expectedTerm: 3,
expectedProgressText: '전투태세 수행중... (3/4)',
},
{
name: 'domestic trait reset starts at term 1 after another command',
action: 'che_내정특기초기화',
actorPatch: {
specialDomestic: 'che_인덕',
lastTurn: { command: '전투태세', term: 3 },
},
expectedCommand: '내정 특기 초기화',
expectedTerm: 1,
expectedProgressText: '새로운 적성을 찾는 중... (1/2)',
},
{
name: 'war trait reset starts at term 1',
action: 'che_전투특기초기화',
actorPatch: { specialWar: 'che_귀병' },
expectedCommand: '전투 특기 초기화',
expectedTerm: 1,
expectedProgressText: '새로운 적성을 찾는 중... (1/2)',
},
{
name: 'retirement starts at term 1 without applying retirement',
action: 'che_은퇴',
actorPatch: { age: 60 },
expectedCommand: '은퇴',
expectedTerm: 1,
expectedProgressText: '은퇴 수행중... (1/2)',
},
];
integration('general command pre-required turn boundary matrix', () => {
it.each(generalPreReqBoundaryCases)(
'$name matches legacy intermediate last-turn state without consuming command RNG',
async ({ action, actorPatch, expectedCommand, expectedTerm, expectedProgressText }) => {
const request = buildRequest(action, undefined, actorPatch);
request.setup!.world!.hiddenSeed = `general-prereq-${action}-${expectedTerm}`;
const reference = runReferenceTurnCommandTraceRequest(
workspaceRoot!,
request as unknown as Record<string, unknown>
);
const core = await runCoreTurnCommandTrace(request, reference.before);
const referenceActor = reference.after.generals.find((entry) => entry.id === 1);
const coreActor = core.after.generals.find((entry) => entry.id === 1);
expect(reference.execution.outcome).toMatchObject({ completed: false });
expect(core.execution.outcome).toMatchObject({
requestedAction: action,
actionKey: action,
usedFallback: false,
});
expect(referenceActor?.lastTurn).toMatchObject({
command: expectedCommand,
term: expectedTerm,
});
expect(coreActor?.lastTurn).toMatchObject({
command: expectedCommand,
term: expectedTerm,
});
expect(reference.after.logs.some((entry) => String(entry.text).includes(expectedProgressText))).toBe(true);
expect(core.after.logs.some((entry) => String(entry.text).includes(expectedProgressText))).toBe(true);
expect(core.rng).toEqual(reference.rng);
expect(
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
ignoredPathPatterns: ignoredLifecyclePaths,
})
).toEqual([]);
},
120_000
);
});
type GeneralConstraintCase = {
name: string;
action: string;