docs(06): create phase plan

This commit is contained in:
unanmed 2026-09-14 08:30:40 +08:00
parent e1a16eae8b
commit 22726773ad
12 changed files with 2767 additions and 10 deletions

View File

@ -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 | - |

View File

@ -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%

View File

@ -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"
---
<objective>
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).
</objective>
<execution_context>
@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md
@C:/Users/book/.config/opencode/gsd-core/templates/summary.md
</execution_context>
<context>
@.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
</context>
<source_coverage_audit>
| Source | Item | Covered by |
|--------|------|-----------|
| GOAL | 为核心系统(数据层等)补齐单元测试 (data-layer slice) | 06-01..06-09 |
| GOAL | Phase 6 non-data (render/legacy) coverage | Deferred per 06-CONTEXT `<deferred>` (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 |
</source_coverage_audit>
<execution_rules>
- 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).
</execution_rules>
<tasks>
<task type="tracer">
<name>Task 1: Tracer — DamageContext/DamageSystem behavior end-to-end (+ phase scaffolding)</name>
<files>packages-user/data-system/src/combat/damage.test.ts, .planning/phases/06-unit-tests/COVERAGE.md</files>
<read_first>
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 (注释规范)
</read_first>
<action>
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.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-system/src/combat/damage.test.ts</automated>
<fails_when>the focused vitest run reports any failed or unexpectedly skipped test, or the file cannot be collected due to an unresolved module/global</fails_when>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>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.</done>
</task>
<task type="auto">
<name>Task 2: MapDamage + EnemyContext behavior</name>
<files>packages-user/data-system/src/combat/mapDamage.test.ts, packages-user/data-system/src/combat/context.test.ts</files>
<read_first>
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)
</read_first>
<action>
`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.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-system/src/combat/mapDamage.test.ts packages-user/data-system/src/combat/context.test.ts</automated>
<fails_when>either focused run fails, or a warning assertion does not observe the documented logger code</fails_when>
</verify>
<acceptance_criteria>
- 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.
</acceptance_criteria>
<done>MapDamage and EnemyContext public behavior is covered and green.</done>
</task>
<task type="auto">
<name>Task 3: CombatFlow binding, script ordering, and battle flow</name>
<files>packages-user/data-system/src/combat/combat.test.ts</files>
<read_first>
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)
</read_first>
<action>
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.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-system/src/combat/combat.test.ts</automated>
<fails_when>the focused run fails or the ordering assertion does not observe the documented before/after sequence</fails_when>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>CombatFlow binding, guard warnings, and awaited script ordering are covered and green.</done>
</task>
</tasks>
<threat_model>
## 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. |
</threat_model>
<verification>
- `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`.
</verification>
<success_criteria>
- 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.
</success_criteria>
<artifacts_this_plan_produces>
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)
</artifacts_this_plan_produces>
<output>
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`.
</output>

View File

@ -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"
---
<objective>
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.
</objective>
<execution_context>
@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md
@C:/Users/book/.config/opencode/gsd-core/templates/summary.md
</execution_context>
<context>
@.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
</context>
<execution_rules>
- 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.
</execution_rules>
<tasks>
<task type="auto">
<name>Task 1: MainDamageCalculator + final effect + comparer</name>
<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</files>
<read_first>
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)
</read_first>
<action>
`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.
</action>
<verify>
<automated>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</automated>
<fails_when>the focused run fails, or a branch assertion does not match the documented arithmetic</fails_when>
</verify>
<acceptance_criteria>
- 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.
</acceptance_criteria>
<done>Calculator, final effect, and comparer behavior is covered and green.</done>
</task>
<task type="auto">
<name>Task 2: Auras + special registration</name>
<files>packages-user/data-state/src/enemy/aura.test.ts, packages-user/data-state/src/enemy/special.test.ts</files>
<read_first>
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)
</read_first>
<action>
`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.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-state/src/enemy/aura.test.ts packages-user/data-state/src/enemy/special.test.ts</automated>
<fails_when>the focused run fails, or a registration/buff assertion does not match the documented behavior</fails_when>
</verify>
<acceptance_criteria>
- 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.
</acceptance_criteria>
<done>Aura conversion and the special registry are covered and green.</done>
</task>
<task type="auto">
<name>Task 3: Map damage views, converter, reducer</name>
<files>packages-user/data-state/src/enemy/mapDamage.test.ts</files>
<read_first>
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)
</read_first>
<action>
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.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-state/src/enemy/mapDamage.test.ts</automated>
<fails_when>the focused run fails, or a view/reducer assertion does not match the documented output</fails_when>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>Map damage views, converter, and reducer behavior is covered and green.</done>
</task>
</tasks>
<threat_model>
## 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. |
</threat_model>
<verification>
- 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`.
</verification>
<success_criteria>
- 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.
</success_criteria>
<artifacts_this_plan_produces>
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`
</artifacts_this_plan_produces>
<output>
Create `.planning/phases/06-unit-tests/06-02-SUMMARY.md` when done. Append suspected-bug findings under plan-scoped anchors `#06-02-N`.
</output>

View File

@ -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"
---
<objective>
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.
</objective>
<execution_context>
@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md
@C:/Users/book/.config/opencode/gsd-core/templates/summary.md
</execution_context>
<context>
@.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
</context>
<execution_rules>
- 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.
</execution_rules>
<tasks>
<task type="auto">
<name>Task 1: Enemy model + special implementations</name>
<files>packages-user/data-base/src/enemy/enemy.test.ts, packages-user/data-base/src/enemy/special.test.ts</files>
<read_first>
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)
</read_first>
<action>
`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).
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-base/src/enemy/enemy.test.ts packages-user/data-base/src/enemy/special.test.ts</automated>
<fails_when>the focused run fails, or the duplicate-special warn 96 / missing-special warn 120 is not observed</fails_when>
</verify>
<acceptance_criteria>
- 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.
</acceptance_criteria>
<done>Enemy and special data models are covered and green.</done>
</task>
<task type="auto">
<name>Task 2: EnemyManager registry, prefabs, reuse, dirty tracking</name>
<files>packages-user/data-base/src/enemy/manager.test.ts</files>
<read_first>
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)
</read_first>
<action>
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`.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-base/src/enemy/manager.test.ts</automated>
<fails_when>the focused run fails, or a registry/dirty assertion or a warning code (53/117/118/119) is not observed</fails_when>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>EnemyManager registry, prefab, reuse, and dirty behavior is covered and green.</done>
</task>
</tasks>
<threat_model>
## 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. |
</threat_model>
<verification>
- 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`.
</verification>
<success_criteria>
- 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.
</success_criteria>
<artifacts_this_plan_produces>
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`
</artifacts_this_plan_produces>
<output>
Create `.planning/phases/06-unit-tests/06-03-SUMMARY.md` when done. Append suspected-bug findings under plan-scoped anchors `#06-03-N`.
</output>

View File

@ -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"
---
<objective>
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`.
</objective>
<execution_context>
@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md
@C:/Users/book/.config/opencode/gsd-core/templates/summary.md
</execution_context>
<context>
@.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
</context>
<execution_rules>
- 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.
</execution_rules>
<tasks>
<task type="auto">
<name>Task 1: ReplaySystem + ReplayArray</name>
<files>packages-user/data-common/src/replay/system.test.ts, packages-user/data-common/src/replay/array.test.ts</files>
<read_first>
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)
</read_first>
<action>
`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.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-common/src/replay/system.test.ts packages-user/data-common/src/replay/array.test.ts</automated>
<fails_when>the focused run fails, or the duplicate-registration warn 163 / route round-trip assertion is not observed</fails_when>
</verify>
<acceptance_criteria>
- 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.
</acceptance_criteria>
<done>ReplaySystem and ReplayArray behavior is covered and green.</done>
</task>
<task type="auto">
<name>Task 2: ReplaySandbox + replay safety helpers</name>
<files>packages-user/data-common/src/replay/sandbox.test.ts, packages-user/data-common/src/replay/func.test.ts</files>
<read_first>
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)
</read_first>
<action>
`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.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-common/src/replay/sandbox.test.ts packages-user/data-common/src/replay/func.test.ts</automated>
<fails_when>the focused run fails, or a stop-on-failure warning (156/157/158/175) or a decorator lifecycle code (159/161) is not observed</fails_when>
</verify>
<acceptance_criteria>
- 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.
</acceptance_criteria>
<done>ReplaySandbox and replay-safety decorators are covered and green.</done>
</task>
<task type="auto">
<name>Task 3: Replay integration through CoreState</name>
<files>packages-user/data-state/test/replayIntegration.test.ts</files>
<read_first>
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)
</read_first>
<action>
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.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-state/test/replayIntegration.test.ts</automated>
<fails_when>the focused run fails, or the first-divergence report does not include index/code/params</fails_when>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>Replay integration through CoreState is covered and green.</done>
</task>
</tasks>
<threat_model>
## 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. |
</threat_model>
<verification>
- 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`.
</verification>
<success_criteria>
- 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.
</success_criteria>
<artifacts_this_plan_produces>
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`
</artifacts_this_plan_produces>
<output>
Create `.planning/phases/06-unit-tests/06-04-SUMMARY.md` when done. Append suspected-bug findings under plan-scoped anchors `#06-04-N`.
</output>

View File

@ -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"
---
<objective>
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.
</objective>
<execution_context>
@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md
@C:/Users/book/.config/opencode/gsd-core/templates/summary.md
</execution_context>
<context>
@.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
</context>
<execution_rules>
- 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.
</execution_rules>
<tasks>
<task type="auto">
<name>Task 1: HeroAttribute + modifiers</name>
<files>packages-user/data-base/src/hero/attribute.test.ts, packages-user/data-base/src/hero/modifier.test.ts</files>
<read_first>
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)
</read_first>
<action>
`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.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-base/src/hero/attribute.test.ts packages-user/data-base/src/hero/modifier.test.ts</automated>
<fails_when>the focused run fails, or the warn 108/109 assertions are not observed</fails_when>
</verify>
<acceptance_criteria>
- 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.
</acceptance_criteria>
<done>HeroAttribute and modifier behavior is covered and green.</done>
</task>
<task type="auto">
<name>Task 2: HeroLocation model</name>
<files>packages-user/data-base/src/hero/location.test.ts</files>
<read_first>
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)
</read_first>
<action>
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).
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-base/src/hero/location.test.ts</automated>
<fails_when>the focused run fails, or a save/load round-trip assertion does not restore a field</fails_when>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>HeroLocation behavior is covered and green.</done>
</task>
<task type="auto">
<name>Task 3: HeroState aggregate and save envelope</name>
<files>packages-user/data-base/src/hero/state.test.ts</files>
<read_first>
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)
</read_first>
<action>
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.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-base/src/hero/state.test.ts</automated>
<fails_when>the focused run fails, or the save/load assertion uses a whole-object deep equality instead of explicit key fields</fails_when>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>HeroState aggregate and save envelope are covered and green.</done>
</task>
</tasks>
<threat_model>
## 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. |
</threat_model>
<verification>
- 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`.
</verification>
<success_criteria>
- 4 hero core behavior test files exist and pass.
- Attribute/modifier/location/state behavior including the save envelope is asserted with explicit key fields.
</success_criteria>
<artifacts_this_plan_produces>
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`
</artifacts_this_plan_produces>
<output>
Create `.planning/phases/06-unit-tests/06-05-SUMMARY.md` when done. Append suspected-bug findings under plan-scoped anchors `#06-05-N`.
</output>

View File

@ -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"
---
<objective>
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.
</objective>
<execution_context>
@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md
@C:/Users/book/.config/opencode/gsd-core/templates/summary.md
</execution_context>
<context>
@.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
</context>
<execution_rules>
- 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.
</execution_rules>
<tasks>
<task type="auto">
<name>Task 1: HeroEquipment + HeroEquipsStore</name>
<files>packages-user/data-base/src/hero/equipment.test.ts, packages-user/data-base/src/hero/equipStore.test.ts</files>
<read_first>
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)
</read_first>
<action>
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`).
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-base/src/hero/equipment.test.ts packages-user/data-base/src/hero/equipStore.test.ts</automated>
<fails_when>the focused run fails, or the modifier apply/remove assertion or warn/error code is not observed</fails_when>
</verify>
<acceptance_criteria>
- 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.
</acceptance_criteria>
<done>Equipment and equip-store behavior is covered and green.</done>
</task>
<task type="auto">
<name>Task 2: HeroItems + followers</name>
<files>packages-user/data-base/src/hero/items.test.ts, packages-user/data-base/src/hero/follower.test.ts</files>
<read_first>
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)
</read_first>
<action>
`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`).
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-base/src/hero/items.test.ts packages-user/data-base/src/hero/follower.test.ts</automated>
<fails_when>the focused run fails, or the async gather assertion does not complete through awaited movers</fails_when>
</verify>
<acceptance_criteria>
- 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.
</acceptance_criteria>
<done>Item and follower behavior is covered and green.</done>
</task>
<task type="auto">
<name>Task 3: HeroMover async movement</name>
<files>packages-user/data-base/src/hero/mover.test.ts</files>
<read_first>
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)
</read_first>
<action>
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.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-base/src/hero/mover.test.ts</automated>
<fails_when>the focused run fails, or a move-code/position-writeback assertion does not match the implementation contract</fails_when>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>HeroMover async movement behavior is covered and green.</done>
</task>
</tasks>
<threat_model>
## 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. |
</threat_model>
<verification>
- 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`.
</verification>
<success_criteria>
- 5 hero storage/movement behavior test files exist and pass.
- Equipment, equip store, items, followers, and the async hero mover are asserted.
</success_criteria>
<artifacts_this_plan_produces>
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`
</artifacts_this_plan_produces>
<output>
Create `.planning/phases/06-unit-tests/06-06-SUMMARY.md` when done. Append suspected-bug findings under plan-scoped anchors `#06-06-N`.
</output>

View File

@ -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"
---
<objective>
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.
</objective>
<execution_context>
@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md
@C:/Users/book/.config/opencode/gsd-core/templates/summary.md
</execution_context>
<context>
@.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
</context>
<execution_rules>
- 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.
</execution_rules>
<tasks>
<task type="auto">
<name>Task 1: MapState registry and raw validation</name>
<files>packages-user/data-base/src/map/mapState.test.ts</files>
<read_first>
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)
</read_first>
<action>
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).
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-base/src/map/mapState.test.ts</automated>
<fails_when>the focused run fails, or a fromRaw/activation/compareWith assertion does not match the documented behavior</fails_when>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>MapState registry, validation, and activation behavior is covered and green.</done>
</task>
<task type="auto">
<name>Task 2: GameMap layers, aliases, dirty, save aggregation</name>
<files>packages-user/data-base/src/map/gameMap.test.ts</files>
<read_first>
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)
</read_first>
<action>
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.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-base/src/map/gameMap.test.ts</automated>
<fails_when>the focused run fails, or the save-aggregation/empty-layer rule or warn 84/131 is not observed</fails_when>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>GameMap layer/alias/dirty/save behavior is covered and green.</done>
</task>
<task type="auto">
<name>Task 3: MapLayer matrix + tiles (static/dynamic default events)</name>
<files>packages-user/data-base/src/map/mapLayer.test.ts, packages-user/data-base/src/map/tile.test.ts</files>
<read_first>
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)
</read_first>
<action>
`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`.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-base/src/map/mapLayer.test.ts packages-user/data-base/src/map/tile.test.ts</automated>
<fails_when>the focused run fails, or the default-event restoration / dynamic-tile index assertion does not match the documented behavior</fails_when>
</verify>
<acceptance_criteria>
- 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.
</acceptance_criteria>
<done>MapLayer matrix/tiles and tile default-event behavior is covered and green.</done>
</task>
</tasks>
<threat_model>
## 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. |
</threat_model>
<verification>
- 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`.
</verification>
<success_criteria>
- 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.
</success_criteria>
<artifacts_this_plan_produces>
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`
</artifacts_this_plan_produces>
<output>
Create `.planning/phases/06-unit-tests/06-07-SUMMARY.md` when done. Append suspected-bug findings under plan-scoped anchors `#06-07-N`.
</output>

View File

@ -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"
---
<objective>
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/`.
</objective>
<execution_context>
@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md
@C:/Users/book/.config/opencode/gsd-core/templates/summary.md
</execution_context>
<context>
@.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
</context>
<execution_rules>
- 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.
</execution_rules>
<tasks>
<task type="auto">
<name>Task 1: Combat closure through CoreState</name>
<files>packages-user/data-state/test/combatClosure.test.ts</files>
<read_first>
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)
</read_first>
<action>
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.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-state/test/combatClosure.test.ts</automated>
<fails_when>the focused run fails, or the handler identity / deterministic damage assertion does not hold</fails_when>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>The combat path is proven end-to-end through CoreState and green.</done>
</task>
<task type="auto">
<name>Task 2: Saveables same-instance round-trip (4 ids × 3 compressions)</name>
<files>packages-user/data-state/test/saveablesRoundTrip.test.ts</files>
<read_first>
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)
</read_first>
<action>
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.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-state/test/saveablesRoundTrip.test.ts</automated>
<fails_when>the focused run fails, or a round-trip assertion for any id/compression pair is missing or uses a whole-object deep equality</fails_when>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>All registered saveables round-trip same-instance across all compressions and are green (D-11, D-12).</done>
</task>
</tasks>
<threat_model>
## 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. |
</threat_model>
<verification>
- 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`.
</verification>
<success_criteria>
- 2 integration test files exist and pass.
- Combat closure and the 4×3 saveables round-trip are asserted with explicit key fields.
</success_criteria>
<artifacts_this_plan_produces>
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`
</artifacts_this_plan_produces>
<output>
Create `.planning/phases/06-unit-tests/06-08-SUMMARY.md` when done. Append suspected-bug findings under plan-scoped anchors `#06-08-N`.
</output>

View File

@ -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"
---
<objective>
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.
</objective>
<execution_context>
@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md
@C:/Users/book/.config/opencode/gsd-core/templates/summary.md
</execution_context>
<context>
@.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
</context>
<execution_rules>
- 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.
</execution_rules>
<tasks>
<task type="auto">
<name>Task 1: FlagSystem full public surface</name>
<files>packages-user/data-base/src/flag/system.test.ts</files>
<read_first>
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)
</read_first>
<action>
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.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-base/src/flag/system.test.ts</automated>
<fails_when>the focused run fails, or the warn 111 / save-load key-field assertion is not observed</fails_when>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>FlagSystem behavior is covered and green.</done>
</task>
<task type="auto">
<name>Task 2: Face utilities + location indexer</name>
<files>packages-user/data-common/src/common/utils.test.ts, packages-user/data-common/src/common/indexer.test.ts</files>
<read_first>
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)
</read_first>
<action>
`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.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-common/src/common/utils.test.ts packages-user/data-common/src/common/indexer.test.ts</automated>
<fails_when>the focused run fails, or a direction/index mapping assertion is missing or wrong</fails_when>
</verify>
<acceptance_criteria>
- 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.
</acceptance_criteria>
<done>Face utilities and the location indexer are covered and green.</done>
</task>
<task type="auto">
<name>Task 3: FaceManager + Dir4/Dir8 handlers</name>
<files>packages-user/data-common/src/common/faceManager.test.ts</files>
<read_first>
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)
</read_first>
<action>
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.
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-common/src/common/faceManager.test.ts</automated>
<fails_when>the focused run fails, or a handler mapping/registry assertion is missing or wrong</fails_when>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>FaceManager and both direction handlers are covered and green.</done>
</task>
</tasks>
<threat_model>
## 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. |
</threat_model>
<verification>
- 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`.
</verification>
<success_criteria>
- 4 auxiliary behavior test files exist and pass.
- D-19's full flag surface and D-20's minimal direct common coverage are asserted.
</success_criteria>
<artifacts_this_plan_produces>
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`
</artifacts_this_plan_produces>
<output>
Create `.planning/phases/06-unit-tests/06-09-SUMMARY.md` when done. Append suspected-bug findings under plan-scoped anchors `#06-09-N`.
</output>

View File

@ -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 <K, V>(
this: Map<K, V>,
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<void>(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<IHeroAttr>;
const handler = {
enemy, context: state.enemyContext, locator: { x: 0, y: 0 }, hero, state
} as IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>;
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<void> {
for (let index = 0; index < 1000 && !sandbox.ended; index++) {
await new Promise<void>(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<number, Record<number, string>> = {},
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<number>('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