mirror of
https://github.com/motajs/template.git
synced 2026-10-16 18:40:17 +08:00
docs(06): create phase plan
This commit is contained in:
parent
e1a16eae8b
commit
22726773ad
@ -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 | - |
|
||||
|
||||
@ -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%
|
||||
|
||||
278
.planning/phases/06-unit-tests/06-01-PLAN.md
Normal file
278
.planning/phases/06-unit-tests/06-01-PLAN.md
Normal 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>
|
||||
252
.planning/phases/06-unit-tests/06-02-PLAN.md
Normal file
252
.planning/phases/06-unit-tests/06-02-PLAN.md
Normal 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>
|
||||
188
.planning/phases/06-unit-tests/06-03-PLAN.md
Normal file
188
.planning/phases/06-unit-tests/06-03-PLAN.md
Normal 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>
|
||||
235
.planning/phases/06-unit-tests/06-04-PLAN.md
Normal file
235
.planning/phases/06-unit-tests/06-04-PLAN.md
Normal 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>
|
||||
225
.planning/phases/06-unit-tests/06-05-PLAN.md
Normal file
225
.planning/phases/06-unit-tests/06-05-PLAN.md
Normal 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>
|
||||
233
.planning/phases/06-unit-tests/06-06-PLAN.md
Normal file
233
.planning/phases/06-unit-tests/06-06-PLAN.md
Normal 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>
|
||||
225
.planning/phases/06-unit-tests/06-07-PLAN.md
Normal file
225
.planning/phases/06-unit-tests/06-07-PLAN.md
Normal 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>
|
||||
186
.planning/phases/06-unit-tests/06-08-PLAN.md
Normal file
186
.planning/phases/06-unit-tests/06-08-PLAN.md
Normal 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>
|
||||
220
.planning/phases/06-unit-tests/06-09-PLAN.md
Normal file
220
.planning/phases/06-unit-tests/06-09-PLAN.md
Normal 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>
|
||||
692
.planning/phases/06-unit-tests/06-PATTERNS.md
Normal file
692
.planning/phases/06-unit-tests/06-PATTERNS.md
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user