From fe4e410cf4c29a8236741da03db3eeef4e98d08e Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Mon, 14 Sep 2026 11:04:33 +0800 Subject: [PATCH] fix(06): revise plans based on checker feedback --- .planning/phases/06-unit-tests/06-01-PLAN.md | 335 +++++++++-------- .planning/phases/06-unit-tests/06-02-PLAN.md | 217 +++++------ .planning/phases/06-unit-tests/06-03-PLAN.md | 156 ++++---- .planning/phases/06-unit-tests/06-04-PLAN.md | 246 ++++++------- .planning/phases/06-unit-tests/06-05-PLAN.md | 305 +++++++++------- .planning/phases/06-unit-tests/06-06-PLAN.md | 305 +++++++++------- .planning/phases/06-unit-tests/06-07-PLAN.md | 307 ++++++++-------- .planning/phases/06-unit-tests/06-08-PLAN.md | 248 ++++++++----- .planning/phases/06-unit-tests/06-09-PLAN.md | 356 +++++++++++-------- 9 files changed, 1368 insertions(+), 1107 deletions(-) diff --git a/.planning/phases/06-unit-tests/06-01-PLAN.md b/.planning/phases/06-unit-tests/06-01-PLAN.md index f4359b7..9a0cf62 100644 --- a/.planning/phases/06-unit-tests/06-01-PLAN.md +++ b/.planning/phases/06-unit-tests/06-01-PLAN.md @@ -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-31);105 在当前实现不可达,排除。" + - "光环范围: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 helper,fixture 全部文件内联(D-02/D-03)" + - "异步路径只用真实计时器,禁用 `vi.useFakeTimers()`(D-04)" + - "不测试 `saveState`/`loadState`(集中到 06-09)(D-32)" --- -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 行)。 @@ -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 - -| Source | Item | Covered by | -|--------|------|-----------| -| GOAL | 为核心系统(数据层等)补齐单元测试 (data-layer slice) | 06-01..06-09 | -| GOAL | Phase 6 non-data (render/legacy) coverage | Deferred per 06-CONTEXT `` (not planned) | -| REQ | TEST-01 | 06-01..06-09 | -| CONTEXT | D-01 mixed tiers | 06-01/02/03 integration-capable; 06-05..09 pure/DI | -| CONTEXT | D-02/D-03 minimal inline fixtures | 06-01..06-09 | -| CONTEXT | D-04 real timers + await onEnd | 06-01 (combat flow), 06-05/06 (mover), 06-08 | -| CONTEXT | D-05/D-06 skip + 06-TEST-FINDINGS.md | 06-01 creates sink; all plans append | -| CONTEXT | D-07 per-system report, no core edits | all | -| CONTEXT | D-08 `pnpm test:ci` gate | all | -| CONTEXT | D-09 no coverage tooling | all | -| CONTEXT | D-10..D-13 saveables round-trip | 06-08 | -| CONTEXT | D-14 enemy data model full | 06-03 | -| CONTEXT | D-15 combat built-ins run | 06-01/02/08 | -| CONTEXT | D-16 replay array + system + first divergence | 06-04/08 | -| CONTEXT | D-17 hero each subsystem | 06-05/06 | -| CONTEXT | D-18 map expanded coverage | 06-07 | -| CONTEXT | D-19 flag public surface | 06-09 | -| CONTEXT | D-20 common natural/ minimal direct | 06-09 | -| RESEARCH | none (no RESEARCH.md; PATTERNS.md is the pattern source) | n/a | -| ASSUMPTION | TEST-01 edge probe unclassified/unresolved | Flagged in must_haves.truths (above); user confirmation required | - - -- This phase writes tests only. Do not create or edit any file under `packages-user/*/src` except the `*.test.ts` files listed in `files_modified`. -- Every `it(...)` must be immediately preceded by a single-line Chinese comment describing the covered behavior (dev.md 注释规范). Update the comment when the test scope changes. -- Use real timers and `await controller/`sandbox` completion` for async paths; never `vi.useFakeTimers()` (D-04). -- If a test exposes a suspected bug: write the test to the CORRECT expectation, mark it `it.skip` / `it.todo`, and put a Chinese comment pointing at `06-TEST-FINDINGS.md` using a plan-scoped anchor `#06-01-N`. `pnpm test:ci` MUST stay green. -- `06-TEST-FINDINGS.md` is a shared append-only sink and is intentionally NOT listed in `files_modified` so wave-1 plans stay parallel; append your own plan-scoped section, never rewrite other sections. -- Report findings per system in the plan SUMMARY; do not fix core code in this phase (D-07). +- 只写测试,绝不修改 `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`。** - Task 1: Tracer — DamageContext/DamageSystem behavior end-to-end (+ phase scaffolding) - packages-user/data-system/src/combat/damage.test.ts, .planning/phases/06-unit-tests/COVERAGE.md + Task 1: Tracer — EnemyContext 光环流水线端到端最小路径(+ 阶段骨架) + packages-user/data-system/src/combat/context.test.ts, .planning/phases/06-unit-tests/06-COVERAGE-MAP.md + + 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-5,A 节 context) + dev.md(注释规范) + + + 创建 `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 暂停并向用户提问,不修改核心代码。 + + + pnpm exec vitest run packages-user/data-system/src/combat/context.test.ts + 聚焦运行出现任何未跳过失败,或文件因未解析的模块/全局而无法收集 + + + - `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:若表头已由其它计划建立,则直接追加/更新本小节而不再新建)。 + + 一条真实的 `EnemyContext` 光环端到端路径在提交前已跑绿,阶段 code 覆盖映射表骨架存在。 + + + + Task 2: EnemyContext 全接口、三范围、嵌套光环、警告码、四阶段顺序与两条刷新路径 + packages-user/data-system/src/combat/context.test.ts + + 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-4,A 节 context) + + + 在 `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-22):Full(`haloRange<=0`)、Rect(`haloSquare`)、Manhattan 各一条;每条同时断言范围外怪物属性不变。 + - 嵌套光环(D-23):一层(光环→特殊属性);两层(光环施加特殊属性→该特殊属性经第二 converter 转出新光环→再施加效果);优先级边界:新转换光环优先级高于当前阶段时被跳过并告警 99;同/跨优先级传播。 + - 警告码(D-24):97(多个 converter 同时命中同一 special)、98(删除同级/更高优先级已生效光环,走 modify 分支与 delete 分支各一次)、99(新增高优先级光环)、100(add 与 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。 + + + pnpm exec vitest run packages-user/data-system/src/combat/context.test.ts + 运行失败,或任一警告码 97/98/99/100/101/110、三范围、嵌套、四阶段顺序断言未被观测 + + + - `context.test.ts` 零非跳过失败。 + - 覆盖 `EnemyContext` 全部公开方法(至少一条正常用例)与全部列出警告码。 + - 三范围 + 范围外不生效、一层/两层嵌套 + 优先级边界、四阶段顺序 + 阶段间可见性、全量/局部两条刷新路径、resize/clear/destroy 生命周期均有断言。 + - 每个 `it` 前有单行中文注释。 + + `EnemyContext` 全接口、光环范围/嵌套/警告码、四阶段顺序与两条刷新路径全部覆盖并跑绿。 + + + + Task 3: DamageContext/DamageSystem(含 D-26 联动)、MapDamage、CombatFlow + 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 packages-user/data-system/src/combat/damage.ts - packages-user/data-system/src/combat/types.ts - packages-user/data-system/src/combat/context.ts - packages-user/data-system/src/path/system.test.ts - .planning/phases/06-unit-tests/06-PATTERNS.md (shared boilerplate 1-5, section A) - dev.md (注释规范) - - -Create `damage.test.ts` as the phase tracer: the thinnest real path that drives the Layer-2 combat damage contract end-to-end through its public classes in Node, with no DOM and no mocking of data-layer interfaces. - -Use the `vi.hoisted` global-stub + dynamic-import bag boilerplate from 06-PATTERNS.md shared pattern 1 (copy the `mapLifecycle.test.ts` shape) because the import chain reaches `@user/data-common` and `@motajs/common`. - -Build a minimal inline fake `IEnemyContext` (or reuse an inline fake shape) and a minimal inline fake `IDamageCalculator` whose `calculate(handler)` returns a fixed `{ damage, turn }`, then: -- construct `DamageContext` with the fake context and assert `getDamageInfo(view)` / `getDamageInfoByComputed(enemy)` return `{ handler, damage, turn }` for the calculator result and `handler` identity matches the computed enemy + locator + hero. -- assert `getDamageInfoByHandler(handler)` returns the raw calculator result. -- assert missing calculator and missing hero paths emit logger codes 106 and 107 using `logger.catch` (never `expect(...).toThrow`) per 06-PATTERNS.md shared pattern 4. -- construct `DamageSystem` and assert caching: two consecutive `getDamageInfo` calls return the same object, `markDirty(view)` / `markAllDirty()` / `deleteEnemy(view)` force recalculation, `useCalculator` / `bindHeroStatus` call `markAllDirty`, and `with(hero)` returns a `DamageContext` sharing the calculator and returning the passed hero. -- cover `calculateCritical(view, attribute, precision)` generator behavior on a driven critical limit using a fake calculator whose `getCriticalLimit` returns a known bound, plus the early-return guards (no hero / no calculator / no locator / already at upper limit). - -Then create the phase scaffolding: -- `COVERAGE.md` in the phase directory with a one-paragraph reasoned declaration: "No external API integration: this phase adds only in-repo Vitest behavior tests over the Node-only data layer; no external SDK/HTTP/CLI client is added or exercised." (api-coverage contribution). -- Create `.planning/phases/06-unit-tests/06-TEST-FINDINGS.md` with a header and the D-06 column table: 模块/接口、现象、最小复现、疑似原因、影响面、建议修复方向、关联 skip 用例、严重度. - -Verify with a focused run before moving on: the tracer MUST pass green on its own. - - - pnpm exec vitest run packages-user/data-system/src/combat/damage.test.ts - the focused vitest run reports any failed or unexpectedly skipped test, or the file cannot be collected due to an unresolved module/global - - - - `damage.test.ts` runs green under `pnpm exec vitest run` with zero non-skipped failures. - - Every `it` has an immediately preceding single-line Chinese comment. - - The file asserts at least: normal result spread, handler identity, logger codes 106 and 107, cache hit/invalidation, `with(hero)`, and one `calculateCritical` yield path. - - No production file is modified; `git status` shows only new test files plus `COVERAGE.md`. - - `COVERAGE.md` and `06-TEST-FINDINGS.md` exist with the required header/columns. - - One real combat damage path is proven end-to-end green, and the phase's findings/coverage scaffolding exists; this sink is where D-05 skipped correct-expectation tests point. - - - - Task 2: MapDamage + EnemyContext behavior - packages-user/data-system/src/combat/mapDamage.test.ts, packages-user/data-system/src/combat/context.test.ts - packages-user/data-system/src/combat/mapDamage.ts - packages-user/data-system/src/combat/context.ts - packages-user/data-system/src/combat/enemy.ts - packages-user/data-system/src/combat/types.ts - packages-user/data-common/src/store/tileStore.test.ts (registry/contract table pattern) - .planning/phases/06-unit-tests/06-PATTERNS.md (section A: mapDamage, context) - - -`mapDamage.test.ts`: drive `MapDamage` through inline fake `IEnemyContext` + fake `IMapDamageConverter` + fake `IMapDamageReducer` + fake `IMapDamageView` whose `getRange`/`getRangeParam`/`getDamageWithoutCheck` are deterministic. Assert: `addMapDamage`/`deleteMapDamage` accumulate sourceless damage and mark the index dirty; `getReducedDamage` returns the reducer result and null when no separated damage; missing reducer warns 103, missing converter warns 102; `getSeparatedDamage` unions sourced+sourceless; `deleteEnemy` removes sourced damage; `markEnemyDirty` warns 104 when the view has no store and no locator. Use `logger.catch` for warning branches. - -`context.test.ts`: drive `EnemyContext` with inline real `Enemy`-like objects (or the real `Enemy` from `@user/data-base` through a created prefab) and assert the registry contract in the `tileStore` style: `setEnemyAt` then `getEnemyByLocator`/`getEnemyByLoc`/`getEnemyLocatorByView`/`getEnemyLocator`/`getViewByComputed` return the registered objects; unknown locators/locators for unknown objects return `null`; `deleteEnemy` removes view+locator mappings; `resize` clears and sets `width`/`height` and indexer width; `scanRange`/`iterateEnemy` yield the registered entries; register/unregister aura converters, common query effects, special query effects, and final effects (assert add then remove leaves the registry empty). Keep fixtures inline and minimal (D-02/D-03). Do not mock data-layer interfaces. - - - pnpm exec vitest run packages-user/data-system/src/combat/mapDamage.test.ts packages-user/data-system/src/combat/context.test.ts - either focused run fails, or a warning assertion does not observe the documented logger code - - - - Both files run green with zero non-skipped failures. - - `mapDamage.test.ts` asserts warn codes 102, 103, 104 and reduce/delete behavior. - - `context.test.ts` asserts register/lookup/unknown-null/delete/resize for the context registry. - - Fixtures are constructed inline; no shared helper file is created. - - Every `it` has a preceding single-line Chinese comment. - - MapDamage and EnemyContext public behavior is covered and green. - - - - Task 3: CombatFlow binding, script ordering, and battle flow - packages-user/data-system/src/combat/combat.test.ts - packages-user/data-system/src/combat/combat.ts packages-user/data-system/src/combat/types.ts - packages-user/data-system/src/event/eventDispatch.test.ts (fake collaborator + manual-resolver await proof) - .planning/phases/06-unit-tests/06-PATTERNS.md (section A: combat) + 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.md(A 节 damage/mapDamage/combat) -Drive `CombatFlow` with a minimal inline fake `IStateBase` and fake enemy view / context / damage objects: -- `bindContext`/`bindDamage` accept same-`state` objects and reject foreign-state objects with logger warn 138 (assert via `logger.catch`); passing `null` clears them. -- `bindHero(null)`/`bindHero(hero)` set `hero`. -- `addCombatScript` sorts by descending priority, rejects a duplicate priority with warn 140 and leaves the list unchanged. -- `battle(view)` and `battleComputed(enemy)` return `null` and warn 139 when context or hero is missing; warn 139 for the missing damage context and warn 141 when `getDamageInfoByHandler` returns null. -- Async completion proof (real timers, no fake timers): a script whose `before` resolves after a manually-released promise must complete before `onAfterCombat` runs; assert call order `[script.before, hooks.onBeforeCombat, script.after, hooks.onAfterCombat]` and that a `before` returning truthy short-circuits (skips hooks/after). -Use the manual-resolver await pattern from `eventDispatch.test.ts` referenced in 06-PATTERNS.md. Keep all fixtures inline. + `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` 保留并注明按接口文档语义。 - pnpm exec vitest run packages-user/data-system/src/combat/combat.test.ts - the focused run fails or the ordering assertion does not observe the documented before/after sequence + 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 + 任一聚焦运行失败,或 106/107/102/103/104/138/139/140/141 中任一码未被观测 - - `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、107;D-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` 前有单行中文注释。 - CombatFlow binding, guard warnings, and awaited script ordering are covered and green. + 伤害上下文/系统、地图伤害、战斗流程覆盖并跑绿,且四阶段/联动边界与既有 `#06-01-N` 跳过用例保留。 @@ -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。 | -- `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` 条目。 +- **执行协议**:执行前向用户确认;执行后暂停并以中文分条汇报。 -- 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 行存在。 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 行;若表头已存在则仅追加/更新本小节) -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` 小节。执行完成后暂停并用中文分条汇报。 diff --git a/.planning/phases/06-unit-tests/06-02-PLAN.md b/.planning/phases/06-unit-tests/06-02-PLAN.md index 98a2916..489bf7f 100644 --- a/.planning/phases/06-unit-tests/06-02-PLAN.md +++ b/.planning/phases/06-unit-tests/06-02-PLAN.md @@ -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 全部文件内联,不建跨文件 helper(D-02/D-03)" + - "不使用 fake timers(D-04)" + - "不测 `saveState`/`loadState`(D-32),不测属性/加成组合语义(D-35,移入 06-07)" --- -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`。 @@ -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 -- 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):执行前向用户确认;执行后暂停并用中文分条汇报。** - Task 1: MainDamageCalculator + final effect + comparer + Task 1: MainDamageCalculator 单分支 + getCriticalLimit + MainEnemyFinalEffect + MainEnemyComparer packages-user/data-state/src/enemy/calculator.test.ts, packages-user/data-state/src/enemy/final.test.ts, packages-user/data-state/src/enemy/comparer.test.ts packages-user/data-state/src/enemy/calculator.ts packages-user/data-state/src/enemy/final.ts packages-user/data-state/src/enemy/comparer.ts - packages-user/data-state/test/dataClosure.test.ts (existing calculator fixture and handler shape) - .planning/phases/06-unit-tests/06-PATTERNS.md (section A: calculator, final, comparer) + packages-user/data-state/src/enemy/types.ts + packages-user/data-state/test/dataClosure.test.ts + .planning/phases/06-unit-tests/06-PATTERNS.md(A 节 calculator/final/comparer) -`calculator.test.ts`: build one inline handler factory (enemy, context, locator, hero, state) in the `dataClosure.test.ts` style and, for each branch of `MainDamageCalculator.calculate`, assert exact `{ damage, turn }`: -- baseline: heroPerDamage = atk - monDef; turn = ceil(monHp / heroPerDamage). -- 无敌 (20) returns `{ damage: Infinity, turn: 0 }` unless `hero.items.getItemState('cross')?.count >= 1`; assert both sides by faking `state.hero.items.getItemState`. -- 魔攻 (2) uses monAtk without subtracting hero def; 连击 4/5 multiply enemyPerDamage by 2/3; 多段 (6) multiplies by `value`. -- 支援 guard (guard set non-empty, not already inGuard): recursively adds the guard's turn/damage; missing guard locator emits warn 137 via `logger.catch`; assert `inGuard` does not leak (a second top-level calculate is unaffected). -- 先攻 (1) adds one enemyPerDamage; 破甲 (7) adds value%/100 * def; 反击 (8) adds value%/100 * atk to enemyPerDamage; 净化 (9) adds value * mdef. -- 吸血 (11): value%/100 * hp added to damage, and to monHp only when `add` is true. -- 负伤 flag: `state.flags.getFieldValueDefaults('enableNegativeDamage', false)` gates clamping negative damage to 0; 固伤 (22) and 仇恨 (17, `flags.getFieldValueDefaults('hatred', 0)`) bypass mdef. -- return value `damage` is floored. -- `getCriticalLimit('atk')`: 坚固 (3) returns Infinity, otherwise `def + hp`. + `calculator.test.ts`:建立一个 inline handler 工厂(enemy/context/locator/hero/state),对 `MainDamageCalculator.calculate` 的每个分支单独断言精确 `{damage,turn}`(基本功能,各分支独立): + - 基线:heroPerDamage = atk - monDef;turn = 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/damage;guard 位置缺失时经 `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 规则处理。 pnpm exec vitest run packages-user/data-state/src/enemy/calculator.test.ts packages-user/data-state/src/enemy/final.test.ts packages-user/data-state/src/enemy/comparer.test.ts - the focused run fails, or a branch assertion does not match the documented arithmetic + 任一聚焦运行失败,或某单分支期望值与文档算术不符、或 warn 137 未被观测 - - 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 137;final.test.ts 与 comparer.test.ts 断言其文档分支。 + - 每个 `it` 前有单行中文注释。 - Calculator, final effect, and comparer behavior is covered and green. + 伤害计算、最终效果、比较器的单分支基本功能覆盖并跑绿。 - Task 2: Auras + special registration + Task 2: CommonAura/GuardAura 与 registerSpecials packages-user/data-state/src/enemy/aura.test.ts, packages-user/data-state/src/enemy/special.test.ts packages-user/data-state/src/enemy/aura.ts packages-user/data-state/src/enemy/special.ts - packages-user/data-base/src/enemy/special.ts (defineCommonSerializableSpecial / defineNonePropertySpecial) - packages-user/data-common/src/store/tileStore.test.ts (registry contract pattern) - .planning/phases/06-unit-tests/06-PATTERNS.md (section A: aura, special) + packages-user/data-base/src/enemy/special.ts + packages-user/data-common/src/store/tileStore.test.ts + .planning/phases/06-unit-tests/06-PATTERNS.md(A 节 aura/special) -`aura.test.ts`: with inline minimal `ISpecial` objects and a fake enemy/context/locator: -- `CommonAuraConverter.shouldConvert` is true only for code 25. -- `CommonAura.createRange`/`getRangeParam`: `haloRange <= 0` uses full range (undefined param); `haloSquare` uses a rect centered on the locator; otherwise a Manhattan range. -- `CommonAura.apply` adds `floor(base * hpBuff/100)`, `floor(base * atkBuff/100)`, `floor(base * defBuff/100)` to the computed enemy, and no-ops when a buff is 0; `couldApplyBase` is true, `couldApplySpecial` false, `priority` 25. -- `GuardAuraConverter.shouldConvert` is true only for code 26; `GuardAura.getRangeParam` is the 3x3 rect; `apply` adds the guard locator to `enemy.getAttribute('guard')` for a different locator and skips the source's own locator; when `context.getViewByComputed` returns null, `apply` no-ops. + `aura.test.ts`:用 inline 最小 `ISpecial` 与 fake enemy/context/locator: + - `CommonAuraConverter.shouldConvert` 仅对 code 25 为真;`CommonAura.createRange`/`getRangeParam`:`haloRange <= 0` 用 Full(param 为 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 内联。 pnpm exec vitest run packages-user/data-state/src/enemy/aura.test.ts packages-user/data-state/src/enemy/special.test.ts - the focused run fails, or a registration/buff assertion does not match the documented behavior + 运行失败,或转换器匹配/范围参数/加成数值/注册数量断言与文档行为不符 - - Both files pass with zero non-skipped failures. - - aura.test.ts asserts range selection, buff math, and guard-locator behavior. - - special.test.ts asserts codes 0-27 registered once plus guard defaults and name/description spot checks. - - Every `it` has a preceding single-line Chinese comment. + - 两个文件零非跳过失败。 + - aura.test.ts 断言范围选择、buff 数学与 guard-locator 行为(各单分支)。 + - special.test.ts 断言 code 0-27 各注册一次 + guard 默认值 + 名称/描述抽查。 + - 每个 `it` 前有单行中文注释。 - Aura conversion and the special registry are covered and green. + 光环转换与特殊属性注册的单分支基本功能覆盖并跑绿。 - Task 3: Map damage views, converter, reducer + Task 3: mapDamage 五视图 + converter + reducer(单分支) packages-user/data-state/src/enemy/mapDamage.test.ts packages-user/data-state/src/enemy/mapDamage.ts - packages-user/data-state/src/enemy/special.ts (IZoneValue) - packages-user/data-state/src/enemy/types.ts (MapDamageType) - packages-user/data-system/src/combat/mapDamage.ts (IMapDamage* contracts) - .planning/phases/06-unit-tests/06-PATTERNS.md (section A: mapDamage) + 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.md(A 节 mapDamage) -Build an inline fake `IEnemyContext` (with `indexer`, `getEnemyByLoc`, `getDamageSystem`) and inline `ISpecial` values, then assert: -- `ZoneDamageView`: range is Manhattan or rect per `zoneSquare`; `getRangeParam` matches `range`/centering; `getDamageWithoutCheck` returns `{ damage: zone, type: MapDamageType.Zone, extra: {catch, repulse} }`. -- `RepulseDamageView`: Manhattan radius 1; returns null at the source locator; otherwise type `Repulse` with `repulse` containing the source locator. -- `LaserDamageView`: range param uses the face handler's `mapMovement` directions; damage type `Layer`. -- `BetweenDamageView`: only the forward-adjacent locator (delta sum 1 and both deltas positive) qualifies; requires the neighbor to have special 16; returns half of hero final hp; when `core.flags.betweenAttackMax` is true it uses the damage system's computed damages (fall back to half when the system is missing); otherwise returns half directly. -- `AmbushDamageView`: type `Unknown`, damage 0, catch contains the locator. -- `MainMapDamageConverter.convert` pushes a view per present special (15, 16, 18, 24, 27) and uses `state.faceManager.get(FaceGroup.Dir4)!` for the laser view. -- `MainMapDamageReducer.reduce`: sums damage, picks the type of the max-damage item, and unions `catch`/`repulse`. -Guard the `core.*` global reads with the shared global stub if the import chain requires it. Keep fixtures inline; if a `core`-dependent expectation is environment-sensitive, follow the skip+findings rule rather than weakening it. + 构建 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 0,catch 含 locator。 + - `MainMapDamageConverter.convert` 对存在的 special(15/16/18/24/27)各推入一个 view,laser 用 `state.faceManager.get(FaceGroup.Dir4)!`。 + - `MainMapDamageReducer.reduce` 求和、取最大伤害项的类型、合并 `catch`/`repulse`。 + 如 import 链需要,使用共享全局 stub;fixture 内联。若某 `core` 依赖期望随环境变化,按 skip+findings 规则处理。 pnpm exec vitest run packages-user/data-state/src/enemy/mapDamage.test.ts - the focused run fails, or a view/reducer assertion does not match the documented output + 运行失败,或某视图/reducer 的分支输出与实现实际语义不符 - - `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` 前有单行中文注释。 - Map damage views, converter, and reducer behavior is covered and green. + 地图伤害视图、converter、reducer 的单分支基本功能覆盖并跑绿;完成后以 create-or-append 方式将本计划的 code→模块→用例行(code 137 → calculator.ts)写入 `06-COVERAGE-MAP.md` 的 06-02 小节:先检查表头,不存在则先创建,再只追加本小节,不重写他节。 @@ -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。 | -- 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);表头不存在则先创建;幂等、顺序无关,不重写他节。 +- **执行协议**:执行前向用户确认;执行后暂停并用中文分条汇报。 -- 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)。 @@ -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 → 用例名) -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` 小节。执行完成后暂停并用中文分条汇报。 diff --git a/.planning/phases/06-unit-tests/06-03-PLAN.md b/.planning/phases/06-unit-tests/06-03-PLAN.md index 9764b4e..1c5d7a3 100644 --- a/.planning/phases/06-unit-tests/06-03-PLAN.md +++ b/.planning/phases/06-unit-tests/06-03-PLAN.md @@ -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 码 53(setAttributeDefaults 非法默认值)、96(重复 special)、117(compareWith 多次调用)、118(无 comparer 时 updateDirty)各至少一条触发断言(D-31 / D-36)。" + - "loadState 专属码 119(prefab 缺失)、120(special 缺失)与 `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 全部文件内联,不建跨文件 helper(D-02/D-03)" + - "不使用 fake timers(D-04)" + - "不测 `saveState`/`loadState`(D-32),不测任何名称含 legacy 的接口/方法(D-30)" --- -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`。 @@ -69,76 +72,82 @@ Output: 3 new co-located `*.test.ts` files. -- 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):执行前向用户确认;执行后暂停并用中文分条汇报。** - Task 1: Enemy model + special implementations + Task 1: Enemy 模型 + special 实现 packages-user/data-base/src/enemy/enemy.test.ts, packages-user/data-base/src/enemy/special.test.ts packages-user/data-base/src/enemy/enemy.ts packages-user/data-base/src/enemy/special.ts packages-user/data-base/src/enemy/types.ts - packages-user/data-state/test/dataClosure.test.ts (create/mutate/save/load enemy shape) - .planning/phases/06-unit-tests/06-PATTERNS.md (section A: enemy, special) + packages-user/data-state/test/dataClosure.test.ts + .planning/phases/06-unit-tests/06-PATTERNS.md(A 节 enemy/special) -`enemy.test.ts`: construct `Enemy` with an inline `IEnemyAttr`-shaped object and cover: -- `getAttribute`/`setAttribute`/`addAttribute` on numeric keys; `cloneAttributes` returns a structured-cloned, non-aliased copy. -- special CRUD: `addSpecial` then `getSpecial`/`hasSpecial`/`iterateSpecials`; adding a duplicate code warns 96 and does not replace the existing special; `deleteSpecial` by object and by numeric code removes it; unknown code returns null. -- `clone`/`copyFrom`: clone copies attributes and cloned specials and is independent of the source; `copyFrom` replaces both attributes and specials. -- `saveState(compression)` / `loadState(state, compression)`: attribute round-trip; a special present on the instance but missing from the saved map warns 120 and is left unchanged; `saveState` returns a deep clone (mutating the returned attrs does not change the enemy). + `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`。 pnpm exec vitest run packages-user/data-base/src/enemy/enemy.test.ts packages-user/data-base/src/enemy/special.test.ts - the focused run fails, or the duplicate-special warn 96 / missing-special warn 120 is not observed + 聚焦运行失败,或重复 special 的 warn 96 未被观测,或 clone 独立性断言失败 - - 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` 前有单行中文注释。 - Enemy and special data models are covered and green. + Enemy 与 special 数据模型覆盖并跑绿。 - Task 2: EnemyManager registry, prefabs, reuse, dirty tracking + Task 2: EnemyManager 注册表、prefab、reuse、脏跟踪 packages-user/data-base/src/enemy/manager.test.ts packages-user/data-base/src/enemy/manager.ts packages-user/data-base/src/enemy/types.ts packages-user/data-common/src/store/tileStore.test.ts - .planning/phases/06-unit-tests/06-PATTERNS.md (section A: manager) + .planning/phases/06-unit-tests/06-PATTERNS.md(A 节 manager) -Drive `EnemyManager` with an inline fake `IEnemyLegacyBridge` (its `fromLegacyEnemy(enemy, attrs)` returns synthetic attrs) and inline `Enemy` prefabs. Assert the registry contract in the tileStore style: -- `registerSpecial`/`registerSpecial` overwrite is allowed (last registration wins for the code); `setAttributeDefaults` sets a default and rejects function/symbol/bigint/undefined with `logger.error(53)`. -- `addPrefab` clones and ignores a duplicate code or id; `createEnemy(code)` / `createEnemyById(id)` return independent clones and null for unknown; `getPrefab`/`getPrefabById` return the stored prefab; `deletePrefab` by code and by id removes both indexes; `changePrefab` replaces and marks dirty. -- `reusePrefab(source, code, id)`: `getPrefab`/`getPrefabById` and `createEnemy` resolve through the reuse mapping to the source prefab; unknown source is a no-op. -- `modifyPrefabAttribute` applies the modify callback, re-indexes on changed code/id, and marks dirty; unknown code is a no-op. -- comparator + dirty: before any `compareWith`, dirty tracking is inert; `attachEnemyComparer` + first `compareWith(reference)` clears the dirty set; a second `compareWith` warns 117 and refreshes dirty; `updateDirty` without a comparer warns 118 and marks dirty; `refreshDirty` drops entries equal to the reference. -- `saveState(compression)` serializes only dirty codes; `loadState` warns 119 for a code missing from the prefab table and refreshes dirty afterwards. -Keep inline fixtures; assert warning codes via `logger.catch`. + 用 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` 观测。 pnpm exec vitest run packages-user/data-base/src/enemy/manager.test.ts - the focused run fails, or a registry/dirty assertion or a warning code (53/117/118/119) is not observed + 聚焦运行失败,或注册表/脏跟踪断言、或 53/117/118 中任一码未被观测 - - `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` 前有单行中文注释。 - EnemyManager registry, prefab, reuse, and dirty behavior is covered and green. + EnemyManager 注册表、prefab、reuse、脏跟踪覆盖并跑绿;完成后以 create-or-append 方式将本计划的 code→模块→用例行(53/96/117/118)写入 `06-COVERAGE-MAP.md` 的 06-03 小节:先检查表头,不存在则先创建,再只追加本小节,不重写他节。 @@ -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。 | -- 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);表头不存在则先创建;幂等、顺序无关,不重写他节。 +- **执行协议**:执行前向用户确认;执行后暂停并用中文分条汇报。 -- 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 与存读档未测。 @@ -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 小节 -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` 小节。执行完成后暂停并用中文分条汇报。 diff --git a/.planning/phases/06-unit-tests/06-04-PLAN.md b/.planning/phases/06-unit-tests/06-04-PLAN.md index d2102d7..fb87b13 100644 --- a/.planning/phases/06-unit-tests/06-04-PLAN.md +++ b/.planning/phases/06-unit-tests/06-04-PLAN.md @@ -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 类型)、149(expand 倍数非法)、150(数组达上限)、151(boolean 越界)、152(bigint 越界)、153(参数超过 255)、154(uint16→uint8 命令截断)、155(读流过期)、156(读流过期需重建沙箱)、157(未知命令)、158(步骤执行失败)、159(安全收集重复开始)、160(安全收集意外结束)、161(安全检测命中)、162(安全详情缺失)、163(命令重复注册)、175(notExecuted 失败)各至少一条触发断言,经 `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 timers)(D-04 / D-37)。" + - "录像系统**完整播放**与**二次录制比对**、`error 2001–2008` 归 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 harness(cast 到 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 全部文件内联,不建跨文件 helper(D-02/D-03)" + - "异步路径只用真实计时器,禁用 `vi.useFakeTimers()`(D-04)" + - "不测 `ReplayArray.saveState`/`loadState`(D-32),不做完整录像播放/二次录制(D-40,移入 06-07)" --- -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`。 @@ -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 -- 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 timers(D-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):执行前向用户确认;执行后暂停并用中文分条汇报。** - Task 1: ReplaySystem + ReplayArray - packages-user/data-common/src/replay/system.test.ts, packages-user/data-common/src/replay/array.test.ts + Task 1: ReplayArray 全公开操作 + 编解码(boolean/整数全位宽/bigint/string/数组) + packages-user/data-common/src/replay/array.test.ts - packages-user/data-common/src/replay/system.ts packages-user/data-common/src/replay/array.ts packages-user/data-common/src/replay/types.ts - packages-user/data-state/src/replay/commands.test.ts (registration order / per-instance independence) - .planning/phases/06-unit-tests/06-PATTERNS.md (section B: array, system) + packages-user/data-state/test/nodeReplay.test.ts + .planning/phases/06-unit-tests/06-PATTERNS.md(B 节 array) -`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-37):boolean(码 151:非法值告警、合法值读回 type/value)、整数(每个可用位宽各一条,含边界值)、bigint(码 152:越界告警、边界内读回)、string、数组参数(含码 153:参数超过 255 时溢出忽略);未知 param 类型触发码 148。 + - 扩容:成倍扩容(合法倍率)后仍读回;非法倍率触发码 149;达到上限触发码 150;读流过期触发码 155。 + 数组保持极小;不测 `saveState`/`loadState`。 - pnpm exec vitest run packages-user/data-common/src/replay/system.test.ts packages-user/data-common/src/replay/array.test.ts - the focused run fails, or the duplicate-registration warn 163 / route round-trip assertion is not observed + pnpm exec vitest run packages-user/data-common/src/replay/array.test.ts + 运行失败,或 148/149/150/151/152/153/154/155 中任一码未被观测,或某类型编解码读回不一致 - - 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` 前有单行中文注释。 - ReplaySystem and ReplayArray behavior is covered and green. + ReplayArray 全操作与全类型编解码覆盖并跑绿。 - Task 2: ReplaySandbox + replay safety helpers - packages-user/data-common/src/replay/sandbox.test.ts, packages-user/data-common/src/replay/func.test.ts + Task 2: ReplaySystem 注册/record + ReplaySandbox 播放器 + packages-user/data-common/src/replay/system.test.ts, packages-user/data-common/src/replay/sandbox.test.ts + 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.md(B 节 system/sandbox) + + + `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-op;route 走完后 `ended` 置位。 + - `setSpeed` 触发 `onSpeedSet`;`getReplayed()` 计数正确。 + 用真实 promise,无 fake timers。 + + + pnpm exec vitest run packages-user/data-common/src/replay/system.test.ts packages-user/data-common/src/replay/sandbox.test.ts + 运行失败,或 156/157/158/163/175 中任一码未被观测,或播放/暂停/继续顺序断言失败 + + + - 两个文件零非跳过失败。 + - system.test.ts 断言注册、163、record+hook、沙箱创建/reset、实例独立性。 + - sandbox.test.ts 断言顺序步进、失败即停、156/157/158/175、play/pause/resume/stop、setSpeed。 + - 每个 `it` 前有单行中文注释。 + + ReplaySystem 与 ReplaySandbox 播放器覆盖并跑绿。 + + + + Task 3: 播放安全装饰器/工具(func.ts) + packages-user/data-common/src/replay/func.test.ts + packages-user/data-common/src/replay/func.ts packages-user/data-common/src/replay/types.ts - packages-user/data-state/src/replay/commands.test.ts (manual sandbox stepping) - .planning/phases/06-unit-tests/06-PATTERNS.md (section B: sandbox, func) + packages-user/data-state/src/replay/commands.test.ts + .planning/phases/06-unit-tests/06-PATTERNS.md(B 节 func) -`sandbox.test.ts`: create a `ReplaySystem` with inline commands and drive the sandbox directly (cast to a manual sandbox interface as in `commands.test.ts`): -- `step()` executes the next command, fires `onStep`, and returns true on success; a failing command warns 158 with code+params and stops (later commands do not execute). -- `finalizeLast` runs `notExecuted` when the command changes or the reader expires; failure warns 175; an unknown last command warns 157; an expired reader warns 156. -- `play`/`pause`/`resume`/`stop`: `pause()` resolves when replay pauses; `resume()` continues from the paused point; `stop()` pauses and fires `onStopReplay`; `play()` is a no-op when already playing or ended; `ended` is set after the route finishes. -- `setSpeed` fires `onSpeedSet`. - -`func.test.ts`: cover the replay-safety decorator lifecycle from `func.ts` (`beginReplaySafetyCollection`, `endReplaySafetyCollection`, `logReplaySafetyDetail`, `shouldReplay`, `ignoreReplay`) using a minimal inline `IReplaySystem` and a decorated inline class/method; assert begin → nested decorated calls → end → detail behavior and the reset lifecycle (codes 159/161). Use `vi.spyOn(console/logger)` only where the source writes output. No fake timers — the `pause()`/`resume()` assertions use real promises. + 覆盖 `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。 - pnpm exec vitest run packages-user/data-common/src/replay/sandbox.test.ts packages-user/data-common/src/replay/func.test.ts - the focused run fails, or a stop-on-failure warning (156/157/158/175) or a decorator lifecycle code (159/161) is not observed + pnpm exec vitest run packages-user/data-common/src/replay/func.test.ts + 运行失败,或 159/160/161/162 中任一码未被观测 - - 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` 前有单行中文注释。 - ReplaySandbox and replay-safety decorators are covered and green. - - - - Task 3: Replay integration through CoreState - packages-user/data-state/test/replayIntegration.test.ts - - packages-user/data-state/test/nodeReplay.test.ts - packages-user/data-state/test/dataClosure.test.ts - packages-user/data-state/test/fixtures/closed-loop.ts - packages-user/data-state/src/replay/types.ts - .planning/phases/03-data-completion/03-REPLAY-CONTRACT.md - .planning/phases/03-data-completion/03-REPLAY-DIAGNOSTICS.md - .planning/phases/06-unit-tests/06-PATTERNS.md (section B: replayIntegration) - - -Create `data-state/test/replayIntegration.test.ts` that drives `createCoreState()` plus `ReplaySystem`/`ReplaySandbox` end-to-end through a small fixed replay: -- assert the eight stable commands are registered in order and every code resolves via `state.replaySystem.getCommand`. -- record a short valid route, create a sandbox, play it to completion with real timers (`await` the sandbox end/`waitForEnded` pattern), and assert the recorded route round-trips. -- first-divergence behavior (Phase 3 D-07 / D-16): a route whose second command fails must stop, keep later commands unexecuted, and emit warn 158 carrying the command code and params; cross-check against `03-REPLAY-DIAGNOSTICS.md`. -Inline minimal fixtures; reuse `createCoreState()` (Node-safe). Do not touch `saveSystem.init`/IndexedDB. If a contract mismatch is found, write the correct-expectation test and mark it skipped with a `#06-04-N` findings entry. - - - pnpm exec vitest run packages-user/data-state/test/replayIntegration.test.ts - the focused run fails, or the first-divergence report does not include index/code/params - - - - `replayIntegration.test.ts` passes with zero non-skipped failures. - - Asserts the eight-command order, a successful short replay, and stop-on-first-divergence with warn 158. - - Every `it` has a preceding single-line Chinese comment. - - Replay integration through CoreState is covered and green. + 播放安全装饰器/工具覆盖并跑绿;完成后以 create-or-append 方式将本计划的 code→模块→用例行(148–163、175)写入 `06-COVERAGE-MAP.md` 的 06-04 小节:先检查表头,不存在则先创建,再只追加本小节,不重写他节。 @@ -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。 | -- 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 个码);表头不存在则先创建;幂等、顺序无关,不重写他节。 +- **执行协议**:执行前向用户确认;执行后暂停并用中文分条汇报。 -- 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)。 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 小节 -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` 小节。执行完成后暂停并用中文分条汇报。 diff --git a/.planning/phases/06-unit-tests/06-05-PLAN.md b/.planning/phases/06-unit-tests/06-05-PLAN.md index 2a7c732..4dd3cce 100644 --- a/.planning/phases/06-unit-tests/06-05-PLAN.md +++ b/.planning/phases/06-unit-tests/06-05-PLAN.md @@ -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)、109(modifier 返回同引用)、116(未注册 modifier 类型)、142(follower id 无法转数字)、144(未绑定 hero move top 实现)、146(未知 uid 装备)、147(无可用装备槽)各至少一条触发断言(D-31)。" + - "loadState 专属码 58/59(equipStore load)与全部 `saveState`/`loadState` 测试归 06-09,本计划不测(D-32)。" + - "勇士每个子系统可正确运行:attribute(base/final/优先级/克隆)、modifier(公式)、location(位置/楼层/朝向/钩子)、state(装配/属性视图/modifier 注册/changeFloor 钩子顺序)、equipment(装备/卸下/替换/compareEquip)、equipStore(实例增删计数排序)、items(常量/消耗/装备/拾取)、follower(增删/gather 异步/链接)、mover(异步移动码)、rendering(alpha + 钩子)。" 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 全部文件内联,不建跨文件 helper(D-02/D-03)" + - "异步路径只用真实计时器 + `await`,禁用 `vi.useFakeTimers()`(D-04)" + - "不测任何 `saveState`/`loadState`(D-32)" --- -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-38):attribute、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 文件)。 @@ -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 -- 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):执行前向用户确认;执行后暂停并用中文分条汇报。** - Task 1: HeroAttribute + modifiers - packages-user/data-base/src/hero/attribute.test.ts, packages-user/data-base/src/hero/modifier.test.ts + Task 1: HeroAttribute + modifier + HeroLocation + HeroState + 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 packages-user/data-base/src/hero/attribute.ts packages-user/data-base/src/hero/modifier.ts - packages-user/data-base/src/hero/types.ts - .planning/phases/06-unit-tests/06-PATTERNS.md (section C: attribute, modifier) - - -`attribute.test.ts`: with an inline numeric attribute object, cover: -- `getBaseAttribute` vs `getFinalAttribute`: base changes via `set`/`add`/`mul`/`div` and final reflects registered modifiers. -- `addModifier` sorts by descending `priority` and recomputes final; adding a modifier already owned by another attribute warns 108 and is ignored; `getModifiers`/`getModifierIndex`/`iterateModifiers` reflect registration; `deleteModifier`/`deleteModifierByIndex` unbind and remove. -- save-enabled gating: `addModifier(name, modifier, false)`, `setModifierSaveEnabled`, `getModifierSaveEnabled`. -- `markModifierDirty` recomputes only the owning attribute; an unbound modifier key is ignored. -- `clone({cloneModifier: false})` copies base only; default clone copies modifiers and recomputes; `getModifiableClone` is an independent clone; `toStructured` returns a detached structured clone. -- In `DEV` a modifier returning the same object reference warns 109 (assert via `logger.catch` on a modifier that returns the same object). - -`modifier.test.ts`: assert `ValueModifier.modify(value)` = `value + this.value` and `PercentageModifier.modify(value, base)` = `value + base * this.value`; default priorities (0 and 10); `type` strings; `clone` produces an equal independent modifier; `setValue` recomputes through the bound attribute and `saveState`/`loadState` round-trip the value. - - - pnpm exec vitest run packages-user/data-base/src/hero/attribute.test.ts packages-user/data-base/src/hero/modifier.test.ts - the focused run fails, or the warn 108/109 assertions are not observed - - - - Both files pass with zero non-skipped failures. - - attribute.test.ts asserts base/final computation, priority ordering, save gating, clone/structured behavior, and warn codes 108/109. - - modifier.test.ts asserts both modifier formulas and clone/save behavior. - - Every `it` has a preceding single-line Chinese comment. - - HeroAttribute and modifier behavior is covered and green. - - - - Task 2: HeroLocation model - packages-user/data-base/src/hero/location.test.ts - packages-user/data-base/src/hero/location.ts - packages-user/data-base/src/hero/mover.ts (constructor face handling) - packages-user/data-state/src/event/event.test.ts (minimal CoreState location fixture) - .planning/phases/06-unit-tests/06-PATTERNS.md (section C: location) - - -Create `location.test.ts` with an inline fake `IDataCommon` and a `Dir8FaceHandler`: -- construct `HeroLocation` from an inline `IFacedTileLocator` and assert initial `x`/`y`/`floorId` (undefined) and `getCurrentFaceDirection()` matching the locator direction. -- `setPos(x, y)` updates coordinates; `setFloor(id)` updates the floor; `getCurrentFaceDirection` reflects `mover.setFaceDir`. -- hook `onSetPos`/`onSetFloor` fire with the new values. -- `saveState()` returns `{x, y, direction, floorId}` and `loadState` restores all four, including resetting `mover.faceDirection`. -Keep the fake state minimal (tileStore/itemStore/mapStore/eventStore/faceManager/directionMapper as needed). - - - pnpm exec vitest run packages-user/data-base/src/hero/location.test.ts - the focused run fails, or a save/load round-trip assertion does not restore a field - - - - `location.test.ts` passes with zero non-skipped failures. - - Asserts getters, setters, hooks, and full save/load round-trip. - - Every `it` has a preceding single-line Chinese comment. - - HeroLocation behavior is covered and green. - - - - Task 3: HeroState aggregate and save envelope - packages-user/data-base/src/hero/state.test.ts - packages-user/data-base/src/hero/state.ts packages-user/data-base/src/hero/types.ts - packages-user/data-state/test/dataClosure.test.ts (saveable hero round-trip) - .planning/phases/06-unit-tests/06-PATTERNS.md (section C: state) + packages-user/data-state/src/event/event.test.ts + .planning/phases/06-unit-tests/06-PATTERNS.md(C 节 attribute/modifier/location/state) -Create `state.test.ts` using an inline fake `IDataCommon` and `Dir8FaceHandler` to construct `HeroState`: -- constructor wires `location`/`rendering`/`followers`/`items`/`equip` and starts at the default locator. -- `getModifiableAttribute`/`getAttribute`/`getIsolatedAttribute` return the shared attribute, a readonly view, and an independent clone respectively; `attachAttribute` swaps the attribute. -- `registerModifier` + `createModifier`/`createAndInsertModifier`: a registered type creates and inserts a modifier; an unknown type warns 116 and returns null. -- `changeFloor(info)` sets face/move direction, position, and floor, and fires before/after hooks in order. -- `saveState(compression)` then mutate key fields (attribute base value, location x/y/floor, a modifier value) then `loadState` restores them — assert only the explicit key fields (D-13), never a whole-object deep equality; a modifier with save disabled is not persisted; followers are cleared then re-added from the save. -Use `SaveCompression.NoCompression`. Keep fixtures inline. + `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`;默认 priority(0 与 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 钩子。不测存读档。 - pnpm exec vitest run packages-user/data-base/src/hero/state.test.ts - the focused run fails, or the save/load assertion uses a whole-object deep equality instead of explicit key fields + 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 + 任一聚焦运行失败,或 108/109/116 未被观测,或钩子顺序断言失败 - - `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/109;modifier 断言两条公式与 priority;location 断言 getter/setter/钩子;state 断言装配/属性视图/码 116/changeFloor 钩子顺序。 + - 每个 `it` 前有单行中文注释。 - HeroState aggregate and save envelope are covered and green. + 属性、modifier、位置、勇士状态覆盖并跑绿。 + + + + Task 2: HeroEquipment + EquipmentState/HeroEquipsStore + HeroItems + 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/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.md(C 节 equipment/equipStore/items) + + + 构造 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 store;Pick 类按 count 调 `effect.useEffect`;未知 tile/item no-op;`useItem` 缺物品或 `canUse` 为 false 返回 false、消耗类减并删、常量类保留、装备类返回 false。不测 `saveState`/`loadState`。 + + + 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 + 运行失败,或 modifier 应用/移除断言、或 146/147 未被观测 + + + - 三个文件零非跳过失败。 + - equipment 断言 equip/unequip/replace、modifier 应用、compareEquip、码 146/147;equipStore 断言实例增删计数排序;items 断言 add/use/count/路由。 + - 每个 `it` 前有单行中文注释。 + + 装备、装备实例库、物品覆盖并跑绿。 + + + + Task 3: followers(异步)+ HeroMover(异步)+ HeroRendering + 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 + + 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.md(C 节 follower/mover) + + + `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`。 + + + 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 + 运行失败,或异步 gather/mover 断言未通过 awaited 控制器完成,或 144/142 未被观测 + + + - 三个文件零非跳过失败。 + - follower 断言 add/remove/gather/链接与码 142;mover 断言 config 往返、Step/CannotMove/Hit、越界、enter/leave 顺序与码 144;rendering 断言 alpha + 钩子。 + - 异步用例用真实计时器并 await 控制器;无 fake timers。 + - 每个 `it` 前有单行中文注释。 + + followers、异步 HeroMover、HeroRendering 覆盖并跑绿;完成后以 create-or-append 方式将本计划的 code→模块→用例行(108/109/116/142/144/146/147)写入 `06-COVERAGE-MAP.md` 的 06-05 小节:先检查表头,不存在则先创建,再只追加本小节,不重写他节。 @@ -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。 | -- 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 个码);表头不存在则先创建;幂等、顺序无关,不重写他节。 +- **执行协议**:执行前向用户确认;执行后暂停并用中文分条汇报。 -- 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)均有覆盖。 +- 未测存读档。 @@ -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 小节 -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` 小节。执行完成后暂停并用中文分条汇报。 diff --git a/.planning/phases/06-unit-tests/06-06-PLAN.md b/.planning/phases/06-unit-tests/06-06-PLAN.md index d0c7cb1..4293f87 100644 --- a/.planning/phases/06-unit-tests/06-06-PLAN.md +++ b/.planning/phases/06-unit-tests/06-06-PLAN.md @@ -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 专属码 55(MapStore 缺 reference)、122(MapStore 楼层缺失)、124(MapLayer 引用缺失)与全部 `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 全部文件内联,不建跨文件 helper(D-02/D-03)" + - "异步路径只用真实计时器 + `await`,禁用 `vi.useFakeTimers()`(D-04)" + - "不测 `saveState`/`loadState`(D-32)" --- -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` 为既有文件扩展)。 @@ -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 -- 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-03);import 链需要时用 `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):执行前向用户确认;执行后暂停并用中文分条汇报。** - Task 1: HeroEquipment + HeroEquipsStore - packages-user/data-base/src/hero/equipment.test.ts, packages-user/data-base/src/hero/equipStore.test.ts + Task 1: MapState 注册/校验/激活 + GameMap 图层/别名/脏/事件层 + packages-user/data-base/src/map/mapState.test.ts, packages-user/data-base/src/map/gameMap.test.ts - packages-user/data-base/src/hero/equipment.ts - packages-user/data-base/src/hero/equipStore.ts - packages-user/data-base/src/hero/types.ts - packages-user/data-state/src/replay/commands.test.ts (equip/unequip boundary) - .planning/phases/06-unit-tests/06-PATTERNS.md (section C: equipment, equipStore) + 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.md(D 节 mapState/gameMap) -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` fixture(TileStore + 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`)。 - pnpm exec vitest run packages-user/data-base/src/hero/equipment.test.ts packages-user/data-base/src/hero/equipStore.test.ts - the focused run fails, or the modifier apply/remove assertion or warn/error code is not observed + pnpm exec vitest run packages-user/data-base/src/map/mapState.test.ts packages-user/data-base/src/map/gameMap.test.ts + 运行失败,或 60/61/62/63/64/84/121/131 中任一码未被观测 - - 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` 前有单行中文注释。 - Equipment and equip-store behavior is covered and green. + MapState 与 GameMap 覆盖并跑绿。 - Task 2: HeroItems + followers - packages-user/data-base/src/hero/items.test.ts, packages-user/data-base/src/hero/follower.test.ts + Task 2: MapLayer 矩阵、静态数组设置与动态图块转换(重点) + packages-user/data-base/src/map/mapLayer.test.ts - packages-user/data-base/src/hero/items.ts - packages-user/data-base/src/hero/follower.ts - packages-user/data-base/src/hero/types.ts - .planning/phases/06-unit-tests/06-PATTERNS.md (section C: items, follower) + 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.md(D 节 mapLayer) -`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。 - pnpm exec vitest run packages-user/data-base/src/hero/items.test.ts packages-user/data-base/src/hero/follower.test.ts - the focused run fails, or the async gather assertion does not complete through awaited movers + pnpm exec vitest run packages-user/data-base/src/map/mapLayer.test.ts + 运行失败,或 8/9/46/80/81/123/127/128/129/130/131 中任一码未被观测,或静态数组/动态转换断言失败 - - 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` 前有单行中文注释。 - Item and follower behavior is covered and green. + MapLayer 矩阵、静态数组与动态转换(重点)覆盖并跑绿。 - Task 3: HeroMover async movement - packages-user/data-base/src/hero/mover.test.ts + Task 3: tile/staticTile/dynamicTile/eventView/mover + 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 - 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.md(D 节 tile) -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`。 - pnpm exec vitest run packages-user/data-base/src/hero/mover.test.ts - the focused run fails, or a move-code/position-writeback assertion does not match the implementation contract + 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 + 运行失败,或 136/143/126 未被观测,或默认事件恢复/动态 tile 生命周期断言失败 - - `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 断言视图操作与码 136;mover 断言生命周期与码 126。 + - 异步 mover 用例用真实计时器并 await 控制器;无 fake timers。 + - 每个 `it` 前有单行中文注释。 - HeroMover async movement behavior is covered and green. + 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 小节:先检查表头,不存在则先创建,再只追加本小节,不重写他节。 @@ -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。 | -- 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);表头不存在则先创建;幂等、顺序无关,不重写他节。 +- **执行协议**:执行前向用户确认;执行后暂停并用中文分条汇报。 -- 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、静态图块、动态转换、静态数组全部断言。 +- 未测存读档。 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 个码) -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` 小节。执行完成后暂停并用中文分条汇报。 diff --git a/.planning/phases/06-unit-tests/06-07-PLAN.md b/.planning/phases/06-unit-tests/06-07-PLAN.md index 7c6fbd4..5b8450a 100644 --- a/.planning/phases/06-unit-tests/06-07-PLAN.md +++ b/.planning/phases/06-unit-tests/06-07-PLAN.md @@ -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: 小地图录像播放 + 二次录制比对 + 2001–2008 覆盖 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 全部文件内联,不建跨文件 helper(D-02/D-03)" + - "异步路径只用真实计时器,禁用 `vi.useFakeTimers()`(D-04)" + - "不测 `saveState`/`loadState` 本身;仅允许为录像重置最小使用存档数据(D-32)" --- -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 2001–2008` 归本计划。 -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/` 集成测试文件。 @@ -62,118 +61,131 @@ Output: 4 new co-located `*.test.ts` files. @.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 -- 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 timers(D-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):执行前向用户确认;执行后暂停并用中文分条汇报。** - - Task 1: MapState registry and raw validation - packages-user/data-base/src/map/mapState.test.ts + + Task 1: 执行前门禁 — 用户确认录像记录已接通 + packages-user/data-state/test/replayPlayback.test.ts(尚未创建,仅作占位说明) + 录像记录在 CoreState 操作路径中的接通,以及驱动小地图录像播放/二次录制所需的公开入口。 + + 1) 确认播放时每步会记入 `ReplaySystem.route`(录像记录已接通)。 + 2) 确认 `CoreState` 暴露构造小地图场景 + 播放 + 二次录制所需的公开入口。 + 3) 确认录像重置所需的最小状态入口可用。 + + 用户回复「录像记录已接通,可以开始」则继续 Task 2/3;否则暂停本计划等待用户改代码。 + .planning/phases/06-unit-tests/06-CONTEXT.md(D-40) + packages-user/data-state/src/core.ts(replaySystem 接线与公开入口) + packages-user/data-state/src/replay/commands.ts(录像指令与 error 2001–2008) + + + 本计划依赖用户先在具体操作中接通录像记录,否则二次录制比对无法进行(D-40)。开始本计划前,向用户展示并确认: + - 录像记录是否已在 CoreState 操作路径中接通(播放时会把每步记入 `ReplaySystem.route`); + - `CoreState` 是否暴露驱动小地图场景 + 播放 + 二次录制所需的公开入口; + - 录像重置所需的最小状态入口是否可用。 + 用户确认后再继续 Task 2/3;若未接通,暂停本计划并等待用户改代码,不得自行修改核心代码。 + + + 用户明确回复「录像记录已接通,可以开始」,或不通过则暂停本计划 + + + - 用户给出明确开始确认;否则本计划保持暂停状态。 + + 录像记录接通得到用户确认,或计划暂停等待用户改代码。 + + + + Task 2: 顶层伤害组合 + 系统层光环/效果组合 + packages-user/data-state/test/enemyCombination.test.ts + + 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.md(A 节 combatClosure) + + + 用真实 `createCoreState()` 与真实顶层实现驱动: + - 顶层多特殊属性组合:一个怪物同时带多个 special(如 坚固 + 连击 + 吸血 等)时,按真实 `MainDamageCalculator` 断言组合后的 `{damage,turn}`(组合语义,非单分支)。 + - 系统层组合:经真实 `EnemyContext` + 真实 `CommonAura`/`GuardAura` + 真实 final effect,覆盖 光环基础效果 ↔ 常规查询效果、光环特殊效果 ↔ 特殊查询效果、final-effect 阶段顺序、同/跨优先级顺序(D-25);属性→伤害联动(D-26)。 + - 支援/guard 递归与未知 guard 的 warn 137 经 `logger.catch` 观测。 + 若某组合需要的公开入口未暴露,按 D-07 暂停并向用户提问,不修改核心代码。 + + + pnpm exec vitest run packages-user/data-state/test/enemyCombination.test.ts + 运行失败,或组合后的伤害/阶段顺序断言与真实实现不符 + + + - `enemyCombination.test.ts` 零非跳过失败。 + - 断言至少一组多 special 伤害组合与一组系统层光环/效果组合(含阶段顺序)。 + - 每个 `it` 前有单行中文注释。 + + 顶层与系统层伤害/效果组合覆盖并跑绿。 + + + + Task 3: 小地图录像播放 + 二次录制逐条比对 + error 2001–2008 + packages-user/data-state/test/replayPlayback.test.ts + + packages-user/data-state/test/nodeReplay.test.ts + packages-user/data-state/test/dataClosure.test.ts + packages-user/data-state/test/fixtures/closed-loop.ts + packages-user/data-state/src/replay/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.md(B 节 replayIntegration) -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 2001–2008:分别触发参数数量不匹配(2001)、参数类型不匹配(2002)、移动前未停止(2003)、移动控制器缺失(2004)、teleport 无路径(2005)、use-item 失败(2006)、equip 失败(2007)、unequip 失败(2008),经 `logger.catch`/`logger.error` spy 观测。 + - 仅允许为录像重置最小使用存档数据,不测存读档本身(D-32)。 + 有界循环,无 fake timers。 - pnpm exec vitest run packages-user/data-base/src/map/mapState.test.ts - the focused run fails, or a fromRaw/activation/compareWith assertion does not match the documented behavior + pnpm exec vitest run packages-user/data-state/test/replayPlayback.test.ts + 运行失败,或 2001–2008 中任一码未被观测,或二次录制逐条比对失败 - - `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)、二次播放完成。 + - 断言 2001–2008 全部触发。 + - 每个 `it` 前有单行中文注释。 - MapState registry, validation, and activation behavior is covered and green. - - - - Task 2: GameMap layers, aliases, dirty, save aggregation - packages-user/data-base/src/map/gameMap.test.ts - - packages-user/data-base/src/map/gameMap.ts - packages-user/data-base/src/map/types.ts - packages-user/data-base/src/map/mapLifecycle.test.ts (save aggregation + compression loop) - .planning/phases/06-unit-tests/06-PATTERNS.md (section D: gameMap) - - -Build `GameMap` with an inline `IDataCommon` and cover: -- `addLayer`/`removeLayer`/`hasLayer` and the `onUpdateLayer` hook; `setLayerAlias`/`getLayerByAlias`/`getLayerAlias`, with a duplicate alias warning 84. -- `setBackground`/`getBackground` and the `onChangeBackground` hook; `setActiveStatus` toggles `active`. -- `setEventLayer`: rejects a layer not in `layerList` with warn 131; accepts an added layer; accepts null. -- `dirty()` is true when any layer or the map self-flag is dirty; `markDirty(true/false)`. -- `resizeLayer(w, h, keepBlock)`: calls `resize` when `keepBlock`, else `resize2`. -- `saveState(compression)`: omits empty layers via the `isEmptyLayerSave` rule and includes a layer that has only point events or only static/dynamic blocks; `loadState` restores background and layers and clears the self dirty flag. -- `compareWith(map of arrays)`: marks a layer dirty when its reference is absent and compares otherwise. - - - pnpm exec vitest run packages-user/data-base/src/map/gameMap.test.ts - the focused run fails, or the save-aggregation/empty-layer rule or warn 84/131 is not observed - - - - `gameMap.test.ts` passes with zero non-skipped failures. - - Asserts layer/alias lifecycle, background/event-layer, dirty, resize, save aggregation, and warn codes 84/131. - - Every `it` has a preceding single-line Chinese comment. - - GameMap layer/alias/dirty/save behavior is covered and green. - - - - Task 3: MapLayer matrix + tiles (static/dynamic default events) - packages-user/data-base/src/map/mapLayer.test.ts, packages-user/data-base/src/map/tile.test.ts - - packages-user/data-base/src/map/mapLayer.ts - packages-user/data-base/src/map/tile.ts - packages-user/data-base/src/map/staticTile.ts - packages-user/data-base/src/map/dynamicTile.ts - packages-user/data-base/src/map/types.ts - packages-user/data-base/src/map/mapLifecycle.test.ts - .planning/phases/06-unit-tests/06-PATTERNS.md (section D: mapLayer, tile) - - -`mapLayer.test.ts` (fill beyond `mapLifecycle.test.ts`): `setBlock`/`getBlock`/`removeBlock` and `inMap`; `getMapData`/`putMapData`/`setMapRef`/`getMapRef` matrix read/write; `createDynamic` indexes the tile and `getDynamicTilesAt`/`iterateDynamicTiles`/`updateDynamicTile` reflect it; `transferToDynamic`/`transferToStatic`/`transferToStaticIfSafe` preserve or discard events per the keep flag; `getLocationData` resolves a locator to a tile; `setZIndex` and `setFaceBinder`; `event`/`getPointEvent` point-event access; `dirty`/`markDirty`/`compareWith`; `openDoor`/`closeDoor` async completion with real timers. Reuse the inline `createMapState` fixture; keep all-compression save assertions to explicit key fields (D-13). - -`tile.test.ts`: for a static and a dynamic tile: `num`/`raw`/`set` reflect the tile store data; changing `set` restores that tile's default `events` and marks the tile event view pure; `tileEvent()` is the tile's own view (dirty false after a default-restoring change) and `pointEvent()` returns the layer's point-event view for the tile's locator (or null out of map); `setFaceDirection` maps a direction through the face binder and returns the resulting num; `saveState`/`loadState` round-trip a dynamic tile's raw num and overridden events, and a static tile round-trips through the layer. Hotspot the default-event restoration into a dedicated `it`. - - - pnpm exec vitest run packages-user/data-base/src/map/mapLayer.test.ts packages-user/data-base/src/map/tile.test.ts - the focused run fails, or the default-event restoration / dynamic-tile index assertion does not match the documented behavior - - - - Both files pass with zero non-skipped failures. - - mapLayer.test.ts asserts matrix read/write, dynamic-tile index, transfer semantics, point events, dirty/compareWith, and async doors. - - tile.test.ts asserts default-event restoration, point-event linkage, face direction, and save/load. - - Every `it` has a preceding single-line Chinese comment. - - MapLayer matrix/tiles and tile default-event behavior is covered and green. + 小地图录像播放、二次录制比对与 2001–2008 覆盖并跑绿;完成后以 create-or-append 方式将本计划的 code→模块→用例行(2001–2008)写入 `06-COVERAGE-MAP.md` 的 06-07 小节:先检查表头,不存在则先创建,再只追加本小节,不重写他节。 @@ -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。 | -- 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 小节(2001–2008);表头不存在则先创建;幂等、顺序无关,不重写他节。 +- **执行协议**:执行前必须先向用户确认录像记录已接通;执行后暂停并用中文分条汇报。 -- 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 个集成测试文件存在并通过(在用户确认录像记录接通之后)。 +- 顶层伤害组合、系统层光环/效果组合、小地图录像播放、二次录制逐条比对、2001–2008 全部断言。 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 小节 -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` 小节。执行完成后暂停并用中文分条汇报。 diff --git a/.planning/phases/06-unit-tests/06-08-PLAN.md b/.planning/phases/06-unit-tests/06-08-PLAN.md index 522d400..dce8e94 100644 --- a/.planning/phases/06-unit-tests/06-08-PLAN.md +++ b/.planning/phases/06-unit-tests/06-08-PLAN.md @@ -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 全覆盖:本计划可达的码 43(bind 未知 main)、44(bind 主朝向冲突)、111(对非数值 flag 字段 add)各至少一条触发断言(D-31)。" + - "`FlagSystem` 公开接口(`occupied`/`insertField`/`getField`/`getOrInsert`/`getOrInsertComputed`/`deleteField`/`setFieldValue`/`addFieldValue`/`getFieldValue`/`getFieldValueDefaults`)行为与文档一致;`saveState`/`loadState` 归 06-09(flag `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 全部文件内联,不建跨文件 helper(D-02/D-03)" + - "异步路径只用真实计时器 + `await controller.onEnd`,禁用 `vi.useFakeTimers()`(D-04)" + - "不测任何 `saveState`/`loadState`(D-32)" --- -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 / mover)(D-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` 为既有文件扩展)。 @@ -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 -- 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):执行前向用户确认;执行后暂停并用中文分条汇报。** - Task 1: Combat closure through CoreState - packages-user/data-state/test/combatClosure.test.ts + Task 1: FlagSystem 完整公开表面 + packages-user/data-base/src/flag/system.test.ts + packages-user/data-base/src/flag/system.ts + packages-user/data-base/src/flag/field.ts + packages-user/data-base/src/flag/types.ts packages-user/data-state/test/dataClosure.test.ts - 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.md(F 节 flag) -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()` 无压缩参数)。 - pnpm exec vitest run packages-user/data-state/test/combatClosure.test.ts - the focused run fails, or the handler identity / deterministic damage assertion does not hold + pnpm exec vitest run packages-user/data-base/src/flag/system.test.ts + 聚焦运行失败,或码 111 未被观测,或某公开方法缺少正常用例 - - `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` 前有单行中文注释。 - The combat path is proven end-to-end through CoreState and green. + FlagSystem 覆盖并跑绿。 - Task 2: Saveables same-instance round-trip (4 ids × 3 compressions) - packages-user/data-state/test/saveablesRoundTrip.test.ts + Task 2: 朝向工具 + 位置索引 + FaceManager/handlers + RoleFaceBinder + 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-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.md(G 节) -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)。 - pnpm exec vitest run packages-user/data-state/test/saveablesRoundTrip.test.ts - the focused run fails, or a round-trip assertion for any id/compression pair is missing or uses a whole-object deep equality + 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 + 运行失败,或 43/44 未被观测,或某方向/索引映射断言缺失或错误 - - `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` 前有单行中文注释。 - All registered saveables round-trip same-instance across all compressions and are green (D-11, D-12). + 朝向工具、位置索引、FaceManager/handlers、RoleFaceBinder 覆盖并跑绿。 + + + + Task 3: ObjectMover 扩展覆盖 + packages-user/data-common/src/common/mover.test.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 + .planning/phases/06-unit-tests/06-PATTERNS.md(共享样板 5) + + + 在既有 `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。 + + + pnpm exec vitest run packages-user/data-common/src/common/mover.test.ts + 运行失败,或既有用例被破坏,或某公开 mover 方法仍无正常用例 + + + - `mover.test.ts` 零非跳过失败且既有覆盖保持。 + - `ObjectMover` 全部列出公开方法至少一条正常用例;异步路径用真实计时器 + await。 + - 每个新增 `it` 前有单行中文注释。 + + ObjectMover 公开表面扩展覆盖并跑绿;完成后以 create-or-append 方式将本计划的 code→模块→用例行(43/44/111)写入 `06-COVERAGE-MAP.md` 的 06-08 小节:先检查表头,不存在则先创建,再只追加本小节,不重写他节。 @@ -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。 | -- 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);表头不存在则先创建;幂等、顺序无关,不重写他节。 +- **执行协议**:执行前向用户确认;执行后暂停并用中文分条汇报。 -- 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 的 common(utils/indexer/faceManager+face/mover)覆盖。 +- 未测存读档。 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 小节 -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` 小节。执行完成后暂停并用中文分条汇报。 diff --git a/.planning/phases/06-unit-tests/06-09-PLAN.md b/.planning/phases/06-unit-tests/06-09-PLAN.md index 22ca93a..6980a57 100644 --- a/.planning/phases/06-unit-tests/06-09-PLAN.md +++ b/.planning/phases/06-unit-tests/06-09-PLAN.md @@ -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 全覆盖:本计划可达的码 55(MapStore load 缺 reference)、58(equipment store load 找不到 maxUid)、59(equipment load 找不到 item raw data)、112(重复 saveable id)、113(bindSaveableExecuter 目标未添加)、119(loadState 缺 prefab)、120(loadState 缺 special)、122(MapStore load 楼层缺失)、124(MapLayer 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`/IndexedDB(D-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 全部文件内联,不建跨文件 helper(D-02/D-03)" + - "不使用 fake timers(D-04)" + - "不做整体深度相等;不比较派生/缓存/序列化容器结构(D-13)" --- -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`。 @@ -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 -- 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-42)CoreState 可存档内容与公开 save/load 入口已就绪;未确认前不得开始。** +- `06-COVERAGE-MAP.md` 为共享 create-or-append 文件(幂等、顺序无关):写 06-09 小节前先检查表头(`| code | 模块 | 用例 | 计划 |`)是否存在,不存在则先创建,再只追加/更新本小节,绝不重写他节。 +- **执行协议(D-33):执行前向用户确认;执行后暂停并用中文分条汇报。** - - Task 1: FlagSystem full public surface - packages-user/data-base/src/flag/system.test.ts + + Task 1: 执行前门禁 — 用户确认 CoreState 可存档内容与公开入口已调整 + packages-user/data-state/test/saveablesRoundTrip.test.ts(尚未创建,仅作占位说明) + 调整后的 CoreState 可存档内容(含录像存档计入)与新增的公开 save/load 入口。 + + 1) 确认 4 个 saveable(`@system/hero`/`flags`/`maps`/`enemy`)及其新增内容已就绪。 + 2) 确认是否提供公开 save/load 入口(有则一并验证,无则仅走 `getSaveableContent(id)` 的 saveables 层)。 + 3) 确认录像存档是否已计入。 + + 用户回复「存读档入口已就绪,可以开始」则继续 Task 2/3;否则暂停本计划等待用户改代码。 + .planning/phases/06-unit-tests/06-CONTEXT.md(D-42) + packages-user/data-state/src/core.ts(saveables 注册与 saveableContent 入口) + packages-user/data-common/src/save/types.ts(ISaveableContent/ISaveSystem 契约) + + + 本计划依赖用户先调整 CoreState 的可存档内容(例如录像存档目前未计入但应计入),并新增公开 save/load 入口(D-42)。开始前向用户展示并确认: + - 4 个 saveable(`@system/hero`/`flags`/`maps`/`enemy`)及其新增内容是否已就绪; + - 是否已提供公开 save/load 入口(若有,本计划将一并经该入口验证;若无,则仅走 `getSaveableContent(id)` 的 saveables 层); + - 录像存档是否已计入。 + 用户确认后再继续 Task 2/3;若未就绪,暂停本计划等待用户改代码,不得自行修改核心代码。 + + + 用户明确回复「存读档入口已就绪,可以开始」,或不通过则暂停本计划 + + + - 用户给出明确开始确认;否则本计划保持暂停状态。 + + CoreState 可存档内容与入口就绪得到用户确认,或计划暂停等待用户改代码。 + + + + Task 2: enemy + hero 各类存读档往返(含压缩档与码 119/120/58/59) + 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 + + 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.md(C/E 节) + + + `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 与后续维护的对照基准。 + + + pnpm exec vitest run packages-user/data-base/src/enemy/saveLoad.test.ts packages-user/data-base/src/hero/saveLoad.test.ts + 运行失败,或 119/120/58/59 中任一码未被观测,或某类缺少同实例往返断言 + + + - 两个文件零非跳过失败。 + - enemy/hero 每个含存读档的类至少一条同实例往返;断言码 119/120/58/59。 + - 只用关键字段显式断言,无整体深度相等。 + - `06-SAVE-EXCLUSIONS.md` 存在并逐类列出排除字段与依据。 + - 每个 `it` 前有单行中文注释。 + + enemy/hero 存读档往返与排除清单覆盖并跑绿。 + + + + Task 3: map + replay + flag 存读档往返(码 55/122/124)与 CoreState 顶层验证(码 112/113) + 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 + + 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.md(D/E/F 节) -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)。 - pnpm exec vitest run packages-user/data-base/src/flag/system.test.ts - the focused run fails, or the warn 111 / save-load key-field assertion is not observed + 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 + 运行失败,或 55/112/113/122/124 中任一码未被观测,或 4×3 往返矩阵缺失某组合 - - `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` 前有单行中文注释。 - FlagSystem behavior is covered and green. - - - - Task 2: Face utilities + location indexer - packages-user/data-common/src/common/utils.test.ts, packages-user/data-common/src/common/indexer.test.ts - - packages-user/data-common/src/common/utils.ts - packages-user/data-common/src/common/indexer.ts - packages-user/data-common/src/common/types.ts (FaceDirection) - packages-user/data-common/src/common/mover.test.ts (same-directory boilerplate) - .planning/phases/06-unit-tests/06-PATTERNS.md (section G) - - -`utils.test.ts`: assert `getFaceMovement` for every `FaceDirection` (including `Unknown -> {0,0}`), `degradeFace` for all diagonals and the custom `unknown` argument, `nextFaceDirection` for 4-way clockwise/counter-clockwise and 8-way clockwise/counter-clockwise plus `Unknown` passthrough, and `fromDirectionString` for every string plus an unknown string. These are pure functions; synchronous asserts. - -`indexer.test.ts`: with `MapLocIndexer` and `setWidth(w)`, assert `locToIndex(x,y)` and `locaterToIndex({x,y})` agree, `indexToLocator` is the inverse for in-range indices, and a wider width changes the row stride. Use a `vi.hoisted` stub only if the import chain requires it. - -Per D-20, keep these minimal and only where the main suites do not already exercise the helper. - - - pnpm exec vitest run packages-user/data-common/src/common/utils.test.ts packages-user/data-common/src/common/indexer.test.ts - the focused run fails, or a direction/index mapping assertion is missing or wrong - - - - Both files pass with zero non-skipped failures. - - utils.test.ts covers all four helpers across every direction/string; indexer.test.ts covers round-trip and width changes. - - Every `it` has a preceding single-line Chinese comment. - - Face utilities and the location indexer are covered and green. - - - - Task 3: FaceManager + Dir4/Dir8 handlers - packages-user/data-common/src/common/faceManager.test.ts - - packages-user/data-common/src/common/faceManager.ts - packages-user/data-common/src/common/types.ts - packages-user/data-common/src/common/mover.test.ts - .planning/phases/06-unit-tests/06-PATTERNS.md (section G) - - -Create `faceManager.test.ts`: -- `FaceManager.register`/`registerById` then `get`/`getById` return the handler; unknown group/id return null (registry shape). -- `Dir8FaceHandler`: `degrade` passthrough; `movement` for every direction and for an out-of-range input (`ZERO_DESCRIPTOR`); `move` scales and supports negative counts; `opposite` for each direction and `Unknown -> Unknown`; `next` clockwise/counter-clockwise and `Unknown` passthrough; `mapDirection`/`mapMovement` include `Unknown` with `{0,0}`. -- `Dir4FaceHandler`: `degrade` maps diagonals to their 4-way direction and unknown values to `Unknown`; `movement`/`move`/`opposite`/`next`/`mapDirection`/`mapMovement` behave for the 4-way set. -Keep fixtures inline. If a handler's documented descriptor is contradicted by the implementation, follow the skip+findings rule. - - - pnpm exec vitest run packages-user/data-common/src/common/faceManager.test.ts - the focused run fails, or a handler mapping/registry assertion is missing or wrong - - - - `faceManager.test.ts` passes with zero non-skipped failures. - - Asserts the registry and both handlers' full `IFaceHandler` surface. - - Every `it` has a preceding single-line Chinese comment. - - FaceManager and both direction handlers are covered and green. + 地图/录像/flag 存读档往返与 CoreState 顶层验证覆盖并跑绿;完成后以 create-or-append 方式将本计划的 code→模块→用例行(55/58/59/112/113/119/120/122/124)写入 `06-COVERAGE-MAP.md` 的 06-09 小节:先检查表头,不存在则先创建,再只追加本小节,不重写他节。 @@ -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。 | -- 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 可存档内容与入口就绪;执行后暂停并用中文分条汇报。 -- 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 个可达码全部触发断言。 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 小节 -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` 小节。执行完成后暂停并用中文分条汇报。