fix(06): revise plans based on checker feedback

This commit is contained in:
unanmed 2026-09-14 11:04:33 +08:00
parent 384e5673ad
commit fe4e410cf4
9 changed files with 1368 additions and 1107 deletions

View File

@ -5,56 +5,59 @@ type: execute
wave: 1
depends_on: []
files_modified:
- packages-user/data-system/src/combat/context.test.ts
- 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
- .planning/phases/06-unit-tests/06-COVERAGE-MAP.md
autonomous: true
requirements: [TEST-01]
estimate:
tokens: 52000
raw_tokens: 52000
tokens: 64000
raw_tokens: 64000
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
- "TEST-01 由数据端行为单测满足,门禁为固定非 watch 命令 `pnpm test:ci` 全绿D-08。"
- "接口全覆盖:`EnemyContext`(含 `resize`/`clear`/`destroy`/`buildup`/`requestRefresh`/`markDirty`/`scanRange`/`iterateEnemy`/全部 register/unregister/bind/attach/get/set/delete 与 `addAura`/`deleteAura`)、`DamageContext`、`DamageSystem`、`MapDamage`、`CombatFlow` 的每个公开方法至少一条正常用例;名称含 legacy 的接口/方法一律排除D-27 / D-30。"
- "code 全覆盖:本计划可达的 warn/error 码 97、98、99、100、101、110光环/修饰器流水线、106、107伤害上下文/系统、102、103、104地图伤害、138、139、140、141战斗流程各至少一条触发断言`logger.catch` / `logger.error` spy 观测D-24 / D-31105 在当前实现不可达,排除。"
- "光环范围Full`haloRange<=0`/ Rect`haloSquare`/ Manhattan 三种范围全部覆盖并断言光环范围之外的怪物不被加成D-22。"
- "嵌套光环:一层(光环施加特殊属性)与两层(光环→特殊属性→再经转换器产生新光环→再施加效果)均覆盖,并含优先级边界(新增光环只能影响更低优先级阶段;同/跨优先级传播D-23。"
- "效果阶段顺序:显式断言 `buildupSpecials``buildupBase``buildupQuery``buildupFinal` 四阶段顺序并验证阶段间可见性前阶段修改被后阶段看到后阶段不能反向影响前阶段D-25。"
- "属性→伤害联动:`final-effect`/光环改完属性后,`DamageContext` 缓存与脏标记随之更新,`getDamageInfo`/`markDirty`/`deleteEnemy`/`with(hero)` 行为正确(注入 fake `IDamageCalculator`D-26。"
- "刷新路径:`buildup`(全量)与 `requestRefresh`(局部)两条路径都覆盖;`resize`/`clear`/`destroy` 生命周期清空并解绑附件D-27。"
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
provides: EnemyContext 全接口、光环流水线、效果阶段顺序与生命周期覆盖
- path: packages-user/data-system/src/combat/damage.test.ts
provides: DamageContext/DamageSystem 覆盖(含 D-26 联动)
- path: packages-user/data-system/src/combat/mapDamage.test.ts
provides: MapDamage 覆盖
- 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
provides: CombatFlow 覆盖
- path: .planning/phases/06-unit-tests/06-COVERAGE-MAP.md
provides: 阶段级 code → 模块 → 用例 映射表(本计划创建并写入 06-01 行)
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"
- "context.test.ts 用测试用 fake `IAuraConverter`/`IAuraView` 手动装配(`new EnemyContext()` + `registerAuraConverter` + `setEnemyAt` + `bindHero`)驱动,绝不引用/重测顶层真实实现D-21"
- "全部文件经自身模块路径 + `@user/data-base` / `@user/data-common` / `@motajs/common` barrel 导入,证明 Node 下公开导入链可用,不 mock 数据层接口"
- "`.planning/phases/06-unit-tests/06-COVERAGE-MAP.md` 为共享 create-or-append 文件(幂等、顺序无关):本计划首次写入时创建表头(`| code | 模块 | 用例 | 计划 |`+ 06-01 小节;若表头已存在则只追加/更新本小节,绝不重写他节。后续计划用同一幂等协议,因此与其它 wave 1 计划执行顺序无关"
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"
- "不创建或修改任何生产/核心源码,只新增 `*.test.ts` 与规划产物"
- "不安装覆盖率工具(`@vitest/coverage-*`不设覆盖率阈值D-09"
- "不引入 `fake-indexeddb`,不做 CoreState 端到端存读档D-10"
- "疑似 bug 不得弱化为通过;按正确预期编写并标记 `it.skip`/`it.todo`D-05"
- "不建立跨文件共享 fixture/factory helperfixture 全部文件内联D-02/D-03"
- "异步路径只用真实计时器,禁用 `vi.useFakeTimers()`D-04"
- "不测试 `saveState`/`loadState`(集中到 06-09D-32"
---
<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).
以一条端到端 tracer 建立战斗系统(`packages-user/data-system/src/combat`)测试骨架,随后按 D-21..D-27 完成 `EnemyContext` 光环流水线、效果组合、四阶段顺序与全接口/全 code 覆盖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: `data-system/src/combat` 是纯 Layer-2 战斗契约,无 DOM、经注入 fake 协作者驱动是证明「Node 全局 stub + 动态 import 包 + fake 注入 + `logger.catch` + 真实计时器异步」测试骨架的最干净位置。本计划同时建立阶段级 `06-COVERAGE-MAP.md`
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).
Output: 4 个同目录 `*.test.ts``06-COVERAGE-MAP.md`(表头 + 06-01 行)。
</objective>
<execution_context>
@ -71,9 +74,9 @@ Output: 4 new co-located `*.test.ts` files, a `COVERAGE.md` api-coverage declara
@.planning/phases/06-unit-tests/06-PATTERNS.md
@.planning/codebase/TESTING.md
@dev.md
@packages-user/data-system/src/combat/context.ts
@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
@ -82,144 +85,130 @@ Output: 4 new co-located `*.test.ts` files, a `COVERAGE.md` api-coverage declara
@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).
- 只写测试,绝不修改 `packages-user/*/src` 下的非 `*.test.ts` 文件。
- 每个 `it(...)` 之前必须有一行中文注释说明覆盖内容dev.md 注释规范),测试范围变化时同步更新。
- 异步动作(战斗脚本/钩子)用真实计时器 + await 控制器完成,禁用 `vi.useFakeTimers()`D-04
- 疑似 bug按正确预期编写、标记 `it.skip`/`it.todo`、中文注释锚定 `#06-01-N` 并登记 `06-TEST-FINDINGS.md``pnpm test:ci` 必须保持全绿D-05
- 本计划不测 `saveState`/`loadState`D-32
- `06-COVERAGE-MAP.md` 为共享 create-or-append 文件(幂等、顺序无关):写入前先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在,不存在则先创建,再只追加/更新本计划小节,绝不重写他节。其它计划使用同一协议,故执行顺序不影响结果。
- **执行协议D-33执行本计划前必须先向用户确认执行完成后暂停用中文分条简要汇报本计划验证结果详细结果写入共用的 `06-TEST-FINDINGS.md`。**
</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>
<name>Task 1: Tracer — EnemyContext 光环流水线端到端最小路径(+ 阶段骨架)</name>
<files>packages-user/data-system/src/combat/context.test.ts, .planning/phases/06-unit-tests/06-COVERAGE-MAP.md</files>
<read_first>
packages-user/data-system/src/combat/context.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
.planning/phases/06-unit-tests/06-PATTERNS.md共享样板 1-5A 节 context
dev.md注释规范
</read_first>
<action>
创建 `context.test.ts` 作为战斗系统 tracer走通唯一一条真实端到端路径证明整套 harness 可用:
用共享样板 1 的 `vi.hoisted` 全局 stub + `beforeAll` 动态 import 包global `main`/`location`/`Map.prototype.getOrInsertComputed` 与 `Map.prototype.getOrInsert` 都要 polyfill`EnemyContext` 使用后者)。
手动装配:`new EnemyContext(state)``state` 用最小 inline fake `IStateBase`)→ `resize(3,3)``registerAuraConverter(fakeConverter)``setEnemyAt({x,y}, fakeEnemy)``bindHero(fakeHero)``buildup()`,断言 fake 光环对范围内怪物的基础属性加成生效hp/atk/def 按百分比加成后的具体数值)。
fake `IAuraConverter` 实现 `shouldConvert(special, handler)``convert(special, handler, context)`fake `IAuraView` 实现 `priority`/`range`(用真实 `IRange` 或最小 inline range/`getRangeParam`/`couldApplyBase`/`couldApplySpecial`/`apply`/`applySpecial`。fake `IEnemy`/`IReadonlyEnemy` 暴露 `iterateSpecials`/`addSpecial`/`deleteSpecial`/`getAttribute`/`setAttribute`/`addAttribute` 等被 `EnemyView` 使用的最小表面。
仅此一条路径,不做其它 call site、不做批量。
然后建立阶段骨架create-or-append幂等、顺序无关`.planning/phases/06-unit-tests/06-COVERAGE-MAP.md` 为目标,先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在——不存在则创建表头,已存在则保留;随后只新增/更新 06-01 小节占位,绝不重写他节。并写入本计划已覆盖的 code→模块→用例行97/98/99/100/101/110/106/107/102/103/104/138/139/140/141 的映射将在 Task 2/3 完成后补全用例名;此时先建表头与模块归属)。
以聚焦运行验证tracer 必须单独跑绿再进入扩展任务。若 `EnemyContext``buildup` 因缺少 hero 绑定或 range `bindHost` 契约而无法驱动,按 D-07 暂停并向用户提问,不修改核心代码。
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-system/src/combat/context.test.ts</automated>
<fails_when>聚焦运行出现任何未跳过失败,或文件因未解析的模块/全局而无法收集</fails_when>
</verify>
<acceptance_criteria>
- `context.test.ts``pnpm exec vitest run` 下零非跳过失败。
- 每个 `it` 前有单行中文注释。
- 至少断言一条完整光环路径fake converter 命中 → 光环入表 → `buildup` → 范围内怪物基础属性被加成。
- 未修改任何生产文件;`git status` 只显示 `*.test.ts`(新增或 MODIFIED既有测试文件允许被修改与规划产物`06-COVERAGE-MAP.md` / `06-TEST-FINDINGS.md`)。
- `06-COVERAGE-MAP.md` 存在且含表头与 06-01 小节create-or-append若表头已由其它计划建立则直接追加/更新本小节而不再新建)。
</acceptance_criteria>
<done>一条真实的 `EnemyContext` 光环端到端路径在提交前已跑绿,阶段 code 覆盖映射表骨架存在。</done>
</task>
<task type="auto">
<name>Task 2: EnemyContext 全接口、三范围、嵌套光环、警告码、四阶段顺序与两条刷新路径</name>
<files>packages-user/data-system/src/combat/context.test.ts</files>
<read_first>
packages-user/data-system/src/combat/context.ts
packages-user/data-system/src/combat/types.ts
packages-user/data-system/src/combat/enemy.ts
.planning/phases/06-unit-tests/06-PATTERNS.md共享样板 3-4A 节 context
</read_first>
<action>
`context.test.ts` 中扩展覆盖(每个 `it` 前一行中文注释):
- 接口全覆盖D-27/D-30逐个覆盖 `resize`、`registerAuraConverter`/`unregisterAuraConverter`/`setAuraConverterEnabled`、`registerCommonQueryEffect`/`unregisterCommonQueryEffect`、`registerSpecialQueryEffect`/`unregisterSpecialQueryEffect`、`registerFinalEffect`/`unregisterFinalEffect`、`bindHero`/`getBindedHero`、`getEnemyLocator`/`getEnemyLocatorByView`/`getEnemyByLocator`/`getEnemyByLoc`/`getViewByComputed`、`setEnemyAt`/`deleteEnemy`、`scanRange`/`iterateEnemy`、`addAura`/`deleteAura`、`attachMapDamage`/`getMapDamage`、`attachDamageSystem`/`getDamageSystem`、`buildup`、`markDirty`、`requestRefresh`、`clear`、`destroy`。未知/空输入返回 null 或 no-op未知 locator、未注册 view 的 `markDirty`、未绑定 converter 的 `setAuraConverterEnabled`)。
- 三范围D-22Full`haloRange<=0`、Rect`haloSquare`、Manhattan 各一条;每条同时断言范围外怪物属性不变。
- 嵌套光环D-23一层光环→特殊属性两层光环施加特殊属性→该特殊属性经第二 converter 转出新光环→再施加效果);优先级边界:新转换光环优先级高于当前阶段时被跳过并告警 99同/跨优先级传播。
- 警告码D-2497多个 converter 同时命中同一 special、98删除同级/更高优先级已生效光环,走 modify 分支与 delete 分支各一次、99新增高优先级光环、100add 与 delete 同时非空,`processSpecialModifier` 与 `refreshSpecialModifier` 各一次、101局部刷新期间新增/删除/修改了已转换光环)。均经 `logger.catch``vi.spyOn(logger,'warn')` 观测,绝不 `expect(...).toThrow`
- 110未绑定 hero 时 `buildup` 告警且不刷新。
- 四阶段顺序与可见性D-25用记录调用顺序的 fake effect/aura 断言 `buildupSpecials``buildupBase``buildupQuery``buildupFinal`;断言前阶段对属性的修改在后阶段可见,后阶段对属性只读不可反向影响前阶段。覆盖组合:光环基础效果 ↔ 常规查询效果(`registerCommonQueryEffect` 读到光环改过的属性)、光环特殊效果 ↔ 特殊查询效果(`registerSpecialQueryEffect` 与 `processSpecialModifier` 交互、final-effect 阶段顺序、同/跨优先级顺序。
- 两条刷新路径D-27`buildup`(全量,`needUpdate` 为真)与 `requestRefresh`(局部,仅脏 view + `requestedCommonContext``needTotallyRefresh` 命中时 `requestRefresh` 退回全量 `buildup`
- 生命周期D-27`resize` 清空并设置 width/height/indexer width`clear` 清空全部映射与集合;`destroy` 额外解绑 `mapDamage`/`damageSystem`、清空 converter/query/final 注册与 hero 绑定(断言之后 getter 返回 null/空)。
全部 fixture 内联;不新建跨文件 helper。
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-system/src/combat/context.test.ts</automated>
<fails_when>运行失败,或任一警告码 97/98/99/100/101/110、三范围、嵌套、四阶段顺序断言未被观测</fails_when>
</verify>
<acceptance_criteria>
- `context.test.ts` 零非跳过失败。
- 覆盖 `EnemyContext` 全部公开方法(至少一条正常用例)与全部列出警告码。
- 三范围 + 范围外不生效、一层/两层嵌套 + 优先级边界、四阶段顺序 + 阶段间可见性、全量/局部两条刷新路径、resize/clear/destroy 生命周期均有断言。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>`EnemyContext` 全接口、光环范围/嵌套/警告码、四阶段顺序与两条刷新路径全部覆盖并跑绿。</done>
</task>
<task type="auto">
<name>Task 3: DamageContext/DamageSystem含 D-26 联动、MapDamage、CombatFlow</name>
<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/combat.test.ts</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)
packages-user/data-system/src/combat/context.ts
packages-user/data-system/src/event/eventDispatch.test.ts
packages-user/data-common/src/store/tileStore.test.ts
.planning/phases/06-unit-tests/06-PATTERNS.mdA 节 damage/mapDamage/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.
`damage.test.ts`:注入 fake `IEnemyContext` 与 fake `IDamageCalculator``calculate(handler)` 返回固定 `{damage,turn}`
- `DamageContext.getDamageInfo`/`getDamageInfoByComputed`/`getDamageInfoByHandler` 返回结果与 handler 身份computed 怪 + locator + 绑定 hero
- 缺 calculator 告警 106、缺 hero 告警 107`logger.catch`)。
- `DamageSystem` 缓存:连续两次 `getDamageInfo` 同对象;`markDirty(view)`/`markAllDirty()`/`deleteEnemy(view)` 强制重算;`useCalculator`/`bindHeroStatus` 触发 `markAllDirty`
- D-26 联动:在 `EnemyContext` 完成 `buildup`fake 光环改属性)后接入该 `DamageSystem`,断言 `getDamageInfo` 反映新属性、且 `buildup`/`clear`/`setEnemyAt` 使缓存失效;`with(hero)` 返回共享 calculator、返回传入 hero 的 `DamageContext`
- `calculateCritical(view, attribute, precision)` 生成器:用 fake `getCriticalLimit` 与已知临界值,断言 yield 的临界点与 `info`;已知 `#06-01-1``info.damage` 与 `nextValue` 不对应)继续以正确预期 + `it.skip` 保留。
`mapDamage.test.ts`fake `IEnemyContext` + fake `IMapDamageConverter`/`IMapDamageReducer`/`IMapDamageView``getRange`/`getRangeParam`/`getDamageWithoutCheck` 确定)。
- `addMapDamage`/`deleteMapDamage` 累积无来源伤害并标脏;`getReducedDamage` 返回 reducer 结果、无分离伤害返回 null缺 reducer 告警 103、缺 converter 告警 102`getSeparatedDamage` 合并有来源 + 无来源;`deleteEnemy` 移除有来源;`markEnemyDirty` 在 view 无 store 且无 locator 时告警 104。已知 `#06-01-2``deleteEnemy` 幽灵伤害)以正确预期 + `it.skip` 保留。105 在当前实现不可达,不写断言。
`combat.test.ts`fake `IStateBase` 与 fake enemy view/context/damage 驱动 `CombatFlow`
- `bindContext`/`bindDamage` 接受同 state 对象、拒绝异 state 对象告警 138`logger.catch`),传 null 清空。
- `bindHero(null)`/`bindHero(hero)` 设置 hero。
- `addCombatScript` 按优先级降序;重复优先级告警 140 且列表不变。
- `battle(view)`/`battleComputed(enemy)` 在缺 context 或 hero 时返回 null 并告警 139缺 damage context 告警 139`getDamageInfoByHandler` 返回 null 告警 141。
- 异步完成证明(真实计时器,无 fake timers手动 resolver 控制的 `before` 必须先于 `onAfterCombat` 完成;断言调用顺序 `[script.before, hooks.onBeforeCombat, script.after, hooks.onAfterCombat]`。已知 `#06-01-3``before` 真值短路与文档相反)以正确预期 + `it.skip` 保留并注明按接口文档语义。
</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>
<automated>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/combat.test.ts</automated>
<fails_when>任一聚焦运行失败,或 106/107/102/103/104/138/139/140/141 中任一码未被观测</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.
- 三个文件零非跳过失败。
- damage.test.ts 覆盖缓存/失效/`with(hero)`/`calculateCritical` 并断言 106、107D-26 联动有断言。
- mapDamage.test.ts 断言 102、103、104 与 reduce/delete 行为。
- combat.test.ts 断言 138、139、140、141 与 awaited 脚本顺序。
- 无 fake timers`it.skip` 用例与 `06-TEST-FINDINGS.md``#06-01-N` 一一对应。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>CombatFlow binding, guard warnings, and awaited script ordering are covered and green.</done>
<done>伤害上下文/系统、地图伤害、战斗流程覆盖并跑绿,且四阶段/联动边界与既有 `#06-01-N` 跳过用例保留。</done>
</task>
</tasks>
@ -229,50 +218,50 @@ Use the manual-resolver await pattern from `eventDispatch.test.ts` referenced in
| 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. |
| none | 仅新增仓库内 Node Vitest 测试不引入运行时输入面、网络调用、DOM 访问或文件写入。 |
## 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. |
| T-06-01-01 | Tampering | `data-system/src/combat` 下测试文件 | low | mitigate | 只导入仓库内已跟踪模块、构造合成内存 fixture不安装包、不改核心源码门禁 `pnpm test:ci` + 复核仅 `*.test.ts` 变更。 |
| T-06-01-02 | Information disclosure | 测试 fixture | low | accept | 合成数值/id无密钥、无真实游戏数据D-02 |
| T-06-01-03 | Denial of service | 假光环/效果无限循环 | medium | mitigate | `buildup` 的优先级 while 循环用有限优先级集合每条用例有界30s 测试超时为兜底。 |
| T-06-01-SC | Tampering | 包安装 | high | mitigate | 本计划不安装任何新依赖;若确需安装,停止并先走包合法性 checkpoint。 |
</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`.
- 聚焦运行:`packages-user/data-system/src/combat` 下 4 个测试文件全部通过。
- `pnpm test:ci` 全量绿(既有测试文件保持绿)。
- `git status` 仅显示 `*.test.ts`(新增或 MODIFIED既有测试文件允许被修改与规划产物`06-COVERAGE-MAP.md` / `06-TEST-FINDINGS.md`);无生产文件改动。
- 任何 `it.skip`/`it.todo` 均有对应 `#06-01-N``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.
- 4 个战斗 Layer-2 行为测试文件存在并通过。
- D-21..D-27 的光环范围/嵌套/优先级/警告码/四阶段顺序/属性→伤害联动全部断言。
- `EnemyContext` 每个公开方法至少一条正常用例15 个可达 code 全部触发断言。
- `06-COVERAGE-MAP.md` 表头与 06-01 行存在。
</success_criteria>
<artifacts_this_plan_produces>
Test files:
- `packages-user/data-system/src/combat/context.test.ts`
- `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`
Inline helper symbols(每文件内联,不共享):
- `context.test.ts`: `TestModules`、`modules`、`FakeConverter`、`FakeAura`、`createContextFixture`
- `damage.test.ts`: `createEnemyContext`、`createCalculator`、`createHandler`
- `mapDamage.test.ts`: `createMapDamageFixture`、`FakeConverter`、`FakeReducer`、`FakeView`
- `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)
- `.planning/phases/06-unit-tests/06-COVERAGE-MAP.md`create-or-append表头 + 06-01 行;若表头已存在则仅追加/更新本小节)
</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`.
完成时创建 `.planning/phases/06-unit-tests/06-01-SUMMARY.md`,并将 06-01 的 code→模块→用例行写入 `06-COVERAGE-MAP.md`;疑似 bug 追加到 `.planning/phases/06-unit-tests/06-TEST-FINDINGS.md``#06-01-N` 小节。执行完成后暂停并用中文分条汇报。
</output>

View File

@ -14,51 +14,53 @@ files_modified:
autonomous: true
requirements: [TEST-01]
estimate:
tokens: 58000
raw_tokens: 58000
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."
- 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
- "TEST-01 由数据端行为单测满足,门禁为固定非 watch 命令 `pnpm test:ci` 全绿D-08。"
- "接口全覆盖:`MainDamageCalculator``calculate`/`getCriticalLimit`)、`MainEnemyFinalEffect``apply`/`priority`)、`MainEnemyComparer``compare`)、`CommonAura`/`CommonAuraConverter`、`GuardAura`/`GuardAuraConverter`、`registerSpecials`、mapDamage 五视图 + `MainMapDamageConverter` + `MainMapDamageReducer` 的每个公开方法至少一条正常用例;名称含 legacy 的接口/方法排除D-30 / D-35。"
- "code 全覆盖:本计划可达的 warn 码 137支援怪缺失 guard至少一条触发断言`logger.catch` 观测D-31数据层 `logger.error(53)` 归 06-03本计划用 fake manager 不触发。"
- "本计划只测顶层实现的**基本功能**(单分支、按预期输出);属性/加成等**组合语义后移至 06-07**D-35。"
- "`MainDamageCalculator.calculate` 对 无敌/魔攻/连击/多段/支援/先攻/破甲/反击/净化/吸血/固伤/仇恨/负伤 各单分支输出确定 `{damage,turn}``getCriticalLimit` 对 坚固 与一般怪正确。"
- "`CommonAura`/`GuardAura` 各自转换器 `shouldConvert` 仅匹配对应 code范围参数与基础/guard 加成按单分支计算。"
- "`registerSpecials` 注册 code 0-27 各一次并设置 guard 属性默认值。"
- "mapDamage 五视图单分支输出、converter 按 special 产生视图、reducer 求和/取最大类型/合并 extra。"
artifacts:
- path: packages-user/data-state/src/enemy/calculator.test.ts
provides: MainDamageCalculator branch coverage
provides: MainDamageCalculator 与 getCriticalLimit 覆盖
- path: packages-user/data-state/src/enemy/final.test.ts
provides: MainEnemyFinalEffect coverage
provides: MainEnemyFinalEffect 覆盖
- path: packages-user/data-state/src/enemy/comparer.test.ts
provides: MainEnemyComparer coverage
provides: MainEnemyComparer 覆盖
- path: packages-user/data-state/src/enemy/aura.test.ts
provides: CommonAura/GuardAura coverage
provides: CommonAura/GuardAura 及转换器覆盖
- path: packages-user/data-state/src/enemy/special.test.ts
provides: registerSpecials coverage
provides: registerSpecials 覆盖
- path: packages-user/data-state/src/enemy/mapDamage.test.ts
provides: map damage view/converter/reducer coverage
provides: mapDamage 视图/converter/reducer 覆盖
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"
- "calculator.test.ts 复用 `data-state/test/dataClosure.test.ts` 的 inline fake `IReadonlyEnemyHandler` 形状,扩展既有单点覆盖而非重复"
- "special.test.ts 用 inline fake `IEnemyManager`(不 mock 数据层接口)驱动 registerSpecials"
- "`06-COVERAGE-MAP.md` 为共享 create-or-append 文件:写 06-02 小节前先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在,不存在则先创建,再只追加/更新本小节;幂等且与其它 wave 1 计划顺序无关,不重写他节"
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"
- "不创建或修改任何生产/核心源码,只新增 `*.test.ts`"
- "不安装覆盖率工具D-09"
- "不引入 `fake-indexeddb`,不做 CoreState 端到端存读档D-10"
- "疑似 bug 按正确预期编写并标记 `it.skip`/`it.todo`D-05"
- "fixture 全部文件内联,不建跨文件 helperD-02/D-03"
- "不使用 fake timersD-04"
- "不测 `saveState`/`loadState`D-32不测属性/加成组合语义D-35移入 06-07"
---
<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.
覆盖 `packages-user/data-state/src/enemy` 顶层战斗实现的**基本功能**D-35伤害计算、最终效果、比较器、光环、特殊属性注册、地图伤害。
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: 这些类是接入 `CoreState` 的真实内置战斗实现;`dataClosure.test.ts` 仅覆盖单个确定的 `MainDamageCalculator` 场景。本计划只验证每个组件的单分支/单路径正确性,把跨特殊属性、跨光环/效果的**组合**留到 06-07。
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.
Output: 6 个同目录 `*.test.ts`
</objective>
<execution_context>
@ -79,124 +81,123 @@ Output: 6 new co-located `*.test.ts` files.
@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
@packages-user/data-state/src/replay/commands.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.
- 只写测试,绝不修改任何生产文件。
- 每个 `it(...)` 前必须有一行中文注释dev.md
- fixture 内联且最小D-02/D-03不读取真实游戏数据。
- 警告路径经 `logger.catch` / `vi.spyOn(logger, ...)`,绝不 `expect(...).toThrow`
- 疑似 bug正确预期 + `it.skip`/`it.todo` + 中文注释锚定 `#06-02-N``pnpm test:ci` 保持全绿。
- 本计划只做单分支基本功能;`saveState`/`loadState` 与组合语义不测D-32 / D-35
- `06-COVERAGE-MAP.md` 为共享 create-or-append 文件(幂等、顺序无关):写 06-02 小节前先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在,不存在则先创建,再只追加/更新本小节,绝不重写他节。
- **执行协议D-33执行前向用户确认执行后暂停并用中文分条汇报。**
</execution_rules>
<tasks>
<task type="auto">
<name>Task 1: MainDamageCalculator + final effect + comparer</name>
<name>Task 1: MainDamageCalculator 单分支 + getCriticalLimit + MainEnemyFinalEffect + MainEnemyComparer</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)
packages-user/data-state/src/enemy/types.ts
packages-user/data-state/test/dataClosure.test.ts
.planning/phases/06-unit-tests/06-PATTERNS.mdA 节 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`.
`calculator.test.ts`:建立一个 inline handler 工厂enemy/context/locator/hero/state`MainDamageCalculator.calculate` 的每个分支单独断言精确 `{damage,turn}`(基本功能,各分支独立):
- 基线heroPerDamage = atk - monDefturn = ceil(monHp / heroPerDamage)。
- 无敌20默认 `{damage: Infinity, turn: 0}`;当 `state.hero.items.getItemState('cross')?.count >= 1` 时走普通计算fake `state.hero.items.getItemState`)。
- 魔攻2用 monAtk 不减 hero def连击 4/5 敌伤乘 2/3多段6`value`
- 先攻1加一次敌伤破甲7`value%/100 * def`反击8`value%/100 * atk` 到敌伤净化9`value * mdef`吸血11`value%/100 * hp` 加到伤害、`add` 为真时加到 monHp固伤22与仇恨17`flags.getFieldValueDefaults('hatred',0)`)绕过 mdef负伤 flag 控制负伤害截断为 0。
- 支援 guard单分支guard 非空且未 inGuard 时递归累加 guard 的 turn/damageguard 位置缺失时经 `logger.catch` 观测 warn 137。
- 返回值 `damage` 取整;`getCriticalLimit('atk')`坚固3返回 Infinity否则 `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.
`final.test.ts``MainEnemyFinalEffect.apply` 对 坚固3把 def 抬到 `max(def, heroAtk - 1)`、对 模仿10复制 hero atk/def、其它怪不变`priority` 为 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`).
`comparer.test.ts``MainEnemyComparer.compare` 属性 + specials 完全相同时为 true任一基础属性不同、或 special 集合长度/code/value 不同(`deepEqualsTo`)为 false。
Keep helper `createEnemy`/`createHandler` inline in each file (no cross-file fixture). If an expected value contradicts the implementation, follow the skip+findings rule.
helper `createEnemy`/`createHandler` 各文件内联;若期望与实现矛盾,按 skip+findings 规则处理。
</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>
<fails_when>任一聚焦运行失败,或某单分支期望值与文档算术不符、或 warn 137 未被观测</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.
- 三个文件零非跳过失败。
- calculator.test.ts 断言每个列出分支与 warn 137final.test.ts 与 comparer.test.ts 断言其文档分支。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>Calculator, final effect, and comparer behavior is covered and green.</done>
<done>伤害计算、最终效果、比较器的单分支基本功能覆盖并跑绿。</done>
</task>
<task type="auto">
<name>Task 2: Auras + special registration</name>
<name>Task 2: CommonAura/GuardAura 与 registerSpecials</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)
packages-user/data-base/src/enemy/special.ts
packages-user/data-common/src/store/tileStore.test.ts
.planning/phases/06-unit-tests/06-PATTERNS.mdA 节 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.
`aura.test.ts`:用 inline 最小 `ISpecial` 与 fake enemy/context/locator
- `CommonAuraConverter.shouldConvert` 仅对 code 25 为真;`CommonAura.createRange`/`getRangeParam``haloRange <= 0` 用 Fullparam 为 undefined、`haloSquare` 用以 locator 为中心的 rect、否则 Manhattan。
- `CommonAura.apply` 对 computed enemy 加 `floor(base * hpBuff/100)`、`floor(base * atkBuff/100)`、`floor(base * defBuff/100)`buff 为 0 时不改;`couldApplyBase` 为 true、`couldApplySpecial` 为 false、`priority` 25。
- `GuardAuraConverter.shouldConvert` 仅对 code 26 为真;`GuardAura.getRangeParam` 为 3x3 rect`apply` 对**不同** locator 把 guard 位置加入 `enemy.getAttribute('guard')`、跳过来源自身 locator`context.getViewByComputed` 返回 null 时 no-op。
`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.
`special.test.ts`:用 inline fake `IEnemyManager` 调用 `registerSpecials`,断言 code 0-27 各注册一次且带 `getSpecialName`/`getDescription`,并调用了 `setAttributeDefaults('guard', ...)`;抽查一个可序列化 special如 code 6 返回 `n连击`)与一个 none-property special如 code 1「先攻」的名称/描述。不 mock 数据层接口。fixture 内联。
</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>
<fails_when>运行失败,或转换器匹配/范围参数/加成数值/注册数量断言与文档行为不符</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.
- 两个文件零非跳过失败。
- aura.test.ts 断言范围选择、buff 数学与 guard-locator 行为(各单分支)。
- special.test.ts 断言 code 0-27 各注册一次 + guard 默认值 + 名称/描述抽查。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>Aura conversion and the special registry are covered and green.</done>
<done>光环转换与特殊属性注册的单分支基本功能覆盖并跑绿。</done>
</task>
<task type="auto">
<name>Task 3: Map damage views, converter, reducer</name>
<name>Task 3: mapDamage 五视图 + 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)
packages-user/data-state/src/enemy/special.ts
packages-user/data-state/src/enemy/types.ts
packages-user/data-system/src/combat/mapDamage.ts
.planning/phases/06-unit-tests/06-PATTERNS.mdA 节 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.
构建 inline fake `IEnemyContext`(含 `indexer`/`getEnemyByLoc`/`getDamageSystem`)与 inline `ISpecial`,逐个单分支断言:
- `ZoneDamageView`range 按 `zoneSquare` 为 Manhattan 或 rect`getRangeParam` 与 range/居中一致;`getDamageWithoutCheck` 返回 `{damage: zone, type: MapDamageType.Zone, extra:{catch, repulse}}`
- `RepulseDamageView`Manhattan 半径 1来源 locator 返回 null否则 type `Repulse``repulse` 含来源 locator。
- `LaserDamageView`range param 使用 face handler `mapMovement` 方向damage type `Layer`
- `BetweenDamageView`:按实现实际语义断言(排除 `deltaX<=0 && deltaY<=0`,即向右/向下相邻命中避免同对怪重复计算06-02 SUMMARY 已澄清),需邻居含 special 16返回 hero 最终 hp 的一半;`core.flags.betweenAttackMax` 为真时用伤害系统 computed 值(系统缺失回退一半)。
- `AmbushDamageView`type `Unknown`damage 0catch 含 locator。
- `MainMapDamageConverter.convert` 对存在的 special15/16/18/24/27各推入一个 viewlaser 用 `state.faceManager.get(FaceGroup.Dir4)!`
- `MainMapDamageReducer.reduce` 求和、取最大伤害项的类型、合并 `catch`/`repulse`。
如 import 链需要,使用共享全局 stubfixture 内联。若某 `core` 依赖期望随环境变化,按 skip+findings 规则处理。
</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>
<fails_when>运行失败,或某视图/reducer 的分支输出与实现实际语义不符</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.
- `mapDamage.test.ts` 零非跳过失败。
- 五视图单分支、converter 的每 special 视图集合、reducer 的求和/类型/extra 合并均有断言。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>Map damage views, converter, and reducer behavior is covered and green.</done>
<done>地图伤害视图、converter、reducer 的单分支基本功能覆盖并跑绿;完成后以 create-or-append 方式将本计划的 code→模块→用例行code 137 → calculator.ts写入 `06-COVERAGE-MAP.md` 的 06-02 小节:先检查表头,不存在则先创建,再只追加本小节,不重写他节。</done>
</task>
</tasks>
@ -206,27 +207,30 @@ Guard the `core.*` global reads with the shared global stub if the import chain
| Boundary | Description |
|----------|-------------|
| none | In-repo Node Vitest tests only; no runtime input surface, network, DOM, or filesystem write is introduced. |
| none | 仅新增仓库内 Node Vitest 测试无运行时输入面、网络、DOM 或文件写入。 |
## 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. |
| T-06-02-01 | Tampering | `data-state/src/enemy` 下测试文件 | low | mitigate | 只导入已跟踪模块、合成 fixture不安装包、不改核心源码门禁 `pnpm test:ci` + 复核仅 `*.test.ts` 变更。 |
| T-06-02-02 | Information disclosure | fixture | low | accept | 合成数值/id无真实游戏数据或密钥D-02 |
| T-06-02-03 | Repudiation | 全局 stub 泄漏 | low | accept | 任何 `vi.stubGlobal``afterAll` 拆除;用例相互独立。 |
| T-06-02-SC | Tampering | 包安装 | high | mitigate | 不新增依赖;若确需安装,停止并先走包合法性 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`.
- 聚焦运行全部 6 个文件通过;`pnpm test:ci` 全量绿。
- `git status` 仅显示 `*.test.ts`(新增或 MODIFIED既有测试文件允许被修改与规划产物`06-COVERAGE-MAP.md` / `06-TEST-FINDINGS.md`);无核心文件改动。
- 任何 `it.skip`/`it.todo` 均有 `#06-02-N` 条目。
- 以 create-or-append 方式写入 `06-COVERAGE-MAP.md` 06-02 小节code 137表头不存在则先创建幂等、顺序无关不重写他节。
- **执行协议**:执行前向用户确认;执行后暂停并用中文分条汇报。
</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.
- 6 个 `data-state/src/enemy` 行为测试文件存在或已修改并通过(既有文件允许 MODIFIED
- 顶层实现在单分支层面的全部公开方法均有正常用例warn 137 有触发断言。
- 组合语义未在本计划出现(留给 06-07
</success_criteria>
<artifacts_this_plan_produces>
@ -239,14 +243,17 @@ Test files:
- `packages-user/data-state/src/enemy/mapDamage.test.ts`
Inline helper symbols:
- `calculator.test.ts`: `createHandler`, `createEnemy`, `special`
- `final.test.ts`: `createHandler`, `createEnemy`
- `calculator.test.ts`: `createHandler`、`createEnemy`、`special`
- `final.test.ts`: `createHandler`、`createEnemy`
- `comparer.test.ts`: `createEnemy`
- `aura.test.ts`: `createEnemy`, `createSpecial`, `createContext`
- `aura.test.ts`: `createEnemy`、`createSpecial`、`createContext`
- `special.test.ts`: `FakeEnemyManager`
- `mapDamage.test.ts`: `createContext`, `createSpecial`, `FakeFaceHandler`
- `mapDamage.test.ts`: `createContext`、`createSpecial`、`FakeFaceHandler`
Planning artifacts:
- `06-COVERAGE-MAP.md` create-or-append 06-02 小节code 137 → calculator.ts → 用例名)
</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`.
完成时创建 `.planning/phases/06-unit-tests/06-02-SUMMARY.md`,以 create-or-append 方式写入 `06-COVERAGE-MAP.md` 06-02 小节;疑似 bug 追加到 `06-TEST-FINDINGS.md``#06-02-N` 小节。执行完成后暂停并用中文分条汇报。
</output>

View File

@ -11,43 +11,46 @@ files_modified:
autonomous: true
requirements: [TEST-01]
estimate:
tokens: 42000
raw_tokens: 42000
tokens: 48000
raw_tokens: 48000
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
- "TEST-01 由数据端行为单测满足,门禁为固定非 watch 命令 `pnpm test:ci` 全绿D-08。"
- "接口全覆盖:`Enemy`、`CommonSerializableSpecial`、`NonePropertySpecial`、`EnemyManager` 的每个公开方法至少一条正常用例;名称含 legacy 的接口/方法(`IEnemyLegacyBridge`、`fromLegacyEnemy`、`legacyIdToCode` 等一律排除D-14 / D-30 / D-36。"
- "code 全覆盖:本计划可达的 warn/error 码 53setAttributeDefaults 非法默认值、96重复 special、117compareWith 多次调用、118无 comparer 时 updateDirty各至少一条触发断言D-31 / D-36。"
- "loadState 专属码 119prefab 缺失、120special 缺失)与 `saveState`/`loadState` 一并归 06-09本计划不测D-32。"
- "`Enemy` 属性增删改查、special 增删查迭代、`clone`/`cloneAttributes`/`copyFrom` 行为与文档一致且相互独立。"
- "`CommonSerializableSpecial`/`NonePropertySpecial` 的 value 读写、`getSpecialName`/`getDescription`、clone 独立性、`deepEqualsTo` 判定与文档一致。"
- "`EnemyManager` 注册表special/attribute 默认值、prefab按 code/id 的增删改查与 clone 独立性、reuse 映射、`modifyPrefabAttribute`、comparer/脏跟踪生命周期行为与文档一致。"
artifacts:
- path: packages-user/data-base/src/enemy/enemy.test.ts
provides: Enemy model coverage
provides: Enemy 模型覆盖
- path: packages-user/data-base/src/enemy/special.test.ts
provides: special model coverage
provides: CommonSerializableSpecial/NonePropertySpecial 覆盖
- path: packages-user/data-base/src/enemy/manager.test.ts
provides: EnemyManager registry/dirty coverage
provides: EnemyManager 注册表/prefab/reuse/脏跟踪覆盖
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"
- "enemy.test.ts 扩展 `data-state/test/dataClosure.test.ts:104-118` 的 create/mutate 形状到完整 `Enemy` 表面(去掉其 save/load 部分)"
- "manager.test.ts 用 inline fake `IEnemyLegacyBridge` 与 inline `Enemy` 实例驱动,遵循 tileStore 注册表契约形状"
- "`06-COVERAGE-MAP.md` 为共享 create-or-append 文件:写 06-03 小节前先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在,不存在则先创建,再只追加/更新本小节;幂等且与其它 wave 1 计划顺序无关,不重写他节"
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"
- "不创建或修改任何生产/核心源码,只新增 `*.test.ts`"
- "不安装覆盖率工具D-09"
- "不引入 `fake-indexeddb`D-10"
- "疑似 bug 按正确预期编写并标记 `it.skip`/`it.todo`D-05"
- "fixture 全部文件内联,不建跨文件 helperD-02/D-03"
- "不使用 fake timersD-04"
- "不测 `saveState`/`loadState`D-32不测任何名称含 legacy 的接口/方法D-30"
---
<objective>
Cover the Layer-1 enemy data model `packages-user/data-base/src/enemy` in full (D-14): `Enemy`, the special implementations, and `EnemyManager`.
完整覆盖 Layer-1 怪物数据模型 `packages-user/data-base/src/enemy`D-14/D-36`Enemy`、special 实现、`EnemyManager` 公开接口(除 legacy
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 要求验证数据模型本身(属性/special 增删改查、注册表、模板、复用映射、比较器/脏跟踪),而不只是消费它的战斗计算器。存读档集中到 06-09。
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.
Output: 3 个同目录 `*.test.ts`
</objective>
<execution_context>
@ -69,76 +72,82 @@ Output: 3 new co-located `*.test.ts` files.
</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.
- 只写测试,绝不修改任何生产文件。
- 每个 `it(...)` 前必须有一行中文注释dev.md
- fixture 内联且最小D-02/D-03
- 警告路径经 `logger.catch` / `vi.spyOn(logger, ...)`
- 疑似 bug正确预期 + `it.skip`/`it.todo` + 中文注释锚定 `#06-03-N``pnpm test:ci` 保持全绿。
- 不测存读档D-32排除一切 legacy 接口/方法D-30
- `06-COVERAGE-MAP.md` 为共享 create-or-append 文件(幂等、顺序无关):写 06-03 小节前先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在,不存在则先创建,再只追加/更新本小节,绝不重写他节。
- **执行协议D-33执行前向用户确认执行后暂停并用中文分条汇报。**
</execution_rules>
<tasks>
<task type="auto">
<name>Task 1: Enemy model + special implementations</name>
<name>Task 1: Enemy 模型 + special 实现</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)
packages-user/data-state/test/dataClosure.test.ts
.planning/phases/06-unit-tests/06-PATTERNS.mdA 节 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).
`enemy.test.ts`:用 inline `IEnemyAttr` 形状对象构造 `Enemy`,覆盖:
- `getAttribute`/`setAttribute`/`addAttribute` 对数值键;`cloneAttributes` 返回结构化克隆、非别名副本。
- special CRUD`addSpecial` 后 `getSpecial`/`hasSpecial`/`iterateSpecials`;重复 code 告警 96 且不替换既有 special`deleteSpecial` 按对象与按 code 删除;未知 code 返回 null。
- `clone`/`copyFrom`clone 复制属性与克隆后的 special且与来源独立`copyFrom` 替换属性与 special。
- 不测 `saveState`/`loadState`D-32
`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).
`special.test.ts`:用 `defineCommonSerializableSpecial`/`defineNonePropertySpecial` + inline 配置覆盖:
- `CommonSerializableSpecial``setValue`/`getValue`、`getSpecialName`、`getDescription`、clone 独立性、`deepEqualsTo` 对同 code+同值为 true、对 code 或 value 不同为 false。
- `NonePropertySpecial`value 为 undefined、clone、`deepEqualsTo` 按 code 判定。
- 不调用任何 legacy 命名接口(如 `fromLegacyEnemy`),不测其 `saveState`/`loadState`。
</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>
<fails_when>聚焦运行失败,或重复 special 的 warn 96 未被观测,或 clone 独立性断言失败</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.
- 两个文件零非跳过失败。
- enemy.test.ts 断言属性/special CRUD、clone/copyFrom 独立性、warn 96。
- special.test.ts 断言两个 special 类的全部公开方法(除 legacy 与存读档)。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>Enemy and special data models are covered and green.</done>
<done>Enemy 与 special 数据模型覆盖并跑绿。</done>
</task>
<task type="auto">
<name>Task 2: EnemyManager registry, prefabs, reuse, dirty tracking</name>
<name>Task 2: EnemyManager 注册表、prefab、reuse、脏跟踪</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)
.planning/phases/06-unit-tests/06-PATTERNS.mdA 节 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`.
用 inline fake `IEnemyLegacyBridge`(其 `fromLegacyEnemy` 返回合成属性)与 inline `Enemy` prefab 驱动 `EnemyManager`,按 tileStore 契约形状覆盖:
- `registerSpecial` 允许覆盖(同 code 后注册者胜);`setAttributeDefaults` 设置默认值,对 function/symbol/bigint/undefined 经 `logger.catch` 观测 `logger.error(53)`
- `addPrefab` clone 且忽略重复 code 或 id`createEnemy(code)`/`createEnemyById(id)` 返回独立 clone、未知返回 null`getPrefab`/`getPrefabById``deletePrefab` 按 code 与按 id 删除两个索引;`changePrefab` 替换并标脏。
- `reusePrefab(source, code, id)``getPrefab`/`getPrefabById`/`createEnemy` 经复用映射解析到来源 prefab未知来源 no-op。
- `modifyPrefabAttribute` 应用回调、code/id 变化时重建索引并标脏;未知 code no-op。
- comparer + 脏跟踪:任何 `compareWith` 前脏跟踪惰性;`attachEnemyComparer` + 首次 `compareWith(reference)` 清空脏集;第二次 `compareWith` 告警 117 并刷新脏集;无 comparer 的 `updateDirty` 告警 118 并标脏;`refreshDirty` 丢弃与 reference 相等的条目。
- 不测 `saveState`/`loadState`(含 119/120集中到 06-09。
fixture 内联;警告码经 `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>
<fails_when>聚焦运行失败,或注册表/脏跟踪断言、或 53/117/118 中任一码未被观测</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.
- `manager.test.ts` 零非跳过失败。
- 断言 prefab create/get/delete/change、reuse 映射、modify、comparer 脏跟踪生命周期,以及 warn 53/117/118。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>EnemyManager registry, prefab, reuse, and dirty behavior is covered and green.</done>
<done>EnemyManager 注册表、prefab、reuse、脏跟踪覆盖并跑绿完成后以 create-or-append 方式将本计划的 code→模块→用例行53/96/117/118写入 `06-COVERAGE-MAP.md` 的 06-03 小节:先检查表头,不存在则先创建,再只追加本小节,不重写他节。</done>
</task>
</tasks>
@ -148,27 +157,29 @@ Keep inline fixtures; assert warning codes via `logger.catch`.
| Boundary | Description |
|----------|-------------|
| none | In-repo Node Vitest tests only; no runtime input surface, network, DOM, or filesystem write is introduced. |
| none | 仅新增仓库内 Node Vitest 测试无运行时输入面、网络、DOM 或文件写入。 |
## 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. |
| T-06-03-01 | Tampering | `data-base/src/enemy` 下测试文件 | low | mitigate | 只导入已跟踪模块、合成 fixture不安装包、不改核心源码门禁 `pnpm test:ci` + 复核仅 `*.test.ts` 变更。 |
| T-06-03-02 | Information disclosure | fixture | low | accept | 合成属性/special无真实游戏数据D-02 |
| T-06-03-03 | Repudiation | 全局 stub 泄漏 | low | accept | 任何 `vi.stubGlobal``afterAll` 拆除;用例独立。 |
| T-06-03-SC | Tampering | 包安装 | high | mitigate | 不新增依赖;若确需安装,停止并先走包合法性 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`.
- 三个文件聚焦运行通过;`pnpm test:ci` 全量绿。
- `git status` 仅新增 `*.test.ts`;无核心文件改动。
- 任何 `it.skip`/`it.todo` 均有 `#06-03-N` 条目。
- 以 create-or-append 方式写入 `06-COVERAGE-MAP.md` 06-03 小节code 53/96/117/118表头不存在则先创建幂等、顺序无关不重写他节。
- **执行协议**:执行前向用户确认;执行后暂停并用中文分条汇报。
</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.
- 3 `data-base/src/enemy` 行为测试文件存在并通过。
- D-14/D-36 的数据模型表面(属性/special/注册表/prefab/reuse/comparer 脏全部断言legacy 与存读档未测。
</success_criteria>
<artifacts_this_plan_produces>
@ -178,11 +189,14 @@ Test files:
- `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`
- `enemy.test.ts`: `createEnemy`、`createSpecial`
- `special.test.ts`: `makeConfig`、`makeLegacyEnemy`(仅作合成属性载体,不调用 legacy 接口)
- `manager.test.ts`: `FakeBridge`、`createPrefab`、`createManager`
Planning artifacts:
- `06-COVERAGE-MAP.md` create-or-append 06-03 小节
</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`.
完成时创建 `.planning/phases/06-unit-tests/06-03-SUMMARY.md`,以 create-or-append 方式写入 `06-COVERAGE-MAP.md` 06-03 小节;疑似 bug 追加到 `06-TEST-FINDINGS.md``#06-03-N` 小节。执行完成后暂停并用中文分条汇报。
</output>

View File

@ -5,57 +5,55 @@ 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/system.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
tokens: 60000
raw_tokens: 60000
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
- "TEST-01 由数据端行为单测满足,门禁为固定非 watch 命令 `pnpm test:ci` 全绿D-08。"
- "接口全覆盖:`ReplayArray``add`/`insert`/`delete`/`set`/`setCommandWidth`/`get`/`createReadStream`/`rebuildIndexArray`/`setReplayArray`/`getCommandArray`/`getParamArray`)、`ReplaySystem``registerCommand`/`getCommand`/`record`/`createReplaySandbox`/`releaseSandbox`)、`ReplaySandbox``setSpeed`/`getReplayed`/`play`/`pause`/`resume`/`stop`/`step`)以及 `func.ts` 播放安全装饰器/工具的每个公开方法至少一条正常用例;名称含 legacy 的接口/方法排除D-30 / D-37。"
- "code 全覆盖:本计划可达的码 148未知 param 类型、149expand 倍数非法、150数组达上限、151boolean 越界、152bigint 越界、153参数超过 255、154uint16→uint8 命令截断、155读流过期、156读流过期需重建沙箱、157未知命令、158步骤执行失败、159安全收集重复开始、160安全收集意外结束、161安全检测命中、162安全详情缺失、163命令重复注册、175notExecuted 失败)各至少一条触发断言,经 `logger.catch` / `logger.warn` spy 观测D-31 / D-37。"
- "编解码覆盖boolean、整数全部位宽、bigint、string、数组参数各自经 `add`/`set` 写入后由 `createReadStream`/`get` 正确读回type 与 value 一致D-37。"
- "`ReplayArray` 全部公开操作(含 `rebuildIndexArray`/`setReplayArray`/`getCommandArray`/`getParamArray`/`commandWidth`)行为与文档一致;数组扩容后读回仍正确。"
- "`ReplaySandbox` 播放器 `step`/`play`/`pause`/`resume`/`stop` 与 `finalizeLast` 按顺序执行、失败即停,纯同步断言 + 真实计时器(无 fake timersD-04 / D-37。"
- "录像系统**完整播放**与**二次录制比对**、`error 20012008` 归 06-07本计划不做D-40。"
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
provides: ReplayArray 全操作 + 编解码覆盖
- path: packages-user/data-common/src/replay/system.test.ts
provides: ReplaySystem 覆盖
- path: packages-user/data-common/src/replay/sandbox.test.ts
provides: ReplaySandbox coverage
provides: ReplaySandbox 播放器覆盖
- 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
provides: 播放安全装饰器覆盖
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`"
- "array.test.ts 用 `data-state/test/nodeReplay.test.ts` harness 作为最接近样板,仅经公开 `IReplayArray` 表面断言"
- "sandbox.test.ts 复用 `data-state/src/replay/commands.test.ts` 的手动 stepping harnesscast 到 manual sandbox 接口,直接驱动 `step()`"
- "`06-COVERAGE-MAP.md` 为共享 create-or-append 文件:写 06-04 小节前先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在,不存在则先创建,再只追加/更新本小节;幂等且与其它 wave 1 计划顺序无关,不重写他节"
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"
- "不创建或修改任何生产/核心源码,只新增 `*.test.ts`"
- "不安装覆盖率工具D-09"
- "不引入 `fake-indexeddb`D-10"
- "疑似 bug 按正确预期编写并标记 `it.skip`/`it.todo`D-05"
- "fixture 全部文件内联,不建跨文件 helperD-02/D-03"
- "异步路径只用真实计时器,禁用 `vi.useFakeTimers()`D-04"
- "不测 `ReplayArray.saveState`/`loadState`D-32不做完整录像播放/二次录制D-40移入 06-07"
---
<objective>
Cover the replay system `packages-user/data-common/src/replay` (array/system/sandbox/func) plus one integration path through `CoreState` (D-16).
重点覆盖录像数组 `ReplayArray` 全部公开操作与编解码,并覆盖 `ReplaySystem` 注册/record、`ReplaySandbox` 播放器与播放安全装饰器D-16 / D-37
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: `array.ts`825 行)此前无专门测试,其流式/扩容 API 仅被间接使用;本计划经公开 `IReplayArray` 表面系统验证读、编解码与播放控制。录像存读档集中到 06-09完整播放/二次录制比对归 06-07。
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`.
Output: 4 个同目录 `*.test.ts`
</objective>
<execution_context>
@ -77,111 +75,113 @@ Output: 4 new co-located `*.test.ts` files plus `data-state/test/replayIntegrati
@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.
- 只写测试,绝不修改任何生产文件。
- 每个 `it(...)` 前必须有一行中文注释dev.md
- fixture 内联且最小D-02/D-03使用小录像数组。
- 异步路径用真实计时器,禁用 fake timersD-04
- 警告路径经 `logger.catch` / `vi.spyOn(logger, ...)`
- 疑似 bug正确预期 + `it.skip`/`it.todo` + 中文注释锚定 `#06-04-N``pnpm test:ci` 保持全绿。
- 不测 `ReplayArray` 存读档D-32不做完整播放/二次录制D-40
- `06-COVERAGE-MAP.md` 为共享 create-or-append 文件(幂等、顺序无关):写 06-04 小节前先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在,不存在则先创建,再只追加/更新本小节,绝不重写他节。
- **执行协议D-33执行前向用户确认执行后暂停并用中文分条汇报。**
</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>
<name>Task 1: ReplayArray 全公开操作 + 编解码boolean/整数全位宽/bigint/string/数组)</name>
<files>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)
packages-user/data-state/test/nodeReplay.test.ts
.planning/phases/06-unit-tests/06-PATTERNS.mdB 节 array
</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.
用与 `system.ts` 初始化一致的极小 capacity/width 构造 `ReplayArray`,经公开 `IReplayArray` 表面覆盖:
- `add`/`insert`/`delete`/`set` 的命令与参数读写;`get(index)` 返回步骤 handler`createReadStream(0)` 顺序读回每步;`rebuildIndexArray`、`setReplayArray`、`getCommandArray`、`getParamArray` 的行为与文档一致。
- `setCommandWidth` 切换位宽后读回正确;超过 uint8 的命令触发码 154`logger.catch`)。
- 编解码全覆盖D-37boolean码 151非法值告警、合法值读回 type/value、整数每个可用位宽各一条含边界值、bigint码 152越界告警、边界内读回、string、数组参数含码 153参数超过 255 时溢出忽略);未知 param 类型触发码 148。
- 扩容:成倍扩容(合法倍率)后仍读回;非法倍率触发码 149达到上限触发码 150读流过期触发码 155。
数组保持极小;不测 `saveState`/`loadState`。
</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>
<automated>pnpm exec vitest run packages-user/data-common/src/replay/array.test.ts</automated>
<fails_when>运行失败,或 148/149/150/151/152/153/154/155 中任一码未被观测,或某类型编解码读回不一致</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.
- `array.test.ts` 零非跳过失败。
- 覆盖全部列出公开操作boolean/整数(全位宽)/bigint/string/数组参数均至少一条读回断言。
- 断言码 148、149、150、151、152、153、154、155。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>ReplaySystem and ReplayArray behavior is covered and green.</done>
<done>ReplayArray 全操作与全类型编解码覆盖并跑绿。</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>
<name>Task 2: ReplaySystem 注册/record + ReplaySandbox 播放器</name>
<files>packages-user/data-common/src/replay/system.test.ts, packages-user/data-common/src/replay/sandbox.test.ts</files>
<read_first>
packages-user/data-common/src/replay/system.ts
packages-user/data-common/src/replay/sandbox.ts
packages-user/data-common/src/replay/types.ts
packages-user/data-state/src/replay/commands.test.ts
.planning/phases/06-unit-tests/06-PATTERNS.mdB 节 system/sandbox
</read_first>
<action>
`system.test.ts`:构造 `ReplaySystem` 断言:构造出带配置长度/宽度的 `route``registerCommand` 后 `getCommand` 返回命令、未知返回 null重复注册告警 163 且保留原命令;`record(code, ...params)` 追加到 `route` 并触发 `onRecordCommand``(code, route.length, params)``createReplaySandbox({route,reseter,startIndex?})` 调 `reseter.reset(config.save)`、保存 sandbox、触发 `onCreateSandbox``releaseSandbox()` 停止并清空。断言每个 `ReplaySystem` 实例的注册与 route 相互独立。
`sandbox.test.ts`:用 inline 命令驱动 sandbox`commands.test.ts` cast 到 manual sandbox 接口):
- `step()` 执行下一步、触发 `onStep`、成功返回 true失败命令告警 158含 code+params并停止后续不执行。
- `finalizeLast`:命令变化或读流过期时执行 `notExecuted`,失败告警 175未知末命令告警 157过期读流告警 156。
- `play`/`pause`/`resume`/`stop``pause()` 在暂停时 resolve`resume()` 从暂停点继续;`stop()` 暂停并触发 `onStopReplay`;已在播放或已结束时 `play()` no-oproute 走完后 `ended` 置位。
- `setSpeed` 触发 `onSpeedSet``getReplayed()` 计数正确。
用真实 promise无 fake timers。
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-common/src/replay/system.test.ts packages-user/data-common/src/replay/sandbox.test.ts</automated>
<fails_when>运行失败,或 156/157/158/163/175 中任一码未被观测,或播放/暂停/继续顺序断言失败</fails_when>
</verify>
<acceptance_criteria>
- 两个文件零非跳过失败。
- system.test.ts 断言注册、163、record+hook、沙箱创建/reset、实例独立性。
- sandbox.test.ts 断言顺序步进、失败即停、156/157/158/175、play/pause/resume/stop、setSpeed。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>ReplaySystem 与 ReplaySandbox 播放器覆盖并跑绿。</done>
</task>
<task type="auto">
<name>Task 3: 播放安全装饰器/工具func.ts</name>
<files>packages-user/data-common/src/replay/func.test.ts</files>
<read_first>
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)
packages-user/data-state/src/replay/commands.test.ts
.planning/phases/06-unit-tests/06-PATTERNS.mdB 节 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.
覆盖 `func.ts` 的播放安全装饰器生命周期:`beginReplaySafetyCollection`、`endReplaySafetyCollection`、`logReplaySafetyDetail`(及内部 detail 查询)、`shouldReplay`、`ignoreReplay`
- begin → 嵌套 decorated 调用 → end 的收集与简化详情;
- 在已有收集运行时再次 begin 触发码 159
- 收集外调用 end 触发码 160
- 检测命中时 end/简化详情触发码 161
- 查询不存在的 code 详情触发码 162。
用最小 inline `IReplaySystem` 与一个被装饰的 inline 类/方法;仅在源码实际写输出处 `vi.spyOn(console/logger)`。无 fake timers。
</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>
<automated>pnpm exec vitest run packages-user/data-common/src/replay/func.test.ts</automated>
<fails_when>运行失败,或 159/160/161/162 中任一码未被观测</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.
- `func.test.ts` 零非跳过失败。
- 断言装饰器生命周期begin/nested/end/detail与码 159/160/161/162。
- 每个 `it` 前有单行中文注释。
</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>
<done>播放安全装饰器/工具覆盖并跑绿;完成后以 create-or-append 方式将本计划的 code→模块→用例行148163、175写入 `06-COVERAGE-MAP.md` 的 06-04 小节:先检查表头,不存在则先创建,再只追加本小节,不重写他节。</done>
</task>
</tasks>
@ -191,45 +191,49 @@ Inline minimal fixtures; reuse `createCoreState()` (Node-safe). Do not touch `sa
| Boundary | Description |
|----------|-------------|
| none | In-repo Node Vitest tests only; no runtime input surface, network, DOM, or filesystem write is introduced. |
| none | 仅新增仓库内 Node Vitest 测试无运行时输入面、网络、DOM 或文件写入。 |
## 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. |
| T-06-04-01 | Tampering | `data-common/src/replay` 下测试文件 | low | mitigate | 只导入已跟踪模块、合成 route不安装包、不改核心源码门禁 `pnpm test:ci` + 复核仅 `*.test.ts` 变更。 |
| T-06-04-02 | Denial of service | 播放循环不终止 | medium | mitigate | 所有循环有界bounded `step()` 次数 / `waitForEnded` 迭代上限30s 测试超时兜底。 |
| T-06-04-03 | Information disclosure | fixture | low | accept | 合成命令数组无真实游戏数据D-02 |
| T-06-04-SC | Tampering | 包安装 | high | mitigate | 不新增依赖;若确需安装,停止并先走包合法性 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`.
- 四个文件聚焦运行通过;`pnpm test:ci` 全量绿。
- `git status` 仅新增 `*.test.ts`;无核心文件改动。
- 任何 `it.skip`/`it.todo` 均有 `#06-04-N` 条目。
- 以 create-or-append 方式写入 `06-COVERAGE-MAP.md` 06-04 小节17 个码);表头不存在则先创建;幂等、顺序无关,不重写他节。
- **执行协议**:执行前向用户确认;执行后暂停并用中文分条汇报。
</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.
- 4 个 `data-common/src/replay` 行为测试文件存在并通过。
- `ReplayArray` 全公开操作与全类型编解码覆盖;`ReplaySystem`/`ReplaySandbox`/安全装饰器覆盖。
- 未测存读档;未做完整播放/二次录制(归 06-07
</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/system.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`
- `array.test.ts`: `createArray`、`createRoute`、`paramCases`
- `system.test.ts`: `createSystem`、`createCommand`
- `sandbox.test.ts`: `createSandbox`、`createCommand`
- `func.test.ts`: `FakeReplaySystem`、`DecoratedTarget`
Planning artifacts:
- `06-COVERAGE-MAP.md` create-or-append 06-04 小节
</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`.
完成时创建 `.planning/phases/06-unit-tests/06-04-SUMMARY.md`,以 create-or-append 方式写入 `06-COVERAGE-MAP.md` 06-04 小节;疑似 bug 追加到 `06-TEST-FINDINGS.md``#06-04-N` 小节。执行完成后暂停并用中文分条汇报。
</output>

View File

@ -9,49 +9,68 @@ files_modified:
- 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
- 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
- packages-user/data-base/src/hero/rendering.test.ts
autonomous: true
requirements: [TEST-01]
estimate:
tokens: 46000
raw_tokens: 46000
tokens: 72000
raw_tokens: 72000
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
- "TEST-01 由数据端行为单测满足,门禁为固定非 watch 命令 `pnpm test:ci` 全绿D-08"
- "接口全覆盖:`HeroAttribute`、`ValueModifier`/`PercentageModifier`、`HeroLocation`、`HeroState`、`HeroEquipment`、`EquipmentState`/`HeroEquipsStore`、`HeroItems`、`HeroFollower`/`HeroFollowersController`、`HeroMover`、`HeroRendering` 的每个公开方法至少一条正常用例;名称含 legacy 的接口/方法排除D-17 / D-30 / D-38。"
- "code 全覆盖:本计划可达的码 108重复 add 同一 modifier、109modifier 返回同引用、116未注册 modifier 类型、142follower id 无法转数字、144未绑定 hero move top 实现、146未知 uid 装备、147无可用装备槽各至少一条触发断言D-31。"
- "loadState 专属码 58/59equipStore load与全部 `saveState`/`loadState` 测试归 06-09本计划不测D-32。"
- "勇士每个子系统可正确运行attributebase/final/优先级/克隆、modifier公式、location位置/楼层/朝向/钩子、state装配/属性视图/modifier 注册/changeFloor 钩子顺序、equipment装备/卸下/替换/compareEquip、equipStore实例增删计数排序、items常量/消耗/装备/拾取、follower增删/gather 异步/链接、mover异步移动码、renderingalpha + 钩子)。"
artifacts:
- path: packages-user/data-base/src/hero/attribute.test.ts
provides: HeroAttribute coverage
provides: HeroAttribute 覆盖
- path: packages-user/data-base/src/hero/modifier.test.ts
provides: modifier coverage
provides: modifier 覆盖
- path: packages-user/data-base/src/hero/location.test.ts
provides: HeroLocation coverage
provides: HeroLocation 覆盖
- path: packages-user/data-base/src/hero/state.test.ts
provides: HeroState coverage
provides: HeroState 覆盖
- path: packages-user/data-base/src/hero/equipment.test.ts
provides: HeroEquipment 覆盖
- path: packages-user/data-base/src/hero/equipStore.test.ts
provides: EquipmentState/HeroEquipsStore 覆盖
- path: packages-user/data-base/src/hero/items.test.ts
provides: HeroItems 覆盖
- path: packages-user/data-base/src/hero/follower.test.ts
provides: follower 覆盖(含异步 gather
- path: packages-user/data-base/src/hero/mover.test.ts
provides: HeroMover 异步移动覆盖
- path: packages-user/data-base/src/hero/rendering.test.ts
provides: HeroRendering 覆盖
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"
- "equipment.test.ts 沿用 `data-state/src/replay/commands.test.ts:200-253` 建立的 equip/unequip/getEquipped 边界形状"
- "mover.test.ts 沿用 `data-common/src/common/mover.test.ts` 的抽象 mover 模式:真实计时器 + `await controller.onEnd`D-04"
- "rendering.test.ts 用 Hookable 挂钩验证 `setAlpha` 触发 `onSetAlpha`"
- "`06-COVERAGE-MAP.md` 为共享 create-or-append 文件:写 06-05 小节前先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在,不存在则先创建,再只追加/更新本小节;幂等且与其它 wave 1 计划顺序无关,不重写他节"
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"
- "不创建或修改任何生产/核心源码,只新增 `*.test.ts`"
- "不安装覆盖率工具D-09"
- "不引入 `fake-indexeddb`D-10"
- "疑似 bug 按正确预期编写并标记 `it.skip`/`it.todo`D-05"
- "fixture 全部文件内联,不建跨文件 helperD-02/D-03"
- "异步路径只用真实计时器 + `await`,禁用 `vi.useFakeTimers()`D-04"
- "不测任何 `saveState`/`loadState`D-32"
---
<objective>
Cover the core hero data subsystems `packages-user/data-base/src/hero` — attribute, modifier, location, state (D-17).
验证 `packages-user/data-base/src/hero` 下**每个勇士子系统**可正确运行D-17 / D-38attribute、modifier、location、state、equipment、equipStore、items、follower异步、mover异步、rendering。
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: 原 06-05 与 06-06 合并为本计划;新增 `rendering` 与异步 `mover`。存读档集中到 06-09。
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.
Output: 10 个同目录 `*.test.ts`按子系统分三组Task 1 属性/位置/状态 4 文件、Task 2 装备/物品 3 文件、Task 3 异步 follower/mover/rendering 3 文件)。
</objective>
<execution_context>
@ -68,112 +87,134 @@ Output: 4 new co-located `*.test.ts` files.
@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/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/rendering.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
@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).
- 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.
- 只写测试,绝不修改任何生产文件。
- 每个 `it(...)` 前必须有一行中文注释dev.md
- fixture 内联且最小D-02/D-03
- 异步路径用真实计时器 + `await controller.onEnd`D-04
- 警告路径经 `logger.catch` / `vi.spyOn(logger, ...)`
- 疑似 bug正确预期 + `it.skip`/`it.todo` + 中文注释锚定 `#06-05-N``pnpm test:ci` 保持全绿。
- 不测存读档D-32
- 10 个文件按子系统分三组、一组一任务Task 1 属性/位置/状态 4 文件Task 2 装备/物品 3 文件Task 3 异步 follower/mover/rendering 3 文件),文件合并依据 D-28不拆分为更多计划。
- `06-COVERAGE-MAP.md` 为共享 create-or-append 文件(幂等、顺序无关):写 06-05 小节前先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在,不存在则先创建,再只追加/更新本小节,绝不重写他节。
- **执行协议D-33执行前向用户确认执行后暂停并用中文分条汇报。**
</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>
<name>Task 1: HeroAttribute + modifier + HeroLocation + HeroState</name>
<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</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)
packages-user/data-state/src/event/event.test.ts
.planning/phases/06-unit-tests/06-PATTERNS.mdC 节 attribute/modifier/location/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.
`attribute.test.ts`inline 数值属性对象:
- `getBaseAttribute` vs `getFinalAttribute`base 经 `set`/`add`/`mul`/`div` 变化final 反映已注册 modifier。
- `addModifier` 按 priority 降序排序并重算 final已被别的属性拥有的 modifier 告警 108 且忽略;`getModifiers`/`getModifierIndex`/`iterateModifiers``deleteModifier`/`deleteModifierByIndex`。
- modifier 存盘开关:`addModifier(name, modifier, false)`、`setModifierSaveEnabled`、`getModifierSaveEnabled`。
- `markModifierDirty` 只重算所属属性;未绑定键忽略。
- `clone({cloneModifier:false})` 只复制 base默认 clone 复制 modifier 并重算;`getModifiableClone` 独立;`toStructured` 返回分离的结构化克隆。
- DEV 下 modifier 返回同对象引用告警 109`logger.catch`)。
`modifier.test.ts``ValueModifier.modify(value)` = `value + this.value``PercentageModifier.modify(value, base)` = `value + base * this.value`;默认 priority0 与 10`type` 字符串;`setValue` 经绑定属性重算。不测 `saveState`/`loadState`。
`location.test.ts`inline fake `IDataCommon` + `Dir8FaceHandler` 构造 `HeroLocation`:初始 `x`/`y`/`floorId`、`getCurrentFaceDirection` 与 locator 方向一致;`setFloor`/`setPos``onSetPos`/`onSetFloor` 钩子携带新值。不测 `saveState`/`loadState`。
`state.test.ts`inline fake `IDataCommon` + `Dir8FaceHandler` 构造 `HeroState`:构造器装配 `location`/`rendering`/`followers`/`items`/`equip` 并停在默认 locator`getModifiableAttribute`/`getAttribute`/`getIsolatedAttribute` 分别返回共享属性、只读视图、独立克隆;`attachAttribute` 替换;`registerModifier` + `createModifier`/`createAndInsertModifier` 对已注册类型创建插入、未知类型告警 116 并返回 null`changeFloor(info)` 设置朝向/移动方向/位置/楼层并按顺序触发 before/after 钩子。不测存读档。
</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>
<automated>pnpm exec vitest run 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</automated>
<fails_when>任一聚焦运行失败,或 108/109/116 未被观测,或钩子顺序断言失败</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.
- 四个文件零非跳过失败。
- attribute 断言 base/final、优先级、存盘开关、clone/structured 与码 108/109modifier 断言两条公式与 prioritylocation 断言 getter/setter/钩子state 断言装配/属性视图/码 116/changeFloor 钩子顺序。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>HeroState aggregate and save envelope are covered and green.</done>
<done>属性、modifier、位置、勇士状态覆盖并跑绿。</done>
</task>
<task type="auto">
<name>Task 2: HeroEquipment + EquipmentState/HeroEquipsStore + HeroItems</name>
<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</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/items.ts
packages-user/data-base/src/hero/types.ts
packages-user/data-state/src/replay/commands.test.ts
.planning/phases/06-unit-tests/06-PATTERNS.mdC 节 equipment/equipStore/items
</read_first>
<action>
构造 inline fake `IDataCommon``tileStore.num`/`idToNumber`、`itemStore.getData` 返回合成装备/物品,装备含 `equip.value`/`equip.percentage` 映射与 `equip.slots`+ inline `HeroAttribute`
`equipment.test.ts``setSlots``canEquipTo` 对数值/字符串槽返回 `CanEquip`/`NeedReplace`/`CannotEquip``equip` 到空槽返回 undefined 并应用 modifier 使 final 属性变化;覆盖占用槽返回旧 uid 并替换 modifier同 uid 槽位变化且 `autoUnload` 时移动;`unequip` 返回被移除 uid 并移除 modifier、空槽返回 undefined`equipped`/`getEquipped`/`getEquips``compareEquip` 逐属性 diff未知 uid 告警 146、无可用槽告警 147`logger.catch`)。不测 `saveState`/`loadState`。
`equipStore.test.ts``EquipmentState` 的公开方法与 `HeroEquipsStore.add`(返回 uid、未知 tile 返回 -1/`get`/`count`/`delete`/`useSorter`/`instancesOf`/`instances`(按 sorter 排序、回退 uid 升序)。不测 `saveState`/`loadState`58/59 归 06-09
`items.test.ts``itemCount`/`getItemState`/`addItem`/`getItem`/`useItem`:常量物品计数增减并在 <=0 删除;装备类路由到 equip storePick 类按 count 调 `effect.useEffect`;未知 tile/item no-op`useItem` 缺物品或 `canUse` 为 false 返回 false、消耗类减并删、常量类保留、装备类返回 false。不测 `saveState`/`loadState`。
</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 packages-user/data-base/src/hero/items.test.ts</automated>
<fails_when>运行失败,或 modifier 应用/移除断言、或 146/147 未被观测</fails_when>
</verify>
<acceptance_criteria>
- 三个文件零非跳过失败。
- equipment 断言 equip/unequip/replace、modifier 应用、compareEquip、码 146/147equipStore 断言实例增删计数排序items 断言 add/use/count/路由。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>装备、装备实例库、物品覆盖并跑绿。</done>
</task>
<task type="auto">
<name>Task 3: followers异步+ HeroMover异步+ HeroRendering</name>
<files>packages-user/data-base/src/hero/follower.test.ts, packages-user/data-base/src/hero/mover.test.ts, packages-user/data-base/src/hero/rendering.test.ts</files>
<read_first>
packages-user/data-base/src/hero/follower.ts
packages-user/data-base/src/hero/mover.ts
packages-user/data-base/src/hero/rendering.ts
packages-user/data-base/src/hero/types.ts
packages-user/data-common/src/common/mover.ts
packages-user/data-common/src/common/mover.test.ts
.planning/phases/06-unit-tests/06-PATTERNS.mdC 节 follower/mover
</read_first>
<action>
`follower.test.ts`inline fake `IDataCommon` + `Dir8FaceHandler` + `HeroLocation`
- `addFollower` 在 hero locator 追加并触发 `onAddFollower``getFollower`/`getAllFollowers`/`getFollowersById`(数值与字符串 id`next`/`last` 邻居链接。
- `removeFollower`/`removeAllFollowers` 触发 `onRemoveFollower` 并清空。
- `gatherFollowers`(异步,真实计时器)逐步移动并触发 `onGatherFollowers(false)``gatherFollowersSync` 直接吸附并触发 `onGatherFollowers(true)`
- 未知字符串 follower id 经 `logger.catch` 观测码 142。不测 follower 的 `saveState`/`loadState`。
`mover.test.ts`:镜像 `data-common/src/common/mover.test.ts` 但针对 `HeroMover`inline `IHeroLocation` 形状 tile + fake `IDataCommon` + `Dir8FaceHandler``config`/`getConfig` 往返 `noRoute`/`ignoreTerrain`/`autoSave`/`allowOutBound`inline fake `IHeroMoveTopImpl`Step 经 `setPos` 写入下一位置、`CannotMove` 调 `cannotEnter` 并不移动、`Hit` 调 `hit` 并停、`allowOutBound` 允许越界而默认阻挡、`ignoreTerrain` 跳过 pass/hit/enter/leave缺 top 实现时告警 144 且 `onStepStart` 返回 `HeroMoveCode.Stop``onStepSettled` 在开启地形触发时按 before/current locator 调 `leave``enter`。真实计时器 + `await controller.onEnd`
`rendering.test.ts``HeroRendering` 构造后 `alpha` 默认 1`setAlpha` 更新 `alpha` 并触发挂钩的 `onSetAlpha``createController` 注册的钩子可解除。不测 `saveState`/`loadState`。
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-base/src/hero/follower.test.ts packages-user/data-base/src/hero/mover.test.ts packages-user/data-base/src/hero/rendering.test.ts</automated>
<fails_when>运行失败,或异步 gather/mover 断言未通过 awaited 控制器完成,或 144/142 未被观测</fails_when>
</verify>
<acceptance_criteria>
- 三个文件零非跳过失败。
- follower 断言 add/remove/gather/链接与码 142mover 断言 config 往返、Step/CannotMove/Hit、越界、enter/leave 顺序与码 144rendering 断言 alpha + 钩子。
- 异步用例用真实计时器并 await 控制器;无 fake timers。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>followers、异步 HeroMover、HeroRendering 覆盖并跑绿;完成后以 create-or-append 方式将本计划的 code→模块→用例行108/109/116/142/144/146/147写入 `06-COVERAGE-MAP.md` 的 06-05 小节:先检查表头,不存在则先创建,再只追加本小节,不重写他节。</done>
</task>
</tasks>
@ -183,27 +224,30 @@ Use `SaveCompression.NoCompression`. Keep fixtures inline.
| Boundary | Description |
|----------|-------------|
| none | In-repo Node Vitest tests only; no runtime input surface, network, DOM, or filesystem write is introduced. |
| none | 仅新增仓库内 Node Vitest 测试无运行时输入面、网络、DOM 或文件写入。 |
## 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. |
| T-06-05-01 | Tampering | `data-base/src/hero` 下测试文件 | low | mitigate | 只导入已跟踪模块、合成 fixture不安装包、不改核心源码门禁 `pnpm test:ci` + 复核仅 `*.test.ts` 变更。 |
| T-06-05-02 | Denial of service | 异步 mover/gather 循环 | medium | mitigate | 异步断言 await 控制器 `onEnd`30s 测试超时兜底;无无界轮询。 |
| T-06-05-03 | Information disclosure | fixture | low | accept | 合成数值/id无真实游戏数据D-02 |
| T-06-05-SC | Tampering | 包安装 | high | mitigate | 不新增依赖;若确需安装,停止并先走包合法性 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`.
- 十个文件聚焦运行通过;`pnpm test:ci` 全量绿。
- `git status` 仅新增 `*.test.ts`;无核心文件改动。
- 任何 `it.skip`/`it.todo` 均有 `#06-05-N` 条目。
- 以 create-or-append 方式写入 `06-COVERAGE-MAP.md` 06-05 小节7 个码);表头不存在则先创建;幂等、顺序无关,不重写他节。
- **执行协议**:执行前向用户确认;执行后暂停并用中文分条汇报。
</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.
- 10 个 `data-base/src/hero` 行为测试文件存在并通过。
- 每个勇士子系统(含 rendering 与异步 mover/gather 与 follower均有覆盖。
- 未测存读档。
</success_criteria>
<artifacts_this_plan_produces>
@ -212,14 +256,29 @@ Test files:
- `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`
- `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`
- `packages-user/data-base/src/hero/rendering.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`
- `attribute.test.ts`: `TestModifier`、`createAttribute`
- `modifier.test.ts`: `createAttribute`
- `location.test.ts`: `createState`、`createFaceHandler`、`createLocation`
- `state.test.ts`: `createState`、`createFaceHandler`、`createHeroState`
- `equipment.test.ts`: `createState`、`createEnv`、`createItem`
- `equipStore.test.ts`: `createState`、`createEnv`、`FakeSorter`
- `items.test.ts`: `createState`、`createEnv`、`createItem`
- `follower.test.ts`: `createState`、`createLocation`、`createController`
- `mover.test.ts`: `TestTile`、`FakeTopImpl`、`createMover`
- `rendering.test.ts`: `createState`、`FakeRenderingHook`
Planning artifacts:
- `06-COVERAGE-MAP.md` create-or-append 06-05 小节
</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`.
完成时创建 `.planning/phases/06-unit-tests/06-05-SUMMARY.md`,以 create-or-append 方式写入 `06-COVERAGE-MAP.md` 06-05 小节;疑似 bug 追加到 `06-TEST-FINDINGS.md``#06-05-N` 小节。执行完成后暂停并用中文分条汇报。
</output>

View File

@ -5,57 +5,67 @@ 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
- 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
- packages-user/data-base/src/map/staticTile.test.ts
- packages-user/data-base/src/map/dynamicTile.test.ts
- packages-user/data-base/src/map/eventView.test.ts
- packages-user/data-base/src/map/mover.test.ts
autonomous: true
requirements: [TEST-01]
estimate:
tokens: 56000
raw_tokens: 56000
tokens: 72000
raw_tokens: 72000
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
- "TEST-01 由数据端行为单测满足,门禁为固定非 watch 命令 `pnpm test:ci` 全绿D-08"
- "接口全覆盖:`MapState`、`GameMap`、`MapLayer`、`MapTileBase`tile、`StaticTile`、`DynamicTile`、`LayerEventView`eventView、`DynamicTileMover`mover的每个公开方法至少一条正常用例名称含 legacy 的接口/方法排除D-18 / D-30 / D-39。"
- "code 全覆盖:本计划可达的码 8、9、46、60、61、62、63、64、80、81、84、121、123、126、127、128、129、130、131、136、143 各至少一条触发断言D-31。其中 60各图层长度不一致与 61总长度不能被 raw.width 整除)经 `MapState.fromRaw`(内部先走 `validateRaw` 校验)触发,二者均可达,不得省略。"
- "loadState 专属码 55MapStore 缺 reference、122MapStore 楼层缺失、124MapLayer 引用缺失)与全部 `saveState`/`loadState` 测试归 06-09本计划不测D-32。"
- "重点覆盖:静态图块(`StaticTile` num/raw/set/shouldSave/toDynamic、动态图块转换`transferToDynamic`/`transferToStatic`/`transferToStaticIfSafe`/`createDynamic`/`deleteDynamic`/`updateDynamicTile`)与静态数组设置(`getMapData`/`putMapData`/`setMapRef`/`getMapRef`)行为正确。"
- "地图矩阵、点事件、图层别名/背景/脏标记、`resize`/`resize2`/crop、`compareWith`、`canPass`/`shouldHit` 委托、异步门开关openDoor/closeDoor均有覆盖。"
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
- path: packages-user/data-base/src/map/mapState.test.ts
provides: MapState 覆盖
- path: packages-user/data-base/src/map/gameMap.test.ts
provides: GameMap 覆盖
- path: packages-user/data-base/src/map/mapLayer.test.ts
provides: MapLayer 矩阵/静态数组/动态转换覆盖
- path: packages-user/data-base/src/map/tile.test.ts
provides: MapTileBase 覆盖
- path: packages-user/data-base/src/map/staticTile.test.ts
provides: StaticTile 覆盖
- path: packages-user/data-base/src/map/dynamicTile.test.ts
provides: DynamicTile 覆盖
- path: packages-user/data-base/src/map/eventView.test.ts
provides: LayerEventView 扩展覆盖
- path: packages-user/data-base/src/map/mover.test.ts
provides: DynamicTileMover 覆盖
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`"
- "全部新文件复用 `data-base/src/map/mapLifecycle.test.ts` 的 inline `createMapState` fixture + `vi.hoisted` 全局 stub 形状"
- "mapState.test.ts 镜像 `eventPath.test.ts` 的 fromRaw 校验与 logger 警告路径"
- "eventView.test.ts 为既有文件扩展(保留既有覆盖)"
- "`06-COVERAGE-MAP.md` 为共享 create-or-append 文件:写 06-06 小节前先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在,不存在则先创建,再只追加/更新本小节;幂等且与其它 wave 1 计划顺序无关,不重写他节"
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"
- "不创建或修改任何生产/核心源码,只修改 `*.test.ts`"
- "不安装覆盖率工具D-09"
- "不引入 `fake-indexeddb`D-10"
- "疑似 bug 按正确预期编写并标记 `it.skip`/`it.todo`D-05"
- "fixture 全部文件内联,不建跨文件 helperD-02/D-03"
- "异步路径只用真实计时器 + `await`,禁用 `vi.useFakeTimers()`D-04"
- "不测 `saveState`/`loadState`D-32"
---
<objective>
Cover the hero storage/movement subsystems `packages-user/data-base/src/hero` — equipment, equipStore, items, follower, mover (D-17).
在既有 `eventPath`/`eventView`/`mapLifecycle` 测试之上,全面覆盖地图系统 `packages-user/data-base/src/map`D-18 / D-39`MapState`、`GameMap`、`MapLayer`、tile 与 eventView、map mover**重点**静态图块、动态图块转换、静态数组设置。
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-18 要求更全面的地图覆盖矩阵、点事件、图层、脏标记、resize/crop。存读档集中到 06-09。
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.
Output: 8 个 `*.test.ts`(其中 `eventView.test.ts` 为既有文件扩展)。
</objective>
<execution_context>
@ -68,118 +78,139 @@ Output: 5 new co-located `*.test.ts` files.
@.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
@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/eventView.ts
@packages-user/data-base/src/map/mover.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).
- 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.
- 只改测试,绝不修改任何生产文件。
- 每个 `it(...)` 前必须有一行中文注释dev.md
- fixture 内联且最小D-02/D-03import 链需要时用 `vi.hoisted` 全局 stub + 动态模块包。
- 警告路径经 `logger.catch` / `vi.spyOn(logger, ...)`
- 疑似 bug正确预期 + `it.skip`/`it.todo` + 中文注释锚定 `#06-06-N``pnpm test:ci` 保持全绿。
- 不测存读档D-32
- `06-COVERAGE-MAP.md` 为共享 create-or-append 文件(幂等、顺序无关):写 06-06 小节前先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在,不存在则先创建,再只追加/更新本小节,绝不重写他节。
- **执行协议D-33执行前向用户确认执行后暂停并用中文分条汇报。**
</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>
<name>Task 1: MapState 注册/校验/激活 + GameMap 图层/别名/脏/事件层</name>
<files>packages-user/data-base/src/map/mapState.test.ts, packages-user/data-base/src/map/gameMap.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)
packages-user/data-base/src/map/mapState.ts
packages-user/data-base/src/map/gameMap.ts
packages-user/data-base/src/map/types.ts
packages-user/data-base/src/map/eventPath.test.ts
packages-user/data-base/src/map/mapLifecycle.test.ts
.planning/phases/06-unit-tests/06-PATTERNS.mdD 节 mapState/gameMap
</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`).
`mapState.test.ts`:用 inline `createMapState` fixtureTileStore + FaceManager + RoleFaceBinder + DirectionMapper fake `IDataCommon`)覆盖:
- `createMap(id,w,h)` 注册、`getMap` 命中/未知返回 null`fromRaw` 构建 map并对非法 raw 返回 null 且告警码 60/61/62/63/64缺 floor、维度非法、容器/类型非法、非数值键等分支各一条,`logger.catch`)。其中码 60 用两层长度不等的 raw`map``{'0': 长度6, '1': 长度5}`)经 `MapState.fromRaw` 触发;码 61 用各层等长但总长度不能被 `raw.width` 整除的 raw如单层长度 5、`raw.width` 为 2`MapState.fromRaw` 触发;两者均由 `fromRaw` 内部的 `validateRaw` 校验发出,断言最终返回 null 且相应码被观测。
- 激活迭代:`setMapActiveStatus`/`isMapActive`/`getActiveMap`/`iterateAllMaps``fromRaw` 默认 inactive`setMapList` 顺序与 `getMap` 解析;`setArea`/`activeArea`/`deactiveArea`/`useAutoActivitor`/`notifyEnterFloor` 改变激活集。
- `createLayerState` 对已存在楼层告警码 121。
- `compareWith(ref)` 按参考标记脏;`canPass`/`shouldHit` 委托活跃 map/谓词inline fake 谓词)。
- 不测 `saveState`/`loadState`55/122 归 06-09
`gameMap.test.ts`inline `IDataCommon` 构造 `GameMap`
- `addLayer`/`removeLayer`/`hasLayer` 与 `onUpdateLayer` 钩子;`setLayerAlias`/`getLayerByAlias`/`getLayerAlias`,重复别名告警 84。
- `setBackground`/`getBackground` 与 `onChangeBackground``setActiveStatus` 切换 `active`
- `setEventLayer`:拒绝不在 `layerList` 的层并告警 131接受已添加层接受 null。
- `dirty()`(任一层或自身脏为真)、`markDirty(true/false)``resizeLayer(w,h,keepBlock)` 按 keepBlock 调 `resize`/`resize2``compareWith(map)` 缺参考标脏、否则比较。
- 不测 `saveState`/`loadState`(含 `isEmptyLayerSave`)。
</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>
<automated>pnpm exec vitest run packages-user/data-base/src/map/mapState.test.ts packages-user/data-base/src/map/gameMap.test.ts</automated>
<fails_when>运行失败,或 60/61/62/63/64/84/121/131 中任一码未被观测</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.
- 两个文件零非跳过失败。
- mapState 断言 create/get/fromRaw 校验码60/61/62/63/64/激活迭代/area 激活/121/compareWith/canPass-shouldHit。
- gameMap 断言层与别名生命周期、背景/事件层、脏、resize、compareWith 与码 84/131。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>Equipment and equip-store behavior is covered and green.</done>
<done>MapState 与 GameMap 覆盖并跑绿。</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>
<name>Task 2: MapLayer 矩阵、静态数组设置与动态图块转换(重点)</name>
<files>packages-user/data-base/src/map/mapLayer.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)
packages-user/data-base/src/map/mapLayer.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.mdD 节 mapLayer
</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`).
用 inline `createMapState` fixture 构造 layer覆盖重点静态图块 / 动态转换 / 静态数组):
- 矩阵:`setBlock`/`getBlock`/`removeBlock`/`inMap``getMapData()`0 或 4 参,参数个数非法告警 80、越界填充告警 81`putMapData(array,x,y,w)``setMapRef(array)` 长度不匹配告警 123、否则设置引用`getMapRef`。
- 静态数组诊断:`getMapData(4 参越界)` 告警 8不完整数据与 9越界忽略
- 动态转换:`createDynamic(num,x,y)` 建立索引并触发 `onCreateDynamic``getDynamicTilesAt`/`iterateDynamicTiles`/`updateDynamicTile``transferToDynamic(x,y,keepEvent)`(越图告警 131、block 为 0 告警 127、清事件语义`transferToStatic`(越界告警 128、覆盖静态告警 129、`keepEvent` 语义);`transferToStaticIfSafe`(占用返回 null`deleteDynamic`(非本层 tile 告警 130`onDeleteDynamic` 钩子)。
- 点位/事件:`event(x,y)`/`getPointEvent``getTile`/`getLocationData``setStaticDirection`/`setDynamicDirection`/`setZIndex`/`setFaceBinder`。
- 脏与比较:`dirty`/`markDirty`/`compareWith`。
- 异步门:`openDoor`/`closeDoor``closeDoor` 目标非空告警 46用真实计时器 await。
- 不测 `saveState`/`loadState`124 归 06-09
注:若 `transferToDynamic` 的越图路径实际发出的码与语义不符(当前实现发 131 而 `transferToStatic` 发 128在断言中触发并记录为 `#06-06-N` 待用户确认;若用户判定为缺陷则按 D-05 改为正确预期 + skip。
</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>
<automated>pnpm exec vitest run packages-user/data-base/src/map/mapLayer.test.ts</automated>
<fails_when>运行失败,或 8/9/46/80/81/123/127/128/129/130/131 中任一码未被观测,或静态数组/动态转换断言失败</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.
- `mapLayer.test.ts` 零非跳过失败。
- 断言矩阵读写、静态数组getMapData/putMapData/setMapRef、动态转换语义、点位事件、脏/compareWith、异步门与全部列出码。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>Item and follower behavior is covered and green.</done>
<done>MapLayer 矩阵、静态数组与动态转换(重点)覆盖并跑绿。</done>
</task>
<task type="auto">
<name>Task 3: HeroMover async movement</name>
<files>packages-user/data-base/src/hero/mover.test.ts</files>
<name>Task 3: tile/staticTile/dynamicTile/eventView/mover</name>
<files>packages-user/data-base/src/map/tile.test.ts, packages-user/data-base/src/map/staticTile.test.ts, packages-user/data-base/src/map/dynamicTile.test.ts, packages-user/data-base/src/map/eventView.test.ts, packages-user/data-base/src/map/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)
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/eventView.ts
packages-user/data-base/src/map/mover.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.mdD 节 tile
</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.
`tile.test.ts`:对 static 与 dynamic tile`num`/`raw`/`set` 反映 tileStore 数据;`set` 变更后恢复该图块默认 `events` 并把事件视图标纯;`tileEvent()` 为自身视图(默认恢复后 dirty 为 false、`pointEvent()` 返回该 locator 的层点位视图(越图为 null`setFaceDirection` 经 face binder 映射并返回结果 num。单独一条 `it` 聚焦默认事件恢复。不测存读档。
`staticTile.test.ts``StaticTile` 的 `num`/`raw`/`set`/`toDynamic`/`shouldSave` 行为。不测 `saveState`/`loadState`。
`dynamicTile.test.ts``DynamicTile` 构造缺失 raw 告警 143`num`/`raw`/`set`(缺失告警 143`setPos``getCurrentFaceDirection``toStatic`/`toStaticIfSafe``step` 返回控制器;`delete`。不测 `saveState`/`loadState`。
`eventView.test.ts`(扩展既有文件,保留既有覆盖):`LayerEventView` 的 `get`/`ref`/`set`/`delete`/`clear`/`markPure`/`dirty` 与逐优先级脏跟踪;重复 priority 触发码 136`logger.catch`)。
`mover.test.ts`:经一个 `DynamicTile` 构造其 `DynamicTileMover` 并驱动控制器:成功步进、非 `Success` 移动码触发码 126`logger.catch`)、`onMoveStart`/`onStepSettled` 生命周期顺序;真实计时器 + `await controller.onEnd`
</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>
<automated>pnpm exec vitest run packages-user/data-base/src/map/tile.test.ts packages-user/data-base/src/map/staticTile.test.ts packages-user/data-base/src/map/dynamicTile.test.ts packages-user/data-base/src/map/eventView.test.ts packages-user/data-base/src/map/mover.test.ts</automated>
<fails_when>运行失败,或 136/143/126 未被观测,或默认事件恢复/动态 tile 生命周期断言失败</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.
- 五个文件零非跳过失败。
- tile 断言默认事件恢复与点位联动staticTile/dynamicTile 断言各自公开方法eventView 断言视图操作与码 136mover 断言生命周期与码 126。
- 异步 mover 用例用真实计时器并 await 控制器;无 fake timers。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>HeroMover async movement behavior is covered and green.</done>
<done>tile/staticTile/dynamicTile/eventView/mover 覆盖并跑绿;完成后以 create-or-append 方式将本计划的 code→模块→用例行8/9/46/60/61/62/63/64/80/81/84/121/123/126/127/128/129/130/131/136/143写入 `06-COVERAGE-MAP.md` 的 06-06 小节:先检查表头,不存在则先创建,再只追加本小节,不重写他节。</done>
</task>
</tasks>
@ -189,45 +220,57 @@ Use real timers + `await controller.onEnd`. Keep fixtures inline.
| Boundary | Description |
|----------|-------------|
| none | In-repo Node Vitest tests only; no runtime input surface, network, DOM, or filesystem write is introduced. |
| none | 仅修改/新增仓库内 Node Vitest 测试无运行时输入面、网络、DOM 或文件写入。 |
## 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. |
| T-06-06-01 | Tampering | `data-base/src/map` 下测试文件 | low | mitigate | 只导入已跟踪模块、合成地图;不安装包、不改核心源码;门禁 `pnpm test:ci` + 复核仅 `*.test.ts` 变更。 |
| T-06-06-02 | Denial of service | 大矩阵/异步门 | low | mitigate | fixture 用 1x1..3x2 小矩阵,运行开销可忽略;异步门 await 控制器30s 超时兜底。 |
| T-06-06-03 | Information disclosure | fixture | low | accept | 合成 tile id/事件无真实游戏数据D-02 |
| T-06-06-SC | Tampering | 包安装 | high | mitigate | 不新增依赖;若确需安装,停止并先走包合法性 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`.
- 八个文件聚焦运行通过;`pnpm test:ci` 全量绿(既有 map 测试保持绿)。
- `git status` 仅显示 `*.test.ts` 变更;无核心文件改动。
- 任何 `it.skip`/`it.todo` 均有 `#06-06-N` 条目。
- 以 create-or-append 方式写入 `06-COVERAGE-MAP.md` 06-06 小节21 个码:在 19 个基础上新增 60/61表头不存在则先创建幂等、顺序无关不重写他节。
- **执行协议**:执行前向用户确认;执行后暂停并用中文分条汇报。
</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.
- 8 个 `data-base/src/map` 行为测试文件存在/扩展并通过。
- 地图矩阵、点事件、图层、脏标记、resize/crop、静态图块、动态转换、静态数组全部断言。
- 未测存读档。
</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`
- `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`
- `packages-user/data-base/src/map/staticTile.test.ts`
- `packages-user/data-base/src/map/dynamicTile.test.ts`
- `packages-user/data-base/src/map/eventView.test.ts`(扩展)
- `packages-user/data-base/src/map/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`
- `mapState.test.ts`: `createMapState`、`createRawMap`
- `gameMap.test.ts`: `createGameMap`、`createState`、`createLayer`
- `mapLayer.test.ts`: `createMapState`、`createLayerFixture`
- `tile.test.ts`: `createMapState`、`createTiles`
- `staticTile.test.ts`: `createMapState`、`createStaticTile`
- `dynamicTile.test.ts`: `createMapState`、`createDynamicTile`
- `eventView.test.ts`: `createView`
- `mover.test.ts`: `createMoverFixture`
Planning artifacts:
- `06-COVERAGE-MAP.md` create-or-append 06-06 小节21 个码)
</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`.
完成时创建 `.planning/phases/06-unit-tests/06-06-SUMMARY.md`,以 create-or-append 方式写入 `06-COVERAGE-MAP.md` 06-06 小节;疑似 bug 追加到 `06-TEST-FINDINGS.md``#06-06-N` 小节。执行完成后暂停并用中文分条汇报。
</output>

View File

@ -2,56 +2,55 @@
phase: 06-unit-tests
plan: 07
type: execute
wave: 1
depends_on: []
wave: 2
depends_on: [06-02, 06-04, 06-06]
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
- packages-user/data-state/test/enemyCombination.test.ts
- packages-user/data-state/test/replayPlayback.test.ts
autonomous: false
requirements: [TEST-01]
estimate:
tokens: 56000
raw_tokens: 56000
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 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
- "TEST-01 由数据端行为单测满足,门禁为固定非 watch 命令 `pnpm test:ci` 全绿D-08。"
- "接口全覆盖:本计划驱动的顶层/系统层组合入口与录像播放入口的每个公开方法至少一条正常用例;名称含 legacy 的接口/方法排除D-30 / D-40。"
- "code 全覆盖:录像执行错误 2001、2002、2003、2004、2005、2006、2007、2008 各至少一条触发断言,经 `logger.catch` / `logger.error` spy 观测D-31 / D-40。"
- "① 伤害组合:顶层多特殊属性组合(同时存在多个 special 时的伤害计算)+ 系统层光环/各效果组合D-25/D-26经真实 API 驱动并断言最终 `{damage,turn}`。"
- "② 小地图场景:用真实 API`createCoreState` + 地图/怪物/勇士公开接口)构造小地图场景 + 一段录像步并正常播放合成数据D-02。"
- "③ 二次录制比对:首次播放完毕后录像系统记录一遍录像步,断言二次录像与原录像**逐条完全相等**(步数 + 每步 code + 各 param 的 type/value随后再次播放检验。"
- "本计划允许为录像重置**最小使用**存档数据但不测存读档本身D-32。"
- "执行前必须先向用户确认依赖用户先接通录像记录D-40。"
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
- path: packages-user/data-state/test/enemyCombination.test.ts
provides: 顶层伤害组合 + 系统层光环/效果组合覆盖
- path: packages-user/data-state/test/replayPlayback.test.ts
provides: 小地图录像播放 + 二次录制比对 + 20012008 覆盖
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"
- "enemyCombination.test.ts 扩展 `data-state/test/dataClosure.test.ts` 的真实 `createCoreState` + `enemyManager.addPrefab` + `MainDamageCalculator` 形状到多 special/组合路径"
- "replayPlayback.test.ts 复用 `data-state/test/nodeReplay.test.ts` 的逐步回放 harness 与 `dataClosure.test.ts``waitForEnded` 有界等待"
- "`06-COVERAGE-MAP.md` 为共享 create-or-append 文件:写 06-07 小节前先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在,不存在则先创建,再只追加/更新本小节;幂等且与其它计划顺序无关,不重写他节"
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"
- "不创建或修改任何生产/核心源码,只新增 `*.test.ts`"
- "不安装覆盖率工具D-09"
- "不引入 `fake-indexeddb`D-10"
- "疑似 bug 按正确预期编写并标记 `it.skip`/`it.todo`D-05"
- "fixture 全部文件内联,不建跨文件 helperD-02/D-03"
- "异步路径只用真实计时器,禁用 `vi.useFakeTimers()`D-04"
- "不测 `saveState`/`loadState` 本身仅允许为录像重置最小使用存档数据D-32"
---
<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.
顶层集成D-40① 伤害系统的属性/加成组合(顶层多特殊属性 + 系统层光环/效果组合D-25/D-26② 用真实 API 构造小地图场景 + 一段录像步并正常播放;③ 二次录制验证:首次播放后系统记录一遍录像步,判定二次录像与原录像逐条完全相等,并再次播放检验。`error 20012008` 归本计划。
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: 这是唯一验证多系统接线(战斗上下文 + 流程 + 计算器;地图 + 勇士 + 录像)的阶段工作。
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.
**执行前必须先向用户确认**D-40依赖用户先接通录像记录
Output: 4 new co-located `*.test.ts` files.
Output: 2 个 `data-state/test/` 集成测试文件。
</objective>
<execution_context>
@ -62,118 +61,131 @@ Output: 4 new co-located `*.test.ts` files.
<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-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
@packages-user/data-state/src/core.ts
@packages-user/data-state/src/enemy/calculator.ts
@packages-user/data-state/src/enemy/aura.ts
@packages-user/data-state/src/enemy/mapDamage.ts
@packages-user/data-state/src/replay/commands.ts
@packages-user/data-system/src/combat/context.ts
@packages-user/data-system/src/combat/combat.ts
@packages-user/data-state/test/dataClosure.test.ts
@packages-user/data-state/test/nodeReplay.test.ts
@packages-user/data-state/test/fixtures/closed-loop.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.
- 只写测试,绝不修改任何生产文件。
- 每个 `it(...)` 前必须有一行中文注释dev.md
- fixture 内联且最小D-02/D-03可复用 `createCoreState()`,必要时引用 `closed-loop.ts`
- 异步路径用真实计时器 + 有界等待(`waitForEnded`),禁用 fake timersD-04
- 警告/错误路径经 `logger.catch` / `vi.spyOn(logger, ...)`
- 疑似 bug正确预期 + `it.skip`/`it.todo` + 中文注释锚定 `#06-07-N``pnpm test:ci` 保持全绿。
- **本计划执行前必须先向用户确认录像记录已接通D-40确认前不得开始写/跑用例。**
- `06-COVERAGE-MAP.md` 为共享 create-or-append 文件(幂等、顺序无关):写 06-07 小节前先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在,不存在则先创建,再只追加/更新本小节,绝不重写他节。
- **执行协议D-33执行前向用户确认执行后暂停并用中文分条汇报。**
</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>
<task type="checkpoint:human-verify" gate="blocking-human">
<name>Task 1: 执行前门禁 — 用户确认录像记录已接通</name>
<files>packages-user/data-state/test/replayPlayback.test.ts尚未创建仅作占位说明</files>
<what-built>录像记录在 CoreState 操作路径中的接通,以及驱动小地图录像播放/二次录制所需的公开入口。</what-built>
<how-to-verify>
1) 确认播放时每步会记入 `ReplaySystem.route`(录像记录已接通)。
2) 确认 `CoreState` 暴露构造小地图场景 + 播放 + 二次录制所需的公开入口。
3) 确认录像重置所需的最小状态入口可用。
</how-to-verify>
<resume-signal>用户回复「录像记录已接通,可以开始」则继续 Task 2/3否则暂停本计划等待用户改代码。</resume-signal>
<read_first>
.planning/phases/06-unit-tests/06-CONTEXT.mdD-40
packages-user/data-state/src/core.tsreplaySystem 接线与公开入口)
packages-user/data-state/src/replay/commands.ts录像指令与 error 20012008
</read_first>
<action>
本计划依赖用户先在具体操作中接通录像记录否则二次录制比对无法进行D-40。开始本计划前向用户展示并确认
- 录像记录是否已在 CoreState 操作路径中接通(播放时会把每步记入 `ReplaySystem.route`
- `CoreState` 是否暴露驱动小地图场景 + 播放 + 二次录制所需的公开入口;
- 录像重置所需的最小状态入口是否可用。
用户确认后再继续 Task 2/3若未接通暂停本计划并等待用户改代码不得自行修改核心代码。
</action>
<verify>
<human-check>用户明确回复「录像记录已接通,可以开始」,或不通过则暂停本计划</human-check>
</verify>
<acceptance_criteria>
- 用户给出明确开始确认;否则本计划保持暂停状态。
</acceptance_criteria>
<done>录像记录接通得到用户确认,或计划暂停等待用户改代码。</done>
</task>
<task type="auto">
<name>Task 2: 顶层伤害组合 + 系统层光环/效果组合</name>
<files>packages-user/data-state/test/enemyCombination.test.ts</files>
<read_first>
packages-user/data-state/test/dataClosure.test.ts
packages-user/data-state/src/enemy/calculator.ts
packages-user/data-state/src/enemy/aura.ts
packages-user/data-state/src/enemy/mapDamage.ts
packages-user/data-system/src/combat/context.ts
packages-user/data-system/src/combat/combat.ts
.planning/phases/06-unit-tests/06-PATTERNS.mdA 节 combatClosure
</read_first>
<action>
用真实 `createCoreState()` 与真实顶层实现驱动:
- 顶层多特殊属性组合:一个怪物同时带多个 special如 坚固 + 连击 + 吸血 等)时,按真实 `MainDamageCalculator` 断言组合后的 `{damage,turn}`(组合语义,非单分支)。
- 系统层组合:经真实 `EnemyContext` + 真实 `CommonAura`/`GuardAura` + 真实 final effect覆盖 光环基础效果 ↔ 常规查询效果、光环特殊效果 ↔ 特殊查询效果、final-effect 阶段顺序、同/跨优先级顺序D-25属性→伤害联动D-26
- 支援/guard 递归与未知 guard 的 warn 137 经 `logger.catch` 观测。
若某组合需要的公开入口未暴露,按 D-07 暂停并向用户提问,不修改核心代码。
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-state/test/enemyCombination.test.ts</automated>
<fails_when>运行失败,或组合后的伤害/阶段顺序断言与真实实现不符</fails_when>
</verify>
<acceptance_criteria>
- `enemyCombination.test.ts` 零非跳过失败。
- 断言至少一组多 special 伤害组合与一组系统层光环/效果组合(含阶段顺序)。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>顶层与系统层伤害/效果组合覆盖并跑绿。</done>
</task>
<task type="auto">
<name>Task 3: 小地图录像播放 + 二次录制逐条比对 + error 20012008</name>
<files>packages-user/data-state/test/replayPlayback.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/commands.ts
packages-user/data-base/src/map/mapState.ts
packages-user/data-base/src/map/types.ts
packages-user/data-base/src/map/eventPath.test.ts (fromRaw validation + warning paths)
.planning/phases/06-unit-tests/06-PATTERNS.md (section D: mapState)
.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.mdB 节 replayIntegration
</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).
用真实 API 构造小地图场景(合成数据)+ 一段录像步并播放:
- 小地图:用真实 `createCoreState()` 的 map 公开接口构造极小地图1..3 格),放置怪物/勇士,绑定必要状态。
- 播放:记录一小段有效录像,创建 sandbox 并用有界等待(`waitForEnded`)播放完毕;断言关键状态到达预期。
- 二次录制:首次播放完毕后录像系统记录一遍录像步;断言二次录像与原录像**逐条完全相等**:步数相同、每步 code 相同、每步各 param 的 type/value 相同(严格逐条比较,不做整体 deep equal 之外的结构忽略);随后再次播放二次录像并断言同样结束。
- error 20012008分别触发参数数量不匹配2001、参数类型不匹配2002、移动前未停止2003、移动控制器缺失2004、teleport 无路径2005、use-item 失败2006、equip 失败2007、unequip 失败2008`logger.catch`/`logger.error` spy 观测。
- 仅允许为录像重置最小使用存档数据不测存读档本身D-32
有界循环,无 fake timers。
</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>
<automated>pnpm exec vitest run packages-user/data-state/test/replayPlayback.test.ts</automated>
<fails_when>运行失败,或 20012008 中任一码未被观测,或二次录制逐条比对失败</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.
- `replayPlayback.test.ts` 零非跳过失败。
- 断言小地图场景播放完成、二次录像与原录像逐条相等(步数 + code + 各 param type/value、二次播放完成。
- 断言 20012008 全部触发。
- 每个 `it` 前有单行中文注释。
</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>
<done>小地图录像播放、二次录制比对与 20012008 覆盖并跑绿;完成后以 create-or-append 方式将本计划的 code→模块→用例行20012008写入 `06-COVERAGE-MAP.md` 的 06-07 小节:先检查表头,不存在则先创建,再只追加本小节,不重写他节。</done>
</task>
</tasks>
@ -183,43 +195,44 @@ Build `GameMap` with an inline `IDataCommon` and cover:
| Boundary | Description |
|----------|-------------|
| none | In-repo Node Vitest tests only; no runtime input surface, network, DOM, or filesystem write is introduced. |
| none | 仅新增仓库内 Node Vitest 测试无运行时输入面、网络、DOM 或文件写入。 |
## 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. |
| T-06-07-01 | Tampering | `data-state/test` 下测试文件 | low | mitigate | 只导入已跟踪模块、合成 fixture不安装包、不改核心源码门禁 `pnpm test:ci` + 复核仅 `*.test.ts` 变更。 |
| T-06-07-02 | Denial of service | 录像播放/二次录制循环 | medium | mitigate | 所有 await 循环有界(`waitForEnded` 迭代上限30s 测试超时兜底。 |
| T-06-07-03 | Information disclosure | fixture | low | accept | 合成状态无真实存档或密钥D-02 |
| T-06-07-SC | Tampering | 包安装 | high | mitigate | 不新增依赖;若确需安装,停止并先走包合法性 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`.
- 两个文件聚焦运行通过;`pnpm test:ci` 全量绿。
- `git status` 仅新增 `*.test.ts`;无核心文件改动。
- 任何 `it.skip`/`it.todo` 均有 `#06-07-N` 条目。
- 以 create-or-append 方式写入 `06-COVERAGE-MAP.md` 06-07 小节20012008表头不存在则先创建幂等、顺序无关不重写他节。
- **执行协议**:执行前必须先向用户确认录像记录已接通;执行后暂停并用中文分条汇报。
</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.
- 2 个集成测试文件存在并通过(在用户确认录像记录接通之后)。
- 顶层伤害组合、系统层光环/效果组合、小地图录像播放、二次录制逐条比对、20012008 全部断言。
</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`
- `packages-user/data-state/test/enemyCombination.test.ts`
- `packages-user/data-state/test/replayPlayback.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`
- `enemyCombination.test.ts`: `createMultiSpecialEnemy`、`createHandler`、`createAuraFixture`
- `replayPlayback.test.ts`: `waitForEnded`、`createSmallMapScene`、`assertReplayEqual`、`triggerReplayError`
Planning artifacts:
- `06-COVERAGE-MAP.md` create-or-append 06-07 小节
</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`.
完成时创建 `.planning/phases/06-unit-tests/06-07-SUMMARY.md`,以 create-or-append 方式写入 `06-COVERAGE-MAP.md` 06-07 小节;疑似 bug 追加到 `06-TEST-FINDINGS.md``#06-07-N` 小节。执行完成后暂停并用中文分条汇报。
</output>

View File

@ -5,45 +5,59 @@ type: execute
wave: 1
depends_on: []
files_modified:
- packages-user/data-state/test/combatClosure.test.ts
- packages-user/data-state/test/saveablesRoundTrip.test.ts
- 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
- packages-user/data-common/src/common/face.test.ts
- packages-user/data-common/src/common/mover.test.ts
autonomous: true
requirements: [TEST-01]
estimate:
tokens: 42000
raw_tokens: 42000
tasks: 2
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."
- 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
- "TEST-01 由数据端行为单测满足,门禁为固定非 watch 命令 `pnpm test:ci` 全绿D-08。"
- "接口全覆盖:`FlagSystem`/`FlagCommonField`、`getFaceMovement`/`degradeFace`/`nextFaceDirection`/`fromDirectionString`、`MapLocIndexer`、`FaceManager`/`Dir4FaceHandler`/`Dir8FaceHandler`、`RoleFaceBinder`、`ObjectMover` 的每个公开方法至少一条正常用例;名称含 legacy 的接口/方法排除D-19 / D-20 / D-30 / D-41。"
- "code 全覆盖:本计划可达的码 43bind 未知 main、44bind 主朝向冲突、111对非数值 flag 字段 add各至少一条触发断言D-31。"
- "`FlagSystem` 公开接口(`occupied`/`insertField`/`getField`/`getOrInsert`/`getOrInsertComputed`/`deleteField`/`setFieldValue`/`addFieldValue`/`getFieldValue`/`getFieldValueDefaults`)行为与文档一致;`saveState`/`loadState` 归 06-09flag `saveState()` 无压缩参数)。"
- "`data-common/src/common` 工具utils/indexer/faceManager+face/mover行为与文档一致D-20 要求:主要测试未触及的辅助函数才补最小直接单测。"
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
- path: packages-user/data-base/src/flag/system.test.ts
provides: FlagSystem 覆盖
- path: packages-user/data-common/src/common/utils.test.ts
provides: 朝向工具覆盖
- path: packages-user/data-common/src/common/indexer.test.ts
provides: MapLocIndexer 覆盖
- path: packages-user/data-common/src/common/faceManager.test.ts
provides: FaceManager + Dir4/Dir8 handler 覆盖
- path: packages-user/data-common/src/common/face.test.ts
provides: RoleFaceBinder 覆盖(码 43/44
- path: packages-user/data-common/src/common/mover.test.ts
provides: ObjectMover 扩展覆盖(既有文件)
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)"
- "flag/system.test.ts 扩展 `data-state/test/dataClosure.test.ts:120-132` 的 flag 往返形状到完整公开表面(去掉 save/load"
- "common 相关文件沿用同目录 `mover.test.ts` 样板;`mover.test.ts` 为既有文件扩展,保留既有覆盖"
- "`06-COVERAGE-MAP.md` 为共享 create-or-append 文件:写 06-08 小节前先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在,不存在则先创建,再只追加/更新本小节;幂等且与其它 wave 1 计划顺序无关,不重写他节"
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"
- "不创建或修改任何生产/核心源码,只改/增 `*.test.ts`"
- "不安装覆盖率工具D-09"
- "不引入 `fake-indexeddb`D-10"
- "疑似 bug 按正确预期编写并标记 `it.skip`/`it.todo`D-05"
- "fixture 全部文件内联,不建跨文件 helperD-02/D-03"
- "异步路径只用真实计时器 + `await controller.onEnd`,禁用 `vi.useFakeTimers()`D-04"
- "不测任何 `saveState`/`loadState`D-32"
---
<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).
覆盖两个辅助模块:全局 flag 系统 `packages-user/data-base/src/flag`D-19与通用接口 `packages-user/data-common/src/common`utils / indexer / faceManager + face / moverD-20 / D-41
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: D-19 要求 flag 公开接口行为D-20/D-41 要求 common 由主要测试自然覆盖,并对 tools/indexer/faceManager+face/mover 补直接单测。存读档集中到 06-09。
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/`.
Output: 6 个 `*.test.ts`(其中 `mover.test.ts` 为既有文件扩展)。
</objective>
<execution_context>
@ -56,89 +70,121 @@ Output: 2 new integration test files under `data-state/test/`.
@.planning/phases/06-unit-tests/06-PATTERNS.md
@.planning/codebase/TESTING.md
@dev.md
@packages-user/data-state/src/core.ts
@packages-user/data-base/src/flag/system.ts
@packages-user/data-base/src/flag/field.ts
@packages-user/data-base/src/flag/types.ts
@packages-user/data-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/face.ts
@packages-user/data-common/src/common/mover.ts
@packages-user/data-common/src/common/mover.test.ts
@packages-user/data-common/src/common/types.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.
- 只改测试,绝不修改任何生产文件。
- 每个 `it(...)` 前必须有一行中文注释dev.md
- fixture 内联且最小D-02/D-03
- 警告路径经 `logger.catch` / `vi.spyOn(logger, ...)`
- 疑似 bug正确预期 + `it.skip`/`it.todo` + 中文注释锚定 `#06-08-N``pnpm test:ci` 保持全绿。
- 不测存读档D-32
- `06-COVERAGE-MAP.md` 为共享 create-or-append 文件(幂等、顺序无关):写 06-08 小节前先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在,不存在则先创建,再只追加/更新本小节,绝不重写他节。
- **执行协议D-33执行前向用户确认执行后暂停并用中文分条汇报。**
</execution_rules>
<tasks>
<task type="auto">
<name>Task 1: Combat closure through CoreState</name>
<files>packages-user/data-state/test/combatClosure.test.ts</files>
<name>Task 1: FlagSystem 完整公开表面</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
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)
.planning/phases/06-unit-tests/06-PATTERNS.mdF 节 flag
</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.
创建 `flag/system.test.ts`import 链需要时使用 `vi.hoisted``Map.getOrInsertComputed`/`getOrInsert` polyfill
- `occupied` 初始 false`insertField`/`setFieldValue` 后 true。
- `insertField` 返回 `FlagCommonField`,其 `get`/`set`/`toStructured` 行为符合文档;`getField` 未知返回 null`getOrInsert` 插入默认并二次调用返回同实例。
- `getOrInsertComputed` 收到 key 并插入计算默认值;`deleteField` 移除。
- `setFieldValue` 覆盖;`addFieldValue` 对数值字段累加、对**非数值**字段经 `logger.catch` 观测码 111`getFieldValue` 未知返回 undefined`getFieldValueDefaults` 返回默认并插入。
- 不测 `saveState`/`loadState`(归 06-09注意 flag `saveState()` 无压缩参数)。
</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>
<automated>pnpm exec vitest run packages-user/data-base/src/flag/system.test.ts</automated>
<fails_when>聚焦运行失败,或码 111 未被观测,或某公开方法缺少正常用例</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.
- `system.test.ts` 零非跳过失败。
- 断言 D-19 列出的完整公开表面与码 111。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>The combat path is proven end-to-end through CoreState and green.</done>
<done>FlagSystem 覆盖并跑绿。</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>
<name>Task 2: 朝向工具 + 位置索引 + FaceManager/handlers + RoleFaceBinder</name>
<files>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, packages-user/data-common/src/common/face.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)
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/face.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.mdG 节)
</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.
`utils.test.ts``getFaceMovement` 对每个 `FaceDirection`(含 `Unknown -> {0,0}``degradeFace` 对全部对角线与自定义 `unknown` 参数;`nextFaceDirection` 对 4 向顺/逆时针与 8 向顺/逆时针及 `Unknown` 透传;`fromDirectionString` 对每个字符串与未知字符串。纯函数同步断言。
`indexer.test.ts``MapLocIndexer` + `setWidth(w)``locToIndex(x,y)` 与 `locaterToIndex({x,y})` 一致,`indexToLocator` 对范围内索引为逆运算,换更宽 width 改变行步长。
`faceManager.test.ts``FaceManager.register`/`registerById` 后 `get`/`getById` 命中、未知返回 null`Dir8FaceHandler` 的 `degrade` 透传、`movement` 对每个方向及越界输入(`ZERO_DESCRIPTOR`)、`move` 缩放与负值、`opposite`(含 `Unknown -> Unknown`)、`next` 顺/逆时针与 `Unknown` 透传、`mapDirection`/`mapMovement` 含 `Unknown``{0,0}``Dir4FaceHandler` 的 `degrade` 把对角映射到 4 向、未知映射到 `Unknown`,其余方法对 4 向集正确。
`face.test.ts``RoleFaceBinder.malloc` 建立主朝向映射;`bind` 正常绑定;`bind` 对未知 main 经 `logger.catch` 观测码 43`bind` 对与主朝向相同的 face 观测码 44`getFaceOf`(命中/未知标识/未知 face 返回 null`getFaceDirection``getMainFace`(命中/未知返回 null
</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>
<automated>pnpm exec vitest run 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 packages-user/data-common/src/common/face.test.ts</automated>
<fails_when>运行失败,或 43/44 未被观测,或某方向/索引映射断言缺失或错误</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.
- 四个文件零非跳过失败。
- utils 覆盖全部方向/字符串indexer 覆盖往返与宽度变化faceManager 覆盖注册表与两个 handler 全表面face.test 覆盖 RoleFaceBinder 与码 43/44。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>All registered saveables round-trip same-instance across all compressions and are green (D-11, D-12).</done>
<done>朝向工具、位置索引、FaceManager/handlers、RoleFaceBinder 覆盖并跑绿。</done>
</task>
<task type="auto">
<name>Task 3: ObjectMover 扩展覆盖</name>
<files>packages-user/data-common/src/common/mover.test.ts</files>
<read_first>
packages-user/data-common/src/common/mover.ts
packages-user/data-common/src/common/mover.test.ts
packages-user/data-common/src/common/types.ts
.planning/phases/06-unit-tests/06-PATTERNS.md共享样板 5
</read_first>
<action>
在既有 `mover.test.ts` 上扩展(保留既有覆盖),补齐 `ObjectMover` 全部公开方法的正常/边界用例:`setPos`/`setFaceDir`/`setMoveDir`/`tp`/`jump`/`step`/`stepFace`/`forward`/`backward`/`speed`/`face`/`animDir`/`push`/`clear`/`start`
- 构建步骤序列(`tp`/`jump`/`step`/`stepFace`/`forward`/`backward`)后 `start()` 返回控制器,真实计时器 + `await controller.onEnd` 断言最终位置/朝向。
- `speed`/`face`/`animDir` 影响执行;`clear` 清除待执行步骤;已有移动进行中时 `start()` 返回 null契约检测点
- `push` 追加多步按序执行。
- 既有断言不变;仅新增缺失覆盖。无 fake timers。
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-common/src/common/mover.test.ts</automated>
<fails_when>运行失败,或既有用例被破坏,或某公开 mover 方法仍无正常用例</fails_when>
</verify>
<acceptance_criteria>
- `mover.test.ts` 零非跳过失败且既有覆盖保持。
- `ObjectMover` 全部列出公开方法至少一条正常用例;异步路径用真实计时器 + await。
- 每个新增 `it` 前有单行中文注释。
</acceptance_criteria>
<done>ObjectMover 公开表面扩展覆盖并跑绿;完成后以 create-or-append 方式将本计划的 code→模块→用例行43/44/111写入 `06-COVERAGE-MAP.md` 的 06-08 小节:先检查表头,不存在则先创建,再只追加本小节,不重写他节。</done>
</task>
</tasks>
@ -148,39 +194,53 @@ Create `saveablesRoundTrip.test.ts` through `CoreState.getSaveableContent(id)` o
| Boundary | Description |
|----------|-------------|
| none | In-repo Node Vitest tests only; no runtime input surface, network, DOM, or filesystem write is introduced. |
| none | 仅修改/新增仓库内 Node Vitest 测试无运行时输入面、网络、DOM 或文件写入。 |
## 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. |
| T-06-08-01 | Tampering | `data-base/src/flag`、`data-common/src/common` 下测试文件 | low | mitigate | 只导入已跟踪模块、合成 fixture不安装包、不改核心源码门禁 `pnpm test:ci` + 复核仅 `*.test.ts` 变更。 |
| T-06-08-02 | Denial of service | mover 异步循环 | low | mitigate | 异步断言 await 控制器 `onEnd`30s 超时兜底。 |
| T-06-08-03 | Information disclosure | fixture | low | accept | 合成 flag 值/方向无真实游戏数据D-02 |
| T-06-08-SC | Tampering | 包安装 | high | mitigate | 不新增依赖;若确需安装,停止并先走包合法性 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`.
- 六个文件聚焦运行通过;`pnpm test:ci` 全量绿。
- `git status` 仅显示 `*.test.ts` 变更;无核心文件改动。
- 任何 `it.skip`/`it.todo` 均有 `#06-08-N` 条目。
- 以 create-or-append 方式写入 `06-COVERAGE-MAP.md` 06-08 小节43/44/111表头不存在则先创建幂等、顺序无关不重写他节。
- **执行协议**:执行前向用户确认;执行后暂停并用中文分条汇报。
</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.
- 6 个辅助行为测试文件存在/扩展并通过。
- D-19 的 flag 全表面与 D-20/D-41 的 commonutils/indexer/faceManager+face/mover覆盖。
- 未测存读档。
</success_criteria>
<artifacts_this_plan_produces>
Test files:
- `packages-user/data-state/test/combatClosure.test.ts`
- `packages-user/data-state/test/saveablesRoundTrip.test.ts`
- `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`
- `packages-user/data-common/src/common/face.test.ts`
- `packages-user/data-common/src/common/mover.test.ts`(扩展)
Inline helper symbols:
- `combatClosure.test.ts`: `createEnemy`, `addEnemy`, `createHandler`
- `saveablesRoundTrip.test.ts`: `SAVEABLE_IDS`, `COMPRESSIONS`, `mutateKeyField`
- `system.test.ts`: `createField`
- `utils.test.ts`: `ALL_DIRECTIONS`
- `indexer.test.ts`: `createIndexer`
- `faceManager.test.ts`: `createHandler`
- `face.test.ts`: `createBinder`
- `mover.test.ts`: `TestTile`、`createMover`(沿用既有)
Planning artifacts:
- `06-COVERAGE-MAP.md` create-or-append 06-08 小节
</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`.
完成时创建 `.planning/phases/06-unit-tests/06-08-SUMMARY.md`,以 create-or-append 方式写入 `06-COVERAGE-MAP.md` 06-08 小节;疑似 bug 追加到 `06-TEST-FINDINGS.md``#06-08-N` 小节。执行完成后暂停并用中文分条汇报。
</output>

View File

@ -2,56 +2,70 @@
phase: 06-unit-tests
plan: 09
type: execute
wave: 1
depends_on: []
wave: 2
depends_on: [06-03, 06-05, 06-06, 06-08]
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
- packages-user/data-base/src/enemy/saveLoad.test.ts
- packages-user/data-base/src/hero/saveLoad.test.ts
- packages-user/data-base/src/map/saveLoad.test.ts
- packages-user/data-common/src/replay/saveLoad.test.ts
- packages-user/data-base/src/flag/saveLoad.test.ts
- packages-user/data-state/test/saveablesRoundTrip.test.ts
- .planning/phases/06-unit-tests/06-SAVE-EXCLUSIONS.md
autonomous: false
requirements: [TEST-01]
estimate:
tokens: 40000
raw_tokens: 40000
tokens: 68000
raw_tokens: 68000
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
- "TEST-01 由数据端行为单测满足,门禁为固定非 watch 命令 `pnpm test:ci` 全绿D-08。"
- "存读档集中:本计划是唯一测试 `saveState`/`loadState` 的计划其它计划一律不测D-32 / D-42。"
- "接口全覆盖:所有覆盖系统内含 `saveState`/`loadState` 的类Enemy/special、EnemyManager、HeroAttribute/modifier、HeroLocation、HeroState、HeroEquipment、EquipmentState/HeroEquipsStore、HeroItems、HeroFollower、HeroRendering、MapState、GameMap、MapLayer、StaticTile、DynamicTile、ReplayArray、FlagSystem的这两个方法至少一条正常用例名称含 legacy 的接口/方法排除D-30 / D-42。"
- "code 全覆盖:本计划可达的码 55MapStore load 缺 reference、58equipment store load 找不到 maxUid、59equipment load 找不到 item raw data、112重复 saveable id、113bindSaveableExecuter 目标未添加、119loadState 缺 prefab、120loadState 缺 special、122MapStore load 楼层缺失、124MapLayer load 引用缺失各至少一条触发断言D-31 / D-42。"
- "往返形式为**同实例恢复**:对可存档对象 `saveState` → 继续修改状态 → `loadState` → 关键字段回到存档点D-12。"
- "压缩档全覆盖:`NoCompression`/`LowCompression`/`HighCompression` 三档(按各 `saveState`/`loadState` 签名适用者D-11 / D-42。"
- "「状态相同」只用**关键字段显式断言**,不比较派生字段/缓存/序列化容器结构也不做整体深度相等D-13。"
- "推导并写出**排除清单**(存档无关/元数据字段如最后存档时间、dirty、缓存落到 `06-SAVE-EXCLUSIONS.md`D-42。"
- "CoreState 顶层验证:`getSaveableContent(id)` 对 4 个已注册 id`@system/hero`/`flags`/`maps`/`enemy`逐个往返D-11并验证 `addSaveableContent`/`bindSaveableExecuter` 守卫112/113若用户已新增公开 save/load 入口,则一并经该入口验证。"
- "执行前必须先向用户确认(依赖用户调整 CoreState 可存档内容并新增公开 save/load 入口例如录像存档目前未计入但应计入D-42。"
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
- path: packages-user/data-base/src/enemy/saveLoad.test.ts
provides: Enemy/special/EnemyManager 存读档覆盖119/120
- path: packages-user/data-base/src/hero/saveLoad.test.ts
provides: hero 各子系统存读档覆盖58/59
- path: packages-user/data-base/src/map/saveLoad.test.ts
provides: 地图各层/tile 存读档覆盖55/122/124
- path: packages-user/data-common/src/replay/saveLoad.test.ts
provides: ReplayArray 存读档覆盖
- path: packages-user/data-base/src/flag/saveLoad.test.ts
provides: FlagSystem 存读档覆盖
- path: packages-user/data-state/test/saveablesRoundTrip.test.ts
provides: CoreState 顶层 4 saveables × 3 压缩档往返112/113
- path: .planning/phases/06-unit-tests/06-SAVE-EXCLUSIONS.md
provides: 存读档排除清单(存档无关/元数据字段)
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"
- "saveablesRoundTrip.test.ts 经 `CoreState.getSaveableContent(id)``data-state/src/core.ts:486`)进入,对应 `core.ts:237-240` 注册的 4 个 id不接触 `saveSystem.init`/IndexedDBD-10"
- "各 saveLoad.test.ts 用 inline 合成对象/小地图/小录像数组;三档压缩循环参照 `data-base/src/map/mapLifecycle.test.ts:203-234` 的显式关键字段形状"
- "`06-COVERAGE-MAP.md` 为共享 create-or-append 文件:写 06-09 小节前先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在,不存在则先创建,再只追加/更新本小节;幂等且与其它计划顺序无关,不重写他节"
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"
- "不创建或修改任何生产/核心源码,只新增 `*.test.ts` 与规划产物"
- "不安装覆盖率工具D-09"
- "不引入 `fake-indexeddb`,不做 IndexedDB 端到端存档D-10"
- "疑似 bug 按正确预期编写并标记 `it.skip`/`it.todo`D-05"
- "fixture 全部文件内联,不建跨文件 helperD-02/D-03"
- "不使用 fake timersD-04"
- "不做整体深度相等;不比较派生/缓存/序列化容器结构D-13"
---
<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).
存读档独立系统D-42覆盖所有含 `saveState`/`loadState` 的类逐类同实例往返 + `CoreState` 顶层验证;仅断言关键状态字段,排除存档无关/元数据字段;压缩档全覆盖。
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-42依赖用户调整 CoreState 可存档内容,例如录像存档目前未计入但应计入,并新增公开 save/load 入口)。
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.
Output: 6 个 `*.test.ts``06-SAVE-EXCLUSIONS.md`
</objective>
<execution_context>
@ -64,111 +78,157 @@ Output: 4 new co-located `*.test.ts` files.
@.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-common/src/save/types.ts
@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-base/src/map/mapLifecycle.test.ts
@packages-user/data-base/src/enemy/enemy.ts
@packages-user/data-base/src/enemy/manager.ts
@packages-user/data-base/src/hero/equipStore.ts
@packages-user/data-base/src/hero/state.ts
@packages-user/data-base/src/map/mapState.ts
@packages-user/data-base/src/map/mapLayer.ts
@packages-user/data-common/src/replay/array.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.
- 只写测试,绝不修改任何生产文件。
- 每个 `it(...)` 前必须有一行中文注释dev.md
- fixture 内联且最小D-02/D-03`SaveCompression` 三档。
- 警告/错误路径经 `logger.catch` / `vi.spyOn(logger, ...)`
- 疑似 bug正确预期 + `it.skip`/`it.todo` + 中文注释锚定 `#06-09-N``pnpm test:ci` 保持全绿。
- **执行前必须先向用户确认D-42CoreState 可存档内容与公开 save/load 入口已就绪;未确认前不得开始。**
- `06-COVERAGE-MAP.md` 为共享 create-or-append 文件(幂等、顺序无关):写 06-09 小节前先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在,不存在则先创建,再只追加/更新本小节,绝不重写他节。
- **执行协议D-33执行前向用户确认执行后暂停并用中文分条汇报。**
</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>
<task type="checkpoint:human-verify" gate="blocking-human">
<name>Task 1: 执行前门禁 — 用户确认 CoreState 可存档内容与公开入口已调整</name>
<files>packages-user/data-state/test/saveablesRoundTrip.test.ts尚未创建仅作占位说明</files>
<what-built>调整后的 CoreState 可存档内容(含录像存档计入)与新增的公开 save/load 入口。</what-built>
<how-to-verify>
1) 确认 4 个 saveable`@system/hero`/`flags`/`maps`/`enemy`)及其新增内容已就绪。
2) 确认是否提供公开 save/load 入口(有则一并验证,无则仅走 `getSaveableContent(id)` 的 saveables 层)。
3) 确认录像存档是否已计入。
</how-to-verify>
<resume-signal>用户回复「存读档入口已就绪,可以开始」则继续 Task 2/3否则暂停本计划等待用户改代码。</resume-signal>
<read_first>
.planning/phases/06-unit-tests/06-CONTEXT.mdD-42
packages-user/data-state/src/core.tssaveables 注册与 saveableContent 入口)
packages-user/data-common/src/save/types.tsISaveableContent/ISaveSystem 契约)
</read_first>
<action>
本计划依赖用户先调整 CoreState 的可存档内容(例如录像存档目前未计入但应计入),并新增公开 save/load 入口D-42。开始前向用户展示并确认
- 4 个 saveable`@system/hero`/`flags`/`maps`/`enemy`)及其新增内容是否已就绪;
- 是否已提供公开 save/load 入口(若有,本计划将一并经该入口验证;若无,则仅走 `getSaveableContent(id)` 的 saveables 层);
- 录像存档是否已计入。
用户确认后再继续 Task 2/3若未就绪暂停本计划等待用户改代码不得自行修改核心代码。
</action>
<verify>
<human-check>用户明确回复「存读档入口已就绪,可以开始」,或不通过则暂停本计划</human-check>
</verify>
<acceptance_criteria>
- 用户给出明确开始确认;否则本计划保持暂停状态。
</acceptance_criteria>
<done>CoreState 可存档内容与入口就绪得到用户确认,或计划暂停等待用户改代码。</done>
</task>
<task type="auto">
<name>Task 2: enemy + hero 各类存读档往返(含压缩档与码 119/120/58/59</name>
<files>packages-user/data-base/src/enemy/saveLoad.test.ts, packages-user/data-base/src/hero/saveLoad.test.ts, .planning/phases/06-unit-tests/06-SAVE-EXCLUSIONS.md</files>
<read_first>
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/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/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/rendering.ts
.planning/phases/06-unit-tests/06-PATTERNS.mdC/E 节)
</read_first>
<action>
`enemy/saveLoad.test.ts`:对 `Enemy`、`CommonSerializableSpecial`/`NonePropertySpecial`、`EnemyManager` 分别同实例往返(三档压缩):
- Enemy属性关键字段 + special 关键值;`loadState` 时实例上存在但存档缺失的 special 经 `logger.catch` 观测码 120`saveState` 返回分离克隆(改返回对象不影响实例)。
- EnemyManager`saveState` 只序列化脏 code`loadState` 缺 prefab 经 `logger.catch` 观测码 119。
- 只断言关键状态字段D-13
`hero/saveLoad.test.ts`:对 hero 各子系统同实例往返(三档压缩):
- `HeroAttribute`/`ValueModifier`/`PercentageModifier`base/值关键字段恢复modifier 存盘开关生效。
- `HeroLocation`x/y/floorId/direction 四字段恢复。
- `HeroState`attribute base 值、location、modifier 值关键字段恢复save 禁用 modifier 不持久化。
- `HeroEquipment`/`EquipmentState`/`HeroEquipsStore`/`HeroItems`/`HeroFollower`/`HeroRendering`各自关键字段恢复equipStore `loadState` 缺 item raw 观测码 59、空 equipments 观测码 58`logger.catch`)。
- 只断言关键字段D-13
`06-SAVE-EXCLUSIONS.md`:逐类推导并写出**排除清单**——列出**不**在断言范围内的存档无关/元数据字段如最后存档时间、dirty/脏标记、缓存/派生字段、序列化容器结构、副本容量等),并注明依据(为什么与存档语义无关)。此文档作为 06-09 与后续维护的对照基准。
</action>
<verify>
<automated>pnpm exec vitest run packages-user/data-base/src/enemy/saveLoad.test.ts packages-user/data-base/src/hero/saveLoad.test.ts</automated>
<fails_when>运行失败,或 119/120/58/59 中任一码未被观测,或某类缺少同实例往返断言</fails_when>
</verify>
<acceptance_criteria>
- 两个文件零非跳过失败。
- enemy/hero 每个含存读档的类至少一条同实例往返;断言码 119/120/58/59。
- 只用关键字段显式断言,无整体深度相等。
- `06-SAVE-EXCLUSIONS.md` 存在并逐类列出排除字段与依据。
- 每个 `it` 前有单行中文注释。
</acceptance_criteria>
<done>enemy/hero 存读档往返与排除清单覆盖并跑绿。</done>
</task>
<task type="auto">
<name>Task 3: map + replay + flag 存读档往返(码 55/122/124与 CoreState 顶层验证(码 112/113</name>
<files>packages-user/data-base/src/map/saveLoad.test.ts, packages-user/data-common/src/replay/saveLoad.test.ts, packages-user/data-base/src/flag/saveLoad.test.ts, packages-user/data-state/test/saveablesRoundTrip.test.ts</files>
<read_first>
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/staticTile.ts
packages-user/data-base/src/map/dynamicTile.ts
packages-user/data-common/src/replay/array.ts
packages-user/data-base/src/flag/system.ts
packages-user/data-base/src/flag/field.ts
packages-user/data-base/src/flag/types.ts
packages-user/data-state/test/dataClosure.test.ts (flag round-trip)
.planning/phases/06-unit-tests/06-PATTERNS.md (section F: flag)
packages-user/data-state/src/core.ts
packages-user/data-state/test/fixtures/closed-loop.ts
packages-user/data-base/src/map/mapLifecycle.test.ts
.planning/phases/06-unit-tests/06-PATTERNS.mdD/E/F 节)
</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.
`map/saveLoad.test.ts`:对 `MapState`、`GameMap`、`MapLayer`、`StaticTile`、`DynamicTile` 同实例往返(三档压缩):
- MapState`loadState` 在非 `NoCompression` 且未 `compareWith` 时观测码 55缺楼层观测码 122。
- MapLayer关键块/点位事件恢复;引用缺失观测码 124三档压缩循环参照 `mapLifecycle.test.ts` 的显式关键字段。
- StaticTile/DynamicTile关键 num 与覆盖事件恢复。
- 只断言关键字段D-13
`replay/saveLoad.test.ts``ReplayArray.saveState`/`loadState` 同实例往返:命令与参数(含 boolean/整数/bigint/string/数组)读回一致;不比较内部容量/元数据。
`flag/saveLoad.test.ts``FlagSystem.saveState()`(无压缩参数)/`loadState` 同实例往返:已知字段值 + `occupied` 恢复;`saveState` 返回分离克隆。
`saveablesRoundTrip.test.ts`:经 `CoreState.getSaveableContent(id)`
- 对 4 个 id`@system/hero`/`flags`/`maps`/`enemy`× 3 档 `SaveCompression`:设已知关键字段 → `saveState` → 改字段 → `loadState` → 只断言关键字段hero base 属性、flag 字段、map 层块/点位事件、enemy prefab 属性)。不做整体深度相等。
- 断言快照分离save 后改活对象不影响快照关键字段。
- 未知 id 返回 null。
- 守卫:重复 `addSaveableContent``logger.catch` 观测码 112对未添加对象 `bindSaveableExecuter` 观测码 113。
- 若用户已提供公开 save/load 入口,则经该入口再跑一遍等价往返。
- Node-safe `createCoreState()`;不 `saveSystem.init`、不加 `fake-indexeddb`D-10
</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>
<automated>pnpm exec vitest run packages-user/data-base/src/map/saveLoad.test.ts packages-user/data-common/src/replay/saveLoad.test.ts packages-user/data-base/src/flag/saveLoad.test.ts packages-user/data-state/test/saveablesRoundTrip.test.ts</automated>
<fails_when>运行失败,或 55/112/113/122/124 中任一码未被观测,或 4×3 往返矩阵缺失某组合</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.
- 四个文件零非跳过失败。
- map/replay/flag 各类同实例往返;断言码 55/122/124。
- saveablesRoundTrip 覆盖 4 id × 3 压缩档,显式关键字段 + 快照分离 + 未知 id null + 码 112/113。
- 每个 `it` 前有单行中文注释。
</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>
<done>地图/录像/flag 存读档往返与 CoreState 顶层验证覆盖并跑绿;完成后以 create-or-append 方式将本计划的 code→模块→用例行55/58/59/112/113/119/120/122/124写入 `06-COVERAGE-MAP.md` 的 06-09 小节:先检查表头,不存在则先创建,再只追加本小节,不重写他节。</done>
</task>
</tasks>
@ -178,43 +238,55 @@ Keep fixtures inline. If a handler's documented descriptor is contradicted by th
| Boundary | Description |
|----------|-------------|
| none | In-repo Node Vitest tests only; no runtime input surface, network, DOM, or filesystem write is introduced. |
| none | 仅新增仓库内 Node Vitest 测试无运行时输入面、网络、DOM 或文件写入(不碰 IndexedDB |
## 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. |
| T-06-09-01 | Tampering | `data-base`/`data-common`/`data-state/test` 下测试文件 | low | mitigate | 只导入已跟踪模块、合成快照;不安装包、不改核心源码;门禁 `pnpm test:ci` + 复核仅 `*.test.ts` 变更。 |
| T-06-09-02 | Information disclosure | 快照 fixture | low | accept | 合成状态无真实存档文件或密钥D-02 |
| T-06-09-03 | Repudiation | 全局 stub 泄漏 | low | accept | 任何 `vi.stubGlobal``afterAll` 拆除;用例独立。 |
| T-06-09-SC | Tampering | 包安装(尤其 `fake-indexeddb` | high | mitigate | 明确不引入 `fake-indexeddb` 或任何新依赖;若确需安装,停止并先走包合法性 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`.
- 六个测试文件聚焦运行通过;`pnpm test:ci` 全量绿。
- `git status` 仅新增 `*.test.ts``06-SAVE-EXCLUSIONS.md`;无核心文件改动。
- 任何 `it.skip`/`it.todo` 均有 `#06-09-N` 条目。
- 以 create-or-append 方式写入 `06-COVERAGE-MAP.md` 06-09 小节9 个码);表头不存在则先创建;幂等、顺序无关,不重写他节。
- **执行协议**:执行前必须先向用户确认 CoreState 可存档内容与入口就绪;执行后暂停并用中文分条汇报。
</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.
- 6 个存读档测试文件存在并通过(在用户确认入口就绪之后)。
- 每个含 `saveState`/`loadState` 的类至少一条同实例往返4 saveable × 3 压缩档覆盖。
- 排除清单 `06-SAVE-EXCLUSIONS.md` 落盘。
- 9 个可达码全部触发断言。
</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`
- `packages-user/data-base/src/enemy/saveLoad.test.ts`
- `packages-user/data-base/src/hero/saveLoad.test.ts`
- `packages-user/data-base/src/map/saveLoad.test.ts`
- `packages-user/data-common/src/replay/saveLoad.test.ts`
- `packages-user/data-base/src/flag/saveLoad.test.ts`
- `packages-user/data-state/test/saveablesRoundTrip.test.ts`
Inline helper symbols:
- `system.test.ts`: `createField`
- `utils.test.ts`: `ALL_DIRECTIONS`
- `indexer.test.ts`: `createIndexer`
- `faceManager.test.ts`: `createHandler`
- `enemy/saveLoad.test.ts`: `createEnemy`、`SAVE_COMPRESSIONS`
- `hero/saveLoad.test.ts`: `createState`、`createHeroState`、`SAVE_COMPRESSIONS`
- `map/saveLoad.test.ts`: `createMapState`、`SAVE_COMPRESSIONS`
- `replay/saveLoad.test.ts`: `createArray`、`createRoute`
- `flag/saveLoad.test.ts`: `createFlags`
- `saveablesRoundTrip.test.ts`: `SAVEABLE_IDS`、`COMPRESSIONS`、`mutateKeyField`
Planning artifacts:
- `.planning/phases/06-unit-tests/06-SAVE-EXCLUSIONS.md`
- `06-COVERAGE-MAP.md` create-or-append 06-09 小节
</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`.
完成时创建 `.planning/phases/06-unit-tests/06-09-SUMMARY.md`,以 create-or-append 方式写入 `06-COVERAGE-MAP.md` 06-09 小节;疑似 bug 追加到 `06-TEST-FINDINGS.md``#06-09-N` 小节。执行完成后暂停并用中文分条汇报。
</output>