diff --git a/docs/architecture/general-command-differential-testing.md b/docs/architecture/general-command-differential-testing.md index e92518b..9c6c3c2 100644 --- a/docs/architecture/general-command-differential-testing.md +++ b/docs/architecture/general-command-differential-testing.md @@ -8,12 +8,15 @@ - 범위: 명령 결과, RNG 소비, 로그, 예약 턴 lifecycle, DB 영속화 현재 ref MariaDB와 core memory를 잇는 공통 runner, 성공 경로 55개, -실행 중 확률 실패 9개가 구현됐다. 실패 9개는 내정 critical +실행 중 확률 실패 9개와 full constraint fallback 7개가 구현됐다. +실패 9개는 내정 critical `주민선정/정착장려/상업투자/기술연구/물자조달`과 모략 `화계/선동/파괴/탈취`이며 RNG 전체 trace, semantic state delta와 실패 -로그 본문을 비교한다. 나머지 제약 실패·값 경계·alternative와 전체 -core PostgreSQL 재조회가 완료 기준을 통과하기 전까지 55개 명령 전체의 -동적 호환 상태를 `확인`으로 올리지 않는다. +로그 본문을 비교한다. 제약 7개는 무소속, 방랑국, 타국 도시, 보급 단절, +금·쌀 부족과 민심 상한을 대표하며 휴식 fallback과 RNG/state delta를 +비교한다. 나머지 명령별 제약 실패·값 경계·alternative와 전체 core +PostgreSQL 재조회가 완료 기준을 통과하기 전까지 55개 명령 전체의 동적 +호환 상태를 `확인`으로 올리지 않는다. ## 결정 요약 diff --git a/docs/architecture/turn-state-differential-testing.md b/docs/architecture/turn-state-differential-testing.md index 46eddba..ee3d389 100644 --- a/docs/architecture/turn-state-differential-testing.md +++ b/docs/architecture/turn-state-differential-testing.md @@ -179,8 +179,12 @@ responses pass a separate reference trace and core resolver/API boundary. Nine general in-action failure cases now also pass the common differential: five domestic critical failures and four sabotage failures. They compare the full command RNG trace, semantic state delta and the exact action-log body. -This is not yet a claim that the remaining constraint, clamp, alternative and -persistence boundaries have been dynamically compared. +Seven full-constraint cases cover neutral status, wandering nation, city +ownership, supply, gold, rice and trust-cap rejection. Both engines replace +the denied command with rest, consume the same RNG and produce the same +semantic delta. This is not yet a claim that every command-specific +constraint, clamp, alternative and persistence boundary has been dynamically +compared. The fixture runner also reports whether the requested legacy command reached its completed execution path. For multi-turn commands this is derived from the diff --git a/tools/integration-tests/test/turnCommandGeneralMatrix.integration.test.ts b/tools/integration-tests/test/turnCommandGeneralMatrix.integration.test.ts index 0c0a1f6..e61a01f 100644 --- a/tools/integration-tests/test/turnCommandGeneralMatrix.integration.test.ts +++ b/tools/integration-tests/test/turnCommandGeneralMatrix.integration.test.ts @@ -484,3 +484,81 @@ integration('general command in-action failure matrix', () => { 120_000 ); }); + +type GeneralConstraintCase = { + name: string; + action: string; + args?: Record; + actorPatch?: Record; + fixturePatches?: FixturePatches; +}; + +const constraintCases: GeneralConstraintCase[] = [ + { + name: 'neutral general', + action: 'che_훈련', + actorPatch: { nationId: 0, officerLevel: 0 }, + }, + { + name: 'wandering nation', + action: 'che_농지개간', + fixturePatches: { + nations: { 1: { level: 0, capitalCityId: 0, typeCode: 'None' } }, + }, + }, + { + name: 'city not occupied by actor nation', + action: 'che_농지개간', + fixturePatches: { cities: { 3: { nationId: 2 } } }, + }, + { + name: 'unsupplied city', + action: 'che_농지개간', + fixturePatches: { cities: { 3: { supplyState: 0 } } }, + }, + { + name: 'insufficient gold', + action: 'che_상업투자', + actorPatch: { gold: 0 }, + }, + { + name: 'insufficient rice', + action: 'che_주민선정', + actorPatch: { rice: 0 }, + }, + { + name: 'maximum city trust', + action: 'che_주민선정', + fixturePatches: { cities: { 3: { trust: 100 } } }, + }, +]; + +integration('general command full-constraint fallback matrix', () => { + it.each(constraintCases)( + '$name: $action falls back exactly like legacy', + async ({ action, args, actorPatch, fixturePatches }) => { + const request = buildRequest(action, args, actorPatch, fixturePatches); + request.setup!.world!.hiddenSeed = `general-constraint-${action}`; + const reference = runReferenceTurnCommandTraceRequest( + workspaceRoot!, + request as unknown as Record + ); + const core = await runCoreTurnCommandTrace(request, reference.before); + + expect(reference.execution.outcome).toMatchObject({ completed: false }); + expect(core.execution.outcome).toMatchObject({ + requestedAction: action, + actionKey: '휴식', + usedFallback: true, + }); + expect(core.execution.outcome).toHaveProperty('blockedReason'); + expect(core.rng).toEqual(reference.rng); + expect( + compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, { + ignoredPathPatterns: ignoredLifecyclePaths, + }) + ).toEqual([]); + }, + 120_000 + ); +});