diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index a4ef9eb..eb1264e 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -210,7 +210,30 @@ Plans: 3. 测试在本地可运行且全部通过 4. 测试由 AI 编写并运行,通过验证后可提交 -**Plans**: TBD +**Plans**: 9 plans (data-layer slice; non-data render/legacy coverage deferred) + +Plans: +- [ ] 06-01-PLAN.md — Combat L2 (data-system/src/combat) behavior tests + phase tracer +- [ ] 06-02-PLAN.md — Combat L3 (data-state/src/enemy) calculator/aura/special/mapDamage tests +- [ ] 06-03-PLAN.md — Enemy data model (data-base/src/enemy) tests +- [ ] 06-04-PLAN.md — Replay (data-common/src/replay) tests + replay integration +- [ ] 06-05-PLAN.md — Hero core (attribute/modifier/location/state) tests +- [ ] 06-06-PLAN.md — Hero storage (equipment/equipStore/items/follower/mover) tests +- [ ] 06-07-PLAN.md — Map (gameMap/mapState/mapLayer/tile) expanded tests +- [ ] 06-08-PLAN.md — Combat closure + saveables round-trip integration tests +- [ ] 06-09-PLAN.md — Flag + common auxiliary tests + +**Wave 1** *(all plans; independent new test files, no shared production edits)* + +- [ ] 06-01-PLAN.md +- [ ] 06-02-PLAN.md +- [ ] 06-03-PLAN.md +- [ ] 06-04-PLAN.md +- [ ] 06-05-PLAN.md +- [ ] 06-06-PLAN.md +- [ ] 06-07-PLAN.md +- [ ] 06-08-PLAN.md +- [ ] 06-09-PLAN.md ## Progress @@ -224,4 +247,4 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 | 3. 数据端完成 | 19/19 | Complete | 2026-09-12 | | 4. 渲染适配与双布局 | 0/TBD | Not started | - | | 5. Legacy 移植 | 0/TBD | Not started | - | -| 6. 单元测试 | 0/TBD | Not started | - | +| 6. 单元测试 | 0/9 | Planned | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 4fa5af2..faaa0b8 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -1,18 +1,18 @@ --- gsd_state_version: "1.0" milestone: v1.0 -current_phase: 4 -current_phase_name: 渲染适配与双布局 -status: planning +current_phase: 06 +current_phase_name: unit-tests +status: executing stopped_at: Phase 6 context gathered -last_updated: "2026-09-13T23:35:56.298Z" +last_updated: "2026-09-14T00:30:13.250Z" last_activity: 2026-09-13 last_activity_desc: Completed quick task 260913-qtq (event built-ins refactor + anon-tokyo import rename) -state_head: 6a3179b76f4b62776d553435efe2bf21149102b5 +state_head: e1a16eae8b10c3aa44a9ca5908871d431fa396a2 progress: total_phases: 6 completed_phases: 3 - total_plans: 37 + total_plans: 46 completed_plans: 37 milestone_name: milestone --- @@ -28,9 +28,9 @@ See: .planning/PROJECT.md (updated 2026-09-10) ## Current Position -Phase: 4 — 渲染适配与双布局 +Phase: 06 (unit-tests) — READY TO EXECUTE Plan: Not started -Status: Ready to plan +Status: Ready to execute Last activity: 2026-09-12 — Phase 03 complete, transitioned to Phase 4 Progress: [█████░░░░░] 50% diff --git a/.planning/phases/06-unit-tests/06-01-PLAN.md b/.planning/phases/06-unit-tests/06-01-PLAN.md new file mode 100644 index 0000000..f4359b7 --- /dev/null +++ b/.planning/phases/06-unit-tests/06-01-PLAN.md @@ -0,0 +1,278 @@ +--- +phase: 06-unit-tests +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - packages-user/data-system/src/combat/damage.test.ts + - packages-user/data-system/src/combat/mapDamage.test.ts + - packages-user/data-system/src/combat/context.test.ts + - packages-user/data-system/src/combat/combat.test.ts + - .planning/phases/06-unit-tests/COVERAGE.md +autonomous: true +requirements: [TEST-01] +estimate: + tokens: 52000 + raw_tokens: 52000 + tasks: 3 + confidence: low +must_haves: + truths: + - "FLAGGED ASSUMPTION (unverified): TEST-01 '为核心系统(数据层等)补齐单元测试' is treated as satisfied by behavior unit tests over the seven in-scope data-layer systems, gated by `pnpm test:ci` green; no ROADMAP edge verifier resolved this requirement (spec-less probe returned unclassified/unresolved), so this remains an assumption the user must confirm, not a proven edge." + - DamageContext/DamageSystem return calculator output and emit logger codes 106/107 on missing calculator/hero, and expose the `with(hero)` isolated context + - MapDamage registers converter/reducer, reduces separated damages, warns 102/103 on missing converter/reducer, and drops enemy-sourced damage on deleteEnemy + - EnemyContext resolves locators/views/computed enemies and returns null for unknown lookups + - CombatFlow rejects foreign-state contexts/damage with warn 138, rejects duplicate script priority with warn 140, and runs scripts/hooks in the documented order + artifacts: + - path: packages-user/data-system/src/combat/damage.test.ts + provides: DamageContext/DamageSystem behavior coverage + - path: packages-user/data-system/src/combat/mapDamage.test.ts + provides: MapDamage behavior coverage + - path: packages-user/data-system/src/combat/context.test.ts + provides: EnemyContext behavior coverage + - path: packages-user/data-system/src/combat/combat.test.ts + provides: CombatFlow behavior coverage + - path: .planning/phases/06-unit-tests/COVERAGE.md + provides: api-coverage declaration for this test-only phase + key_links: + - "damage.test.ts / combat.test.ts drive `DamageContext`, `DamageSystem`, `CombatFlow` through injected fake `IEnemyContext`/`IDamageCalculator` without mocking the data-layer interfaces" + - "all four files import the system under test through its own module path plus `@user/data-base` / `@user/data-common` barrels, proving the public import chain in Node" + prohibitions: + - "FLAGGED/UNVERIFIED: no production/core source file is created or modified in this phase" + - "FLAGGED/UNVERIFIED: no coverage provider is installed and no coverage threshold is enforced" + - "FLAGGED/UNVERIFIED: no `fake-indexeddb` or CoreState end-to-end save/load path is introduced" + - "FLAGGED/UNVERIFIED: a suspected-bug test is never weakened into passing; it is written to the correct expectation and marked it.skip/it.todo" + - "FLAGGED/UNVERIFIED: no shared cross-file fixture/factory helper is created; fixtures stay inline per test file" + - "FLAGGED/UNVERIFIED: no fake timers are used for async action tests" +--- + + +Establish the phase's test harness with a leading end-to-end tracer, then complete behavior coverage for the Layer-2 combat system `packages-user/data-system/src/combat` (D-01, D-15). + +Phase constraints. D-04: async action paths (combat scripts/hooks) use real timers and await controller completion, never fake timers; D-05: a suspected bug becomes a test written to the correct expectation and marked `it.skip`/`it.todo` with a Chinese comment pointing into `06-TEST-FINDINGS.md`, keeping `pnpm test:ci` green; D-08: verification uses the fixed non-watch `pnpm test:ci` command; D-09: coverage is reference-only, not a judgment standard, so no coverage tooling is introduced and `@vitest/coverage-*` is not installed. + +Purpose: Prove the Node test harness (global stubs, dynamic module bag, injected fake collaborators, `logger.catch` assertion, real-timer async) on one real system path before the rest of the phase expands. `packages-user/data-system/src/combat` is the pure Layer-2 combat contract (`DamageContext`/`DamageSystem`/`MapDamage`/`EnemyContext`/`CombatFlow`) and the cleanest place to prove the tracer because it is driven entirely through injected `IEnemyContext`/`IDamageCalculator` fakes with no DOM. + +Output: 4 new co-located `*.test.ts` files, a `COVERAGE.md` api-coverage declaration, and the initial `06-TEST-FINDINGS.md` schema (shared append-only sink). + + + +@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md +@C:/Users/book/.config/opencode/gsd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/REQUIREMENTS.md +@.planning/phases/06-unit-tests/06-CONTEXT.md +@.planning/phases/06-unit-tests/06-PATTERNS.md +@.planning/codebase/TESTING.md +@dev.md +@packages-user/data-system/src/combat/damage.ts +@packages-user/data-system/src/combat/mapDamage.ts +@packages-user/data-system/src/combat/context.ts +@packages-user/data-system/src/combat/combat.ts +@packages-user/data-system/src/combat/types.ts +@packages-user/data-system/src/combat/enemy.ts +@packages-user/data-system/src/path/system.test.ts +@packages-user/data-system/src/event/eventDispatch.test.ts +@packages-user/data-common/src/store/tileStore.test.ts + + + +| Source | Item | Covered by | +|--------|------|-----------| +| GOAL | 为核心系统(数据层等)补齐单元测试 (data-layer slice) | 06-01..06-09 | +| GOAL | Phase 6 non-data (render/legacy) coverage | Deferred per 06-CONTEXT `` (not planned) | +| REQ | TEST-01 | 06-01..06-09 | +| CONTEXT | D-01 mixed tiers | 06-01/02/03 integration-capable; 06-05..09 pure/DI | +| CONTEXT | D-02/D-03 minimal inline fixtures | 06-01..06-09 | +| CONTEXT | D-04 real timers + await onEnd | 06-01 (combat flow), 06-05/06 (mover), 06-08 | +| CONTEXT | D-05/D-06 skip + 06-TEST-FINDINGS.md | 06-01 creates sink; all plans append | +| CONTEXT | D-07 per-system report, no core edits | all | +| CONTEXT | D-08 `pnpm test:ci` gate | all | +| CONTEXT | D-09 no coverage tooling | all | +| CONTEXT | D-10..D-13 saveables round-trip | 06-08 | +| CONTEXT | D-14 enemy data model full | 06-03 | +| CONTEXT | D-15 combat built-ins run | 06-01/02/08 | +| CONTEXT | D-16 replay array + system + first divergence | 06-04/08 | +| CONTEXT | D-17 hero each subsystem | 06-05/06 | +| CONTEXT | D-18 map expanded coverage | 06-07 | +| CONTEXT | D-19 flag public surface | 06-09 | +| CONTEXT | D-20 common natural/ minimal direct | 06-09 | +| RESEARCH | none (no RESEARCH.md; PATTERNS.md is the pattern source) | n/a | +| ASSUMPTION | TEST-01 edge probe unclassified/unresolved | Flagged in must_haves.truths (above); user confirmation required | + + + +- This phase writes tests only. Do not create or edit any file under `packages-user/*/src` except the `*.test.ts` files listed in `files_modified`. +- Every `it(...)` must be immediately preceded by a single-line Chinese comment describing the covered behavior (dev.md 注释规范). Update the comment when the test scope changes. +- Use real timers and `await controller/`sandbox` completion` for async paths; never `vi.useFakeTimers()` (D-04). +- If a test exposes a suspected bug: write the test to the CORRECT expectation, mark it `it.skip` / `it.todo`, and put a Chinese comment pointing at `06-TEST-FINDINGS.md` using a plan-scoped anchor `#06-01-N`. `pnpm test:ci` MUST stay green. +- `06-TEST-FINDINGS.md` is a shared append-only sink and is intentionally NOT listed in `files_modified` so wave-1 plans stay parallel; append your own plan-scoped section, never rewrite other sections. +- Report findings per system in the plan SUMMARY; do not fix core code in this phase (D-07). + + + + + + Task 1: Tracer — DamageContext/DamageSystem behavior end-to-end (+ phase scaffolding) + packages-user/data-system/src/combat/damage.test.ts, .planning/phases/06-unit-tests/COVERAGE.md + + packages-user/data-system/src/combat/damage.ts + packages-user/data-system/src/combat/types.ts + packages-user/data-system/src/combat/context.ts + packages-user/data-system/src/path/system.test.ts + .planning/phases/06-unit-tests/06-PATTERNS.md (shared boilerplate 1-5, section A) + dev.md (注释规范) + + +Create `damage.test.ts` as the phase tracer: the thinnest real path that drives the Layer-2 combat damage contract end-to-end through its public classes in Node, with no DOM and no mocking of data-layer interfaces. + +Use the `vi.hoisted` global-stub + dynamic-import bag boilerplate from 06-PATTERNS.md shared pattern 1 (copy the `mapLifecycle.test.ts` shape) because the import chain reaches `@user/data-common` and `@motajs/common`. + +Build a minimal inline fake `IEnemyContext` (or reuse an inline fake shape) and a minimal inline fake `IDamageCalculator` whose `calculate(handler)` returns a fixed `{ damage, turn }`, then: +- construct `DamageContext` with the fake context and assert `getDamageInfo(view)` / `getDamageInfoByComputed(enemy)` return `{ handler, damage, turn }` for the calculator result and `handler` identity matches the computed enemy + locator + hero. +- assert `getDamageInfoByHandler(handler)` returns the raw calculator result. +- assert missing calculator and missing hero paths emit logger codes 106 and 107 using `logger.catch` (never `expect(...).toThrow`) per 06-PATTERNS.md shared pattern 4. +- construct `DamageSystem` and assert caching: two consecutive `getDamageInfo` calls return the same object, `markDirty(view)` / `markAllDirty()` / `deleteEnemy(view)` force recalculation, `useCalculator` / `bindHeroStatus` call `markAllDirty`, and `with(hero)` returns a `DamageContext` sharing the calculator and returning the passed hero. +- cover `calculateCritical(view, attribute, precision)` generator behavior on a driven critical limit using a fake calculator whose `getCriticalLimit` returns a known bound, plus the early-return guards (no hero / no calculator / no locator / already at upper limit). + +Then create the phase scaffolding: +- `COVERAGE.md` in the phase directory with a one-paragraph reasoned declaration: "No external API integration: this phase adds only in-repo Vitest behavior tests over the Node-only data layer; no external SDK/HTTP/CLI client is added or exercised." (api-coverage contribution). +- Create `.planning/phases/06-unit-tests/06-TEST-FINDINGS.md` with a header and the D-06 column table: 模块/接口、现象、最小复现、疑似原因、影响面、建议修复方向、关联 skip 用例、严重度. + +Verify with a focused run before moving on: the tracer MUST pass green on its own. + + + pnpm exec vitest run packages-user/data-system/src/combat/damage.test.ts + the focused vitest run reports any failed or unexpectedly skipped test, or the file cannot be collected due to an unresolved module/global + + + - `damage.test.ts` runs green under `pnpm exec vitest run` with zero non-skipped failures. + - Every `it` has an immediately preceding single-line Chinese comment. + - The file asserts at least: normal result spread, handler identity, logger codes 106 and 107, cache hit/invalidation, `with(hero)`, and one `calculateCritical` yield path. + - No production file is modified; `git status` shows only new test files plus `COVERAGE.md`. + - `COVERAGE.md` and `06-TEST-FINDINGS.md` exist with the required header/columns. + + One real combat damage path is proven end-to-end green, and the phase's findings/coverage scaffolding exists; this sink is where D-05 skipped correct-expectation tests point. + + + + Task 2: MapDamage + EnemyContext behavior + packages-user/data-system/src/combat/mapDamage.test.ts, packages-user/data-system/src/combat/context.test.ts + + packages-user/data-system/src/combat/mapDamage.ts + packages-user/data-system/src/combat/context.ts + packages-user/data-system/src/combat/enemy.ts + packages-user/data-system/src/combat/types.ts + packages-user/data-common/src/store/tileStore.test.ts (registry/contract table pattern) + .planning/phases/06-unit-tests/06-PATTERNS.md (section A: mapDamage, context) + + +`mapDamage.test.ts`: drive `MapDamage` through inline fake `IEnemyContext` + fake `IMapDamageConverter` + fake `IMapDamageReducer` + fake `IMapDamageView` whose `getRange`/`getRangeParam`/`getDamageWithoutCheck` are deterministic. Assert: `addMapDamage`/`deleteMapDamage` accumulate sourceless damage and mark the index dirty; `getReducedDamage` returns the reducer result and null when no separated damage; missing reducer warns 103, missing converter warns 102; `getSeparatedDamage` unions sourced+sourceless; `deleteEnemy` removes sourced damage; `markEnemyDirty` warns 104 when the view has no store and no locator. Use `logger.catch` for warning branches. + +`context.test.ts`: drive `EnemyContext` with inline real `Enemy`-like objects (or the real `Enemy` from `@user/data-base` through a created prefab) and assert the registry contract in the `tileStore` style: `setEnemyAt` then `getEnemyByLocator`/`getEnemyByLoc`/`getEnemyLocatorByView`/`getEnemyLocator`/`getViewByComputed` return the registered objects; unknown locators/locators for unknown objects return `null`; `deleteEnemy` removes view+locator mappings; `resize` clears and sets `width`/`height` and indexer width; `scanRange`/`iterateEnemy` yield the registered entries; register/unregister aura converters, common query effects, special query effects, and final effects (assert add then remove leaves the registry empty). Keep fixtures inline and minimal (D-02/D-03). Do not mock data-layer interfaces. + + + pnpm exec vitest run packages-user/data-system/src/combat/mapDamage.test.ts packages-user/data-system/src/combat/context.test.ts + either focused run fails, or a warning assertion does not observe the documented logger code + + + - Both files run green with zero non-skipped failures. + - `mapDamage.test.ts` asserts warn codes 102, 103, 104 and reduce/delete behavior. + - `context.test.ts` asserts register/lookup/unknown-null/delete/resize for the context registry. + - Fixtures are constructed inline; no shared helper file is created. + - Every `it` has a preceding single-line Chinese comment. + + MapDamage and EnemyContext public behavior is covered and green. + + + + Task 3: CombatFlow binding, script ordering, and battle flow + packages-user/data-system/src/combat/combat.test.ts + + packages-user/data-system/src/combat/combat.ts + packages-user/data-system/src/combat/types.ts + packages-user/data-system/src/event/eventDispatch.test.ts (fake collaborator + manual-resolver await proof) + .planning/phases/06-unit-tests/06-PATTERNS.md (section A: combat) + + +Drive `CombatFlow` with a minimal inline fake `IStateBase` and fake enemy view / context / damage objects: +- `bindContext`/`bindDamage` accept same-`state` objects and reject foreign-state objects with logger warn 138 (assert via `logger.catch`); passing `null` clears them. +- `bindHero(null)`/`bindHero(hero)` set `hero`. +- `addCombatScript` sorts by descending priority, rejects a duplicate priority with warn 140 and leaves the list unchanged. +- `battle(view)` and `battleComputed(enemy)` return `null` and warn 139 when context or hero is missing; warn 139 for the missing damage context and warn 141 when `getDamageInfoByHandler` returns null. +- Async completion proof (real timers, no fake timers): a script whose `before` resolves after a manually-released promise must complete before `onAfterCombat` runs; assert call order `[script.before, hooks.onBeforeCombat, script.after, hooks.onAfterCombat]` and that a `before` returning truthy short-circuits (skips hooks/after). +Use the manual-resolver await pattern from `eventDispatch.test.ts` referenced in 06-PATTERNS.md. Keep all fixtures inline. + + + pnpm exec vitest run packages-user/data-system/src/combat/combat.test.ts + the focused run fails or the ordering assertion does not observe the documented before/after sequence + + + - `combat.test.ts` runs green with zero non-skipped failures. + - Asserts warn codes 138, 139, 140, 141 and the script/hook ordering with an awaited promise. + - No fake timers used; async proof relies on real timers plus a manual resolver. + - Every `it` has a preceding single-line Chinese comment. + + CombatFlow binding, guard warnings, and awaited script ordering are covered and green. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| none | This plan only adds in-repo Node Vitest tests; it introduces no runtime input surface, network call, DOM access, or filesystem write. | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-06-01-01 | Tampering | test files under `packages-user/data-system/src/combat` | low | mitigate | Tests import only tracked project modules and construct synthetic in-memory fixtures; no package install and no core source edit (gate: `pnpm test:ci` plus review that only `*.test.ts` changed). | +| T-06-01-02 | Information disclosure | test fixtures | low | accept | Fixtures are synthetic numbers/ids; no secrets or real game data are read (D-02). | +| T-06-01-03 | Elevation of privilege | test execution environment | low | accept | Vitest runs in Node without elevated privileges; no network or filesystem side effects are introduced by the tests. | +| T-06-01-SC | Tampering | package installs | high | mitigate | No new dependency is installed in this plan; if any install becomes necessary, stop and require a package-legitimacy checkpoint before proceeding. | + + + +- `pnpm exec vitest run packages-user/data-system/src/combat/damage.test.ts packages-user/data-system/src/combat/mapDamage.test.ts packages-user/data-system/src/combat/context.test.ts packages-user/data-system/src/combat/combat.test.ts` passes. +- `pnpm test:ci` passes for the full suite (existing 18 test files stay green). +- `git status` confirms only new `*.test.ts`, `COVERAGE.md`, and `06-TEST-FINDINGS.md` are added; no production file is modified. +- No `it.skip`/`it.todo` is added unless a corresponding `#06-01-N` entry exists in `06-TEST-FINDINGS.md`. + + + +- 4 combat Layer-2 behavior test files exist and pass. +- Warn codes 106/107/138/139/140/141 and MapDamage 102/103/104 are asserted through `logger.catch`. +- The tracer passed on its own before expansion tasks ran. +- `COVERAGE.md` declaration and the `06-TEST-FINDINGS.md` schema exist. + + + +Test files: +- `packages-user/data-system/src/combat/damage.test.ts` +- `packages-user/data-system/src/combat/mapDamage.test.ts` +- `packages-user/data-system/src/combat/context.test.ts` +- `packages-user/data-system/src/combat/combat.test.ts` + +Inline helper symbols (defined once per file, not shared): +- `damage.test.ts`: `TestModules`, `modules`, `createEnemyContext`, `createCalculator`, `createEnemy` +- `mapDamage.test.ts`: `createMapDamageFixture`, `FakeConverter`, `FakeReducer`, `FakeView` +- `context.test.ts`: `createContextFixture`, `createEnemy` +- `combat.test.ts`: `createCombatFixture`, `FakeScript`, `createDeferred` + +Planning artifacts: +- `.planning/phases/06-unit-tests/COVERAGE.md` +- `.planning/phases/06-unit-tests/06-TEST-FINDINGS.md` (initial header + schema) + + + +Create `.planning/phases/06-unit-tests/06-01-SUMMARY.md` when done. Append suspected-bug findings to `.planning/phases/06-unit-tests/06-TEST-FINDINGS.md` under plan-scoped anchors `#06-01-N`. + diff --git a/.planning/phases/06-unit-tests/06-02-PLAN.md b/.planning/phases/06-unit-tests/06-02-PLAN.md new file mode 100644 index 0000000..98a2916 --- /dev/null +++ b/.planning/phases/06-unit-tests/06-02-PLAN.md @@ -0,0 +1,252 @@ +--- +phase: 06-unit-tests +plan: 02 +type: execute +wave: 1 +depends_on: [] +files_modified: + - packages-user/data-state/src/enemy/calculator.test.ts + - packages-user/data-state/src/enemy/final.test.ts + - packages-user/data-state/src/enemy/comparer.test.ts + - packages-user/data-state/src/enemy/aura.test.ts + - packages-user/data-state/src/enemy/special.test.ts + - packages-user/data-state/src/enemy/mapDamage.test.ts +autonomous: true +requirements: [TEST-01] +estimate: + tokens: 58000 + raw_tokens: 58000 + tasks: 3 + confidence: low +must_haves: + truths: + - "FLAGGED ASSUMPTION (unverified): TEST-01 is treated as satisfied by behavior unit tests gated by `pnpm test:ci`; no ROADMAP edge verifier resolved it (spec-less probe unclassified/unresolved), so user confirmation is required." + - MainDamageCalculator produces the documented damage/turn for each special branch (无敌/魔攻/连击/支援/先攻/破甲/反击/净化/固伤/仇恨/吸血/负伤) against a synthetic handler + - MainEnemyFinalEffect applies 坚固 and 模仿; MainEnemyComparer compares base attributes and deep-equal specials + - Aura converters select the correct range and apply hp/atk/def buffs; GuardAura registers guard locators + - registerSpecials registers the full special code table (0-27) and guard defaults + - Map damage views compute range params and per-locator damage; the converter builds views per special; the reducer sums damage and unions catch/repulse extras + artifacts: + - path: packages-user/data-state/src/enemy/calculator.test.ts + provides: MainDamageCalculator branch coverage + - path: packages-user/data-state/src/enemy/final.test.ts + provides: MainEnemyFinalEffect coverage + - path: packages-user/data-state/src/enemy/comparer.test.ts + provides: MainEnemyComparer coverage + - path: packages-user/data-state/src/enemy/aura.test.ts + provides: CommonAura/GuardAura coverage + - path: packages-user/data-state/src/enemy/special.test.ts + provides: registerSpecials coverage + - path: packages-user/data-state/src/enemy/mapDamage.test.ts + provides: map damage view/converter/reducer coverage + key_links: + - "calculator.test.ts builds an inline fake `IReadonlyEnemyHandler` (enemy/context/locator/hero/state) exactly as `data-state/test/dataClosure.test.ts` does, extending the existing partial coverage rather than duplicating it" + - "mapDamage.test.ts drives the view classes and `MainMapDamageConverter`/`MainMapDamageReducer` through the `@user/data-system` interfaces" + prohibitions: + - "FLAGGED/UNVERIFIED: no production/core source file is created or modified in this phase" + - "FLAGGED/UNVERIFIED: no coverage provider is installed and no coverage threshold is enforced" + - "FLAGGED/UNVERIFIED: no `fake-indexeddb` or CoreState end-to-end save/load path is introduced" + - "FLAGGED/UNVERIFIED: a suspected-bug test is never weakened into passing; it is written to the correct expectation and marked it.skip/it.todo" + - "FLAGGED/UNVERIFIED: no shared cross-file fixture/factory helper is created; fixtures stay inline per test file" + - "FLAGGED/UNVERIFIED: no fake timers are used for async action tests" +--- + + +Complete behavior coverage for the top-level combat implementation `packages-user/data-state/src/enemy` (D-01, D-15): damage calculation, final effects, comparison, auras, special registration, and map damage. + +Phase constraints. D-05: a suspected bug becomes a test written to the correct expectation and marked `it.skip`/`it.todo` with a Chinese comment pointing into `06-TEST-FINDINGS.md`, keeping `pnpm test:ci` green; D-08: verification uses the fixed non-watch `pnpm test:ci` command. + +Purpose: These classes are the real built-in combat implementation wired into `CoreState`. `dataClosure.test.ts` covers only a single deterministic `MainDamageCalculator` case; every special branch, the guard/support path, aura conversion, and the map-damage views are currently untested. + +Output: 6 new co-located `*.test.ts` files. + + + +@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md +@C:/Users/book/.config/opencode/gsd-core/templates/summary.md + + + +@.planning/phases/06-unit-tests/06-CONTEXT.md +@.planning/phases/06-unit-tests/06-PATTERNS.md +@.planning/codebase/TESTING.md +@dev.md +@packages-user/data-state/src/enemy/calculator.ts +@packages-user/data-state/src/enemy/aura.ts +@packages-user/data-state/src/enemy/special.ts +@packages-user/data-state/src/enemy/mapDamage.ts +@packages-user/data-state/src/enemy/comparer.ts +@packages-user/data-state/src/enemy/final.ts +@packages-user/data-state/src/enemy/types.ts +@packages-user/data-state/test/dataClosure.test.ts +@packages-user/data-state/src/replay/commands.test.ts +@packages-user/data-system/src/path/system.test.ts + + + +- Tests only. Do not modify any production file. +- Every `it(...)` is immediately preceded by a single-line Chinese comment (dev.md). +- Fixtures are inline and minimal (D-02/D-03); do not read real game data. +- Use `logger.catch` / `vi.spyOn(logger, ...)` for warning paths (never `expect(...).toThrow`). +- Suspected bugs: correct expectation + `it.skip`/`it.todo` + Chinese comment anchored `#06-02-N` into `06-TEST-FINDINGS.md`; `pnpm test:ci` stays green. + + + + + + Task 1: MainDamageCalculator + final effect + comparer + packages-user/data-state/src/enemy/calculator.test.ts, packages-user/data-state/src/enemy/final.test.ts, packages-user/data-state/src/enemy/comparer.test.ts + + packages-user/data-state/src/enemy/calculator.ts + packages-user/data-state/src/enemy/final.ts + packages-user/data-state/src/enemy/comparer.ts + packages-user/data-state/test/dataClosure.test.ts (existing calculator fixture and handler shape) + .planning/phases/06-unit-tests/06-PATTERNS.md (section A: calculator, final, comparer) + + +`calculator.test.ts`: build one inline handler factory (enemy, context, locator, hero, state) in the `dataClosure.test.ts` style and, for each branch of `MainDamageCalculator.calculate`, assert exact `{ damage, turn }`: +- baseline: heroPerDamage = atk - monDef; turn = ceil(monHp / heroPerDamage). +- 无敌 (20) returns `{ damage: Infinity, turn: 0 }` unless `hero.items.getItemState('cross')?.count >= 1`; assert both sides by faking `state.hero.items.getItemState`. +- 魔攻 (2) uses monAtk without subtracting hero def; 连击 4/5 multiply enemyPerDamage by 2/3; 多段 (6) multiplies by `value`. +- 支援 guard (guard set non-empty, not already inGuard): recursively adds the guard's turn/damage; missing guard locator emits warn 137 via `logger.catch`; assert `inGuard` does not leak (a second top-level calculate is unaffected). +- 先攻 (1) adds one enemyPerDamage; 破甲 (7) adds value%/100 * def; 反击 (8) adds value%/100 * atk to enemyPerDamage; 净化 (9) adds value * mdef. +- 吸血 (11): value%/100 * hp added to damage, and to monHp only when `add` is true. +- 负伤 flag: `state.flags.getFieldValueDefaults('enableNegativeDamage', false)` gates clamping negative damage to 0; 固伤 (22) and 仇恨 (17, `flags.getFieldValueDefaults('hatred', 0)`) bypass mdef. +- return value `damage` is floored. +- `getCriticalLimit('atk')`: 坚固 (3) returns Infinity, otherwise `def + hp`. + +`final.test.ts`: assert `MainEnemyFinalEffect.apply` raises def to `max(def, heroAtk - 1)` for 坚固 (3), copies hero atk/def for 模仿 (10), and leaves other enemies untouched; `priority` is 0. + +`comparer.test.ts`: assert `MainEnemyComparer.compare` is true for identical attributes+specials and false when any base attribute differs or when special sets differ in length/code/value (`deepEqualsTo`). + +Keep helper `createEnemy`/`createHandler` inline in each file (no cross-file fixture). If an expected value contradicts the implementation, follow the skip+findings rule. + + + pnpm exec vitest run packages-user/data-state/src/enemy/calculator.test.ts packages-user/data-state/src/enemy/final.test.ts packages-user/data-state/src/enemy/comparer.test.ts + the focused run fails, or a branch assertion does not match the documented arithmetic + + + - All three files pass with zero non-skipped failures. + - calculator.test.ts asserts every listed special branch and warn 137. + - final.test.ts and comparer.test.ts assert their documented branches. + - Every `it` has a preceding single-line Chinese comment. + + Calculator, final effect, and comparer behavior is covered and green. + + + + Task 2: Auras + special registration + packages-user/data-state/src/enemy/aura.test.ts, packages-user/data-state/src/enemy/special.test.ts + + packages-user/data-state/src/enemy/aura.ts + packages-user/data-state/src/enemy/special.ts + packages-user/data-base/src/enemy/special.ts (defineCommonSerializableSpecial / defineNonePropertySpecial) + packages-user/data-common/src/store/tileStore.test.ts (registry contract pattern) + .planning/phases/06-unit-tests/06-PATTERNS.md (section A: aura, special) + + +`aura.test.ts`: with inline minimal `ISpecial` objects and a fake enemy/context/locator: +- `CommonAuraConverter.shouldConvert` is true only for code 25. +- `CommonAura.createRange`/`getRangeParam`: `haloRange <= 0` uses full range (undefined param); `haloSquare` uses a rect centered on the locator; otherwise a Manhattan range. +- `CommonAura.apply` adds `floor(base * hpBuff/100)`, `floor(base * atkBuff/100)`, `floor(base * defBuff/100)` to the computed enemy, and no-ops when a buff is 0; `couldApplyBase` is true, `couldApplySpecial` false, `priority` 25. +- `GuardAuraConverter.shouldConvert` is true only for code 26; `GuardAura.getRangeParam` is the 3x3 rect; `apply` adds the guard locator to `enemy.getAttribute('guard')` for a different locator and skips the source's own locator; when `context.getViewByComputed` returns null, `apply` no-ops. + +`special.test.ts`: call `registerSpecials` on an inline fake `IEnemyManager` and assert every code 0-27 is registered exactly once with a `getSpecialName`/`getDescription` function, and `setAttributeDefaults('guard', ...)` was called; spot-check `getSpecialName` for a serializable special (e.g. code 6 returns `n连击` for value n; code 25 returns a halo description) and a none-property special (code 1 「先攻」). Use a minimal inline fake manager (no mock of data-layer interfaces). Keep fixtures inline. + + + pnpm exec vitest run packages-user/data-state/src/enemy/aura.test.ts packages-user/data-state/src/enemy/special.test.ts + the focused run fails, or a registration/buff assertion does not match the documented behavior + + + - Both files pass with zero non-skipped failures. + - aura.test.ts asserts range selection, buff math, and guard-locator behavior. + - special.test.ts asserts codes 0-27 registered once plus guard defaults and name/description spot checks. + - Every `it` has a preceding single-line Chinese comment. + + Aura conversion and the special registry are covered and green. + + + + Task 3: Map damage views, converter, reducer + packages-user/data-state/src/enemy/mapDamage.test.ts + + packages-user/data-state/src/enemy/mapDamage.ts + packages-user/data-state/src/enemy/special.ts (IZoneValue) + packages-user/data-state/src/enemy/types.ts (MapDamageType) + packages-user/data-system/src/combat/mapDamage.ts (IMapDamage* contracts) + .planning/phases/06-unit-tests/06-PATTERNS.md (section A: mapDamage) + + +Build an inline fake `IEnemyContext` (with `indexer`, `getEnemyByLoc`, `getDamageSystem`) and inline `ISpecial` values, then assert: +- `ZoneDamageView`: range is Manhattan or rect per `zoneSquare`; `getRangeParam` matches `range`/centering; `getDamageWithoutCheck` returns `{ damage: zone, type: MapDamageType.Zone, extra: {catch, repulse} }`. +- `RepulseDamageView`: Manhattan radius 1; returns null at the source locator; otherwise type `Repulse` with `repulse` containing the source locator. +- `LaserDamageView`: range param uses the face handler's `mapMovement` directions; damage type `Layer`. +- `BetweenDamageView`: only the forward-adjacent locator (delta sum 1 and both deltas positive) qualifies; requires the neighbor to have special 16; returns half of hero final hp; when `core.flags.betweenAttackMax` is true it uses the damage system's computed damages (fall back to half when the system is missing); otherwise returns half directly. +- `AmbushDamageView`: type `Unknown`, damage 0, catch contains the locator. +- `MainMapDamageConverter.convert` pushes a view per present special (15, 16, 18, 24, 27) and uses `state.faceManager.get(FaceGroup.Dir4)!` for the laser view. +- `MainMapDamageReducer.reduce`: sums damage, picks the type of the max-damage item, and unions `catch`/`repulse`. +Guard the `core.*` global reads with the shared global stub if the import chain requires it. Keep fixtures inline; if a `core`-dependent expectation is environment-sensitive, follow the skip+findings rule rather than weakening it. + + + pnpm exec vitest run packages-user/data-state/src/enemy/mapDamage.test.ts + the focused run fails, or a view/reducer assertion does not match the documented output + + + - `mapDamage.test.ts` passes with zero non-skipped failures. + - Asserts each of the five views, the converter's per-special view set, and the reducer's sum/type/extra union. + - Every `it` has a preceding single-line Chinese comment. + + Map damage views, converter, and reducer behavior is covered and green. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| none | In-repo Node Vitest tests only; no runtime input surface, network, DOM, or filesystem write is introduced. | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-06-02-01 | Tampering | tests under `packages-user/data-state/src/enemy` | low | mitigate | Only tracked in-repo modules are imported; fixtures are synthetic; no package install; gate `pnpm test:ci` plus review that only `*.test.ts` changed. | +| T-06-02-02 | Information disclosure | fixtures | low | accept | Synthetic numbers/ids only; no real game data or secrets (D-02). | +| T-06-02-03 | Denial of service | global monkey-patching in tests | low | accept | Any `vi.stubGlobal` is cleaned up in `afterAll`; tests do not run long loops or spawn processes. | +| T-06-02-SC | Tampering | package installs | high | mitigate | No new dependency; if an install becomes necessary, stop for a package-legitimacy checkpoint. | + + + +- Focused runs for all 6 files pass; `pnpm test:ci` full suite stays green. +- `git status` shows only new `*.test.ts`; no core file modified. +- Any `it.skip`/`it.todo` has a matching `#06-02-N` entry in `06-TEST-FINDINGS.md`. + + + +- 6 `data-state/src/enemy` behavior test files exist and pass. +- Every special branch of `MainDamageCalculator`, the aura converters, the special registry, and the map-damage views/converter/reducer are asserted. + + + +Test files: +- `packages-user/data-state/src/enemy/calculator.test.ts` +- `packages-user/data-state/src/enemy/final.test.ts` +- `packages-user/data-state/src/enemy/comparer.test.ts` +- `packages-user/data-state/src/enemy/aura.test.ts` +- `packages-user/data-state/src/enemy/special.test.ts` +- `packages-user/data-state/src/enemy/mapDamage.test.ts` + +Inline helper symbols: +- `calculator.test.ts`: `createHandler`, `createEnemy`, `special` +- `final.test.ts`: `createHandler`, `createEnemy` +- `comparer.test.ts`: `createEnemy` +- `aura.test.ts`: `createEnemy`, `createSpecial`, `createContext` +- `special.test.ts`: `FakeEnemyManager` +- `mapDamage.test.ts`: `createContext`, `createSpecial`, `FakeFaceHandler` + + + +Create `.planning/phases/06-unit-tests/06-02-SUMMARY.md` when done. Append suspected-bug findings under plan-scoped anchors `#06-02-N`. + diff --git a/.planning/phases/06-unit-tests/06-03-PLAN.md b/.planning/phases/06-unit-tests/06-03-PLAN.md new file mode 100644 index 0000000..9764b4e --- /dev/null +++ b/.planning/phases/06-unit-tests/06-03-PLAN.md @@ -0,0 +1,188 @@ +--- +phase: 06-unit-tests +plan: 03 +type: execute +wave: 1 +depends_on: [] +files_modified: + - packages-user/data-base/src/enemy/enemy.test.ts + - packages-user/data-base/src/enemy/special.test.ts + - packages-user/data-base/src/enemy/manager.test.ts +autonomous: true +requirements: [TEST-01] +estimate: + tokens: 42000 + raw_tokens: 42000 + tasks: 2 + confidence: low +must_haves: + truths: + - "FLAGGED ASSUMPTION (unverified): TEST-01 is treated as satisfied by behavior unit tests gated by `pnpm test:ci`; no ROADMAP edge verifier resolved it (spec-less probe unclassified/unresolved), so user confirmation is required." + - Enemy attribute get/set/add, special add/get/has/iterate/delete, clone/cloneAttributes/copyFrom, and saveState/loadState round-trip behave as documented + - CommonSerializableSpecial / NonePropertySpecial clone/save/load and deepEqualsTo behave as documented + - EnemyManager registry (special/attribute), prefab by code/id, reuse mapping, dirty tracking, and compareWith/comparer logic behave as documented + artifacts: + - path: packages-user/data-base/src/enemy/enemy.test.ts + provides: Enemy model coverage + - path: packages-user/data-base/src/enemy/special.test.ts + provides: special model coverage + - path: packages-user/data-base/src/enemy/manager.test.ts + provides: EnemyManager registry/dirty coverage + key_links: + - "enemy.test.ts extends the exact behavior already covered in `data-state/test/dataClosure.test.ts:104-118` (create/mutate/save/load) to the full `Enemy` surface" + - "manager.test.ts drives `EnemyManager` through an inline fake `IEnemyLegacyBridge` and inline `Enemy` instances, following the tileStore registry contract shape" + prohibitions: + - "FLAGGED/UNVERIFIED: no production/core source file is created or modified in this phase" + - "FLAGGED/UNVERIFIED: no coverage provider is installed and no coverage threshold is enforced" + - "FLAGGED/UNVERIFIED: no `fake-indexeddb` or CoreState end-to-end save/load path is introduced" + - "FLAGGED/UNVERIFIED: a suspected-bug test is never weakened into passing; it is written to the correct expectation and marked it.skip/it.todo" + - "FLAGGED/UNVERIFIED: no shared cross-file fixture/factory helper is created; fixtures stay inline per test file" + - "FLAGGED/UNVERIFIED: no fake timers are used for async action tests" +--- + + +Cover the Layer-1 enemy data model `packages-user/data-base/src/enemy` in full (D-14): `Enemy`, the special implementations, and `EnemyManager`. + +Phase constraints. D-05: a suspected bug becomes a test written to the correct expectation and marked `it.skip`/`it.todo` with a Chinese comment pointing into `06-TEST-FINDINGS.md`, keeping `pnpm test:ci` green; D-08: verification uses the fixed non-watch `pnpm test:ci` command. + +Purpose: D-14 requires the data model itself (attribute/special CRUD, save/load, dirty tracking, registry, prefab creation by code/id, reuse mapping, comparers) to be verified, not only the combat calculators that consume it. + +Output: 3 new co-located `*.test.ts` files. + + + +@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md +@C:/Users/book/.config/opencode/gsd-core/templates/summary.md + + + +@.planning/phases/06-unit-tests/06-CONTEXT.md +@.planning/phases/06-unit-tests/06-PATTERNS.md +@.planning/codebase/TESTING.md +@dev.md +@packages-user/data-base/src/enemy/enemy.ts +@packages-user/data-base/src/enemy/manager.ts +@packages-user/data-base/src/enemy/special.ts +@packages-user/data-base/src/enemy/types.ts +@packages-user/data-common/src/store/tileStore.test.ts +@packages-user/data-state/test/dataClosure.test.ts + + + +- Tests only. Do not modify any production file. +- Every `it(...)` is immediately preceded by a single-line Chinese comment (dev.md). +- Fixtures inline and minimal (D-02/D-03). Use `SaveCompression` for save/load. +- Warning paths via `logger.catch` / `vi.spyOn(logger, ...)`. +- Suspected bugs: correct expectation + `it.skip`/`it.todo` + Chinese comment anchored `#06-03-N`; `pnpm test:ci` stays green. + + + + + + Task 1: Enemy model + special implementations + packages-user/data-base/src/enemy/enemy.test.ts, packages-user/data-base/src/enemy/special.test.ts + + packages-user/data-base/src/enemy/enemy.ts + packages-user/data-base/src/enemy/special.ts + packages-user/data-base/src/enemy/types.ts + packages-user/data-state/test/dataClosure.test.ts (create/mutate/save/load enemy shape) + .planning/phases/06-unit-tests/06-PATTERNS.md (section A: enemy, special) + + +`enemy.test.ts`: construct `Enemy` with an inline `IEnemyAttr`-shaped object and cover: +- `getAttribute`/`setAttribute`/`addAttribute` on numeric keys; `cloneAttributes` returns a structured-cloned, non-aliased copy. +- special CRUD: `addSpecial` then `getSpecial`/`hasSpecial`/`iterateSpecials`; adding a duplicate code warns 96 and does not replace the existing special; `deleteSpecial` by object and by numeric code removes it; unknown code returns null. +- `clone`/`copyFrom`: clone copies attributes and cloned specials and is independent of the source; `copyFrom` replaces both attributes and specials. +- `saveState(compression)` / `loadState(state, compression)`: attribute round-trip; a special present on the instance but missing from the saved map warns 120 and is left unchanged; `saveState` returns a deep clone (mutating the returned attrs does not change the enemy). + +`special.test.ts`: use `defineCommonSerializableSpecial` / `defineNonePropertySpecial` with an inline config to cover `CommonSerializableSpecial` (setValue/getValue, getSpecialName, getDescription, fromLegacyEnemy via an inline legacy object, clone independence, saveState/loadState round-trip including structured-clone semantics, deepEqualsTo true for same code+equal value and false for code/value mismatch) and `NonePropertySpecial` (value undefined, clone, saveState returns undefined, loadState no-op, deepEqualsTo by code). + + + pnpm exec vitest run packages-user/data-base/src/enemy/enemy.test.ts packages-user/data-base/src/enemy/special.test.ts + the focused run fails, or the duplicate-special warn 96 / missing-special warn 120 is not observed + + + - Both files pass with zero non-skipped failures. + - enemy.test.ts asserts attribute/special CRUD, clone/copyFrom independence, save/load round-trip, warn 120, and the deep-clone contract of saveState. + - special.test.ts asserts both special classes' full surface. + - Every `it` has a preceding single-line Chinese comment. + + Enemy and special data models are covered and green. + + + + Task 2: EnemyManager registry, prefabs, reuse, dirty tracking + packages-user/data-base/src/enemy/manager.test.ts + + packages-user/data-base/src/enemy/manager.ts + packages-user/data-base/src/enemy/types.ts + packages-user/data-common/src/store/tileStore.test.ts + .planning/phases/06-unit-tests/06-PATTERNS.md (section A: manager) + + +Drive `EnemyManager` with an inline fake `IEnemyLegacyBridge` (its `fromLegacyEnemy(enemy, attrs)` returns synthetic attrs) and inline `Enemy` prefabs. Assert the registry contract in the tileStore style: +- `registerSpecial`/`registerSpecial` overwrite is allowed (last registration wins for the code); `setAttributeDefaults` sets a default and rejects function/symbol/bigint/undefined with `logger.error(53)`. +- `addPrefab` clones and ignores a duplicate code or id; `createEnemy(code)` / `createEnemyById(id)` return independent clones and null for unknown; `getPrefab`/`getPrefabById` return the stored prefab; `deletePrefab` by code and by id removes both indexes; `changePrefab` replaces and marks dirty. +- `reusePrefab(source, code, id)`: `getPrefab`/`getPrefabById` and `createEnemy` resolve through the reuse mapping to the source prefab; unknown source is a no-op. +- `modifyPrefabAttribute` applies the modify callback, re-indexes on changed code/id, and marks dirty; unknown code is a no-op. +- comparator + dirty: before any `compareWith`, dirty tracking is inert; `attachEnemyComparer` + first `compareWith(reference)` clears the dirty set; a second `compareWith` warns 117 and refreshes dirty; `updateDirty` without a comparer warns 118 and marks dirty; `refreshDirty` drops entries equal to the reference. +- `saveState(compression)` serializes only dirty codes; `loadState` warns 119 for a code missing from the prefab table and refreshes dirty afterwards. +Keep inline fixtures; assert warning codes via `logger.catch`. + + + pnpm exec vitest run packages-user/data-base/src/enemy/manager.test.ts + the focused run fails, or a registry/dirty assertion or a warning code (53/117/118/119) is not observed + + + - `manager.test.ts` passes with zero non-skipped failures. + - Asserts prefab create/get/delete/change, reuse mapping, modify, comparator dirty lifecycle, save/load, and warn codes 53/117/118/119. + - Every `it` has a preceding single-line Chinese comment. + + EnemyManager registry, prefab, reuse, and dirty behavior is covered and green. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| none | In-repo Node Vitest tests only; no runtime input surface, network, DOM, or filesystem write is introduced. | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-06-03-01 | Tampering | tests under `packages-user/data-base/src/enemy` | low | mitigate | Only tracked in-repo modules; synthetic fixtures; no package install; gate `pnpm test:ci` plus review that only `*.test.ts` changed. | +| T-06-03-02 | Information disclosure | fixtures | low | accept | Synthetic attrs/specials only; no real game data (D-02). | +| T-06-03-03 | Repudiation | global stub leakage | low | accept | Any `vi.stubGlobal` is torn down in `afterAll`; tests are independent. | +| T-06-03-SC | Tampering | package installs | high | mitigate | No new dependency; if an install becomes necessary, stop for a package-legitimacy checkpoint. | + + + +- Focused runs for all 3 files pass; `pnpm test:ci` stays green. +- `git status` shows only new `*.test.ts`; no core file modified. +- Any `it.skip`/`it.todo` has a matching `#06-03-N` entry in `06-TEST-FINDINGS.md`. + + + +- 3 `data-base/src/enemy` behavior test files exist and pass. +- D-14's full data-model surface (attributes/specials/save/load/dirty/registry/prefab/reuse/comparer) is asserted. + + + +Test files: +- `packages-user/data-base/src/enemy/enemy.test.ts` +- `packages-user/data-base/src/enemy/special.test.ts` +- `packages-user/data-base/src/enemy/manager.test.ts` + +Inline helper symbols: +- `enemy.test.ts`: `createEnemy`, `createSpecial` +- `special.test.ts`: `makeConfig`, `makeLegacyEnemy` +- `manager.test.ts`: `FakeBridge`, `createPrefab`, `createManager` + + + +Create `.planning/phases/06-unit-tests/06-03-SUMMARY.md` when done. Append suspected-bug findings under plan-scoped anchors `#06-03-N`. + diff --git a/.planning/phases/06-unit-tests/06-04-PLAN.md b/.planning/phases/06-unit-tests/06-04-PLAN.md new file mode 100644 index 0000000..d2102d7 --- /dev/null +++ b/.planning/phases/06-unit-tests/06-04-PLAN.md @@ -0,0 +1,235 @@ +--- +phase: 06-unit-tests +plan: 04 +type: execute +wave: 1 +depends_on: [] +files_modified: + - packages-user/data-common/src/replay/system.test.ts + - packages-user/data-common/src/replay/array.test.ts + - packages-user/data-common/src/replay/sandbox.test.ts + - packages-user/data-common/src/replay/func.test.ts + - packages-user/data-state/test/replayIntegration.test.ts +autonomous: true +requirements: [TEST-01] +estimate: + tokens: 54000 + raw_tokens: 54000 + tasks: 3 + confidence: low +must_haves: + truths: + - "FLAGGED ASSUMPTION (unverified): TEST-01 is treated as satisfied by behavior unit tests gated by `pnpm test:ci`; no ROADMAP edge verifier resolved it (spec-less probe unclassified/unresolved), so user confirmation is required." + - ReplayArray stores commands/params, expands within bounds, and streams reads with the configured command width + - ReplaySystem registers commands, rejects duplicates with warn 163, records to `route`, and dispatches hooks + - ReplaySandbox steps sequentially, stops on first failure (warns 156/157/158/175), and handles play/pause/resume/stop + - Replay safety decorators collect and end correctly (codes 159/161) + - An integration replay against the fixed contract stops on first divergence and reports index/code/params + artifacts: + - path: packages-user/data-common/src/replay/system.test.ts + provides: ReplaySystem coverage + - path: packages-user/data-common/src/replay/array.test.ts + provides: ReplayArray coverage + - path: packages-user/data-common/src/replay/sandbox.test.ts + provides: ReplaySandbox coverage + - path: packages-user/data-common/src/replay/func.test.ts + provides: replay-safety decorator coverage + - path: packages-user/data-state/test/replayIntegration.test.ts + provides: replay integration coverage + key_links: + - "sandbox.test.ts and replayIntegration.test.ts reuse the manual-stepping harness from `data-state/src/replay/commands.test.ts` and `data-state/test/nodeReplay.test.ts` (cast to a manual sandbox interface, drive `step()` directly)" + - "replayIntegration.test.ts cross-checks behavior against `.planning/phases/03-data-completion/03-REPLAY-CONTRACT.md` and `03-REPLAY-DIAGNOSTICS.md`" + prohibitions: + - "FLAGGED/UNVERIFIED: no production/core source file is created or modified in this phase" + - "FLAGGED/UNVERIFIED: no coverage provider is installed and no coverage threshold is enforced" + - "FLAGGED/UNVERIFIED: no `fake-indexeddb` or CoreState end-to-end save/load path is introduced" + - "FLAGGED/UNVERIFIED: a suspected-bug test is never weakened into passing; it is written to the correct expectation and marked it.skip/it.todo" + - "FLAGGED/UNVERIFIED: no shared cross-file fixture/factory helper is created; fixtures stay inline per test file" + - "FLAGGED/UNVERIFIED: no fake timers are used for async action tests" +--- + + +Cover the replay system `packages-user/data-common/src/replay` (array/system/sandbox/func) plus one integration path through `CoreState` (D-16). + +Phase constraints. D-04: async replay paths use real timers and await controller/sandbox completion (`await` controller `onEnd`), never fake timers, while pure computation asserts synchronously; D-05: a suspected bug becomes a test written to the correct expectation and marked `it.skip`/`it.todo` with a Chinese comment pointing into `06-TEST-FINDINGS.md`, keeping `pnpm test:ci` green; D-08: verification uses the fixed non-watch `pnpm test:ci` command. + +Purpose: D-16 requires the replay array read/write and the replay system itself to be verified against the existing contracts (`03-REPLAY-CONTRACT.md`, `03-REPLAY-DIAGNOSTICS.md`), including stopping and reporting the first divergence (Phase 3 D-07). `array.ts` (825 lines) currently has no dedicated test. + +Output: 4 new co-located `*.test.ts` files plus `data-state/test/replayIntegration.test.ts`. + + + +@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md +@C:/Users/book/.config/opencode/gsd-core/templates/summary.md + + + +@.planning/phases/06-unit-tests/06-CONTEXT.md +@.planning/phases/06-unit-tests/06-PATTERNS.md +@.planning/phases/03-data-completion/03-REPLAY-CONTRACT.md +@.planning/phases/03-data-completion/03-REPLAY-DIAGNOSTICS.md +@.planning/codebase/TESTING.md +@dev.md +@packages-user/data-common/src/replay/array.ts +@packages-user/data-common/src/replay/system.ts +@packages-user/data-common/src/replay/sandbox.ts +@packages-user/data-common/src/replay/func.ts +@packages-user/data-common/src/replay/types.ts +@packages-user/data-state/src/replay/commands.test.ts +@packages-user/data-state/test/nodeReplay.test.ts +@packages-user/data-state/test/dataClosure.test.ts + + + +- Tests only. Do not modify any production file. +- Every `it(...)` is immediately preceded by a single-line Chinese comment (dev.md). +- Fixtures inline and minimal (D-02/D-03); small replay arrays. +- Async paths use real timers; no fake timers (D-04). +- Warning paths via `logger.catch` / `vi.spyOn(logger, ...)`. +- Suspected bugs: correct expectation + `it.skip`/`it.todo` + Chinese comment anchored `#06-04-N`; `pnpm test:ci` stays green. + + + + + + Task 1: ReplaySystem + ReplayArray + packages-user/data-common/src/replay/system.test.ts, packages-user/data-common/src/replay/array.test.ts + + packages-user/data-common/src/replay/system.ts + packages-user/data-common/src/replay/array.ts + packages-user/data-common/src/replay/types.ts + packages-user/data-state/src/replay/commands.test.ts (registration order / per-instance independence) + .planning/phases/06-unit-tests/06-PATTERNS.md (section B: array, system) + + +`system.test.ts`: construct a `ReplaySystem` and assert: +- the constructor builds a `route` `ReplayArray` with the configured init lengths/width; `registerCommand` then `getCommand` returns the command and an unknown code returns null; a duplicate registration warns 163 and keeps the original; `record(code, ...params)` appends to `route` and fires `onRecordCommand` hooks with `(code, route.length, params)`; `createReplaySandbox({route, reseter, startIndex?})` calls `reseter.reset(config.save)`, stores `sandbox`, and fires `onCreateSandbox`; `releaseSandbox()` stops and clears it. +- each `ReplaySystem` instance has independent registrations and route. + +`array.test.ts`: build a small `ReplayArray` (use the same init sizes/width as `system.ts:33-41`) and assert through the public `IReplayArray` surface referenced in `types.ts`: `add(command, params)` then streaming `createReadStream(0)` reads each step in order; `length`/expiry behavior at the end; `commandWidth` is honored; `saveState`/`loadState` round-trip a small route; expansion beyond init capacity still reads back correctly. Keep arrays tiny. + + + pnpm exec vitest run packages-user/data-common/src/replay/system.test.ts packages-user/data-common/src/replay/array.test.ts + the focused run fails, or the duplicate-registration warn 163 / route round-trip assertion is not observed + + + - Both files pass with zero non-skipped failures. + - system.test.ts asserts registration, warn 163, record+hook, sandbox creation/reset, and per-instance independence. + - array.test.ts asserts add/stream/save/load over the public `IReplayArray` surface. + - Every `it` has a preceding single-line Chinese comment. + + ReplaySystem and ReplayArray behavior is covered and green. + + + + Task 2: ReplaySandbox + replay safety helpers + packages-user/data-common/src/replay/sandbox.test.ts, packages-user/data-common/src/replay/func.test.ts + + packages-user/data-common/src/replay/sandbox.ts + packages-user/data-common/src/replay/func.ts + packages-user/data-common/src/replay/types.ts + packages-user/data-state/src/replay/commands.test.ts (manual sandbox stepping) + .planning/phases/06-unit-tests/06-PATTERNS.md (section B: sandbox, func) + + +`sandbox.test.ts`: create a `ReplaySystem` with inline commands and drive the sandbox directly (cast to a manual sandbox interface as in `commands.test.ts`): +- `step()` executes the next command, fires `onStep`, and returns true on success; a failing command warns 158 with code+params and stops (later commands do not execute). +- `finalizeLast` runs `notExecuted` when the command changes or the reader expires; failure warns 175; an unknown last command warns 157; an expired reader warns 156. +- `play`/`pause`/`resume`/`stop`: `pause()` resolves when replay pauses; `resume()` continues from the paused point; `stop()` pauses and fires `onStopReplay`; `play()` is a no-op when already playing or ended; `ended` is set after the route finishes. +- `setSpeed` fires `onSpeedSet`. + +`func.test.ts`: cover the replay-safety decorator lifecycle from `func.ts` (`beginReplaySafetyCollection`, `endReplaySafetyCollection`, `logReplaySafetyDetail`, `shouldReplay`, `ignoreReplay`) using a minimal inline `IReplaySystem` and a decorated inline class/method; assert begin → nested decorated calls → end → detail behavior and the reset lifecycle (codes 159/161). Use `vi.spyOn(console/logger)` only where the source writes output. No fake timers — the `pause()`/`resume()` assertions use real promises. + + + pnpm exec vitest run packages-user/data-common/src/replay/sandbox.test.ts packages-user/data-common/src/replay/func.test.ts + the focused run fails, or a stop-on-failure warning (156/157/158/175) or a decorator lifecycle code (159/161) is not observed + + + - Both files pass with zero non-skipped failures. + - sandbox.test.ts asserts sequential stepping, first-failure stop, warn codes 156/157/158/175, and play/pause/resume/stop. + - func.test.ts asserts the decorator lifecycle and codes 159/161. + - Every `it` has a preceding single-line Chinese comment. + + ReplaySandbox and replay-safety decorators are covered and green. + + + + Task 3: Replay integration through CoreState + packages-user/data-state/test/replayIntegration.test.ts + + packages-user/data-state/test/nodeReplay.test.ts + packages-user/data-state/test/dataClosure.test.ts + packages-user/data-state/test/fixtures/closed-loop.ts + packages-user/data-state/src/replay/types.ts + .planning/phases/03-data-completion/03-REPLAY-CONTRACT.md + .planning/phases/03-data-completion/03-REPLAY-DIAGNOSTICS.md + .planning/phases/06-unit-tests/06-PATTERNS.md (section B: replayIntegration) + + +Create `data-state/test/replayIntegration.test.ts` that drives `createCoreState()` plus `ReplaySystem`/`ReplaySandbox` end-to-end through a small fixed replay: +- assert the eight stable commands are registered in order and every code resolves via `state.replaySystem.getCommand`. +- record a short valid route, create a sandbox, play it to completion with real timers (`await` the sandbox end/`waitForEnded` pattern), and assert the recorded route round-trips. +- first-divergence behavior (Phase 3 D-07 / D-16): a route whose second command fails must stop, keep later commands unexecuted, and emit warn 158 carrying the command code and params; cross-check against `03-REPLAY-DIAGNOSTICS.md`. +Inline minimal fixtures; reuse `createCoreState()` (Node-safe). Do not touch `saveSystem.init`/IndexedDB. If a contract mismatch is found, write the correct-expectation test and mark it skipped with a `#06-04-N` findings entry. + + + pnpm exec vitest run packages-user/data-state/test/replayIntegration.test.ts + the focused run fails, or the first-divergence report does not include index/code/params + + + - `replayIntegration.test.ts` passes with zero non-skipped failures. + - Asserts the eight-command order, a successful short replay, and stop-on-first-divergence with warn 158. + - Every `it` has a preceding single-line Chinese comment. + + Replay integration through CoreState is covered and green. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| none | In-repo Node Vitest tests only; no runtime input surface, network, DOM, or filesystem write is introduced. | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-06-04-01 | Tampering | tests under `data-common/src/replay` and `data-state/test` | low | mitigate | Only tracked in-repo modules; synthetic routes; no package install; gate `pnpm test:ci` plus review that only `*.test.ts` changed. | +| T-06-04-02 | Denial of service | replay loop termination | medium | mitigate | Every loop is bounded (bounded `step()` count / `waitForEnded` iteration cap) so a non-terminating replay cannot hang the suite; the existing 30s test timeout is the backstop. | +| T-06-04-03 | Information disclosure | fixtures | low | accept | Synthetic command arrays only; no real game data (D-02). | +| T-06-04-SC | Tampering | package installs | high | mitigate | No new dependency; if an install becomes necessary, stop for a package-legitimacy checkpoint. | + + + +- Focused runs for all 5 files pass; `pnpm test:ci` stays green. +- `git status` shows only new `*.test.ts`; no core file modified. +- Any `it.skip`/`it.todo` has a matching `#06-04-N` entry in `06-TEST-FINDINGS.md`. + + + +- 4 `data-common/src/replay` behavior test files plus `data-state/test/replayIntegration.test.ts` exist and pass. +- First-divergence stop-and-report behavior is asserted against the recorded contract. + + + +Test files: +- `packages-user/data-common/src/replay/system.test.ts` +- `packages-user/data-common/src/replay/array.test.ts` +- `packages-user/data-common/src/replay/sandbox.test.ts` +- `packages-user/data-common/src/replay/func.test.ts` +- `packages-user/data-state/test/replayIntegration.test.ts` + +Inline helper symbols: +- `system.test.ts`: `createSystem`, `createCommand` +- `array.test.ts`: `createArray`, `createRoute` +- `sandbox.test.ts`: `createSandbox`, `createCommand` +- `func.test.ts`: `FakeReplaySystem`, `DecoratedTarget` +- `replayIntegration.test.ts`: `waitForEnded`, `createFailingRoute` + + + +Create `.planning/phases/06-unit-tests/06-04-SUMMARY.md` when done. Append suspected-bug findings under plan-scoped anchors `#06-04-N`. + diff --git a/.planning/phases/06-unit-tests/06-05-PLAN.md b/.planning/phases/06-unit-tests/06-05-PLAN.md new file mode 100644 index 0000000..2a7c732 --- /dev/null +++ b/.planning/phases/06-unit-tests/06-05-PLAN.md @@ -0,0 +1,225 @@ +--- +phase: 06-unit-tests +plan: 05 +type: execute +wave: 1 +depends_on: [] +files_modified: + - packages-user/data-base/src/hero/attribute.test.ts + - packages-user/data-base/src/hero/modifier.test.ts + - packages-user/data-base/src/hero/location.test.ts + - packages-user/data-base/src/hero/state.test.ts +autonomous: true +requirements: [TEST-01] +estimate: + tokens: 46000 + raw_tokens: 46000 + tasks: 3 + confidence: low +must_haves: + truths: + - "FLAGGED ASSUMPTION (unverified): TEST-01 is treated as satisfied by behavior unit tests gated by `pnpm test:ci`; no ROADMAP edge verifier resolved it (spec-less probe unclassified/unresolved), so user confirmation is required." + - HeroAttribute computes base vs final values, sorts modifiers by priority, gates modifier saving, clones, and round-trips structured base values + - ValueModifier/PercentageModifier modify values by the documented formulas + - HeroLocation stores position/floor/face and round-trips its save state + - HeroState wires its subsystems, creates/registers modifiers, and save/loads the full hero snapshot with explicit key-field assertions + artifacts: + - path: packages-user/data-base/src/hero/attribute.test.ts + provides: HeroAttribute coverage + - path: packages-user/data-base/src/hero/modifier.test.ts + provides: modifier coverage + - path: packages-user/data-base/src/hero/location.test.ts + provides: HeroLocation coverage + - path: packages-user/data-base/src/hero/state.test.ts + provides: HeroState coverage + key_links: + - "state.test.ts extends the existing saveable-hero round-trip in `data-state/test/dataClosure.test.ts:169-179` with explicit key-field assertions (D-13) and modifier/follower recovery" + - "location.test.ts mirrors the minimal CoreState fixture in `data-state/src/event/event.test.ts` for setFloor/setPos/face getters" + prohibitions: + - "FLAGGED/UNVERIFIED: no production/core source file is created or modified in this phase" + - "FLAGGED/UNVERIFIED: no coverage provider is installed and no coverage threshold is enforced" + - "FLAGGED/UNVERIFIED: no `fake-indexeddb` or CoreState end-to-end save/load path is introduced" + - "FLAGGED/UNVERIFIED: a suspected-bug test is never weakened into passing; it is written to the correct expectation and marked it.skip/it.todo" + - "FLAGGED/UNVERIFIED: no shared cross-file fixture/factory helper is created; fixtures stay inline per test file" + - "FLAGGED/UNVERIFIED: no fake timers are used for async action tests" +--- + + +Cover the core hero data subsystems `packages-user/data-base/src/hero` — attribute, modifier, location, state (D-17). + +Phase constraints. D-05: a suspected bug becomes a test written to the correct expectation and marked `it.skip`/`it.todo` with a Chinese comment pointing into `06-TEST-FINDINGS.md`, keeping `pnpm test:ci` green; D-08: verification uses the fixed non-watch `pnpm test:ci` command. + +Purpose: D-17 requires each hero subsystem to be verified as runnable. This plan covers the attribute/modifier computation core, the location model, and the hero state aggregate/save envelope; the storage subsystems (equipment/equipStore/items/follower/mover) are covered by 06-06. + +Output: 4 new co-located `*.test.ts` files. + + + +@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md +@C:/Users/book/.config/opencode/gsd-core/templates/summary.md + + + +@.planning/phases/06-unit-tests/06-CONTEXT.md +@.planning/phases/06-unit-tests/06-PATTERNS.md +@.planning/codebase/TESTING.md +@dev.md +@packages-user/data-base/src/hero/attribute.ts +@packages-user/data-base/src/hero/modifier.ts +@packages-user/data-base/src/hero/location.ts +@packages-user/data-base/src/hero/state.ts +@packages-user/data-base/src/hero/types.ts +@packages-user/data-state/test/dataClosure.test.ts +@packages-user/data-state/src/event/event.test.ts +@packages-user/data-system/src/path/system.test.ts + + + +- Tests only. Do not modify any production file. +- Every `it(...)` is immediately preceded by a single-line Chinese comment (dev.md). +- Fixtures inline and minimal (D-02/D-03). +- Warning paths via `logger.catch` / `vi.spyOn(logger, ...)`. +- Suspected bugs: correct expectation + `it.skip`/`it.todo` + Chinese comment anchored `#06-05-N`; `pnpm test:ci` stays green. + + + + + + Task 1: HeroAttribute + modifiers + packages-user/data-base/src/hero/attribute.test.ts, packages-user/data-base/src/hero/modifier.test.ts + + packages-user/data-base/src/hero/attribute.ts + packages-user/data-base/src/hero/modifier.ts + packages-user/data-base/src/hero/types.ts + .planning/phases/06-unit-tests/06-PATTERNS.md (section C: attribute, modifier) + + +`attribute.test.ts`: with an inline numeric attribute object, cover: +- `getBaseAttribute` vs `getFinalAttribute`: base changes via `set`/`add`/`mul`/`div` and final reflects registered modifiers. +- `addModifier` sorts by descending `priority` and recomputes final; adding a modifier already owned by another attribute warns 108 and is ignored; `getModifiers`/`getModifierIndex`/`iterateModifiers` reflect registration; `deleteModifier`/`deleteModifierByIndex` unbind and remove. +- save-enabled gating: `addModifier(name, modifier, false)`, `setModifierSaveEnabled`, `getModifierSaveEnabled`. +- `markModifierDirty` recomputes only the owning attribute; an unbound modifier key is ignored. +- `clone({cloneModifier: false})` copies base only; default clone copies modifiers and recomputes; `getModifiableClone` is an independent clone; `toStructured` returns a detached structured clone. +- In `DEV` a modifier returning the same object reference warns 109 (assert via `logger.catch` on a modifier that returns the same object). + +`modifier.test.ts`: assert `ValueModifier.modify(value)` = `value + this.value` and `PercentageModifier.modify(value, base)` = `value + base * this.value`; default priorities (0 and 10); `type` strings; `clone` produces an equal independent modifier; `setValue` recomputes through the bound attribute and `saveState`/`loadState` round-trip the value. + + + pnpm exec vitest run packages-user/data-base/src/hero/attribute.test.ts packages-user/data-base/src/hero/modifier.test.ts + the focused run fails, or the warn 108/109 assertions are not observed + + + - Both files pass with zero non-skipped failures. + - attribute.test.ts asserts base/final computation, priority ordering, save gating, clone/structured behavior, and warn codes 108/109. + - modifier.test.ts asserts both modifier formulas and clone/save behavior. + - Every `it` has a preceding single-line Chinese comment. + + HeroAttribute and modifier behavior is covered and green. + + + + Task 2: HeroLocation model + packages-user/data-base/src/hero/location.test.ts + + packages-user/data-base/src/hero/location.ts + packages-user/data-base/src/hero/mover.ts (constructor face handling) + packages-user/data-state/src/event/event.test.ts (minimal CoreState location fixture) + .planning/phases/06-unit-tests/06-PATTERNS.md (section C: location) + + +Create `location.test.ts` with an inline fake `IDataCommon` and a `Dir8FaceHandler`: +- construct `HeroLocation` from an inline `IFacedTileLocator` and assert initial `x`/`y`/`floorId` (undefined) and `getCurrentFaceDirection()` matching the locator direction. +- `setPos(x, y)` updates coordinates; `setFloor(id)` updates the floor; `getCurrentFaceDirection` reflects `mover.setFaceDir`. +- hook `onSetPos`/`onSetFloor` fire with the new values. +- `saveState()` returns `{x, y, direction, floorId}` and `loadState` restores all four, including resetting `mover.faceDirection`. +Keep the fake state minimal (tileStore/itemStore/mapStore/eventStore/faceManager/directionMapper as needed). + + + pnpm exec vitest run packages-user/data-base/src/hero/location.test.ts + the focused run fails, or a save/load round-trip assertion does not restore a field + + + - `location.test.ts` passes with zero non-skipped failures. + - Asserts getters, setters, hooks, and full save/load round-trip. + - Every `it` has a preceding single-line Chinese comment. + + HeroLocation behavior is covered and green. + + + + Task 3: HeroState aggregate and save envelope + packages-user/data-base/src/hero/state.test.ts + + packages-user/data-base/src/hero/state.ts + packages-user/data-base/src/hero/types.ts + packages-user/data-state/test/dataClosure.test.ts (saveable hero round-trip) + .planning/phases/06-unit-tests/06-PATTERNS.md (section C: state) + + +Create `state.test.ts` using an inline fake `IDataCommon` and `Dir8FaceHandler` to construct `HeroState`: +- constructor wires `location`/`rendering`/`followers`/`items`/`equip` and starts at the default locator. +- `getModifiableAttribute`/`getAttribute`/`getIsolatedAttribute` return the shared attribute, a readonly view, and an independent clone respectively; `attachAttribute` swaps the attribute. +- `registerModifier` + `createModifier`/`createAndInsertModifier`: a registered type creates and inserts a modifier; an unknown type warns 116 and returns null. +- `changeFloor(info)` sets face/move direction, position, and floor, and fires before/after hooks in order. +- `saveState(compression)` then mutate key fields (attribute base value, location x/y/floor, a modifier value) then `loadState` restores them — assert only the explicit key fields (D-13), never a whole-object deep equality; a modifier with save disabled is not persisted; followers are cleared then re-added from the save. +Use `SaveCompression.NoCompression`. Keep fixtures inline. + + + pnpm exec vitest run packages-user/data-base/src/hero/state.test.ts + the focused run fails, or the save/load assertion uses a whole-object deep equality instead of explicit key fields + + + - `state.test.ts` passes with zero non-skipped failures. + - Asserts subsystem wiring, modifier registry (incl. warn 116), changeFloor hook order, and explicit key-field save/load with a save-disabled modifier excluded. + - Every `it` has a preceding single-line Chinese comment. + + HeroState aggregate and save envelope are covered and green. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| none | In-repo Node Vitest tests only; no runtime input surface, network, DOM, or filesystem write is introduced. | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-06-05-01 | Tampering | tests under `packages-user/data-base/src/hero` | low | mitigate | Only tracked in-repo modules; synthetic fixtures; no package install; gate `pnpm test:ci` plus review that only `*.test.ts` changed. | +| T-06-05-02 | Information disclosure | fixtures | low | accept | Synthetic attribute/location numbers only; no real game data (D-02). | +| T-06-05-03 | Repudiation | hook ordering assertions | low | accept | Tests assert documented ordering with plain arrays; no shared mutable global. | +| T-06-05-SC | Tampering | package installs | high | mitigate | No new dependency; if an install becomes necessary, stop for a package-legitimacy checkpoint. | + + + +- Focused runs for all 4 files pass; `pnpm test:ci` stays green. +- `git status` shows only new `*.test.ts`; no core file modified. +- Any `it.skip`/`it.todo` has a matching `#06-05-N` entry in `06-TEST-FINDINGS.md`. + + + +- 4 hero core behavior test files exist and pass. +- Attribute/modifier/location/state behavior including the save envelope is asserted with explicit key fields. + + + +Test files: +- `packages-user/data-base/src/hero/attribute.test.ts` +- `packages-user/data-base/src/hero/modifier.test.ts` +- `packages-user/data-base/src/hero/location.test.ts` +- `packages-user/data-base/src/hero/state.test.ts` + +Inline helper symbols: +- `attribute.test.ts`: `TestModifier`, `createAttribute` +- `modifier.test.ts`: `TestModifier`, `createAttribute` +- `location.test.ts`: `createState`, `createFaceHandler`, `createLocation` +- `state.test.ts`: `createState`, `createFaceHandler`, `createHeroState` + + + +Create `.planning/phases/06-unit-tests/06-05-SUMMARY.md` when done. Append suspected-bug findings under plan-scoped anchors `#06-05-N`. + diff --git a/.planning/phases/06-unit-tests/06-06-PLAN.md b/.planning/phases/06-unit-tests/06-06-PLAN.md new file mode 100644 index 0000000..d0c7cb1 --- /dev/null +++ b/.planning/phases/06-unit-tests/06-06-PLAN.md @@ -0,0 +1,233 @@ +--- +phase: 06-unit-tests +plan: 06 +type: execute +wave: 1 +depends_on: [] +files_modified: + - packages-user/data-base/src/hero/equipment.test.ts + - packages-user/data-base/src/hero/equipStore.test.ts + - packages-user/data-base/src/hero/items.test.ts + - packages-user/data-base/src/hero/follower.test.ts + - packages-user/data-base/src/hero/mover.test.ts +autonomous: true +requirements: [TEST-01] +estimate: + tokens: 56000 + raw_tokens: 56000 + tasks: 3 + confidence: low +must_haves: + truths: + - "FLAGGED ASSUMPTION (unverified): TEST-01 is treated as satisfied by behavior unit tests gated by `pnpm test:ci`; no ROADMAP edge verifier resolved it (spec-less probe unclassified/unresolved), so user confirmation is required." + - HeroEquipment equips/unequips/replaces, applies and removes equipment modifiers, and reports equip status; compareEquip diff is deterministic + - HeroEquipsStore adds/deletes/counts/sorts instances and round-trips save/load with uid continuity + - HeroItems adds/uses/counts constant and consumable items, routes equipment, and round-trips save/load + - HeroFollowersController adds/removes/gathers followers and HeroFollower links/reports neighbors and save/loads + - HeroMover moves asynchronously through its top implementation with the documented move codes + artifacts: + - path: packages-user/data-base/src/hero/equipment.test.ts + provides: HeroEquipment coverage + - path: packages-user/data-base/src/hero/equipStore.test.ts + provides: HeroEquipsStore coverage + - path: packages-user/data-base/src/hero/items.test.ts + provides: HeroItems coverage + - path: packages-user/data-base/src/hero/follower.test.ts + provides: follower coverage + - path: packages-user/data-base/src/hero/mover.test.ts + provides: HeroMover coverage + key_links: + - "equipment.test.ts follows the established equipment boundary (`equip`/`unequip`/`getEquipped`) already spied in `data-state/src/replay/commands.test.ts:200-253`" + - "mover.test.ts follows the abstract-mover pattern from `data-common/src/common/mover.test.ts` with real timers and `await controller.onEnd`" + prohibitions: + - "FLAGGED/UNVERIFIED: no production/core source file is created or modified in this phase" + - "FLAGGED/UNVERIFIED: no coverage provider is installed and no coverage threshold is enforced" + - "FLAGGED/UNVERIFIED: no `fake-indexeddb` or CoreState end-to-end save/load path is introduced" + - "FLAGGED/UNVERIFIED: a suspected-bug test is never weakened into passing; it is written to the correct expectation and marked it.skip/it.todo" + - "FLAGGED/UNVERIFIED: no shared cross-file fixture/factory helper is created; fixtures stay inline per test file" + - "FLAGGED/UNVERIFIED: no fake timers are used for async action tests" +--- + + +Cover the hero storage/movement subsystems `packages-user/data-base/src/hero` — equipment, equipStore, items, follower, mover (D-17). + +Phase constraints. D-04: async paths (the hero mover and async follower gather) use real timers and `await` controller `onEnd`, never fake timers, while pure computation asserts synchronously; D-05: a suspected bug becomes a test written to the correct expectation and marked `it.skip`/`it.todo` with a Chinese comment pointing into `06-TEST-FINDINGS.md`, keeping `pnpm test:ci` green; D-08: verification uses the fixed non-watch `pnpm test:ci` command. + +Purpose: D-17 requires each hero subsystem to run correctly. Equipment slotting, the equipment instance store, item add/use, follower management, and the asynchronous hero mover are the remaining untested subsystems after 06-05. + +Output: 5 new co-located `*.test.ts` files. + + + +@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md +@C:/Users/book/.config/opencode/gsd-core/templates/summary.md + + + +@.planning/phases/06-unit-tests/06-CONTEXT.md +@.planning/phases/06-unit-tests/06-PATTERNS.md +@.planning/codebase/TESTING.md +@dev.md +@packages-user/data-base/src/hero/equipment.ts +@packages-user/data-base/src/hero/equipStore.ts +@packages-user/data-base/src/hero/items.ts +@packages-user/data-base/src/hero/follower.ts +@packages-user/data-base/src/hero/mover.ts +@packages-user/data-base/src/hero/types.ts +@packages-user/data-state/src/replay/commands.test.ts +@packages-user/data-common/src/common/mover.test.ts + + + +- Tests only. Do not modify any production file. +- Every `it(...)` is immediately preceded by a single-line Chinese comment (dev.md). +- Fixtures inline and minimal (D-02/D-03). +- Async paths use real timers + `await controller.onEnd`; no fake timers (D-04). +- Warning paths via `logger.catch` / `vi.spyOn(logger, ...)`. +- Suspected bugs: correct expectation + `it.skip`/`it.todo` + Chinese comment anchored `#06-06-N`; `pnpm test:ci` stays green. + + + + + + Task 1: HeroEquipment + HeroEquipsStore + packages-user/data-base/src/hero/equipment.test.ts, packages-user/data-base/src/hero/equipStore.test.ts + + packages-user/data-base/src/hero/equipment.ts + packages-user/data-base/src/hero/equipStore.ts + packages-user/data-base/src/hero/types.ts + packages-user/data-state/src/replay/commands.test.ts (equip/unequip boundary) + .planning/phases/06-unit-tests/06-PATTERNS.md (section C: equipment, equipStore) + + +Build an inline fake `IDataCommon` whose `tileStore.num`/`idToNumber` and `itemStore.getData` return small synthetic equipment items (with `equip.value`/`equip.percentage` maps and `equip.slots`), plus an inline `HeroAttribute`. +`equipment.test.ts`: `setSlots`; `canEquipTo` returns `CanEquip`/`NeedReplace`/`CannotEquip` for numeric and string slots; `equip` onto an empty numeric slot returns undefined and applies modifiers so final attributes change; equipping over an occupied numeric slot returns the previous uid and replaces modifiers; equipping the same uid whose slot changes with `autoUnload` moves it; `unequip` returns the removed uid and removes its modifiers, returns undefined for an empty slot; `equipped`/`getEquipped`/`getEquips` reflect state; `compareEquip` returns the per-attribute diff; unknown uid paths warn 146 (assert via `logger.catch`); `saveState`/`loadState` restores slots and re-equips. +`equipStore.test.ts`: `add` returns a uid and -1 for an unknown tile; `get`/`count`/`delete`; `instancesOf`/`instances` sort by a supplied sorter and fall back to ascending uid; `useSorter(null)` restores uid ordering; `saveState`/`loadState` round-trips instances and continues `nextUid` after the max loaded uid; a missing item on load logs `logger.error(59)` (assert via `logger.catch`). + + + pnpm exec vitest run packages-user/data-base/src/hero/equipment.test.ts packages-user/data-base/src/hero/equipStore.test.ts + the focused run fails, or the modifier apply/remove assertion or warn/error code is not observed + + + - Both files pass with zero non-skipped failures. + - equipment.test.ts asserts equip/unequip/replace, modifier application, compareEquip, and warn 146. + - equipStore.test.ts asserts add/get/count/delete/sort, uid continuity after load, and error 59. + - Every `it` has a preceding single-line Chinese comment. + + Equipment and equip-store behavior is covered and green. + + + + Task 2: HeroItems + followers + packages-user/data-base/src/hero/items.test.ts, packages-user/data-base/src/hero/follower.test.ts + + packages-user/data-base/src/hero/items.ts + packages-user/data-base/src/hero/follower.ts + packages-user/data-base/src/hero/types.ts + .planning/phases/06-unit-tests/06-PATTERNS.md (section C: items, follower) + + +`items.test.ts`: inline fake `IDataCommon` itemStore/hero.items with synthetic constant/consumable/equipment/pick items: +- `addItem` increments a constant item count and deletes at <=0; `addItem` for equipment routes to the equip store; `addItem` for a Pick item invokes `effect.useEffect` per count; unknown tile/item is a no-op. +- `itemCount`/`getItemState`; `getItem` adds one; `useItem` returns false for a missing item or when `canUse` is false, applies `useEffect`, and decrements/deletes a consumable, leaves a constant; returns false for equipment. +- `saveState`/`loadState` round-trips constants/consumables/equipment with explicit key-field assertions. + +`follower.test.ts`: inline fake `IDataCommon` + `Dir8FaceHandler` + a `HeroLocation`: +- `addFollower` appends at the hero locator and fires `onAddFollower`; `getFollower`/`getAllFollowers`/`getFollowersById` (numeric and string id); `next`/`last` neighbor links. +- `removeFollower`/`removeAllFollowers` fire `onRemoveFollower` and clear the list. +- `gatherFollowers` (async, real timers) steps each follower and fires `onGatherFollowers(false)`; `gatherFollowersSync` snaps followers to the hero and fires `onGatherFollowers(true)`. +- `HeroFollower.saveState`/`loadState` round-trips rendering+location; a string follower id not found warns 142 (assert via `logger.catch`). + + + pnpm exec vitest run packages-user/data-base/src/hero/items.test.ts packages-user/data-base/src/hero/follower.test.ts + the focused run fails, or the async gather assertion does not complete through awaited movers + + + - Both files pass with zero non-skipped failures. + - items.test.ts asserts add/use/count/routing/save-load; follower.test.ts asserts add/remove/gather/links/save-load and warn 142. + - `gatherFollowers` uses real timers and awaits the gathered controllers. + - Every `it` has a preceding single-line Chinese comment. + + Item and follower behavior is covered and green. + + + + Task 3: HeroMover async movement + packages-user/data-base/src/hero/mover.test.ts + + packages-user/data-base/src/hero/mover.ts + packages-user/data-common/src/common/mover.ts (ObjectMover) + packages-user/data-common/src/common/mover.test.ts (abstract mover test pattern) + .planning/phases/06-unit-tests/06-PATTERNS.md (section C: mover) + + +Create `mover.test.ts` mirroring `data-common/src/common/mover.test.ts` but for `HeroMover`: +- build an inline `IHeroLocation`-shaped tile + `IDataCommon` fake and a `Dir8FaceHandler`. +- `config`/`getConfig` round-trips `noRoute`/`ignoreTerrain`/`autoSave`/`allowOutBound`. +- with an inline fake `IHeroMoveTopImpl`: a Step move writes the next location through `setPos`; `CannotMove` calls `cannotEnter` (when terrain not ignored) and stops without moving; `Hit` calls `hit` and stops; `allowOutBound` permits an out-of-bound step while the default blocks it; `ignoreTerrain` skips pass/hit/enter/leave callbacks. +- missing top implementation: `onMoveStart` warns 144 and `onStepStart` returns `HeroMoveCode.Stop` (assert via a subclass observable behavior). +- `onStepSettled` calls `leave` then `enter` with before/current locators when terrain triggering is on. +Use real timers + `await controller.onEnd`. Keep fixtures inline. + + + pnpm exec vitest run packages-user/data-base/src/hero/mover.test.ts + the focused run fails, or a move-code/position-writeback assertion does not match the implementation contract + + + - `mover.test.ts` passes with zero non-skipped failures. + - Asserts config round-trip, Step/CannotMove/Hit, out-of-bound handling, enter/leave ordering, and warn 144. + - Uses real timers and `await controller.onEnd`; no fake timers. + - Every `it` has a preceding single-line Chinese comment. + + HeroMover async movement behavior is covered and green. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| none | In-repo Node Vitest tests only; no runtime input surface, network, DOM, or filesystem write is introduced. | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-06-06-01 | Tampering | tests under `packages-user/data-base/src/hero` | low | mitigate | Only tracked in-repo modules; synthetic fixtures; no package install; gate `pnpm test:ci` plus review that only `*.test.ts` changed. | +| T-06-06-02 | Denial of service | async mover/gather loops | medium | mitigate | Async assertions await controller `onEnd` under the existing 30s test timeout; no unbounded polling. | +| T-06-06-03 | Information disclosure | fixtures | low | accept | Synthetic item/equipment numbers only; no real game data (D-02). | +| T-06-06-SC | Tampering | package installs | high | mitigate | No new dependency; if an install becomes necessary, stop for a package-legitimacy checkpoint. | + + + +- Focused runs for all 5 files pass; `pnpm test:ci` stays green. +- `git status` shows only new `*.test.ts`; no core file modified. +- Any `it.skip`/`it.todo` has a matching `#06-06-N` entry in `06-TEST-FINDINGS.md`. + + + +- 5 hero storage/movement behavior test files exist and pass. +- Equipment, equip store, items, followers, and the async hero mover are asserted. + + + +Test files: +- `packages-user/data-base/src/hero/equipment.test.ts` +- `packages-user/data-base/src/hero/equipStore.test.ts` +- `packages-user/data-base/src/hero/items.test.ts` +- `packages-user/data-base/src/hero/follower.test.ts` +- `packages-user/data-base/src/hero/mover.test.ts` + +Inline helper symbols: +- `equipment.test.ts`: `createState`, `createEnv`, `createItem` +- `equipStore.test.ts`: `createState`, `createEnv`, `createItem`, `FakeSorter` +- `items.test.ts`: `createState`, `createEnv`, `createItem` +- `follower.test.ts`: `createState`, `createLocation`, `createController` +- `mover.test.ts`: `TestTile`, `FakeTopImpl`, `createMover` + + + +Create `.planning/phases/06-unit-tests/06-06-SUMMARY.md` when done. Append suspected-bug findings under plan-scoped anchors `#06-06-N`. + diff --git a/.planning/phases/06-unit-tests/06-07-PLAN.md b/.planning/phases/06-unit-tests/06-07-PLAN.md new file mode 100644 index 0000000..7c6fbd4 --- /dev/null +++ b/.planning/phases/06-unit-tests/06-07-PLAN.md @@ -0,0 +1,225 @@ +--- +phase: 06-unit-tests +plan: 07 +type: execute +wave: 1 +depends_on: [] +files_modified: + - packages-user/data-base/src/map/gameMap.test.ts + - packages-user/data-base/src/map/mapState.test.ts + - packages-user/data-base/src/map/mapLayer.test.ts + - packages-user/data-base/src/map/tile.test.ts +autonomous: true +requirements: [TEST-01] +estimate: + tokens: 56000 + raw_tokens: 56000 + tasks: 3 + confidence: low +must_haves: + truths: + - "FLAGGED ASSUMPTION (unverified): TEST-01 is treated as satisfied by behavior unit tests gated by `pnpm test:ci`; no ROADMAP edge verifier resolved it (spec-less probe unclassified/unresolved), so user confirmation is required." + - MapState creates/looks up/iterates maps, validates raw map data, and honors active status/area activation + - GameMap manages layers, aliases, background, dirty state, and layer-only save aggregation + - MapLayer reads/writes blocks, manages static/dynamic tiles and point events, tracks dirty state, and resizes/crops correctly + - Static/dynamic tiles restore default events, expose their raw data, and round-trip their save state + artifacts: + - path: packages-user/data-base/src/map/gameMap.test.ts + provides: GameMap coverage + - path: packages-user/data-base/src/map/mapState.test.ts + provides: MapState coverage + - path: packages-user/data-base/src/map/mapLayer.test.ts + provides: MapLayer coverage + - path: packages-user/data-base/src/map/tile.test.ts + provides: static/dynamic tile coverage + key_links: + - "all four files reuse the inline `createMapState` fixture + `vi.hoisted` global stub shape from `data-base/src/map/mapLifecycle.test.ts` and assert all-compression key fields (D-13)" + - "mapState.test.ts mirrors `eventPath.test.ts` fromRaw validation and logger-warning paths" + prohibitions: + - "FLAGGED/UNVERIFIED: no production/core source file is created or modified in this phase" + - "FLAGGED/UNVERIFIED: no coverage provider is installed and no coverage threshold is enforced" + - "FLAGGED/UNVERIFIED: no `fake-indexeddb` or CoreState end-to-end save/load path is introduced" + - "FLAGGED/UNVERIFIED: a suspected-bug test is never weakened into passing; it is written to the correct expectation and marked it.skip/it.todo" + - "FLAGGED/UNVERIFIED: no shared cross-file fixture/factory helper is created; fixtures stay inline per test file" + - "FLAGGED/UNVERIFIED: no fake timers are used for async action tests" +--- + + +Expand map coverage in `packages-user/data-base/src/map` beyond the existing `eventPath`/`eventView`/`mapLifecycle` tests (D-18): `MapState`, `GameMap`, `MapLayer`, and the tile classes. + +Phase constraints. D-04: async paths (async door open/close) use real timers and `await` controller completion, never fake timers, while pure computation asserts synchronously; D-05: a suspected bug becomes a test written to the correct expectation and marked `it.skip`/`it.todo` with a Chinese comment pointing into `06-TEST-FINDINGS.md`, keeping `pnpm test:ci` green; D-08: verification uses the fixed non-watch `pnpm test:ci` command. + +Purpose: D-18 requires a fuller map-system pass (matrix, point events, layers, dirty flags, resize/crop). The existing tests cover point-event lifecycle and tile defaults; this plan covers map registry/activation, per-map layer/alias/background/dirty, the layer matrix and dynamic-tile index, and tile default-event restoration. + +Output: 4 new co-located `*.test.ts` files. + + + +@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md +@C:/Users/book/.config/opencode/gsd-core/templates/summary.md + + + +@.planning/phases/06-unit-tests/06-CONTEXT.md +@.planning/phases/06-unit-tests/06-PATTERNS.md +@.planning/codebase/TESTING.md +@dev.md +@packages-user/data-base/src/map/mapState.ts +@packages-user/data-base/src/map/gameMap.ts +@packages-user/data-base/src/map/mapLayer.ts +@packages-user/data-base/src/map/tile.ts +@packages-user/data-base/src/map/staticTile.ts +@packages-user/data-base/src/map/dynamicTile.ts +@packages-user/data-base/src/map/types.ts +@packages-user/data-base/src/map/mapLifecycle.test.ts +@packages-user/data-base/src/map/eventPath.test.ts +@packages-user/data-base/src/map/eventView.test.ts + + + +- Tests only. Do not modify any production file. +- Every `it(...)` is immediately preceded by a single-line Chinese comment (dev.md). +- Fixtures inline and minimal (D-02/D-03); use the `vi.hoisted` global stub + dynamic module bag when the import chain requires it. +- Warning paths via `logger.catch` / `vi.spyOn(logger, ...)`. +- Suspected bugs: correct expectation + `it.skip`/`it.todo` + Chinese comment anchored `#06-07-N`; `pnpm test:ci` stays green. + + + + + + Task 1: MapState registry and raw validation + packages-user/data-base/src/map/mapState.test.ts + + packages-user/data-base/src/map/mapState.ts + packages-user/data-base/src/map/types.ts + packages-user/data-base/src/map/eventPath.test.ts (fromRaw validation + warning paths) + .planning/phases/06-unit-tests/06-PATTERNS.md (section D: mapState) + + +Build the inline `createMapState` fixture (TileStore + FaceManager + RoleFaceBinder + DirectionMapper fake `IDataCommon`) from 06-PATTERNS.md and cover `MapState`: +- `createMap(id, w, h)` registers a map; `getMap` returns it and null for unknown; `fromRaw` builds a map from raw data and returns null for invalid raw (missing floor, invalid dimensions) with the documented logger warning. +- active/inactive iteration: `setMapActiveStatus`, `isMapActive`, `getActiveMap`, `iterateActiveMaps`, `iterateInactiveMaps`, `iterateAllMaps`; `fromRaw` maps start inactive. +- `setMapList` ordering and `getMap` resolution; `setArea`/`activeArea`/`deactiveArea`/`useAutoActivitor`/`notifyEnterFloor` change the active set as documented. +- `compareWith(ref)` marks each map against its reference (`Uint32Array` per zIndex), marking missing references dirty. +- `canPass`/`shouldHit` delegate to the active map/predicate as documented (use an inline fake predicate). + + + pnpm exec vitest run packages-user/data-base/src/map/mapState.test.ts + the focused run fails, or a fromRaw/activation/compareWith assertion does not match the documented behavior + + + - `mapState.test.ts` passes with zero non-skipped failures. + - Asserts create/get/fromRaw validation, active-state iteration, area activation, compareWith, and canPass/shouldHit delegation. + - Every `it` has a preceding single-line Chinese comment. + + MapState registry, validation, and activation behavior is covered and green. + + + + Task 2: GameMap layers, aliases, dirty, save aggregation + packages-user/data-base/src/map/gameMap.test.ts + + packages-user/data-base/src/map/gameMap.ts + packages-user/data-base/src/map/types.ts + packages-user/data-base/src/map/mapLifecycle.test.ts (save aggregation + compression loop) + .planning/phases/06-unit-tests/06-PATTERNS.md (section D: gameMap) + + +Build `GameMap` with an inline `IDataCommon` and cover: +- `addLayer`/`removeLayer`/`hasLayer` and the `onUpdateLayer` hook; `setLayerAlias`/`getLayerByAlias`/`getLayerAlias`, with a duplicate alias warning 84. +- `setBackground`/`getBackground` and the `onChangeBackground` hook; `setActiveStatus` toggles `active`. +- `setEventLayer`: rejects a layer not in `layerList` with warn 131; accepts an added layer; accepts null. +- `dirty()` is true when any layer or the map self-flag is dirty; `markDirty(true/false)`. +- `resizeLayer(w, h, keepBlock)`: calls `resize` when `keepBlock`, else `resize2`. +- `saveState(compression)`: omits empty layers via the `isEmptyLayerSave` rule and includes a layer that has only point events or only static/dynamic blocks; `loadState` restores background and layers and clears the self dirty flag. +- `compareWith(map of arrays)`: marks a layer dirty when its reference is absent and compares otherwise. + + + pnpm exec vitest run packages-user/data-base/src/map/gameMap.test.ts + the focused run fails, or the save-aggregation/empty-layer rule or warn 84/131 is not observed + + + - `gameMap.test.ts` passes with zero non-skipped failures. + - Asserts layer/alias lifecycle, background/event-layer, dirty, resize, save aggregation, and warn codes 84/131. + - Every `it` has a preceding single-line Chinese comment. + + GameMap layer/alias/dirty/save behavior is covered and green. + + + + Task 3: MapLayer matrix + tiles (static/dynamic default events) + packages-user/data-base/src/map/mapLayer.test.ts, packages-user/data-base/src/map/tile.test.ts + + packages-user/data-base/src/map/mapLayer.ts + packages-user/data-base/src/map/tile.ts + packages-user/data-base/src/map/staticTile.ts + packages-user/data-base/src/map/dynamicTile.ts + packages-user/data-base/src/map/types.ts + packages-user/data-base/src/map/mapLifecycle.test.ts + .planning/phases/06-unit-tests/06-PATTERNS.md (section D: mapLayer, tile) + + +`mapLayer.test.ts` (fill beyond `mapLifecycle.test.ts`): `setBlock`/`getBlock`/`removeBlock` and `inMap`; `getMapData`/`putMapData`/`setMapRef`/`getMapRef` matrix read/write; `createDynamic` indexes the tile and `getDynamicTilesAt`/`iterateDynamicTiles`/`updateDynamicTile` reflect it; `transferToDynamic`/`transferToStatic`/`transferToStaticIfSafe` preserve or discard events per the keep flag; `getLocationData` resolves a locator to a tile; `setZIndex` and `setFaceBinder`; `event`/`getPointEvent` point-event access; `dirty`/`markDirty`/`compareWith`; `openDoor`/`closeDoor` async completion with real timers. Reuse the inline `createMapState` fixture; keep all-compression save assertions to explicit key fields (D-13). + +`tile.test.ts`: for a static and a dynamic tile: `num`/`raw`/`set` reflect the tile store data; changing `set` restores that tile's default `events` and marks the tile event view pure; `tileEvent()` is the tile's own view (dirty false after a default-restoring change) and `pointEvent()` returns the layer's point-event view for the tile's locator (or null out of map); `setFaceDirection` maps a direction through the face binder and returns the resulting num; `saveState`/`loadState` round-trip a dynamic tile's raw num and overridden events, and a static tile round-trips through the layer. Hotspot the default-event restoration into a dedicated `it`. + + + pnpm exec vitest run packages-user/data-base/src/map/mapLayer.test.ts packages-user/data-base/src/map/tile.test.ts + the focused run fails, or the default-event restoration / dynamic-tile index assertion does not match the documented behavior + + + - Both files pass with zero non-skipped failures. + - mapLayer.test.ts asserts matrix read/write, dynamic-tile index, transfer semantics, point events, dirty/compareWith, and async doors. + - tile.test.ts asserts default-event restoration, point-event linkage, face direction, and save/load. + - Every `it` has a preceding single-line Chinese comment. + + MapLayer matrix/tiles and tile default-event behavior is covered and green. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| none | In-repo Node Vitest tests only; no runtime input surface, network, DOM, or filesystem write is introduced. | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-06-07-01 | Tampering | tests under `packages-user/data-base/src/map` | low | mitigate | Only tracked in-repo modules; synthetic maps; no package install; gate `pnpm test:ci` plus review that only `*.test.ts` changed. | +| T-06-07-02 | Denial of service | large map matrices | low | mitigate | Fixtures use tiny 1x1..3x2 matrices, keeping runtime negligible. | +| T-06-07-03 | Information disclosure | fixtures | low | accept | Synthetic tile ids/events only; no real game data (D-02). | +| T-06-07-SC | Tampering | package installs | high | mitigate | No new dependency; if an install becomes necessary, stop for a package-legitimacy checkpoint. | + + + +- Focused runs for all 4 files pass; `pnpm test:ci` stays green. +- `git status` shows only new `*.test.ts`; no core file modified. +- Any `it.skip`/`it.todo` has a matching `#06-07-N` entry in `06-TEST-FINDINGS.md`. + + + +- 4 map behavior test files exist and pass. +- D-18's fuller map coverage (matrix, point events, layers, dirty, resize/crop, tile defaults) is asserted. + + + +Test files: +- `packages-user/data-base/src/map/mapState.test.ts` +- `packages-user/data-base/src/map/gameMap.test.ts` +- `packages-user/data-base/src/map/mapLayer.test.ts` +- `packages-user/data-base/src/map/tile.test.ts` + +Inline helper symbols: +- `mapState.test.ts`: `createMapState`, `createRawMap` +- `gameMap.test.ts`: `createGameMap`, `createState`, `createLayer` +- `mapLayer.test.ts`: `createMapState`, `createLayerFixture` +- `tile.test.ts`: `createMapState`, `createTiles` + + + +Create `.planning/phases/06-unit-tests/06-07-SUMMARY.md` when done. Append suspected-bug findings under plan-scoped anchors `#06-07-N`. + diff --git a/.planning/phases/06-unit-tests/06-08-PLAN.md b/.planning/phases/06-unit-tests/06-08-PLAN.md new file mode 100644 index 0000000..522d400 --- /dev/null +++ b/.planning/phases/06-unit-tests/06-08-PLAN.md @@ -0,0 +1,186 @@ +--- +phase: 06-unit-tests +plan: 08 +type: execute +wave: 1 +depends_on: [] +files_modified: + - packages-user/data-state/test/combatClosure.test.ts + - packages-user/data-state/test/saveablesRoundTrip.test.ts +autonomous: true +requirements: [TEST-01] +estimate: + tokens: 42000 + raw_tokens: 42000 + tasks: 2 + confidence: low +must_haves: + truths: + - "FLAGGED ASSUMPTION (unverified): TEST-01 is treated as satisfied by behavior unit tests gated by `pnpm test:ci`; no ROADMAP edge verifier resolved it (spec-less probe unclassified/unresolved), so user confirmation is required." + - A full battle path through a real `CoreState` (enemyManager + EnemyContext + CombatFlow + MainDamageCalculator) produces deterministic damage for a synthetic enemy + - All 4 registered saveables (@system/hero, @system/flags, @system/maps, @system/enemy) round-trip same-instance across NoCompression/LowCompression/HighCompression with explicit key-field assertions + artifacts: + - path: packages-user/data-state/test/combatClosure.test.ts + provides: combat integration through CoreState coverage + - path: packages-user/data-state/test/saveablesRoundTrip.test.ts + provides: saveables round-trip coverage + key_links: + - "combatClosure.test.ts extends the existing `data-state/test/dataClosure.test.ts` shape (real `createCoreState`, `enemyManager.addPrefab`, `MainDamageCalculator`) to the context+flow path" + - "saveablesRoundTrip.test.ts enters through `CoreState.getSaveableContent(id)` (data-state/src/core.ts:486) for the ids registered at core.ts:237-240 and never touches `saveSystem.init`/IndexedDB (D-10)" + prohibitions: + - "FLAGGED/UNVERIFIED: no production/core source file is created or modified in this phase" + - "FLAGGED/UNVERIFIED: no coverage provider is installed and no coverage threshold is enforced" + - "FLAGGED/UNVERIFIED: no `fake-indexeddb` or CoreState end-to-end save/load path is introduced" + - "FLAGGED/UNVERIFIED: a suspected-bug test is never weakened into passing; it is written to the correct expectation and marked it.skip/it.todo" + - "FLAGGED/UNVERIFIED: no shared cross-file fixture/factory helper is created; fixtures stay inline per test file" + - "FLAGGED/UNVERIFIED: no fake timers are used for async action tests" +--- + + +Cover the two integration seams that cross layers: the combat closure through `CoreState`, and saveables-level same-instance round-trip for all 4 registered saveables across all 3 compression levels (D-01, D-10..D-13, D-15). + +Phase constraints. D-05: a suspected bug becomes a test written to the correct expectation and marked `it.skip`/`it.todo` with a Chinese comment pointing into `06-TEST-FINDINGS.md`, keeping `pnpm test:ci` green; D-08: verification uses the fixed non-watch `pnpm test:ci` command; D-11: the saveables round-trip covers the 4 registered ids (`@system/hero`, `@system/flags`, `@system/maps`, `@system/enemy`) across all compression levels (`NoCompression`/`LowCompression`/`HighCompression`); D-12: the round-trip is same-instance (`saveState` then keep mutating then `loadState`, so state returns to the save point). + +Purpose: this is the only phase work that validates a multi-system wiring (combat context + flow + calculator) and the complete saveables contract, without introducing a CoreState end-to-end save/load path or IndexedDB. + +Output: 2 new integration test files under `data-state/test/`. + + + +@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md +@C:/Users/book/.config/opencode/gsd-core/templates/summary.md + + + +@.planning/phases/06-unit-tests/06-CONTEXT.md +@.planning/phases/06-unit-tests/06-PATTERNS.md +@.planning/codebase/TESTING.md +@dev.md +@packages-user/data-state/src/core.ts +@packages-user/data-state/test/dataClosure.test.ts +@packages-user/data-state/test/fixtures/closed-loop.ts +@packages-user/data-system/src/combat/context.ts +@packages-user/data-system/src/combat/combat.ts +@packages-user/data-state/src/enemy/calculator.ts +@packages-user/data-common/src/save/types.ts + + + +- Tests only. Do not modify any production file. +- Every `it(...)` is immediately preceded by a single-line Chinese comment (dev.md). +- Fixtures inline and minimal (D-02/D-03); reuse `createCoreState()`. +- Async paths use real timers + awaited sandbox/controller completion; no fake timers (D-04). +- Suspected bugs: correct expectation + `it.skip`/`it.todo` + Chinese comment anchored `#06-08-N`; `pnpm test:ci` stays green. + + + + + + Task 1: Combat closure through CoreState + packages-user/data-state/test/combatClosure.test.ts + + packages-user/data-state/test/dataClosure.test.ts + packages-user/data-system/src/combat/context.ts + packages-user/data-system/src/combat/combat.ts + packages-user/data-state/src/enemy/calculator.ts + .planning/phases/06-unit-tests/06-PATTERNS.md (section A: combatClosure) + + +Create `combatClosure.test.ts` driving the full combat path through a real `createCoreState()`: +- add a synthetic prefab via `state.enemyManager.addPrefab(...)` with inline enemy attribute helpers (mirror the `dataClosure.test.ts` inline `createEnemy`), then create an enemy and register it in `state.enemyContext` with `resize` + `setEnemyAt`. +- bind the hero: use `state.hero` (or an inline fake `IReadonlyHeroAttribute`) via `state.enemyContext.bindHero(...)` and `state.damageSystem`/`state.combatFlow` if exposed by `CoreState`; otherwise bind `CombatFlow`/`DamageSystem` directly against `state.enemyContext`. +- assert `enemyContext.getEnemyByLocator`/`getEnemyLocatorByView`/`getViewByComputed` resolve the registered enemy. +- run `combatFlow.battle(view)` (or `battleComputed`) and assert `MainDamageCalculator` via the production `DamageSystem`: deterministic `damage`/`turn`, and that the returned `info.handler` references the computed enemy + real locator + bound hero. +- cover one guard/support scenario end-to-end if the context supports it, asserting warn 137 for a missing guard. +Use real `CoreState` wiring; do not re-implement the calculator. If a wiring assumption is wrong (an interface is not exposed), STOP and record it as a `#06-08-N` finding with a skipped correct-expectation test rather than editing core code. + + + pnpm exec vitest run packages-user/data-state/test/combatClosure.test.ts + the focused run fails, or the handler identity / deterministic damage assertion does not hold + + + - `combatClosure.test.ts` passes with zero non-skipped failures. + - Asserts context lookup, a real battle through the production damage path, handler identity, and one guard path (or a recorded skip if unsupported). + - Every `it` has a preceding single-line Chinese comment. + + The combat path is proven end-to-end through CoreState and green. + + + + Task 2: Saveables same-instance round-trip (4 ids × 3 compressions) + packages-user/data-state/test/saveablesRoundTrip.test.ts + + packages-user/data-state/src/core.ts (addSaveableContent/getSaveableContent, ids @system/hero|flags|maps|enemy) + packages-user/data-state/test/fixtures/closed-loop.ts (snapshot envelope) + packages-user/data-base/src/map/mapLifecycle.test.ts (all-compression loop + explicit key fields) + .planning/phases/06-unit-tests/06-PATTERNS.md (section E: saveablesRoundTrip) + + +Create `saveablesRoundTrip.test.ts` through `CoreState.getSaveableContent(id)` only: +- for each of the 4 registered ids (`@system/hero`, `@system/flags`, `@system/maps`, `@system/enemy`) and each `SaveCompression` (`NoCompression`, `LowCompression`, `HighCompression`): + - set a known key field on the saveable to a known value; + - call `getSaveableContent(id)!.saveState(compression)`; + - mutate the key field to a different value; + - call `loadState(snapshot, compression)`; + - assert ONLY the explicit key fields (D-13): hero base attribute value, flag field value, a map layer block/point-event value, an enemy prefab attribute value. Never assert a whole-object deep equality, derived fields, caches, or the serialization container shape. + - additionally assert the snapshot is detached (mutating the live object after save does not change the snapshot's key field). +- cover `getSaveableContent` returning null for a non-registered id. + - Keep `CoreState` construction Node-safe via `createCoreState()`; do NOT call `saveSystem.init` and do NOT add `fake-indexeddb` (D-10). + - The 4 ids x 3 compressions matrix is D-11, and the same-instance form (`saveState` then keep mutating then `loadState`, asserting the state returns to the save point) is D-12. + + + pnpm exec vitest run packages-user/data-state/test/saveablesRoundTrip.test.ts + the focused run fails, or a round-trip assertion for any id/compression pair is missing or uses a whole-object deep equality + + + - `saveablesRoundTrip.test.ts` passes with zero non-skipped failures. + - Covers all 4 ids × 3 compressions with explicit key-field assertions and a snapshot-detachment check. + - Asserts `getSaveableContent` null for an unknown id; no IndexedDB and no `fake-indexeddb`. + - Every `it` has a preceding single-line Chinese comment. + + All registered saveables round-trip same-instance across all compressions and are green (D-11, D-12). + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| none | In-repo Node Vitest tests only; no runtime input surface, network, DOM, or filesystem write is introduced. | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-06-08-01 | Tampering | tests under `data-state/test` | low | mitigate | Only tracked in-repo modules; synthetic fixtures; no package install; gate `pnpm test:ci` plus review that only `*.test.ts` changed. | +| T-06-08-02 | Denial of service | asynchronous closure replay | medium | mitigate | Any replay/await loop is bounded; the existing 30s test timeout is the backstop. | +| T-06-08-03 | Information disclosure | fixtures | low | accept | Synthetic state only; no real save files or secrets (D-02). | +| T-06-08-SC | Tampering | package installs | high | mitigate | No new dependency, explicitly no `fake-indexeddb`; if an install becomes necessary, stop for a package-legitimacy checkpoint. | + + + +- Focused runs for both files pass; `pnpm test:ci` stays green. +- `git status` shows only new `*.test.ts`; no core file modified. +- Any `it.skip`/`it.todo` has a matching `#06-08-N` entry in `06-TEST-FINDINGS.md`. + + + +- 2 integration test files exist and pass. +- Combat closure and the 4×3 saveables round-trip are asserted with explicit key fields. + + + +Test files: +- `packages-user/data-state/test/combatClosure.test.ts` +- `packages-user/data-state/test/saveablesRoundTrip.test.ts` + +Inline helper symbols: +- `combatClosure.test.ts`: `createEnemy`, `addEnemy`, `createHandler` +- `saveablesRoundTrip.test.ts`: `SAVEABLE_IDS`, `COMPRESSIONS`, `mutateKeyField` + + + +Create `.planning/phases/06-unit-tests/06-08-SUMMARY.md` when done. Append suspected-bug findings under plan-scoped anchors `#06-08-N`. + diff --git a/.planning/phases/06-unit-tests/06-09-PLAN.md b/.planning/phases/06-unit-tests/06-09-PLAN.md new file mode 100644 index 0000000..22ca93a --- /dev/null +++ b/.planning/phases/06-unit-tests/06-09-PLAN.md @@ -0,0 +1,220 @@ +--- +phase: 06-unit-tests +plan: 09 +type: execute +wave: 1 +depends_on: [] +files_modified: + - packages-user/data-base/src/flag/system.test.ts + - packages-user/data-common/src/common/utils.test.ts + - packages-user/data-common/src/common/indexer.test.ts + - packages-user/data-common/src/common/faceManager.test.ts +autonomous: true +requirements: [TEST-01] +estimate: + tokens: 40000 + raw_tokens: 40000 + tasks: 3 + confidence: low +must_haves: + truths: + - "FLAGGED ASSUMPTION (unverified): TEST-01 is treated as satisfied by behavior unit tests gated by `pnpm test:ci`; no ROADMAP edge verifier resolved it (spec-less probe unclassified/unresolved), so user confirmation is required." + - FlagSystem exposes its full public surface (occupied/insert/get/getOrInsert/getOrInsertComputed/delete/set/add/get/defaults) and save/load round-trips field values + - getFaceMovement/degradeFace/nextFaceDirection/fromDirectionString return the documented directions + - MapLocIndexer converts location<->index consistently for a configured width + - FaceManager and the Dir4/Dir8 handlers implement degrade/movement/move/opposite/next/mapDirection/mapMovement + artifacts: + - path: packages-user/data-base/src/flag/system.test.ts + provides: FlagSystem coverage + - path: packages-user/data-common/src/common/utils.test.ts + provides: face utility coverage + - path: packages-user/data-common/src/common/indexer.test.ts + provides: MapLocIndexer coverage + - path: packages-user/data-common/src/common/faceManager.test.ts + provides: FaceManager + handlers coverage + key_links: + - "flag/system.test.ts extends the existing flag round-trip in `data-state/test/dataClosure.test.ts:120-132` to the full public surface; note flag `saveState()` takes no compression argument" + - "the common tests only exercise helpers the main suites do not (D-20); where a helper is already covered through combat/map/hero, keep the direct test minimal" + prohibitions: + - "FLAGGED/UNVERIFIED: no production/core source file is created or modified in this phase" + - "FLAGGED/UNVERIFIED: no coverage provider is installed and no coverage threshold is enforced" + - "FLAGGED/UNVERIFIED: no `fake-indexeddb` or CoreState end-to-end save/load path is introduced" + - "FLAGGED/UNVERIFIED: a suspected-bug test is never weakened into passing; it is written to the correct expectation and marked it.skip/it.todo" + - "FLAGGED/UNVERIFIED: no shared cross-file fixture/factory helper is created; fixtures stay inline per test file" + - "FLAGGED/UNVERIFIED: no fake timers are used for async action tests" +--- + + +Cover the two auxiliary modules: the global flag system `packages-user/data-base/src/flag` (D-19) and the common helpers `packages-user/data-common/src/common` (D-20). + +Phase constraints. D-05: a suspected bug becomes a test written to the correct expectation and marked `it.skip`/`it.todo` with a Chinese comment pointing into `06-TEST-FINDINGS.md`, keeping `pnpm test:ci` green; D-08: verification uses the fixed non-watch `pnpm test:ci` command. + +Purpose: D-19 asks for the flag system's public surface (many interfaces, simple logic). D-20 asks that `common` be covered naturally by the main suites, adding minimal direct tests only for helpers the main suites do not touch (`getFaceMovement`/`degradeFace`/`nextFaceDirection`/`fromDirectionString`, `MapLocIndexer`, `FaceManager`). + +Output: 4 new co-located `*.test.ts` files. + + + +@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md +@C:/Users/book/.config/opencode/gsd-core/templates/summary.md + + + +@.planning/phases/06-unit-tests/06-CONTEXT.md +@.planning/phases/06-unit-tests/06-PATTERNS.md +@.planning/codebase/TESTING.md +@dev.md +@packages-user/data-base/src/flag/system.ts +@packages-user/data-base/src/flag/field.ts +@packages-user/data-base/src/flag/types.ts +@packages-user/data-common/src/common/utils.ts +@packages-user/data-common/src/common/indexer.ts +@packages-user/data-common/src/common/faceManager.ts +@packages-user/data-common/src/common/mover.test.ts +@packages-user/data-state/test/dataClosure.test.ts + + + +- Tests only. Do not modify any production file. +- Every `it(...)` is immediately preceded by a single-line Chinese comment (dev.md). +- Fixtures inline and minimal (D-02/D-03). +- Warning paths via `logger.catch` / `vi.spyOn(logger, ...)`. +- Suspected bugs: correct expectation + `it.skip`/`it.todo` + Chinese comment anchored `#06-09-N`; `pnpm test:ci` stays green. + + + + + + Task 1: FlagSystem full public surface + packages-user/data-base/src/flag/system.test.ts + + packages-user/data-base/src/flag/system.ts + packages-user/data-base/src/flag/field.ts + packages-user/data-base/src/flag/types.ts + packages-user/data-state/test/dataClosure.test.ts (flag round-trip) + .planning/phases/06-unit-tests/06-PATTERNS.md (section F: flag) + + +Create `flag/system.test.ts` (include the `vi.hoisted` `Map.getOrInsertComputed` polyfill when the import chain requires it) covering the full `FlagSystem` surface: +- `occupied` false then true after `insertField`/`setFieldValue`. +- `insertField` returns a `FlagCommonField` whose `get`/`set`/`toStructured` behave as documented; `getField` returns null for an unknown field; `getOrInsert` inserts a default and returns the same instance on the next call. +- `getOrInsertComputed` receives the key and inserts a computed default; `deleteField` removes it. +- `setFieldValue` overwrites; `addFieldValue` adds to a numeric field and warns 111 when the current value is not a number (assert via `logger.catch`); `getFieldValue` returns undefined for an unknown field; `getFieldValueDefaults` returns the default and inserts it. +- `saveState()` takes no compression argument; round-trip: set a known value, save, mutate, load, and assert the explicit key field plus `occupied` (D-13). Mutate the live field after save and assert the snapshot is detached. + + + pnpm exec vitest run packages-user/data-base/src/flag/system.test.ts + the focused run fails, or the warn 111 / save-load key-field assertion is not observed + + + - `system.test.ts` passes with zero non-skipped failures. + - Asserts the full public surface listed in D-19, warn 111, and an explicit-key-field save/load round-trip. + - Every `it` has a preceding single-line Chinese comment. + + FlagSystem behavior is covered and green. + + + + Task 2: Face utilities + location indexer + packages-user/data-common/src/common/utils.test.ts, packages-user/data-common/src/common/indexer.test.ts + + packages-user/data-common/src/common/utils.ts + packages-user/data-common/src/common/indexer.ts + packages-user/data-common/src/common/types.ts (FaceDirection) + packages-user/data-common/src/common/mover.test.ts (same-directory boilerplate) + .planning/phases/06-unit-tests/06-PATTERNS.md (section G) + + +`utils.test.ts`: assert `getFaceMovement` for every `FaceDirection` (including `Unknown -> {0,0}`), `degradeFace` for all diagonals and the custom `unknown` argument, `nextFaceDirection` for 4-way clockwise/counter-clockwise and 8-way clockwise/counter-clockwise plus `Unknown` passthrough, and `fromDirectionString` for every string plus an unknown string. These are pure functions; synchronous asserts. + +`indexer.test.ts`: with `MapLocIndexer` and `setWidth(w)`, assert `locToIndex(x,y)` and `locaterToIndex({x,y})` agree, `indexToLocator` is the inverse for in-range indices, and a wider width changes the row stride. Use a `vi.hoisted` stub only if the import chain requires it. + +Per D-20, keep these minimal and only where the main suites do not already exercise the helper. + + + pnpm exec vitest run packages-user/data-common/src/common/utils.test.ts packages-user/data-common/src/common/indexer.test.ts + the focused run fails, or a direction/index mapping assertion is missing or wrong + + + - Both files pass with zero non-skipped failures. + - utils.test.ts covers all four helpers across every direction/string; indexer.test.ts covers round-trip and width changes. + - Every `it` has a preceding single-line Chinese comment. + + Face utilities and the location indexer are covered and green. + + + + Task 3: FaceManager + Dir4/Dir8 handlers + packages-user/data-common/src/common/faceManager.test.ts + + packages-user/data-common/src/common/faceManager.ts + packages-user/data-common/src/common/types.ts + packages-user/data-common/src/common/mover.test.ts + .planning/phases/06-unit-tests/06-PATTERNS.md (section G) + + +Create `faceManager.test.ts`: +- `FaceManager.register`/`registerById` then `get`/`getById` return the handler; unknown group/id return null (registry shape). +- `Dir8FaceHandler`: `degrade` passthrough; `movement` for every direction and for an out-of-range input (`ZERO_DESCRIPTOR`); `move` scales and supports negative counts; `opposite` for each direction and `Unknown -> Unknown`; `next` clockwise/counter-clockwise and `Unknown` passthrough; `mapDirection`/`mapMovement` include `Unknown` with `{0,0}`. +- `Dir4FaceHandler`: `degrade` maps diagonals to their 4-way direction and unknown values to `Unknown`; `movement`/`move`/`opposite`/`next`/`mapDirection`/`mapMovement` behave for the 4-way set. +Keep fixtures inline. If a handler's documented descriptor is contradicted by the implementation, follow the skip+findings rule. + + + pnpm exec vitest run packages-user/data-common/src/common/faceManager.test.ts + the focused run fails, or a handler mapping/registry assertion is missing or wrong + + + - `faceManager.test.ts` passes with zero non-skipped failures. + - Asserts the registry and both handlers' full `IFaceHandler` surface. + - Every `it` has a preceding single-line Chinese comment. + + FaceManager and both direction handlers are covered and green. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| none | In-repo Node Vitest tests only; no runtime input surface, network, DOM, or filesystem write is introduced. | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-06-09-01 | Tampering | tests under `data-base/src/flag` and `data-common/src/common` | low | mitigate | Only tracked in-repo modules; synthetic fixtures; no package install; gate `pnpm test:ci` plus review that only `*.test.ts` changed. | +| T-06-09-02 | Information disclosure | fixtures | low | accept | Synthetic flag values/directions only; no real game data (D-02). | +| T-06-09-03 | Repudiation | global stub leakage | low | accept | Any `vi.stubGlobal` is torn down in `afterAll`; tests are independent. | +| T-06-09-SC | Tampering | package installs | high | mitigate | No new dependency; if an install becomes necessary, stop for a package-legitimacy checkpoint. | + + + +- Focused runs for all 4 files pass; `pnpm test:ci` stays green. +- `git status` shows only new `*.test.ts`; no core file modified. +- Any `it.skip`/`it.todo` has a matching `#06-09-N` entry in `06-TEST-FINDINGS.md`. + + + +- 4 auxiliary behavior test files exist and pass. +- D-19's full flag surface and D-20's minimal direct common coverage are asserted. + + + +Test files: +- `packages-user/data-base/src/flag/system.test.ts` +- `packages-user/data-common/src/common/utils.test.ts` +- `packages-user/data-common/src/common/indexer.test.ts` +- `packages-user/data-common/src/common/faceManager.test.ts` + +Inline helper symbols: +- `system.test.ts`: `createField` +- `utils.test.ts`: `ALL_DIRECTIONS` +- `indexer.test.ts`: `createIndexer` +- `faceManager.test.ts`: `createHandler` + + + +Create `.planning/phases/06-unit-tests/06-09-SUMMARY.md` when done. Append suspected-bug findings under plan-scoped anchors `#06-09-N`. + diff --git a/.planning/phases/06-unit-tests/06-PATTERNS.md b/.planning/phases/06-unit-tests/06-PATTERNS.md new file mode 100644 index 0000000..a94af83 --- /dev/null +++ b/.planning/phases/06-unit-tests/06-PATTERNS.md @@ -0,0 +1,692 @@ +# Phase 6: 单元测试 - Pattern Map + +**Mapped:** 2026-09-14 +**Files analyzed:** 34 proposed test files (implementer may split/merge per 06-CONTEXT "the agent's Discretion") +**Analogs found:** 34 / 34 (all targets have a tracked existing analog) + +**Scope note:** This phase writes **tests only** — no production code, no interface changes. +All new files are `*.test.ts`. The "Role / Data Flow" columns describe the **system under test**; +the file role itself is always `test`. Test file names below are the recommended co-located +split (dev.md: 测试文件与源码同级 `*.test.ts`); 06-CONTEXT leaves exact file split to the +implementer, so the planner may consolidate sibling files into one. + +**Analog gate:** every analog path below was verified with `git ls-files` (all non-empty / tracked). + +## Constraints carried from 06-CONTEXT (planner MUST reflect) + +- **D-01** Mixed tiers: pure modules tested directly with fake / explicit DI; combat, replay and + save/load also exercised through `CoreState` integration paths. +- **D-02** Minimal synthetic fixtures (small map matrix, small enemy attrs, small replay array); + only reference `packages-user/data-state/test/fixtures/closed-loop.ts` when needed. +- **D-03** Fixtures constructed **inline per test file**; no shared cross-file fixture/factory helpers. +- **D-04** Real timers + `await controller.onEnd` for async actions; sync assert for pure math. +- **D-05** Suspected bugs → write to correct expectation but mark `it.skip` / `it.todo` with a + Chinese comment pointing to `06-TEST-FINDINGS.md`; `pnpm test:ci` must stay green. +- **D-08** Gate = `pnpm test:ci` (`vitest run`, confirmed in `package.json:9`). +- **D-09** No coverage tooling. +- **D-10..D-13** Save/load is **saveables-level same-instance round-trip** across the 4 registered + saveables × 3 compression levels; explicit key-field assertions only; no interface changes. +- **dev.md 注释规范** — every `it(...)` is immediately preceded by a single-line Chinese comment + describing the covered behavior; the comment is updated when the test scope changes. + +## File Classification + +| New/Modified File | Role | Data Flow | Closest Analog | Match Quality | +|-------------------|------|-----------|----------------|---------------| +| `data-system/src/combat/damage.test.ts` | test (target: calculation) | transform | `data-system/src/path/system.test.ts` | role-match | +| `data-system/src/combat/mapDamage.test.ts` | test (target: calculation) | transform | `data-system/src/path/system.test.ts` | role-match | +| `data-system/src/combat/combat.test.ts` | test (target: flow/service) | event-driven | `data-system/src/event/eventDispatch.test.ts` | role-match | +| `data-system/src/combat/context.test.ts` | test (target: registry/service) | CRUD | `data-common/src/store/tileStore.test.ts` | role-match | +| `data-state/src/enemy/calculator.test.ts` | test (target: calculation) | transform | `data-state/test/dataClosure.test.ts` | role-match | +| `data-state/src/enemy/aura.test.ts` | test (target: transform) | transform | `data-system/src/path/system.test.ts` | flow-match | +| `data-state/src/enemy/special.test.ts` | test (target: strategy) | CRUD | `data-common/src/store/tileStore.test.ts` | role-match | +| `data-state/src/enemy/mapDamage.test.ts` | test (target: calculation) | transform | `data-system/src/path/system.test.ts` | role-match | +| `data-state/src/enemy/comparer.test.ts` | test (target: sort/compare) | transform | `data-common/src/common/mover.test.ts` | flow-match | +| `data-state/src/enemy/final.test.ts` | test (target: transform) | transform | `data-common/src/common/mover.test.ts` | flow-match | +| `data-state/test/combatClosure.test.ts` | test (integration) | request-response | `data-state/test/dataClosure.test.ts` | exact | +| `data-base/src/enemy/enemy.test.ts` | test (target: model) | CRUD | `data-state/test/dataClosure.test.ts` | role-match | +| `data-base/src/enemy/manager.test.ts` | test (target: registry/model) | CRUD | `data-common/src/store/tileStore.test.ts` | exact | +| `data-base/src/enemy/special.test.ts` | test (target: model) | CRUD | `data-common/src/store/tileStore.test.ts` | role-match | +| `data-common/src/replay/array.test.ts` | test (target: array store) | streaming | `data-state/test/nodeReplay.test.ts` | role-match | +| `data-common/src/replay/system.test.ts` | test (target: system) | request-response | `data-state/src/replay/commands.test.ts` | role-match | +| `data-common/src/replay/sandbox.test.ts` | test (target: loop/state) | event-driven | `data-state/src/replay/commands.test.ts` | exact | +| `data-common/src/replay/func.test.ts` | test (target: decorators/helpers) | transform | `data-state/src/replay/commands.test.ts` | role-match | +| `data-state/test/replayIntegration.test.ts` | test (integration) | event-driven | `data-state/test/nodeReplay.test.ts` | exact | +| `data-base/src/hero/attribute.test.ts` | test (target: model) | CRUD | `data-state/test/dataClosure.test.ts` | role-match | +| `data-base/src/hero/modifier.test.ts` | test (target: transform) | transform | `data-system/src/path/system.test.ts` | flow-match | +| `data-base/src/hero/equipment.test.ts` | test (target: model) | CRUD | `data-state/src/replay/commands.test.ts` | role-match | +| `data-base/src/hero/equipStore.test.ts` | test (target: store) | CRUD | `data-common/src/store/tileStore.test.ts` | role-match | +| `data-base/src/hero/items.test.ts` | test (target: model/store) | CRUD | `data-state/src/replay/commands.test.ts` | role-match | +| `data-base/src/hero/follower.test.ts` | test (target: model) | CRUD | `data-common/src/store/tileStore.test.ts` | role-match | +| `data-base/src/hero/location.test.ts` | test (target: model) | CRUD | `data-state/src/event/event.test.ts` | role-match | +| `data-base/src/hero/mover.test.ts` | test (target: async mover) | event-driven | `data-common/src/common/mover.test.ts` | exact | +| `data-base/src/hero/state.test.ts` | test (target: model + save) | CRUD | `data-state/test/dataClosure.test.ts` | role-match | +| `data-base/src/map/gameMap.test.ts` | test (target: model) | CRUD | `data-base/src/map/mapLifecycle.test.ts` | exact | +| `data-base/src/map/mapState.test.ts` | test (target: registry) | CRUD | `data-base/src/map/eventPath.test.ts` | exact | +| `data-base/src/map/mapLayer.test.ts` | test (target: layer/tile) | CRUD | `data-base/src/map/mapLifecycle.test.ts` | exact | +| `data-base/src/map/tile.test.ts` | test (target: tile/dirty) | transform | `data-base/src/map/mapLifecycle.test.ts` | exact | +| `data-state/test/saveablesRoundTrip.test.ts` | test (integration) | transform | `data-state/test/dataClosure.test.ts` | exact | +| `data-base/src/flag/system.test.ts` | test (target: registry) | CRUD | `data-state/test/dataClosure.test.ts` | role-match | +| `data-common/src/common/utils.test.ts` | test (target: utility) | transform | `data-common/src/common/mover.test.ts` | role-match | +| `data-common/src/common/indexer.test.ts` | test (target: utility) | transform | `data-common/src/common/mover.test.ts` | role-match | +| `data-common/src/common/faceManager.test.ts` | test (target: registry) | CRUD | `data-common/src/store/tileStore.test.ts` | role-match | + +--- + +## Pattern Assignments + +### Shared boilerplate — apply to every `*.test.ts` + +**1. Invalid-analog global stub + dynamic module bag** (use when the import chain touches +browser globals). Source: `data-base/src/map/mapLifecycle.test.ts:10-51`. + +```typescript +// 测试图块默认事件、点事件存读档、压缩聚合和尺寸变化 +import { beforeAll, describe, expect, it, vi } from 'vitest'; + +vi.hoisted(() => { + vi.stubGlobal('main', { replayChecking: true }); + vi.stubGlobal('location', { origin: 'http://localhost' }); + Map.prototype.getOrInsertComputed ??= function ( + this: Map, + key: K, + callback: (key: K) => V + ): V { + const existing = this.get(key); + if (existing !== undefined) return existing; + const value = callback(key); + this.set(key, value); + return value; + }; +}); + +interface TestModules { + MapState: typeof import('./mapState').MapState; + TileStore: typeof import('@user/data-common').TileStore; +} + +let modules: TestModules; + +beforeAll(async () => { + vi.stubGlobal('main', { replayChecking: true }); + vi.stubGlobal('location', { origin: 'http://localhost' }); + const mapModule = await import('./mapState'); + const commonModule = await import('@user/data-common'); + modules = { + MapState: mapModule.MapState, + TileStore: commonModule.TileStore + }; +}); +``` + +Pure modules that import `@user/data-common` barrel still need this stub (see +`data-common/src/store/eventStore.test.ts:21-31` for the stub-only, no `vi.hoisted` variant). + +**2. Chinese single-line coverage comment immediately before every `it`** (dev.md mandatory). +Source: `data-common/src/store/tileStore.test.ts:20-32`. + +```typescript +describe('TileStore events-map contract', () => { + // 验证图块注册时会保存并按优先级返回默认事件映射 + it('returns the default events map for a registered tile', () => { + const store = new TileStore(); + store.addTile(createTile(1, 'floor', { 20: 'late', 10: 'early' })); + + expect(store.getEvent(1)).toEqual( + new Map([ + [20, 'late'], + [10, 'early'] + ]) + ); + }); +}); +``` + +**3. `vi.stubGlobal` cleanup** for files that stub per-suite. Source: +`data-common/src/store/eventStore.test.ts:33-39`. + +```typescript +afterEach(() => { + vi.restoreAllMocks(); +}); + +afterAll(() => { + vi.unstubAllGlobals(); +}); +``` + +**4. `logger.catch` for error/warning paths** — never `expect(() => ...).toThrow`. +Source: `data-system/src/path/system.test.ts:400-414`. + +```typescript +// 验证损失函数返回 NaN 时告警新码 174 并按默认损失 1 处理 +it('warns the cost guard code and falls back to unit cost on NaN', () => { + const result = modules.logger.catch(() => + fixture.system.finder.find({ x: 0, y: 1 }, { x: 2, y: 1 }) + ); + + expect(result.info.map(info => info.code)).toContain(174); + expect(result.ret).toHaveLength(2); +}); +``` + +For code-emitting paths that do not throw, spy the logger instead. Source: +`data-state/src/replay/commands.test.ts:125-141`. + +```typescript +const error = vi.spyOn(logger, 'error'); +await expect(command.execute(step(ReplayCommandCode.Up, []))).resolves.toBe(false); +expect(error).toHaveBeenCalledWith(2003); +error.mockRestore(); +``` + +**5. Async actions: real timers + `await controller.onEnd`.** Source: +`data-common/src/common/mover.test.ts:128-140`. + +```typescript +const mover = createMover(tile); +mover.step(FaceDirection.Right); +const controller = mover.start(); +expect(controller).not.toBeNull(); +await controller!.onEnd; +expect(tile.x).toBe(1); +``` + +**6. CoreState factory** for integration: `createCoreState()` (Node-safe, independent +instances) — `data-state/test/coreNode.test.ts:1-44`. + +**7. Inline fake injection** (D-03): build the collaborator inline, do not mock data-layer +interfaces (TESTING.md). Source: `data-system/src/path/system.test.ts:336-354`. + +```typescript +const system = new modules.PathfindingSystem(commonState as never); +const tile = createTestTile(); +system.useMover(tile.mover); +system.finder.useMapState(maps); +system.finder.useMapLayer(layer); +``` + +--- + +## A. Monster combat + +Targets: `data-system/src/combat/*` (Layer 2, `ICombatFlow`/damage) ↔ `data-state/src/enemy/*` +(top-level implementation) and `data-base/src/enemy/*` (data model). + +### `data-system/src/combat/damage.test.ts` and `.../mapDamage.test.ts` (calculation, transform) + +**Analog:** `data-system/src/path/system.test.ts` (pure algorithm + injected collaborator). +**Secondary analog:** `data-common/src/common/mover.test.ts` (inline `implements` test class). + +Inject a fake hero/enemy handler object (no mocking of interfaces) exactly as the source uses +its handler, then assert deterministic numbers: + +```typescript +// data-system/src/path/system.test.ts:344-354 — injected predicate pattern to copy +function injectPredicate(fixture: SystemFixture): void { + const predicate = new FixturePredicate( + fixture.map, + new modules.Dir8FaceHandler() + ); + fixture.system.finder.usePassPredicate(predicate); +} +``` + +Use `logger.catch` (shared pattern 4) for guard/warn branches in `damage.ts` / `mapDamage.ts`. + +### `data-system/src/combat/combat.test.ts` (flow/service, event-driven) + +**Analog:** `data-system/src/event/eventDispatch.test.ts` (async executor with fake store, DI, +await-ordering proof). **Secondary:** `data-state/src/event/event.test.ts` (`CoreState` fixture). + +Copy the fake collaborator + manual-resolver await proof. Source: +`data-system/src/event/eventDispatch.test.ts:348-411`. + +```typescript +// 验证来源事件按排序顺序逐个 await,不会并行执行 +it('awaits each source before continuing to the next one', async () => { + let release: () => void = () => {}; + const pending = new Promise(resolve => { + release = resolve; + }); + // ... stub execute: async () => { calls.push(...); await pending; return true; } + + const running = fixture.mover.enter({ /* handler */ }); + await Promise.resolve(); + expect(calls.map(call => call.id)).toEqual(['slow-point']); + release(); + await running; + expect(calls.map(call => call.id)).toEqual([ + 'slow-point', + 'dynamic-enter', + 'static-enter' + ]); +}); +``` + +Build a `CombatFlow` against a minimal fake `IStateBase` (inline object) and fake +enemy view / context; assert `bindContext`/`bindDamage` state-identity warn code 138 and +`addCombatScript` duplicate-priority warn code 140 (`data-system/src/combat/combat.ts:50-114`). + +### `data-system/src/combat/context.test.ts` (registry/service, CRUD) + +**Analog:** `data-common/src/store/tileStore.test.ts` (register/query/replace/unknown-returns-safe). +Copy its contract table pattern (`data-common/src/store/tileStore.test.ts:20-74`) for the +enemy-context registry: add → lookup by view / computed / origin, unknown → `null`, duplicate +handling, and "does not expose mutable internals". + +### `data-state/src/enemy/calculator.test.ts` (calculation, transform) + +**Analog:** `data-state/test/dataClosure.test.ts:134-166` — this is the *existing* partial +coverage of `MainDamageCalculator`; extend it. It builds a fake hero handler inline. + +```typescript +// data-state/test/dataClosure.test.ts:135-166 +const hero = { + getBaseAttribute: (name: keyof IHeroAttr): IHeroAttr[typeof name] => + (name === 'hp' ? 100 : 0), + getFinalAttribute: (name: keyof IHeroAttr): IHeroAttr[typeof name] => { + if (name === 'atk') return 20; + if (name === 'def') return 5; + return 0; + } +} as IReadonlyHeroAttribute; +const handler = { + enemy, context: state.enemyContext, locator: { x: 0, y: 0 }, hero, state +} as IReadonlyEnemyHandler; + +const calculator = new MainDamageCalculator(); +const first = calculator.calculate(handler); +const second = calculator.calculate(handler); + +expect(first).not.toBeNull(); +expect(second).toEqual(first); +expect(first?.damage).toBe(3); +expect(first?.turn).toBe(2); +``` + +Extend to the special branches in `calculator.ts`: 无敌(20)/吸血(11)/魔攻(2)/连击(4,5)/ +多段(6)/支援 guard/先攻(1)/破甲(7)/反击(8)/净化(9)/固伤(22)/仇恨(17)/负伤 flag +(`data-state/src/enemy/calculator.ts:31-164`). For the support-enemy branch, inject a fake +`context.getEnemyByLocator` and assert warn code 137 for a missing guard. + +### `data-state/src/enemy/aura.test.ts`, `special.test.ts`, `mapDamage.test.ts`, `comparer.test.ts`, `final.test.ts` + +**Analog:** `data-system/src/path/system.test.ts` for pure transform (aura/final/mapDamage), +`data-common/src/store/tileStore.test.ts` for the special registry, and +`data-common/src/common/mover.test.ts` for tiny compare helpers. + +- `aura.ts` / `final.ts` / `mapDamage.ts`: construct plain attribute objects inline, compute, + assert exact values (transform pattern). +- `special.ts`: register strategies, assert get/has/add/iterate/delete plus duplicate warn + (model 96) — tileStore registry pattern. +- `comparer.ts`: inline fake enemies, assert sort/comparison order (flow-match). + +### `data-state/test/combatClosure.test.ts` (integration, request-response) + +**Analog:** `data-state/test/dataClosure.test.ts` (exact). It already builds an +`IEnemy` via `enemyManager`, a fake hero, and runs `MainDamageCalculator` against a real +`CoreState` (`data-state/test/dataClosure.test.ts:45-166`). Reuse that shape for the full +battle path (context + flow + calculator) driven through `createCoreState()`. + +For async battle/event completion use the closed-loop fixture helpers: +`data-state/test/dataClosure.test.ts:97-102,182-193`. + +```typescript +async function waitForEnded(sandbox: IReplaySandbox): Promise { + for (let index = 0; index < 1000 && !sandbox.ended; index++) { + await new Promise(resolve => setTimeout(resolve, 0)); + } + if (!sandbox.ended) throw new Error('closure replay did not end'); +} +``` + +### `data-base/src/enemy/enemy.test.ts` (model, CRUD) + +**Analog:** `data-state/test/dataClosure.test.ts:104-118` (enemy create/mutate/save/load) — +this is the exact behavior D-14 asks to expand: + +```typescript +// 验证敌人管理器能够创建敌人、修改属性并完成独立存档恢复 +it('creates, mutates, and saves an enemy through the public manager', () => { + const state = createCoreState(); + const enemy = addEnemy(state); + + enemy.setAttribute('hp', 31); + const saved = enemy.saveState(SaveCompression.NoCompression); + enemy.setAttribute('hp', 2); + enemy.loadState(saved, SaveCompression.NoCompression); + + expect(enemy.getAttribute('hp')).toBe(31); + expect(enemy.getAttribute('atk')).toBe(8); + expect(enemy.id).toBe('closure-enemy'); +}); +``` + +Expand to `Enemy` model: attribute get/set/add, special add/get/has/iterate/delete, +`clone`/`cloneAttributes`/`copyFrom`, save/load round-trip, and duplicate-special warn 96 +(`data-base/src/enemy/enemy.ts:5-107`). For load of a missing special, assert warn 120. + +### `data-base/src/enemy/manager.test.ts` (registry/model, CRUD) + +**Analog:** `data-common/src/store/tileStore.test.ts` (exact for registry contract). +Cover D-14: registry (special/attribute), prefab by code/id, reuse mapping, `compareWith` / +comparers. Use the tileStore "aligned indexes / replaces on conflict / missing returns safe" +shape (`data-common/src/store/tileStore.test.ts:42-74`). + +### `data-base/src/enemy/special.test.ts` (model, CRUD) + +**Analog:** `data-common/src/store/tileStore.test.ts`. Tiny synthetic special objects inline; +assert clone/save/load round-trip and registry lookups. + +--- + +## B. Replay + +Targets: `data-common/src/replay/*` (array/system/sandbox/func) plus integration via +`data-state/src/replay/*` and `CoreState`. + +### `data-common/src/replay/array.test.ts` (array store, streaming) + +**Analog:** `data-state/test/nodeReplay.test.ts` harness (`data-state/test/nodeReplay.test.ts:93-120`). +Build a `ReplayArray` with the same small init sizes used in +`data-common/src/replay/system.ts:31-42`, then assert add/read-stream/expand bounds and +`commandWidth` behavior (streaming reads). + +### `data-common/src/replay/system.test.ts` (system, request-response) + +**Analog:** `data-state/src/replay/commands.test.ts:49-93` (exact for registration order / +per-instance independence): + +```typescript +// 验证每个 CoreState 都按稳定顺序装配八个指令 +it('registers the eight stable commands in order on every CoreState', () => { + const state = createCoreState(); + expect(REPLAY_COMMAND_ORDER).toEqual([ + ReplayCommandCode.Up, ReplayCommandCode.Right, ReplayCommandCode.Down, + ReplayCommandCode.Left, ReplayCommandCode.Teleport, + ReplayCommandCode.UseItem, ReplayCommandCode.Equip, ReplayCommandCode.Unequip + ]); + expect(REPLAY_COMMAND_ORDER.every(code => state.replaySystem.getCommand(code))).toBe(true); +}); +``` + +Also cover `registerCommand` duplicate warn 163, `record` writing to `route`, and hook +dispatch (`data-common/src/replay/system.ts:50-86`). + +### `data-common/src/replay/sandbox.test.ts` (loop/state, event-driven) + +**Analog:** `data-state/src/replay/commands.test.ts:255-288` (manual sandbox stepping) and +`data-state/test/dataClosure.test.ts:225-246` (stops on first failure). + +```typescript +// data-state/test/dataClosure.test.ts:225-246 +const replay = new ReplaySystem(); +let laterExecuted = false; +replay.registerCommand(0, { execute: async () => false }); +replay.registerCommand(1, { execute: async () => { laterExecuted = true; return true; } }); +replay.record(0); +replay.record(1); +const sandbox = replay.createReplaySandbox({ + route: replay.route, + reseter: { reset: () => {} } +}) as IManualReplaySandbox; +sandbox.pausing = false; +sandbox.playing = true; + +await expect(sandbox.step()).resolves.toBe(false); +expect(sandbox.getReplayed()).toBe(1); +expect(laterExecuted).toBe(false); +``` + +Also cover `play`/`pause`/`resume`/`stop`, `finalizeLast` (`notExecuted`), and warn codes +156/157/158/175 (`data-common/src/replay/sandbox.ts:64-185`). No fake timers (D-04). + +### `data-common/src/replay/func.test.ts` (decorators/helpers, transform) + +**Analog:** `data-state/src/replay/commands.test.ts:350-417` (decorator/collection-context +fixture with `console`/`logger` spies). Cover the replay safety decorator lifecycle: +begin → nested calls → end → detail, and reset lifecycle (codes 159/161). + +### `data-state/test/replayIntegration.test.ts` (integration, event-driven) + +**Analog:** `data-state/test/nodeReplay.test.ts` (exact) — it already drives +`ReplaySystem` + sandbox through a step harness (`data-state/test/nodeReplay.test.ts:93-120`). +Per D-16, cross-check against `03-REPLAY-CONTRACT.md` / `03-REPLAY-DIAGNOSTICS.md`; on first +divergence stop and report command index/code/params. + +--- + +## C. Hero + +Targets: `data-base/src/hero/*` (attribute, equipment, equipStore, follower, items, location, +modifier, mover, state) — D-17: verify each subsystem. + +### Pure/model subsystems: `attribute.test.ts`, `modifier.test.ts`, `follower.test.ts`, `equipStore.test.ts`, `items.test.ts` + +**Analog:** `data-common/src/store/tileStore.test.ts` (registry CRUD contract) and +`data-system/src/path/system.test.ts` (transform, injected inputs). + +- `attribute.ts` / `modifier.ts`: inline base/final/modifier objects; assert base vs final + computation and modifier ordering (transform). +- `equipStore.ts` / `follower.ts` / `items.ts`: registry CRUD — add/get/unknown-safe/replace. + +### `equipment.test.ts` (model, CRUD) + +**Analog:** `data-state/src/replay/commands.test.ts:200-253` — the established equipment +boundary (`equip`/`unequip`/`getEquipped`) with `vi.spyOn`: + +```typescript +// 验证卸下指令复用既有装备边界并以最终槽位校验结果 +it('unequips through the existing equipment boundary', async () => { + const equipment = state.hero.equip; + const getEquipped = vi.spyOn(equipment, 'getEquipped'); + getEquipped.mockReturnValueOnce(undefined); + // ... assert equip/unequip return values +}); +``` + +### `location.test.ts` (model, CRUD) + +**Analog:** `data-state/src/event/event.test.ts:54-92` — minimal `CoreState` fixture sets +`hero.location.setFloor/setPos` and `mover.setFaceDir`; assert position/floor/face getters +and bounds. + +### `mover.test.ts` (async mover, event-driven) + +**Analog:** `data-common/src/common/mover.test.ts` (exact — same abstract mover pattern) and +`data-system/src/path/system.test.ts:485-521`. Use real timers + `await controller.onEnd`. + +### `state.test.ts` (model + save, CRUD) + +**Analog:** `data-state/test/dataClosure.test.ts:169-179` (saveable hero round-trip): + +```typescript +// 验证可存档勇士内容在修改后能够恢复保存时的属性快照 +it('round-trips a saveable hero state', () => { + const state = createCoreState(); + const hero = state.hero.getModifiableAttribute(); + hero.set('hp', 88); + const saved = state.hero.saveState(SaveCompression.NoCompression); + + hero.set('hp', 1); + state.hero.loadState(saved, SaveCompression.NoCompression); + + expect(state.hero.attribute.getBaseAttribute('hp')).toBe(88); +}); +``` + +--- + +## D. Map (expanded coverage, D-18) + +Targets: `data-base/src/map/*`. Existing tests: `eventPath.test.ts`, `eventView.test.ts`, +`mapLifecycle.test.ts`. New files extend matrix / point events / layers / dirty / resize-crop. + +**Analog (exact) for all four new files:** `data-base/src/map/mapLifecycle.test.ts`. + +Inline fixture + global stub — copy verbatim shape (`data-base/src/map/mapLifecycle.test.ts:10-98`): + +```typescript +function createMapState( + pointEvents: Record> = {}, + blocks: number[] = [1, 1, 1, 1] +) { + const tileStore: ITileStore = new modules.TileStore() as never; + tileStore.addTile({ + num: 1, id: 'base', events: { 10: 'base-event' }, type: 0, + pass: { onlyEvents: false, outPass: 15, inPass: 15 }, eventPass: true + }); + const faceManager = new modules.FaceManager(); + faceManager.register(1, new modules.Dir8FaceHandler()); + const state: IDataCommon = { tileStore, /* ... */ } as never; + const mapState = new modules.MapState(tileStore, state); + const map = mapState.fromRaw({ + floorId: 'F1', width: 2, map: { 0: blocks }, + layerAlias: { 0: 'event' }, events: { 0: pointEvents } + }); + return { map: map!, layer: map!.getLayerByAlias('event')! as IResizableMapLayer }; +} +``` + +All-compression loop + explicit key-field assertions (D-13) — copy +`data-base/src/map/mapLifecycle.test.ts:203-234`: + +```typescript +const compressionLevels = [ + SaveCompression.NoCompression, + SaveCompression.LowCompression, + SaveCompression.HighCompression +]; +for (const compression of compressionLevels) { + point.set(7, 'saved-point'); + const save = layer.saveState(compression); + expect(save.pointEvents?.get(1)).toEqual(new Map([[5, 'raw-point'], [7, 'saved-point']])); + point.set(7, 'changed-after-save'); + layer.loadState(save, compression); + expect(point.get()).toEqual(new Map([[5, 'raw-point'], [7, 'saved-point']])); +} +``` + +Resize/crop/reindex pattern — `data-base/src/map/mapLifecycle.test.ts:278-319`. +`mapState.test.ts` also mirrors `eventPath.test.ts:44-60` for `fromRaw` validation and +logger warning paths. + +--- + +## E. Save / load (saveables round-trip, D-10..D-13) + +### `data-state/test/saveablesRoundTrip.test.ts` (integration, transform) + +**Analog:** `data-state/test/dataClosure.test.ts` (exact) + `closed-loop.ts` envelope + +`mapLifecycle.test.ts` compression loop. + +**Entry point (no interface change):** `CoreState.getSaveableContent(id)` +(`data-state/src/core.ts:486-489`); the four registered ids are set at +`data-state/src/core.ts:237-240`: + +```typescript +this.addSaveableContent('@system/hero', this.hero); +this.addSaveableContent('@system/flags', this.flags); +this.addSaveableContent('@system/maps', this.maps); +this.addSaveableContent('@system/enemy', this.enemyManager); +``` + +Snapshot envelope (same shape as the closed-loop fixture) — +`packages-user/data-state/test/fixtures/closed-loop.ts:174-179`: + +```typescript +const initialState: IClosedLoopInitialState = { + hero: state.hero.saveState(SaveCompression.NoCompression), + flags: state.flags.saveState(SaveCompression.NoCompression), + maps: state.maps.saveState(SaveCompression.NoCompression), + enemy: state.enemyManager.saveState(SaveCompression.NoCompression) +}; +``` + +Same-instance round-trip (D-12): capture via `getSaveableContent(id).saveState(level)` → +mutate a known key field → `loadState(snapshot, level)` → assert only the key fields +(D-13). Loop over the 4 ids × 3 `SaveCompression` levels. Do **not** touch +`saveSystem.init` / IndexedDB and do not add `fake-indexeddb` (D-10). + +--- + +## F. Flag + +### `data-base/src/flag/system.test.ts` (registry, CRUD) + +**Analog:** `data-state/test/dataClosure.test.ts:120-132` (existing flag round-trip) + +`data-common/src/store/tileStore.test.ts` (registry contract). + +```typescript +// 验证 Flag 的设置读取和 save/load round trip 保留字段值 +it('round-trips Flag values through save and load', () => { + const state = createCoreState(); + state.flags.setFieldValue('closureScore', 7); + state.flags.addFieldValue('closureScore', 5); + const saved = state.flags.saveState(SaveCompression.NoCompression); + + state.flags.setFieldValue('closureScore', 0); + state.flags.loadState(saved, SaveCompression.NoCompression); + + expect(state.flags.getFieldValue('closureScore')).toBe(12); + expect(state.flags.occupied('closureScore')).toBe(true); +}); +``` + +Per D-19 cover the full public surface of `FlagSystem` +(`data-base/src/flag/system.ts:8-72`): `occupied`, `insertField`, `getField`, `getOrInsert`, +`getOrInsertComputed`, `deleteField`, `setFieldValue`, `addFieldValue`, `getFieldValue`, +`getFieldValueDefaults`, `saveState`/`loadState`. Note: flag `saveState()` takes **no** +compression argument. `FlagSystem` uses `Map.getOrInsertComputed`, so include the +`vi.hoisted` polyfill when the import chain requires it. + +--- + +## G. Common + +### `data-common/src/common/utils.test.ts`, `indexer.test.ts`, `faceManager.test.ts` (utility/registry) + +**Analog:** `data-common/src/common/mover.test.ts` (same directory, exact boilerplate). +`utils.ts` helpers are pure (`getFaceMovement`, `degradeFace` — `data-common/src/common/utils.ts:8-40`); +`indexer.ts` `MapLocIndexer` is a pure round-trip (`data-common/src/common/indexer.ts:38-59`); +`faceManager.ts` is a registry (`register`/lookup by direction) — use the tileStore registry +shape. + +Per D-20: only add these direct tests for helpers the main suites do not naturally exercise; +otherwise prefer coverage via combat/map/hero tests. + +--- + +## No Analog Found + +None. Every proposed test file maps to a tracked existing analog. Two thin spots the planner +should call out explicitly: + +| Target | Reason / guidance | +|--------|-------------------| +| `data-common/src/replay/array.ts` (825 lines) | No dedicated array test exists; the stream/expand API is only exercised indirectly. Use `nodeReplay.test.ts` harness as the closest analog and assert through the public `IReplayArray` surface. | +| `data-system/src/combat/combat.ts` `combatFlow` | No existing test drives `CombatFlow` directly; model the fake collaborators on `eventDispatch.test.ts` and assert against the documented hook/script ordering (`combat.ts:164-194`). | + +## Test-skip / findings workflow (D-05, D-06) + +No existing analog uses `it.skip` for suspected bugs yet — all current tests pass. Establish +the convention in this phase: + +```typescript +// 疑似 bug:xxx 行为与接口预期不一致,详见 06-TEST-FINDINGS.md #N,修复后取消 skip +it.skip('...', () => { + // 按正确预期编写断言 +}); +``` + +`06-TEST-FINDINGS.md` is created in the phase directory with columns: 模块/接口、现象、 +最小复现、疑似原因、影响面、建议修复方向、关联 skip 用例、严重度. + +## Metadata + +**Analog search scope:** `packages-user/data-common/src`, `packages-user/data-base/src`, +`packages-user/data-system/src`, `packages-user/data-state/src` and `data-state/test` +**Files scanned:** 18 existing test files + 9 fixtures/target modules +**Analog gate:** all cited analog paths return non-empty from `git ls-files` (tracked source) +**Pattern extraction date:** 2026-09-14