From 7ed1b5df7f655eabf1ffade8389f4656b71ae74f Mon Sep 17 00:00:00 2001 From: hided62 Date: Sat, 25 Jul 2026 06:44:01 +0000 Subject: [PATCH] test: audit and strengthen legacy test suite --- app/game-api/test/battleSimProcessor.test.ts | 29 +- app/game-api/test/tournamentWorker.test.ts | 26 +- .../test/npcGeneralDomesticTurn.test.ts | 17 +- .../test/npcNationGrowthScenario.test.ts | 76 +---- app/game-engine/test/voteReward.test.ts | 13 +- docs/test-suite-audit.md | 152 +++++++++ docs/testing-policy.md | 7 + packages/common/test/clock.test.d.ts | 2 - packages/common/test/clock.test.d.ts.map | 1 - packages/common/test/clock.test.js | 44 --- packages/common/test/clock.test.js.map | 1 - packages/common/test/rng.test.d.ts | 2 - packages/common/test/rng.test.d.ts.map | 1 - packages/common/test/rng.test.js | 317 ------------------ packages/common/test/rng.test.js.map | 1 - packages/common/test/rng.test.ts | 72 ++-- packages/common/test/test-rngs.test.d.ts | 2 - packages/common/test/test-rngs.test.d.ts.map | 1 - packages/common/test/test-rngs.test.js | 65 ---- packages/common/test/test-rngs.test.js.map | 1 - .../logic/test/actions/turn/nation.test.ts | 9 +- packages/logic/test/scenarioParser.test.ts | 4 +- .../logic/test/scenarios/diplomacy.test.ts | 172 ++++++---- .../logic/test/scenarios/domestic.test.ts | 3 +- .../scenarios/general/che_NPC능동.test.ts | 18 +- packages/logic/test/scenarios/troops.test.ts | 7 +- packages/logic/test/testEnv.ts | 33 +- 27 files changed, 422 insertions(+), 654 deletions(-) create mode 100644 docs/test-suite-audit.md delete mode 100644 packages/common/test/clock.test.d.ts delete mode 100644 packages/common/test/clock.test.d.ts.map delete mode 100644 packages/common/test/clock.test.js delete mode 100644 packages/common/test/clock.test.js.map delete mode 100644 packages/common/test/rng.test.d.ts delete mode 100644 packages/common/test/rng.test.d.ts.map delete mode 100644 packages/common/test/rng.test.js delete mode 100644 packages/common/test/rng.test.js.map delete mode 100644 packages/common/test/test-rngs.test.d.ts delete mode 100644 packages/common/test/test-rngs.test.d.ts.map delete mode 100644 packages/common/test/test-rngs.test.js delete mode 100644 packages/common/test/test-rngs.test.js.map diff --git a/app/game-api/test/battleSimProcessor.test.ts b/app/game-api/test/battleSimProcessor.test.ts index 69648e6..97178e0 100644 --- a/app/game-api/test/battleSimProcessor.test.ts +++ b/app/game-api/test/battleSimProcessor.test.ts @@ -226,23 +226,36 @@ const buildPayload = (action: BattleSimJobPayload['action']): BattleSimJobPayloa }); describe('battle sim processor', () => { - it('simulates battles and returns summary', () => { + it('returns the fixed-seed battle summary instead of only a successful shape', () => { const payload = buildPayload('battle'); const result = processBattleSimJob(payload); - expect(result.result).toBe(true); - expect(result.reason).toBe('success'); - expect(result.lastWarLog).toBeTruthy(); - expect(result.phase).toBeTypeOf('number'); - expect(result.attackerRice).toBeTypeOf('number'); + expect(result).toMatchObject({ + result: true, + reason: 'success', + datetime: '2026-01-01 00:00:00', + avgWar: 1, + phase: 2, + killed: 625, + maxKilled: 625, + minKilled: 625, + dead: 1000, + maxDead: 1000, + minDead: 1000, + attackerRice: 65, + defenderRice: 83, + attackerSkills: { 부상: 1 }, + defendersSkills: [{ 회피시도: 1, 회피: 1 }], + }); + expect(result.lastWarLog?.generalActionLog).toContain('퇴각했습니다.'); }); - it('returns defender order for reorder action', () => { + it('returns the fixed defender ID order for reorder action', () => { const payload = buildPayload('reorder'); const result = processBattleSimJob(payload); expect(result.result).toBe(true); - expect(result.order?.length).toBe(1); + expect(result.order).toEqual([2]); }); it('executes crew trigger handlers in simulator battles', () => { diff --git a/app/game-api/test/tournamentWorker.test.ts b/app/game-api/test/tournamentWorker.test.ts index 120114c..11b8f62 100644 --- a/app/game-api/test/tournamentWorker.test.ts +++ b/app/game-api/test/tournamentWorker.test.ts @@ -5,12 +5,13 @@ import { createTournamentAutoStartHandler } from '@sammo-ts/common'; import { TournamentStore } from '../src/tournament/store.js'; import { buildTournamentKeys } from '../src/tournament/keys.js'; -import type { TournamentBetEntry, TournamentMatchEntry, TournamentParticipantEntry, TournamentState } from '../src/tournament/types.js'; -import { - applyBattle, - applyPreBattleStage, - settleTournamentOutcome, -} from '../src/tournament/worker.js'; +import type { + TournamentBetEntry, + TournamentMatchEntry, + TournamentParticipantEntry, + TournamentState, +} from '../src/tournament/types.js'; +import { applyBattle, applyPreBattleStage, settleTournamentOutcome } from '../src/tournament/worker.js'; import type { TurnDaemonTransport } from '../src/daemon/transport.js'; class MemoryRedis { @@ -208,7 +209,7 @@ const runTournamentToCompletion = async (options: { const delayTick = async (): Promise => new Promise((resolve) => setTimeout(resolve, 0)); describe('tournament worker (in-memory)', () => { - it('64명(상/중/하 스탯)으로 결승까지 진행된다', async () => { + it('64명 고정 seed 대진에서 15번 참가자가 결승을 이긴다', async () => { const redis = new MemoryRedis(); const store = new TournamentStore(redis, buildTournamentKeys('test')); const participants = createParticipants(16, 16, 32); @@ -219,10 +220,17 @@ describe('tournament worker (in-memory)', () => { const prisma = createPrismaMock({ baseSeed: 'seed' }); const finalState = await runTournamentToCompletion({ store, prisma, baseSeed: 'seed' }); const matches = await store.getMatches(); + const finalMatches = matches.filter((match) => match.stage === 10); expect(finalState.stage).toBe(0); - expect(finalState.winnerId).toBeDefined(); - expect(matches.some((match) => match.stage === 10 && match.winnerId)).toBe(true); + expect(finalState.winnerId).toBe(15); + expect(finalMatches).toHaveLength(1); + expect(finalMatches[0]).toMatchObject({ + attackerId: 15, + defenderId: 1, + winnerId: 15, + lastEnergy: { attacker: -90, defender: -92 }, + }); }); it('우승 결과에 따라 베팅 정산 명령이 생성된다', async () => { diff --git a/app/game-engine/test/npcGeneralDomesticTurn.test.ts b/app/game-engine/test/npcGeneralDomesticTurn.test.ts index ddf6ad4..a6e6948 100644 --- a/app/game-engine/test/npcGeneralDomesticTurn.test.ts +++ b/app/game-engine/test/npcGeneralDomesticTurn.test.ts @@ -100,7 +100,7 @@ const createMockPrisma = (initialGeneralRows: any[] = []) => { const addMinutes = (time: Date, minutes: number): Date => new Date(time.getTime() + minutes * 60_000); describe('NPC 일반 내정 턴', () => { - it('예약 턴이 없어도 내정 수치가 증가한다', async () => { + it('고정 seed에서 예약 턴 없이 치안 명령을 실행하고 다음 턴으로 이동한다', async () => { const generals: TurnGeneral[] = [ { id: 1, @@ -285,15 +285,10 @@ describe('NPC 일반 내정 턴', () => { }); const afterCity = world.getCityById(1)!; - const increased = - afterCity.population > beforeStats.population || - afterCity.agriculture > beforeStats.agriculture || - afterCity.commerce > beforeStats.commerce || - afterCity.security > beforeStats.security || - afterCity.defence > beforeStats.defence || - afterCity.wall > beforeStats.wall; - - expect(increased).toBe(true); - expect(world.getGeneralById(1)!.turnTime.getTime()).toBeGreaterThan(mockDate.getTime()); + expect(afterCity).toMatchObject({ + ...beforeStats, + security: 1050, + }); + expect(world.getGeneralById(1)!.turnTime.getTime()).toBe(addMinutes(mockDate, 10).getTime()); }); }); diff --git a/app/game-engine/test/npcNationGrowthScenario.test.ts b/app/game-engine/test/npcNationGrowthScenario.test.ts index 5a8d029..5984574 100644 --- a/app/game-engine/test/npcNationGrowthScenario.test.ts +++ b/app/game-engine/test/npcNationGrowthScenario.test.ts @@ -67,24 +67,14 @@ describe('NPC 대형 시뮬레이션', () => { for (let i = 0; i < npcPerType; i += 1) { const cityId = smallMediumCityIds[i % smallMediumCityIds.length]; generals.push( - createNpcGeneral( - generals.length + 1, - cityId, - { leadership: 75, strength: 75, intelligence: 10 }, - 2 - ) + createNpcGeneral(generals.length + 1, cityId, { leadership: 75, strength: 75, intelligence: 10 }, 2) ); } for (let i = 0; i < npcPerType; i += 1) { const cityId = smallMediumCityIds[i % smallMediumCityIds.length]; generals.push( - createNpcGeneral( - generals.length + 1, - cityId, - { leadership: 75, strength: 10, intelligence: 75 }, - 2 - ) + createNpcGeneral(generals.length + 1, cityId, { leadership: 75, strength: 10, intelligence: 75 }, 2) ); } @@ -275,7 +265,7 @@ describe('NPC 대형 시뮬레이션', () => { : ` ${trace.requestedAction} -> ${trace.actionKey}`; console.log( `- ${trace.year}-${String(trace.month).padStart(2, '0')} N${trace.nationId} G${trace.generalId} g${round(trace.gold)}/r${round(trace.rice)} c${trace.crew}/t${trace.train}/a${trace.atmos} ` + - `${trace.ok ? 'OK' : 'FAIL'} ${action}${extra}` + `${trace.ok ? 'OK' : 'FAIL'} ${action}${extra}` ); } }; @@ -302,9 +292,7 @@ describe('NPC 대형 시뮬레이션', () => { }; const assertSmallMediumCitiesFounded = () => { - const targetCities = world - .listCities() - .filter((city) => [4, 5].includes(city.level)); + const targetCities = world.listCities().filter((city) => [4, 5].includes(city.level)); for (const city of targetCities) { expect(city.nationId).toBeGreaterThan(0); } @@ -384,54 +372,13 @@ describe('NPC 대형 시뮬레이션', () => { expect(afterTotal).toBeGreaterThan(beforeTotal); }; - const assertDomesticGrowthBy = (year: number, month: number) => { + const assertDomesticGrowth = () => { const citiesNow = world.listCities().filter((city) => city.nationId > 0); for (const city of citiesNow) { const baseline = initialCityStats.get(city.id); if (!baseline) { continue; } - const delta = { - population: city.population - baseline.population, - agriculture: city.agriculture - baseline.agriculture, - commerce: city.commerce - baseline.commerce, - security: city.security - baseline.security, - defence: city.defence - baseline.defence, - wall: city.wall - baseline.wall, - }; - console.log('[DEBUG] domestic delta', { - year, - month, - cityId: city.id, - nationId: city.nationId, - delta, - }); - - const failures: string[] = []; - if (city.population <= baseline.population) failures.push('population'); - if (city.agriculture <= baseline.agriculture) failures.push('agriculture'); - if (city.commerce <= baseline.commerce) failures.push('commerce'); - if (city.security <= baseline.security) failures.push('security'); - if (city.defence <= baseline.defence) failures.push('defence'); - if (city.wall <= baseline.wall) failures.push('wall'); - if (failures.length > 0) { - console.log('[DEBUG] domestic not grown', { - year, - month, - cityId: city.id, - nationId: city.nationId, - failures, - baseline, - current: { - population: city.population, - agriculture: city.agriculture, - commerce: city.commerce, - security: city.security, - defence: city.defence, - wall: city.wall, - }, - }); - } // 182년 시점에는 징병이 병행되어 인구가 순감할 수 있으므로, 대규모 붕괴만 방지한다. expect(city.population).toBeGreaterThan(baseline.population - 15000); expect(city.agriculture).toBeGreaterThan(baseline.agriculture); @@ -446,15 +393,18 @@ describe('NPC 대형 시뮬레이션', () => { ['179-09', () => assertUprisingCount(1)], ['179-10', () => assertUprisingCount(2)], ['179-11', () => assertFoundedCount(1)], - ['179-12', () => { - assertTaxRateUnder(15); - maybeSnapshotGold(); - }], + [ + '179-12', + () => { + assertTaxRateUnder(15); + maybeSnapshotGold(); + }, + ], ['180-01', () => assertGoldIncome()], ['180-07', () => assertSmallMediumCitiesFounded()], ['180-11', () => assertCityTrust(90)], ['181-01', () => assertNationGeneralCount(10)], - ['182-01', () => assertDomesticGrowthBy(182, 1)], + ['182-01', () => assertDomesticGrowth()], ['182-10', () => assertNationRecruitCount(5)], ['183-01', () => assertWarReadiness(10, 70, 70)], ['183-02', () => assertDispatchRecorded(183, 1, 1)], diff --git a/app/game-engine/test/voteReward.test.ts b/app/game-engine/test/voteReward.test.ts index dfa1a6a..e713b6f 100644 --- a/app/game-engine/test/voteReward.test.ts +++ b/app/game-engine/test/voteReward.test.ts @@ -156,7 +156,10 @@ describe('voteReward command', () => { const itemRegistry = createItemModuleRegistry(await loadItemModules([...ITEM_KEYS])); const config = resolveUniqueConfig(snapshot.scenarioConfig.const as Record); - const occupied = countOccupiedUniqueItems(generals.map((general) => general.role.items), itemRegistry); + const occupied = countOccupiedUniqueItems( + generals.map((general) => general.role.items), + itemRegistry + ); const rng = new RandUtil(LiteHashDRBG.build(buildVoteUniqueSeed('seed', 1, 1))); const itemKey = rollUniqueLottery({ rng, @@ -199,7 +202,12 @@ describe('voteReward command', () => { expect(updated?.gold).toBe(1500); expect(updated?.role.items.weapon).toBe('che_무기_12_칠성검'); const meta = updated?.meta as Record; - expect(meta?.voteRewards).toBeTruthy(); + expect(meta.voteRewards).toMatchObject({ + 1: { + awarded: true, + itemKey: 'che_무기_12_칠성검', + }, + }); const diff = world.consumeDirtyState(); const logTexts = diff.logs.map((entry) => entry.text); @@ -211,6 +219,7 @@ describe('voteReward command', () => { throw new Error('voteReward second result missing'); } expect(second.alreadyApplied).toBe(true); + expect(second.itemKey).toBe('che_무기_12_칠성검'); const afterSecond = world.getGeneralById(1); expect(afterSecond?.gold).toBe(1500); }); diff --git a/docs/test-suite-audit.md b/docs/test-suite-audit.md new file mode 100644 index 0000000..8355089 --- /dev/null +++ b/docs/test-suite-audit.md @@ -0,0 +1,152 @@ +# Test Suite Audit + +## Baseline and method + +- Audit date: 2026-07-25 +- Code baseline: `main@46ae79dbe7a0fb64aff9bdcc76eadafd75e10c9e` +- Executed test sources: 66 TypeScript `*.test.ts` files +- Excluded from the source count: ignored `dist/` outputs and non-executable + fixtures/helpers +- Historical generated copies removed by this audit: + `packages/common/test/*.{js,js.map,d.ts,d.ts.map}` (12 files) + +Each suite was read together with its production entry point. Assertions were +classified by what they can actually establish: + +- `contract`: focused input/output, rejection, ordering, or side-effect + regression with concrete assertions. +- `compatibility`: expected values or traces are independently grounded in the + PHP reference implementation. +- `integration`: real database, Redis, process, or HTTP behavior; environment + gating must be reported as a skip rather than a pass. +- `smoke`: a broad scenario that proves only the stated liveness/invariant + bounds. It is useful, but is not compatibility evidence. + +`kept` means the suite's current scope and assertions match its stated role. +`corrected` means this audit changed a misleading, non-deterministic, empty, or +environment-dependent check. + +## Per-suite disposition + +### `packages/common` + +| Test source | Disposition | Layer | What it establishes | +| ------------------------ | ----------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `test/clock.test.ts` | kept | contract | Manual and stepped clock advancement plus invalid step rejection. | +| `test/rng.test.ts` | corrected | compatibility | PHP/Python SHA-512 vectors, byte/bit consumption, integer and weighted selection, explicit boundary outputs, and a genuinely expanded long seed. Removed three assertion-free checks. | +| `test/test-rngs.test.ts` | kept | contract | Exact output sequences for deterministic test RNG implementations. | + +### `app/gateway-api` + +| Test source | Disposition | Layer | What it establishes | +| ------------------------------- | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `test/authFlow.test.ts` | kept | contract | OAuth-backed registration, gateway session issuance, encrypted game token issuance, and successful validation through the router. Token storage/Redis TTL remains integration scope. | +| `test/orchestratorPlan.test.ts` | kept | contract | Exact start/stop/no-op reconciliation decisions for every profile status represented by the planner. | +| `test/scenarioCatalog.test.ts` | kept | contract | Git ref resolution and preview loading from the checked-out repository. | + +### `app/game-api` + +| Test source | Disposition | Layer | What it establishes | +| --------------------------------------------- | ----------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `test/battleSimProcessor.test.ts` | corrected | contract + compatibility support | Fixed-seed numeric battle summary, skill activation, exact defender order, and unknown crew handler rejection. Detailed PHP parity remains in `battleDifferential.test.ts`. | +| `test/battleSimRouter.test.ts` | kept | contract | Queued-to-completed transport state flow through the battle router. | +| `test/commandTable.test.ts` | kept | contract | Minimum constraints, rather than full command constraints, determine command-table availability. This is intentionally a focused positive regression. | +| `test/idempotentTransport.test.ts` | kept | contract | Stable ordered child request IDs derived from one API input event. | +| `test/inputEventBoundary.integration.test.ts` | kept | integration | PostgreSQL atomic commit/rollback, retry, concurrent duplicate rejection, and child payload conflict. Explicitly skipped without `INPUT_EVENT_DATABASE_URL`. | +| `test/realtimeSse.test.ts` | kept | contract | Exact SSE framing, multiline encoding, event parsing rejection, and profile channel namespace. | +| `test/reservedTurns.test.ts` | kept | contract | General/nation queue padding and shift direction. Database constraints are outside this in-memory fake. | +| `test/router.test.ts` | kept | contract | Turn-daemon dispatch and serialization of world/status/reserved-turn responses through tRPC callers. | +| `test/streamKeys.test.ts` | kept | contract | Exact Redis stream namespace contract. | +| `test/tournamentWorker.test.ts` | corrected | contract | Fixed-seed bracket winner and final energies, payout command data, auto-join filtering, NPC/dummy fill, and completion. Replaced winner-exists-only coverage for the fixed bracket. | + +### `app/game-engine` + +| Test source | Disposition | Layer | What it establishes | +| ----------------------------------------------- | ----------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `test/crewTypeExecution.test.ts` | kept | compatibility | Crew action router ordering and legacy NPC arm-type weighting. | +| `test/databaseCommandQueue.integration.test.ts` | kept | integration | PostgreSQL single claim across consumers, persisted result, and expired-versus-active lease recovery. Explicitly skipped without a DB URL. | +| `test/inputEventAtomicity.test.ts` | kept | contract | Dirty-state retention, mutation/commit/respond ordering, and pause/no-ack behavior on handler or commit failure. | +| `test/nationCollapseOnConquest.test.ts` | kept | smoke | Last-city conquest removes the nation and neutralizes its general. It is a focused engine scenario, not full battle parity. | +| `test/nationTurnCompatibility.test.ts` | kept | compatibility | Legacy-backed 12-turn research accumulation and completion, diplomacy-message emission, and exact monthly strategy/diplomacy limit decay through the engine harness. | +| `test/npcGeneralDomesticTurn.test.ts` | corrected | contract | Fixed seed chooses the security command, applies exactly `+50`, leaves other city values unchanged, and advances exactly one tick. | +| `test/npcNationGrowthScenario.test.ts` | corrected | smoke | Long-running NPC growth invariants and collapse guards. Broad thresholds remain intentional smoke bounds; unconditional diagnostic output was removed. | +| `test/npcNationTechResearch.test.ts` | kept | smoke | Long-running monotonic tech growth and higher later recruitment cost. | +| `test/npcNationUprisingUnification.test.ts` | kept | smoke | Long-running founding, conquest, nation-count monotonicity, and unification terminal state. | +| `test/npcNationWarDeclaration.test.ts` | kept | smoke | Declaration, war transition, preparation, conquest, unification, and dispatch occurrence in one end-to-end NPC scenario. | +| `test/npcWarPrepTurns.test.ts` | kept | smoke | Training/morale actions occur in the named months and leave battle-ready generals. | +| `test/reservedTurnExecution.test.ts` | kept | smoke | Multi-command engine application, invalid-argument and constraint fallbacks, uprising/founding transitions, and named failure constraints. Its large workflow scope is recorded as smoke rather than a unit test. | +| `test/scenarioSeeder.test.ts` | kept | integration | Real schema row counts, diplomacy symmetry, and persisted install options. Reported as skipped when required tables are unavailable. | +| `test/turnDaemonLifecycle.test.ts` | kept | contract | Queue-front versus scheduled-boundary selection and exact processor checkpoint arguments. | +| `test/turnOrder.test.ts` | kept | contract | Stable `turnTime`, then ID, ordering independent of insertion order. | +| `test/uniqueLotteryCommand.test.ts` | kept | contract | Fixed-seed eligible command awards the expected unique item and item log. | +| `test/voteReward.test.ts` | corrected | contract | Gold, exact unique item, persisted reward metadata, log, and idempotent second application. | + +### `packages/logic` + +| Test source | Disposition | Layer | What it establishes | +| -------------------------------------------------------- | ----------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `test/actions/instant/nationStopWar.test.ts` | kept | contract | Invalid diplomacy rejection and symmetric neutral diplomacy patches. | +| `test/actions/turn/executionHelper.test.ts` | kept | contract | Successful resolution, fallback loading/log accumulation, and fallback loop termination. | +| `test/actions/turn/nation.test.ts` | corrected | compatibility + contract | Declaration effects/logs, non-neighbor denial, capital/name/city effects, and exact legacy `필사즉생` gains (`+15`, train/morale 100, global delay 9). | +| `test/actions/turn/nationMissing.test.ts` | kept | compatibility + contract | Legacy-backed rejection, message, cooldown, assistance accumulator, and state/resource/meta effects for represented migrated nation commands. Exact full PHP snapshots remain outside this suite. | +| `test/crewType.test.ts` | kept | contract | Unit-set normalization and tech/region/city availability requirements. | +| `test/crewTypeExecution.test.ts` | kept | compatibility | Shipped catalog compilation, unknown handlers, crew action routing, legacy triggers, and numeric pick-score formula. | +| `test/diplomacy.test.ts` | kept | compatibility | Exact monthly declaration/war/non-aggression transitions, casualty extension, and reset behavior. | +| `test/dispatchWarAction.test.ts` | kept | contract | Dispatch emits battle logs and patches for defender general, city, and diplomacy casualty state. Numeric battle parity is delegated to the differential suite. | +| `test/itemInventory.test.ts` | kept | contract | Legacy-slot upgrade, frozen-snapshot reads, exact consumable charge depletion, final unequip, and persisted metadata round-trip for canonical item instances. | +| `test/message.test.ts` | kept | contract | Private sender/receiver copies, national/public single-copy rules, and sender action sanitization. | +| `test/scenarioParser.test.ts` | corrected | contract | Defaults and representative legacy-format rows from tracked `resources/scenario`; removed dependence on an untracked local `legacy/` directory. | +| `test/scenarios/blankStart.test.ts` | kept | smoke | Uprising-to-appointment-to-founding workflow and named founding rejections. | +| `test/scenarios/diplomacy.test.ts` | corrected | contract | Declaration is denied for deployment, then the identical command becomes allowed only after transition to WAR. Removed the previous false check that inspected an always-empty troop list while bypassing constraints. | +| `test/scenarios/domestic.test.ts` | corrected | contract | Representative domestic action state changes and capacity behavior, including the exact `+100` agriculture effect, through the scenario harness. | +| `test/scenarios/general/che_NPC능동.test.ts` | corrected | contract | NPC teleport, non-NPC resolver rejection, and structural argument parsing. Replaced an empty always-passing test. | +| `test/scenarios/general/che_강행.test.ts` | kept | contract | Exact move/cost/experience effects, far-city denial, and wandering subordinate movement. | +| `test/scenarios/general/che_귀환.test.ts` | kept | contract | Capital/officer-city return effects and named location constraints. | +| `test/scenarios/general/che_등용수락.test.ts` | kept | contract | Neutral acceptance, betrayal resource/penalty effects, and monarch/same-nation denials. | +| `test/scenarios/general/che_이동.test.ts` | kept | contract | Exact movement, gold, morale, experience, and leadership-experience effects. | +| `test/scenarios/general/migratedGeneralCommands.test.ts` | kept | contract | Focused state transitions for mutiny, abdication, gift, disband, founding, and duplicate equipment purchase. | +| `test/scenarios/general_commands_new.test.ts` | kept | smoke | Multi-command resolver workflow for procurement/donation/status and employment/sabotage families, including exact one-use strategy-item consumption. Other broad assertions remain workflow smoke only. | +| `test/scenarios/reservedTurnExecution.test.ts` | kept | smoke | Multiple generals consume queued commands over three ticks and produce representative city/general changes. | +| `test/scenarios/troops.test.ts` | corrected | contract | Draft, train, and morale workflow with exact crew, training, and morale results through three commands. | +| `test/specialActions.test.ts` | kept | contract | Trait loading plus exact domestic/stat/heal/battle modifiers. | +| `test/uniqueLottery.test.ts` | kept | contract | Fixed-seed item, eligible acquisition types, founding guarantee, and occupied-item counting. | +| `test/warAftermath.test.ts` | kept | contract | Exact tech/death deltas and conquest/collapse resource/general/city effects. | +| `test/warEngine.test.ts` | kept | contract | Critical trigger multiplier, no-rice defender rout, and exact multi-use/one-use battle-item charge persistence. Detailed phase/RNG parity is differential-test scope. | +| `test/world/distance.test.ts` | kept | contract | Exact shortest distances, range search, and unreachable city handling. | +| `test/worldBootstrap.test.ts` | kept | contract | Scenario/map conversion into exact snapshot and seed fields. | + +### `tools/integration-tests` + +| Test source | Disposition | Layer | What it establishes | +| --------------------------------- | ----------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `test/auctionFlow.test.ts` | kept | integration | Real API/PostgreSQL/Redis/daemon resource and unique-auction bidding, extension, finalization, payout, and ownership constraints. | +| `test/battleDifferential.test.ts` | kept | compatibility | PHP reference metadata and attacker/defender comparisons for all 145 scenario items, plus event order, RNG results, phase values, multi-defender, siege, and no-defender branches. The two exhaustive item cases currently fail on 13 attacker and 2 defender items; this is active compatibility evidence, not a green regression. | +| `test/initialization.test.ts` | kept | integration | Real gateway/game APIs, database/Redis reset, scenario seed, users, joins, turns, and founding state. | +| `test/orchestrator.e2e.test.ts` | kept | integration | Real PM2 game API/daemon startup, command serving, SSE completion, and persisted world update. | + +## Shared test support + +The following support files were also reviewed because failures in them can +invalidate many suites: + +| Support source | Result | +| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| `packages/logic/test/testEnv.ts` | corrected: replaced `Math.random()` with a complete fixed-seed `RandUtil`; missing general IDs now fail setup instead of silently skipping a command. | +| `packages/logic/test/fixtures/minimalMap.ts` | kept: small connected/unreachable map fixture used by focused command tests. | +| `app/game-engine/test/fixtures/largeTestMap.ts` | kept: broad NPC smoke fixture; not a legacy map snapshot. | +| `app/game-engine/test/helpers/turnTestHarness.ts` | kept: engine smoke harness; assertions remain in individual suites. | + +## Trust boundary after this audit + +This audit establishes that every executable test has a stated, non-empty +purpose and that broad simulations are labeled as smoke rather than silently +treated as exact compatibility proof. It does not promote all product behavior +to legacy-compatible: + +- battle compatibility is supported only by the differential fixtures and the + suites explicitly marked `compatibility`; +- broad NPC and multi-command scenarios guard liveness/invariants, not exact + monthly balance or complete side effects; +- DB/Redis/PM2 claims require their integration suites to run rather than skip; +- changing an implementation and observing a green unit suite is still not a + substitute for a new PHP trace when compatibility-sensitive behavior changes. diff --git a/docs/testing-policy.md b/docs/testing-policy.md index 5748bff..cf860ee 100644 --- a/docs/testing-policy.md +++ b/docs/testing-policy.md @@ -39,6 +39,13 @@ An audit must confirm that a test: Until this audit is recorded, test counts and green status are inventory information only, not evidence of correctness. +The executable TypeScript suite was reviewed on 2026-07-25. Its per-suite +disposition, intended scope, and evidence limits are recorded in +[`test-suite-audit.md`](./test-suite-audit.md). A `kept` or `corrected` +disposition means the test has useful regression value at the recorded layer; +it does not promote smoke or contract coverage into legacy-compatibility +evidence. New suites and material scope changes must update that registry. + ## Core Principles - Prefer Repository/DB Port interfaces over ORM mocks; split implementations: diff --git a/packages/common/test/clock.test.d.ts b/packages/common/test/clock.test.d.ts deleted file mode 100644 index f9063a6..0000000 --- a/packages/common/test/clock.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=clock.test.d.ts.map diff --git a/packages/common/test/clock.test.d.ts.map b/packages/common/test/clock.test.d.ts.map deleted file mode 100644 index 62d0533..0000000 --- a/packages/common/test/clock.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"clock.test.d.ts","sourceRoot":"","sources":["clock.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/packages/common/test/clock.test.js b/packages/common/test/clock.test.js deleted file mode 100644 index 008ff7a..0000000 --- a/packages/common/test/clock.test.js +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { ManualClock, StepClock } from '../src/time/Clock.js'; -describe('ManualClock', () => { - it('returns current time without advancing', () => { - const clock = new ManualClock(1000); - expect(clock.nowMs()).toBe(1000); - expect(clock.nowMs()).toBe(1000); - }); - it('advances with sleep and manual advance', async () => { - const clock = new ManualClock(0); - await clock.sleepMs(250); - expect(clock.nowMs()).toBe(250); - clock.advanceMs(750); - expect(clock.nowMs()).toBe(1000); - }); - it('can set time explicitly', () => { - const clock = new ManualClock(10); - clock.setMs(5000); - expect(clock.nowMs()).toBe(5000); - }); -}); -describe('StepClock', () => { - it('advances on each nowMs call', () => { - const clock = new StepClock(100, 0); - expect(clock.nowMs()).toBe(100); - expect(clock.nowMs()).toBe(200); - expect(clock.nowMs()).toBe(300); - }); - it('advances with sleep', async () => { - const clock = new StepClock(50, 1000); - await clock.sleepMs(200); - expect(clock.nowMs()).toBe(1250); - }); - it('advances manually', () => { - const clock = new StepClock(10, 0); - clock.advanceMs(50); - expect(clock.nowMs()).toBe(60); - }); - it('rejects non-positive step', () => { - expect(() => new StepClock(0)).toThrow('stepMs must be positive'); - expect(() => new StepClock(-5)).toThrow('stepMs must be positive'); - }); -}); -//# sourceMappingURL=clock.test.js.map diff --git a/packages/common/test/clock.test.js.map b/packages/common/test/clock.test.js.map deleted file mode 100644 index a04001a..0000000 --- a/packages/common/test/clock.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"clock.test.js","sourceRoot":"","sources":["clock.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/common/test/rng.test.d.ts b/packages/common/test/rng.test.d.ts deleted file mode 100644 index c32592e..0000000 --- a/packages/common/test/rng.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=rng.test.d.ts.map diff --git a/packages/common/test/rng.test.d.ts.map b/packages/common/test/rng.test.d.ts.map deleted file mode 100644 index 230e2fa..0000000 --- a/packages/common/test/rng.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"rng.test.d.ts","sourceRoot":"","sources":["rng.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/packages/common/test/rng.test.js b/packages/common/test/rng.test.js deleted file mode 100644 index de7894e..0000000 --- a/packages/common/test/rng.test.js +++ /dev/null @@ -1,317 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - bufferByteSize, - convertBytesLikeToArrayBuffer, - convertBytesLikeToUint8Array as toBytes, - LiteHashDRBG, - RandUtil, -} from '../src/index.js'; -const range = (count) => Array.from({ length: count }, (_, idx) => idx); -const expectBytes = (actual, expected) => { - expect(Array.from(actual)).toEqual(Array.from(expected)); -}; -function fillBlock(body, filler = '\0', length = bufferByteSize) { - const u8Body = toBytes(body); - const u8Filler = toBytes(filler, false); - if (u8Filler.byteLength < 1) { - throw new Error('filler must have length'); - } - const buffer = new Uint8Array(length); - buffer.set(u8Body, 0); - let bufferIdx = u8Body.byteLength; - while (bufferIdx + u8Filler.byteLength < length) { - buffer.set(u8Filler, bufferIdx); - bufferIdx += u8Filler.byteLength; - } - if (bufferIdx < length) { - const slice = new Uint8Array(u8Filler.buffer, u8Filler.byteOffset, length - bufferIdx); - buffer.set(slice, bufferIdx); - } - return buffer; -} -class DummyBlockRNG extends LiteHashDRBG { - repeatBlockCnt; - repeatBlock; - constructor(repeatBlock, stateIdx = 0) { - super('x'); - this.repeatBlock = []; - for (const rawBlock of repeatBlock) { - const block = convertBytesLikeToArrayBuffer(rawBlock); - if (block.byteLength !== bufferByteSize) { - throw new Error('invalid block size'); - } - this.repeatBlock.push(block); - } - this.repeatBlockCnt = this.repeatBlock.length; - this.stateIdx = stateIdx; - this.bufferIdx = 0; - this.genNextBlock(); - } - genNextBlock() { - if (!this.repeatBlock) { - return; - } - this.buffer = this.repeatBlock[this.stateIdx]; - this.bufferIdx = 0; - this.stateIdx = (this.stateIdx + 1) % this.repeatBlockCnt; - } -} -const fixedKey = 'HelloWorld'; -describe('RNGtestDummy', () => { - const rng = new DummyBlockRNG([fillBlock('', '\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff')]); - it('BasicConvert', () => { - expect(toBytes('\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff', false).length).toBe(16); - }); - it('SimpleByte', () => { - expectBytes(toBytes('\x00', false), rng.nextBytes(1)); - expectBytes(toBytes('\x11\x22', false), rng.nextBytes(2)); - expectBytes(toBytes('\x33\x44\x55', false), rng.nextBytes(3)); - expectBytes(toBytes('\x66\x77\x88\x99', false), rng.nextBytes(4)); - }); - it('OverflowBlock', () => { - for (let idx = 0; idx < 16; idx += 1) { - expectBytes( - toBytes('\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99', false), - rng.nextBytes(16) - ); - } - }); - it('MultiBlock', () => { - expectBytes( - fillBlock('', '\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99', bufferByteSize * 2), - rng.nextBytes(bufferByteSize * 2) - ); - }); - it('bitTest', () => { - expectBytes(toBytes('\x00', false), rng.nextBits(1)); //aa - expectBytes(toBytes('\x01', false), rng.nextBits(1)); //bb - expectBytes(toBytes('\xcc', false), rng.nextBits(8)); //cc - expectBytes(toBytes('\xdd\x02', false), rng.nextBits(10)); //ddee - expectBytes(toBytes('\x7f', false), rng.nextBits(7)); //ff - expectBytes(toBytes('\x00\x11\x22\x33\x44\x55\x06', false), rng.nextBits(53)); - }); - it('int', () => { - expect(rng.nextInt(0xff)).toBe(0x77); - expect(rng.nextInt((1 << 16) - 1)).toBe(0x9988); - expect(rng.nextInt(0xffffffff)).toBe(0xddccbbaa); - expect(rng.nextInt()).toBe(0x0433221100ffee); - expect(rng.nextInt(0x0f)).toBe(0x05); //55 - expect(rng.nextInt(0x12)).toBe(0x06); //66 - expect(rng.nextInt(99)).toBe(0x08); //77(119 -> 7bit) -> 88(136 -> 8bit -> 8) - expect(rng.nextInt(0x99)).toBe(0x99); //99 - expect(rng.nextInt(0xaa)).toBe(0xaa); //aa (fit Max) - }); - it('float', () => { - const floatMax = 2 ** 53; - const fa = rng.nextFloat1(); - expect(fa).toBe(0x1100ffeeddccbb / floatMax); - expect(0.5313720384 > fa).toBe(true); - expect(0.5313720383 < fa).toBe(true); - const fb = rng.nextFloat1(); - expect(fb).toBe(0x08776655443322 / floatMax); - }); -}); -describe('RandUtilDummy', () => { - it('shuffle', () => { - const rng = new DummyBlockRNG([fillBlock('', '\x17\x16\x15\x14\x13\x12\x11\x10')]); - const randUtil = new RandUtil(rng); - /** - * 7, [7,1,2,3,4,5,6,0] - * 6, [7,0,2,3,4,5,6,1] - * 5, [7,0,1,3,4,5,6,2] - * 4, [7,0,1,2,4,5,6,3] - * 3, [7,0,1,2,3,5,6,4] - * 2, [7,0,1,2,3,4,6,5] - * 1, [7,0,1,2,3,4,5,6] - */ - expect(randUtil.shuffle(range(8))).toEqual([7, 0, 1, 2, 3, 4, 5, 6]); - /** - * 0, [0,1,2,3,4,5,6,7,8,9] - * 7, [0,8,2,3,4,5,6,7,1,9] - * 6, [0,8,1,3,4,5,6,7,2,9] - * 5, [0,8,1,2,4,5,6,7,3,9] - * 4, [0,8,1,2,3,5,6,7,4,9] - * 3, [0,8,1,2,3,4,6,7,5,9] - * 2, [0,8,1,2,3,4,5,7,6,9] - * 1, [0,8,1,2,3,4,5,6,7,9] - * 0, [0,8,1,2,3,4,5,6,7,9] - */ - expect(randUtil.shuffle(range(10))).toEqual([0, 8, 1, 2, 3, 4, 5, 6, 7, 9]); - }); - const rng = new DummyBlockRNG([fillBlock('', '\x17\x16\x15\x14\x13\x12\x11\x10')]); - const randUtil = new RandUtil(rng); - it('choice', () => { - //0x17(7), 0x16(6) - expect(randUtil.choice([0, 1, 2, 3, 4, 5])).toBe(5); - //0x15(5), Set 순서 유지 - expect(randUtil.choice(new Set([5, 3, 1, 2, 8, 0]))).toBe(8); - //0x14(4), 정렬 순서상 숫자(소-대) > 문자열(삽입순) > 심볼 순서 - expect(randUtil.choice({ c: 'c', a: 'a', b: 'b', 4: 'x', 2: 't', 3: 'q' })).toBe('c'); - }); - it('choiceUsingWeight', () => { - //0.6275740099377194 * 38.1 = 23.91 - expect( - randUtil.choiceUsingWeight({ - a: 0.1, - b: 10, - tt: 2, - x: -1, - c: 20, - d: 0, - e: 6, - }) - ).toBe('c'); - //0.658946544056166 - expect(randUtil.choiceUsingWeightPair([['xx', 10]])).toBe('xx'); - //0.6903152783785083 * 27.3 = 18.84560709973328 - expect( - randUtil.choiceUsingWeightPair([ - ['e', 10], - ['d', 4], - ['c', 0.1], - ['baba', 0.2], - ['q', 9], - ['xt', 4], - ]) - ).toBe('q'); - }); -}); -describe('RNGexpectedError', () => { - const rng = new LiteHashDRBG(fixedKey); - it('nextBits0', () => { - expect(() => rng.nextBits(0)).toThrow(); - }); - it('nextBits-1', () => { - expect(() => rng.nextBits(-1)).toThrow(); - }); - it('nextBytes0', () => { - expect(() => rng.nextBytes(0)).toThrow(); - }); - it('nextBytes-1', () => { - expect(() => rng.nextBytes(-1)).toThrow(); - }); - const randUtil = new RandUtil(rng); - it('utilEmptyChoice', () => { - expect(() => randUtil.choice([])).toThrow(); - }); - it('utilEmptyChoiceUsingWeight', () => { - expect(() => randUtil.choiceUsingWeight({})).toThrow(); - }); - it('utilEmptyChoiceUsingWeightPair', () => { - expect(() => randUtil.choiceUsingWeightPair([])).toThrow(); - }); -}); -describe('RNGAcceptable', () => { - const rng = new LiteHashDRBG(fixedKey); - it('RNG', () => { - rng.nextInt(0); - rng.nextInt(2 ** 53 - 1); - rng.nextBytes(65); - rng.nextBits(512); - }); - const randUtil = new RandUtil(rng); - it('RandUtil', () => { - randUtil.choice([0, 0, 0]); - randUtil.choiceUsingWeight({ - 0: 0, - 1: -1, - }); - randUtil.choiceUsingWeightPair([ - [0, 0], - [1, 0], - [2, -2], - ]); - randUtil.nextBool(1.1); - randUtil.nextBool(-0.1); - randUtil.shuffle([]); - randUtil.shuffle([1]); - randUtil.nextRange(0, 0); - randUtil.nextRangeInt(0, 0); - randUtil.nextRange(1, -1); - randUtil.nextRangeInt(1, -1); - }); - it('RNGLong', () => { - const longKey = fixedKey; - for (let idx = 0; idx < 8; idx += 1) { - longKey.concat(longKey); - } - const rngLong = new LiteHashDRBG(longKey); - for (let idx = 0; idx < 10; idx += 1) { - rngLong.nextBytes(16); - } - }); -}); -/* Python TestVector -import hashlib -import struct - -fixedKey = 'HelloWorld'.encode('utf-8') - -def hash(key, idx): - idxV = struct.pack(" { - //JS - PHP 일치 확인 정도로. - const testVector = Buffer.from( - [ - '24d9ccd648556255fd0ee9f5b29918de90617341958b3b354d572167e4dee02b757816a2bbe0b502c52413ffd384381a9d7b4e193df6f4345d6a95e111d661c4', - '2e9264512f6f4b080cf1376b74fab6878ecf4a6e185942d2e5b22cf923885b9952d40601a414225d6901417fd4ce9368ac77e4a63d3fc9b58ab952bb8c33f165', - '8e2ebf5af6283a1b18f4c044c86c20d02be3890613c4cc8b7c6b7b35581263b972a82630df69a9289988422d7c3a9be5edf78d5de16fabd01e5dd4e458068d8a', - '398596047ba547bfe371ec863a3e019ab0dbc4bb3b27e9077685aae4283ff6bbccfd981d92f9358f7efffbb72a940414802d98466d132e2ad0a16a12946d5f47', - 'b3606fe9b18c4aa7315e78bb9e47cb51cc4e203fcc2e631f0405c1b872c8e1cb5b6415ea74bbb77fffaaadb002b47cb4f4628dc0709634365b187667f5c708cb', - ].join(''), - 'hex' - ); - it('bytes', () => { - const rng = new LiteHashDRBG(fixedKey); - let offset = 0; - expectBytes(rng.nextBytes(10), testVector.slice(offset, offset + 10)); - offset += 10; - expectBytes(rng.nextBytes(32), testVector.slice(offset, offset + 32)); - offset += 32; - expectBytes(rng.nextBytes(1), testVector.slice(offset, offset + 1)); - offset += 1; - expectBytes(rng.nextBytes(64), testVector.slice(offset, offset + 64)); - offset += 64; - expectBytes(rng.nextBytes(5), testVector.slice(offset, offset + 5)); - offset += 5; - const lastA = rng.nextBytes(16, 18); - const lastB = new Uint8Array(18); - lastB.set(testVector.slice(offset, offset + 16)); - expectBytes(lastA, lastB); - }); - it('bits', () => { - const rng = new LiteHashDRBG(fixedKey); - let offset = 0; - const testBits = [10, 4, 15, 32, 7, 99, 512, 1, 2, 3]; - for (const bits of testBits) { - const bytes = Math.ceil(bits / 8); - const A = rng.nextBits(bits); - const B = new Uint8Array(testVector.slice(offset, offset + bytes)); - offset += bytes; - if (bits % 8 !== 0) { - const bitMask = 0xff >> (8 - (bits % 8)); - B[bytes - 1] &= bitMask; - } - expectBytes(A, B); - } - }); - it('float', () => { - const rng = new LiteHashDRBG(fixedKey); - const rng2 = new DummyBlockRNG([ - new Uint8Array(testVector.slice(bufferByteSize * 0, bufferByteSize * 1)), - new Uint8Array(testVector.slice(bufferByteSize * 1, bufferByteSize * 2)), - new Uint8Array(testVector.slice(bufferByteSize * 2, bufferByteSize * 3)), - new Uint8Array(testVector.slice(bufferByteSize * 3, bufferByteSize * 4)), - new Uint8Array(testVector.slice(bufferByteSize * 4, bufferByteSize * 5)), - ]); - for (let i = 0; i < 18; i++) { - expect(rng.nextFloat1()).toBe(rng2.nextFloat1()); - } - }); -}); -//# sourceMappingURL=rng.test.js.map diff --git a/packages/common/test/rng.test.js.map b/packages/common/test/rng.test.js.map deleted file mode 100644 index e627561..0000000 --- a/packages/common/test/rng.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"rng.test.js","sourceRoot":"","sources":["rng.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/common/test/rng.test.ts b/packages/common/test/rng.test.ts index 2f0f3e0..203ae9a 100644 --- a/packages/common/test/rng.test.ts +++ b/packages/common/test/rng.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import { bufferByteSize, + ConstantRNG, convertBytesLikeToArrayBuffer, convertBytesLikeToUint8Array as toBytes, LiteHashDRBG, @@ -238,46 +239,51 @@ describe('RNGexpectedError', () => { }); }); -describe('RNGAcceptable', () => { - const rng = new LiteHashDRBG(fixedKey); - it('RNG', () => { - rng.nextInt(0); - rng.nextInt(2 ** 53 - 1); - rng.nextBytes(65); - rng.nextBits(512); +describe('RNG boundary behavior', () => { + it('returns values at supported zero/max/large-buffer boundaries', () => { + const rng = new LiteHashDRBG(fixedKey); + + expect(rng.nextInt(0)).toBe(0); + expect(rng.nextInt(2 ** 53 - 1)).toBe(656_721_283_176_740); + expect(rng.nextBytes(65)).toHaveLength(65); + expect(rng.nextBits(512)).toHaveLength(64); }); - const randUtil = new RandUtil(rng); - it('RandUtil', () => { - randUtil.choice([0, 0, 0]); - randUtil.choiceUsingWeight({ - 0: 0, - 1: -1, - }); - randUtil.choiceUsingWeightPair([ - [0, 0], - [1, 0], - [2, -2], - ]); - randUtil.nextBool(1.1); - randUtil.nextBool(-0.1); - randUtil.shuffle([]); - randUtil.shuffle([1]); - randUtil.nextRange(0, 0); - randUtil.nextRangeInt(0, 0); - randUtil.nextRange(1, -1); - randUtil.nextRangeInt(1, -1); + it('defines deterministic behavior for degenerate utility inputs', () => { + const minRand = new RandUtil(new ConstantRNG(0)); + const maxRand = new RandUtil(new ConstantRNG(1)); + + expect(minRand.choice([0, 0, 0])).toBe(0); + expect(minRand.choiceUsingWeight({ 0: 0, 1: -1 })).toBe('0'); + expect( + minRand.choiceUsingWeightPair([ + [0, 0], + [1, 0], + [2, -2], + ]) + ).toBe(0); + expect(minRand.nextBool(1.1)).toBe(true); + expect(minRand.nextBool(-0.1)).toBe(false); + expect(minRand.shuffle([])).toEqual([]); + expect(minRand.shuffle([1])).toEqual([1]); + expect(minRand.nextRange(0, 0)).toBe(0); + expect(minRand.nextRangeInt(0, 0)).toBe(0); + expect(minRand.nextRange(1, -1)).toBe(1); + expect(minRand.nextRangeInt(1, -1)).toBe(1); + expect(maxRand.nextRange(1, -1)).toBe(-1); + expect(maxRand.nextRangeInt(1, -1)).toBe(-1); }); - it('RNGLong', () => { - const longKey = fixedKey; + it('hashes the complete expanded long seed', () => { + let longKey = fixedKey; for (let idx = 0; idx < 8; idx += 1) { - longKey.concat(longKey); + longKey += longKey; } + const rngLong = new LiteHashDRBG(longKey); - for (let idx = 0; idx < 10; idx += 1) { - rngLong.nextBytes(16); - } + expect(Buffer.from(rngLong.nextBytes(32)).toString('hex')).toBe( + '7e44b8180c0d254759a70aee891a5f6ba45c8016e02cb8ee51f0b22e600a4dd1' + ); }); }); diff --git a/packages/common/test/test-rngs.test.d.ts b/packages/common/test/test-rngs.test.d.ts deleted file mode 100644 index b82e41a..0000000 --- a/packages/common/test/test-rngs.test.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=test-rngs.test.d.ts.map diff --git a/packages/common/test/test-rngs.test.d.ts.map b/packages/common/test/test-rngs.test.d.ts.map deleted file mode 100644 index 2bc83e3..0000000 --- a/packages/common/test/test-rngs.test.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"test-rngs.test.d.ts","sourceRoot":"","sources":["test-rngs.test.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/packages/common/test/test-rngs.test.js b/packages/common/test/test-rngs.test.js deleted file mode 100644 index 5593e8d..0000000 --- a/packages/common/test/test-rngs.test.js +++ /dev/null @@ -1,65 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { ConstantRNG, MidpointRNG, SequenceRNG, SineRNG } from '../src/index.js'; -const toArray = (bytes) => Array.from(bytes); -describe('TestRNG:Constant', () => { - it('returns fixed 0', () => { - const rng = new ConstantRNG(0); - expect(rng.nextFloat1()).toBe(0); - expect(rng.nextInt(10)).toBe(0); - expect(toArray(rng.nextBytes(3))).toEqual([0, 0, 0]); - expect(toArray(rng.nextBits(3))).toEqual([0]); - }); - it('returns fixed 1', () => { - const rng = new ConstantRNG(1); - expect(rng.nextFloat1()).toBe(1); - expect(rng.nextInt(10)).toBe(10); - expect(toArray(rng.nextBytes(2))).toEqual([255, 255]); - expect(toArray(rng.nextBits(3))).toEqual([7]); - }); -}); -describe('TestRNG:Midpoint', () => { - it('returns midpoint for int/float', () => { - const rng = new MidpointRNG(); - expect(rng.nextFloat1()).toBe(0.5); - expect(rng.nextInt(9)).toBe(4); - expect(rng.nextInt(10)).toBe(5); - }); - it('alternates bits', () => { - const rng = new MidpointRNG(); - expect(toArray(rng.nextBits(4))).toEqual([10]); - expect(toArray(rng.nextBits(4))).toEqual([10]); - }); -}); -describe('TestRNG:Sine', () => { - it('follows sine wave with period/amplitude', () => { - const rng = new SineRNG(4, 0.5, 0); - expect(rng.nextFloat1()).toBeCloseTo(0.5, 8); - expect(rng.nextFloat1()).toBeCloseTo(1, 8); - expect(rng.nextFloat1()).toBeCloseTo(0.5, 8); - expect(rng.nextFloat1()).toBeCloseTo(0, 8); - }); - it('maps float to int range', () => { - const rng = new SineRNG(4, 0.5, 0); - expect(rng.nextInt(9)).toBe(5); - expect(rng.nextInt(9)).toBe(9); - }); -}); -describe('TestRNG:Sequence', () => { - it('cycles fixed sequence', () => { - const rng = new SequenceRNG([0, 0.25, 0.5, 0.75, 1]); - expect(rng.nextFloat1()).toBe(0); - expect(rng.nextFloat1()).toBe(0.25); - expect(rng.nextFloat1()).toBe(0.5); - expect(rng.nextFloat1()).toBe(0.75); - expect(rng.nextFloat1()).toBe(1); - expect(rng.nextFloat1()).toBe(0); - }); - it('converts sequence to bytes and ints', () => { - const rng = new SequenceRNG([0, 0.5, 1]); - expect(toArray(rng.nextBytes(3))).toEqual([0, 128, 255]); - expect(rng.nextInt(8)).toBe(0); - expect(rng.nextInt(8)).toBe(4); - expect(rng.nextInt(8)).toBe(8); - }); -}); -//# sourceMappingURL=test-rngs.test.js.map diff --git a/packages/common/test/test-rngs.test.js.map b/packages/common/test/test-rngs.test.js.map deleted file mode 100644 index debd872..0000000 --- a/packages/common/test/test-rngs.test.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"test-rngs.test.js","sourceRoot":"","sources":["test-rngs.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/logic/test/actions/turn/nation.test.ts b/packages/logic/test/actions/turn/nation.test.ts index 904dfb1..d3b8cd7 100644 --- a/packages/logic/test/actions/turn/nation.test.ts +++ b/packages/logic/test/actions/turn/nation.test.ts @@ -225,7 +225,7 @@ describe('Nation Actions', () => { }); describe('che_필사즉생 (Last Stand)', () => { - it('increases experience and sets strategic limit', () => { + it('applies the legacy three-turn gains and global delay', () => { const nation = buildNation(1); const general = buildGeneral(1, 1, 1); const definition = new LastStandAction([]); @@ -240,8 +240,11 @@ describe('Nation Actions', () => { definition.resolve(context as any, {}); - expect(general.experience).toBeGreaterThan(100); - expect(nation.meta.strategic_cmd_limit).toBeGreaterThan(0); + expect(general.experience).toBe(115); + expect(general.dedication).toBe(115); + expect(general.train).toBe(100); + expect(general.atmos).toBe(100); + expect(nation.meta.strategic_cmd_limit).toBe(9); }); }); diff --git a/packages/logic/test/scenarioParser.test.ts b/packages/logic/test/scenarioParser.test.ts index 4790c62..160cf43 100644 --- a/packages/logic/test/scenarioParser.test.ts +++ b/packages/logic/test/scenarioParser.test.ts @@ -9,7 +9,7 @@ import { parseScenarioDefaults, parseScenarioDefinition } from '../src/scenario/ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, '..', '..', '..'); -const scenarioRoot = path.join(repoRoot, 'legacy', 'hwe', 'scenario'); +const scenarioRoot = path.join(repoRoot, 'resources', 'scenario'); const readJson = async (filePath: string): Promise => { const raw = await fs.readFile(filePath, 'utf8'); @@ -17,7 +17,7 @@ const readJson = async (filePath: string): Promise => { }; describe('scenario parser', () => { - it('reads defaults from legacy default.json', async () => { + it('reads defaults from the tracked legacy-format default.json', async () => { const defaultsRaw = await readJson(path.join(scenarioRoot, 'default.json')); const defaults = parseScenarioDefaults(defaultsRaw); diff --git a/packages/logic/test/scenarios/diplomacy.test.ts b/packages/logic/test/scenarios/diplomacy.test.ts index 9e2fd6b..18de65b 100644 --- a/packages/logic/test/scenarios/diplomacy.test.ts +++ b/packages/logic/test/scenarios/diplomacy.test.ts @@ -7,10 +7,11 @@ import { commandSpec as declareWarSpec } from '../../src/actions/turn/nation/che import { commandSpec as deploySpec } from '../../src/actions/turn/general/che_출병.js'; import type { TurnCommandEnv } from '../../src/actions/turn/commandEnv.js'; import { processDiplomacyMonth, DIPLOMACY_STATE, type DiplomacyEntry } from '../../src/diplomacy/index.js'; -import { buildWarConfig, buildWarAftermathConfig } from '../../src/actions/turn/actionContextHelpers.js'; +import { evaluateConstraints } from '../../src/constraints/evaluate.js'; +import type { ConstraintContext, RequirementKey, StateView } from '../../src/constraints/types.js'; describe('Diplomacy Scenario', () => { - it('should handle War Declaration and prevent/allow deployment accordingly', async () => { + it('changes deployment from denied to allowed when a declaration becomes war', async () => { // 1. Setup World const NATION_A_ID = 1; const NATION_B_ID = 2; // Target nation @@ -134,8 +135,49 @@ describe('Diplomacy Scenario', () => { unitSet: { id: 'default', name: 'default', - crewTypes: [{ id: 1, name: 'Infantry', armType: 1, cost: 10, requirements: [] }], - } as any, + defaultCrewTypeId: 1, + armTypes: { 0: '성벽', 1: '보병' }, + crewTypes: [ + { + id: 0, + name: '성벽', + armType: 0, + attack: 0, + defence: 100, + speed: 1, + avoid: 0, + magicCoef: 0, + cost: 0, + rice: 0, + requirements: [], + attackCoef: {}, + defenceCoef: {}, + info: [], + initSkillTrigger: null, + phaseSkillTrigger: null, + iActionList: null, + }, + { + id: 1, + name: '보병', + armType: 1, + attack: 100, + defence: 100, + speed: 5, + avoid: 0, + magicCoef: 0, + cost: 10, + rice: 1, + requirements: [], + attackCoef: {}, + defenceCoef: {}, + info: [], + initSkillTrigger: null, + phaseSkillTrigger: null, + iActionList: null, + }, + ], + }, nations: [mockNationA, mockNationB], cities: [mockCityA, mockCityB], generals: [mockGeneralA], @@ -191,55 +233,71 @@ describe('Diplomacy Scenario', () => { expect(diplomacyAB).toBeDefined(); expect(diplomacyAB?.state).toBe(DIPLOMACY_STATE.DECLARATION); - // Pre-calculate context data needed for Dispatch - const warConfig = buildWarConfig(snapshot.scenarioConfig, snapshot.unitSet!); - const aftermathConfig = buildWarAftermathConfig(snapshot.scenarioConfig, warConfig.castleCrewTypeId); - - const warTime = { - year: 200, - month: 1, - season: 0, - turn: 1, - }; - const seedBase = 'test_seed'; - - const buildDispatchContext = (destCityId: number) => { - const destCity = world.getCity(destCityId)!; - const destNation = world.getNation(destCity.nationId); - - warTime.year = runner.currentDate.getFullYear(); - warTime.month = runner.currentDate.getMonth() + 1; - warTime.season = Math.floor(runner.currentDate.getMonth() / 3); + const dispatchArgs = { destCityId: 2 }; + const buildConstraintView = (): StateView => { + const get = (requirement: RequirementKey): unknown | null => { + switch (requirement.kind) { + case 'general': + case 'destGeneral': + return world.getGeneral(requirement.id) ?? null; + case 'generalList': + return world.getAllGenerals(); + case 'city': + case 'destCity': + return world.getCity(requirement.id) ?? null; + case 'nation': + case 'destNation': + return world.getNation(requirement.id) ?? null; + case 'nationList': + return world.getAllNations(); + case 'diplomacy': + return world.getDiplomacy(requirement.srcNationId, requirement.destNationId) ?? null; + case 'diplomacyList': + return world.snapshot.diplomacy; + case 'arg': + return dispatchArgs[requirement.key as keyof typeof dispatchArgs] ?? null; + case 'env': + if (requirement.key === 'map') return world.snapshot.map; + if (requirement.key === 'cities') return world.getAllCities(); + if (requirement.key === 'nations') return world.getAllNations(); + if (requirement.key === 'relYear') return 200; + if (requirement.key === 'openingPartYear') return 200; + return null; + } + }; return { - destCity, - destNation, - warConfig, - aftermathConfig, - time: { ...warTime }, - seedBase, - map: world.snapshot.map, - unitSet: world.snapshot.unitSet, - cities: world.getAllCities(), - nations: world.getAllNations(), - generals: world.getAllGenerals(), - diplomacy: world.snapshot.diplomacy, + has: (requirement) => get(requirement) !== null, + get, }; }; - - // 3. Try to Deploy - const deployDef = deploySpec.createDefinition(systemEnv); - await runner.runTurn([ - { - generalId: 1, - commandKey: 'che_출병', - resolver: deployDef, - args: { destCityId: 2 }, - context: buildDispatchContext(2), + const buildDispatchConstraintContext = (): ConstraintContext => ({ + actorId: mockGeneralA.id, + cityId: mockGeneralA.cityId, + nationId: mockGeneralA.nationId, + destCityId: dispatchArgs.destCityId, + destNationId: NATION_B_ID, + args: dispatchArgs, + env: { + relYear: 200, + openingPartYear: 200, }, - ]); + mode: 'full', + }); - expect(world.snapshot.troops.length).toBe(0); + // 3. A declaration is not yet a war, so deployment must be rejected. + const deployDef = deploySpec.createDefinition(systemEnv); + const declarationResult = evaluateConstraints( + deployDef.buildConstraints(buildDispatchConstraintContext(), dispatchArgs), + buildDispatchConstraintContext(), + buildConstraintView() + ); + expect(declarationResult).toMatchObject({ + kind: 'deny', + constraintName: 'hasRouteWithEnemy', + reason: '교전중인 국가가 아닙니다.', + }); + expect(world.getGeneral(1)?.experience).toBe(100); // 4. Simulate State Transition for (let i = 0; i < 24; i++) { @@ -267,18 +325,12 @@ describe('Diplomacy Scenario', () => { const diplomacyAB_After = world.getDiplomacy(NATION_A_ID, NATION_B_ID); expect(diplomacyAB_After?.state).toBe(DIPLOMACY_STATE.WAR); - // 5. Try to Deploy (Should Succeed) - await runner.runTurn([ - { - generalId: 1, - commandKey: 'che_출병', - resolver: deployDef, - args: { destCityId: 2 }, - context: buildDispatchContext(2), - }, - ]); - - const generalAfter = world.getGeneral(1)!; - console.log(`General Exp: ${mockGeneralA.experience} -> ${generalAfter.experience}`); + // 5. The same command becomes valid after the declaration reaches WAR. + const warResult = evaluateConstraints( + deployDef.buildConstraints(buildDispatchConstraintContext(), dispatchArgs), + buildDispatchConstraintContext(), + buildConstraintView() + ); + expect(warResult).toEqual({ kind: 'allow' }); }); }); diff --git a/packages/logic/test/scenarios/domestic.test.ts b/packages/logic/test/scenarios/domestic.test.ts index 3ed7c71..112e755 100644 --- a/packages/logic/test/scenarios/domestic.test.ts +++ b/packages/logic/test/scenarios/domestic.test.ts @@ -141,8 +141,7 @@ describe('Domestic Affairs Scenario', () => { // 4. Verify const updatedCity = world.getCity(1)!; - console.log(`Agriculture: 500 -> ${updatedCity.agriculture}`); - expect(updatedCity.agriculture).toBeGreaterThan(500); + expect(updatedCity.agriculture).toBe(600); }); it('should not increase agriculture when city is already maxed', async () => { diff --git a/packages/logic/test/scenarios/general/che_NPC능동.test.ts b/packages/logic/test/scenarios/general/che_NPC능동.test.ts index 47aa7f1..ce3fc3d 100644 --- a/packages/logic/test/scenarios/general/che_NPC능동.test.ts +++ b/packages/logic/test/scenarios/general/che_NPC능동.test.ts @@ -224,10 +224,18 @@ describe('che_NPC능동', () => { ).rejects.toThrow('NPC가 아닙니다.'); }); - it('should fail if args are invalid', async () => { - // Arg validation happens in parseArgs or resolver check. - // Simulating invalid args via direct usage of resolver might bypass parseArgs if using `resolver` object directly with raw args? - // No, verify it returns null or throws. - // Here we just skip for brevity or setup minimal test. + it('rejects structurally invalid command arguments', async () => { + const definition = ( + await import('../../../src/actions/turn/general/che_NPC능동.js') + ).commandSpec.createDefinition({} as TurnCommandEnv); + + expect(definition.parseArgs(null)).toBeNull(); + expect(definition.parseArgs({ optionText: '순간이동' })).toBeNull(); + expect(definition.parseArgs({ optionText: '순간이동', destCityId: '101' })).toBeNull(); + expect(definition.parseArgs({ optionText: '알수없음', destCityId: 101 })).toBeNull(); + expect(definition.parseArgs({ optionText: '순간이동', destCityId: 101 })).toEqual({ + optionText: '순간이동', + destCityId: 101, + }); }); }); diff --git a/packages/logic/test/scenarios/troops.test.ts b/packages/logic/test/scenarios/troops.test.ts index 52f1799..2a9ddcb 100644 --- a/packages/logic/test/scenarios/troops.test.ts +++ b/packages/logic/test/scenarios/troops.test.ts @@ -144,7 +144,6 @@ describe('Troop Management Scenario', () => { ]); const generalAfterDraft = world.getGeneral(1)!; - console.log(`Drafted: ${generalAfterDraft.crew}`); expect(generalAfterDraft.crew).toBe(1000); // 3. Train @@ -159,8 +158,7 @@ describe('Troop Management Scenario', () => { ]); const generalAfterTrain = world.getGeneral(1)!; - console.log(`Train: 10 -> ${generalAfterTrain.train}`); - expect(generalAfterTrain.train).toBeGreaterThan(10); + expect(generalAfterTrain.train).toBe(75); // 4. Boost Morale const atmosDef = atmosSpec.createDefinition(systemEnv); @@ -174,7 +172,6 @@ describe('Troop Management Scenario', () => { ]); const generalAfterAtmos = world.getGeneral(1)!; - console.log(`Atmos: 10 -> ${generalAfterAtmos.atmos}`); - expect(generalAfterAtmos.atmos).toBeGreaterThan(10); + expect(generalAfterAtmos.atmos).toBe(75); }); }); diff --git a/packages/logic/test/testEnv.ts b/packages/logic/test/testEnv.ts index 628ea7f..98fda3b 100644 --- a/packages/logic/test/testEnv.ts +++ b/packages/logic/test/testEnv.ts @@ -1,4 +1,5 @@ import { enablePatches } from 'immer'; +import { LiteHashDRBG, RandUtil } from '@sammo-ts/common'; import type { City, General, Nation, GeneralId, NationId, CityId } from '../src/domain/entities.js'; import type { WorldSnapshot } from '../src/world/types.js'; import { @@ -176,10 +177,12 @@ export interface TestCommand { export class TestGameRunner { world: InMemoryWorld; currentDate: Date; + private readonly seedBase: string; - constructor(world: InMemoryWorld, startYear: number, startMonth: number) { + constructor(world: InMemoryWorld, startYear: number, startMonth: number, seedBase = 'test-game-runner') { this.world = world; this.currentDate = new Date(startYear, startMonth - 1); + this.seedBase = seedBase; } nextGeneralId = 1; @@ -190,29 +193,33 @@ export class TestGameRunner { entries: [{ startMinute: 0, tickMinutes: 60 }], }; - for (const cmd of commands) { + for (const [commandIndex, cmd] of commands.entries()) { const general = this.world.getGeneral(cmd.generalId); - if (!general) continue; + if (!general) { + throw new Error(`General ${cmd.generalId} does not exist`); + } const city = this.world.getCity(general.cityId); if (!city) throw new Error(`General ${general.id} is in non-existent city ${general.cityId}`); const nation = general.nationId ? this.world.getNation(general.nationId) : null; + const rng = new RandUtil( + LiteHashDRBG.build( + [ + this.seedBase, + this.currentDate.toISOString(), + String(commandIndex), + String(general.id), + cmd.commandKey, + ].join('|') + ) + ); const inputContext = { general, city, nation: nation || null, - rng: { - real: () => Math.random(), - int: (min: number, max: number) => Math.floor(Math.random() * (max - min)) + min, - nextInt: (min: number, max: number) => Math.floor(Math.random() * (max - min)) + min, - next: () => Math.random(), - nextBool: () => Math.random() < 0.5, - nextRange: (min: number, max: number) => Math.random() * (max - min) + min, - nextRangeInt: (min: number, max: number) => Math.floor(Math.random() * (max - min)) + min, - nextFloat1: () => Math.random(), - } as any, + rng, year: this.currentDate.getFullYear(), month: this.currentDate.getMonth() + 1,