test(06-13): assert all CoreState key states and varied 10-step replay (G-06-09-A/B)

- seedState/mutateState/assertRestored now cover every key state: hero base attrs + modifier final atk + location, flags score/coins/stage, map two blocks + active status, enemy hp/atk, replay steps
- Replay seeded with 10 @system/replay steps mixing Up/Right/Down/Left/Teleport/UseItem/Equip/Unequip and varied param types, asserted per step
- New container test loops flags and replay through CoreState across the three compression tiers
- Append 06-13 create-or-append section to 06-COVERAGE-MAP.md; existing #06-09-1..5 skips unchanged
This commit is contained in:
unanmed 2026-09-15 11:27:57 +08:00
parent 8a7cda95ae
commit 992ca9d585
2 changed files with 161 additions and 12 deletions

View File

@ -315,3 +315,24 @@ D-40真实英雄操作经 `replay.route.add(...)`mover 方向步)录制
本计划**无新增码**,复用既有可达码;两条为受阻塞的正确预期 `it.skip`(锚定既有
`#06-04-1`/`#06-04-2`/`#06-03-1`,不新建 finding 条目G-06-04-B 为可跑绿;
不削弱也不删除任何既有用例或跳过,`06-TEST-FINDINGS.md` 无新增 `#06-12-N` 缺陷条目。
## 06-13 存档缺口补测D-46
本计划为**覆盖缺口补测**G-06-09-A、G-06-09-B**不引入新码**,复用 06-09 既有可达码
55/58/59/112/113/119/120/122/124/177/178 的既有覆盖保持不变);阶段结构为
构件hero 单档类三档化 + 经容器覆盖无参类)→ 组合map tile 三档化)→
完整/集成CoreState 顶层全关键状态 + 录像 ≥10 步多样化 + flags/replay 容器三档)。
| 缺口 | 用例 | 文件 | 计划 |
| --- | --- | --- | --- |
| G-06-09-B | `restores a percentage modifier on the same instance in NoCompression`、`restores constant and consumable item tables across all compressions`、`restores attributes, modifiers and location across all compressions`、`restores location and rendering through the container across all compressions` | `data-base/src/hero/saveLoad.test.ts` | 06-13 |
| G-06-09-B | `restores covered events across all compressions``StaticTile` 与 `DynamicTile` 各一条同名用例) | `data-base/src/map/saveLoad.test.ts` | 06-13 |
| G-06-09-A | `round-trips every registered saveable across all compressions`(每类 saveable 全部关键状态逐一严格断言 + 录像 10 步多样化逐条 exact | `data-state/test/saveablesRoundTrip.test.ts` | 06-13 |
| G-06-09-B | `restores flags and replay through the container across all compressions` | `data-state/test/saveablesRoundTrip.test.ts` | 06-13 |
受既有缺陷阻塞的档位按 D-05 以正确预期 `it.skip` 登记,复用既有锚点、不新建 finding
`restores a percentage modifier on the same instance in LowCompression`
`restores a percentage modifier on the same instance in HighCompression`(锚定 `#06-09-1`)、
`restores the equipped mapping through the container across all compressions`(锚定 `#06-09-2`
既有 `#06-09-1..5` 的 skip 全部保持原样。
本计划**无新增码**`06-TEST-FINDINGS.md` 无新增 `#06-13-N` 缺陷条目。

View File

@ -4,10 +4,12 @@ import { logger } from '@motajs/common';
import {
type IEnemyAttr,
type IHeroAttr,
FaceDirection,
SaveCompression
} from '@user/data-common';
import {
Enemy,
ValueModifier,
type IGameMap,
type IHeroStateSave,
type IMapLayer,
@ -75,10 +77,57 @@ interface SeededState {
layer: IMapLayer;
}
/** 为 5 个 saveable 写入已知关键状态 */
interface ExpectedReplayStep {
command: number;
params: readonly (number | string | boolean)[];
}
/** 顶层录像写入的 10 步多样化命令序列,命令与参数类型均不全相同 */
const REPLAY_STEPS: readonly ExpectedReplayStep[] = [
{ command: 0, params: [] },
{ command: 1, params: [] },
{ command: 2, params: [] },
{ command: 3, params: [] },
{ command: 4, params: [2, 3] },
{ command: 5, params: [12] },
{ command: 6, params: [3, 0, true] },
{ command: 7, params: [0] },
{ command: 1, params: [] },
{ command: 5, params: ['potion'] }
];
/** 向顶层录像写入 10 步混合命令,覆盖移动/瞬移/用道具/装备/卸下 */
function seedReplay(state: CoreState): void {
state.replaySystem.record(0);
state.replaySystem.record(1);
state.replaySystem.record(2);
state.replaySystem.record(3);
state.replaySystem.record(4, 2, 3);
state.replaySystem.record(5, 12);
state.replaySystem.record(6, 3, 0, true);
state.replaySystem.record(7, 0);
state.replaySystem.record(1);
state.replaySystem.record(5, 'potion');
}
/** 为 5 个 saveable 写入已知的全部关键状态 */
function seedState(state: CoreState): SeededState {
state.hero.getModifiableAttribute().set('hp', 88);
state.hero.registerModifier('@system/value', () => new ValueModifier(5));
state.hero.createAndInsertModifier('@system/value', 'atk');
const attribute = state.hero.getModifiableAttribute();
attribute.set('hp', 88);
attribute.set('atk', 10);
attribute.set('def', 6);
attribute.set('money', 20);
attribute.set('exp', 30);
state.hero.location.setPos(3, 4);
state.hero.location.setFloor('F1');
state.hero.location.mover.setFaceDir(FaceDirection.Up);
state.flags.setFieldValue('score', 7);
state.flags.setFieldValue('coins', 12);
state.flags.addFieldValue('stage', 2);
const map = state.maps.createMap('F1', 2, 2);
const layer = map.addLayer();
layer.setZIndex(0);
@ -87,39 +136,84 @@ function seedState(state: CoreState): SeededState {
new Map([['F1', new Map([[0, new Uint32Array(4)]])]])
);
layer.setBlock(5, 0, 0);
layer.setBlock(7, 1, 1);
state.enemyManager.addPrefab(createEnemy());
state.enemyManager.compareWith(new Map([[1, createEnemy()]]));
state.enemyManager.modifyPrefabAttribute(1, prefab => {
prefab.setAttribute('hp', 30);
prefab.setAttribute('atk', 9);
return prefab;
});
state.replaySystem.record(2, 7);
seedReplay(state);
return { map, layer };
}
/** 保存后修改每个 saveable 的关键状态,用于验证读档恢复 */
/** 保存后修改每个 saveable 的全部关键状态,用于验证读档恢复 */
function mutateState(state: CoreState, seeded: SeededState): void {
state.hero.getModifiableAttribute().set('hp', 1);
const attribute = state.hero.getModifiableAttribute();
attribute.set('hp', 1);
attribute.set('atk', 1);
attribute.set('def', 0);
attribute.set('money', 0);
attribute.set('exp', 0);
state.hero.location.setPos(9, 9);
state.hero.location.setFloor('F9');
state.hero.location.mover.setFaceDir(FaceDirection.Down);
state.flags.setFieldValue('score', 0);
state.flags.setFieldValue('coins', 0);
state.flags.setFieldValue('stage', 0);
seeded.layer.setBlock(0, 0, 0);
seeded.layer.setBlock(0, 1, 1);
seeded.map.setActiveStatus(false);
state.enemyManager.modifyPrefabAttribute(1, prefab => {
prefab.setAttribute('hp', 99);
prefab.setAttribute('atk', 1);
return prefab;
});
state.replaySystem.record(9, 1);
}
/** 逐 saveable 断言关键字段回到存档点 */
/** 逐 saveable 严格断言全部关键字段回到存档点 */
function assertRestored(state: CoreState, seeded: SeededState): void {
expect(state.hero.getModifiableAttribute().getBaseAttribute('hp')).toBe(88);
const attribute = state.hero.getModifiableAttribute();
expect(attribute.getBaseAttribute('hp')).toBe(88);
expect(attribute.getBaseAttribute('atk')).toBe(10);
expect(attribute.getBaseAttribute('def')).toBe(6);
expect(attribute.getBaseAttribute('money')).toBe(20);
expect(attribute.getBaseAttribute('exp')).toBe(30);
expect(attribute.getFinalAttribute('atk')).toBe(15);
expect(state.hero.getLocation()).toEqual({
x: 3,
y: 4,
direction: FaceDirection.Up
});
expect(state.hero.location.floorId).toBe('F1');
expect(state.flags.getFieldValue<number>('score')).toBe(7);
expect(state.flags.getFieldValue<number>('coins')).toBe(12);
expect(state.flags.getFieldValue<number>('stage')).toBe(2);
expect(state.maps.isMapActive('F1')).toBe(true);
expect(seeded.layer.getBlock(0, 0)).toBe(5);
expect(seeded.layer.getBlock(1, 1)).toBe(7);
expect(state.enemyManager.getPrefab(1)!.getAttribute('hp')).toBe(30);
expect(state.replaySystem.route.length).toBe(1);
expect(state.replaySystem.route.get(0)).toEqual({
command: 2,
params: [7],
index: 0
expect(state.enemyManager.getPrefab(1)!.getAttribute('atk')).toBe(9);
const route = state.replaySystem.route;
expect(route.length).toBe(REPLAY_STEPS.length);
REPLAY_STEPS.forEach((step, index) => {
expect(route.get(index)).toEqual({
command: step.command,
params: step.params,
index
});
});
}
@ -254,3 +348,37 @@ describe('CoreState save and load guards', () => {
expect(result.info.map(info => info.code)).toContain(178);
});
});
describe('CoreState container coverage for compression-less saveables', () => {
// 验证不接受压缩参数的 flags 与 replay 经容器三档往返均恢复到存档点
it('restores flags and replay through the container across all compressions', () => {
for (const compression of COMPRESSIONS) {
const state = createCoreState();
state.flags.setFieldValue('score', 5);
state.flags.addFieldValue('stage', 3);
state.replaySystem.record(1);
state.replaySystem.record(2, 7);
const snapshot = state.saveState(compression);
state.flags.setFieldValue('score', 0);
state.flags.setFieldValue('stage', 0);
state.replaySystem.record(9, 1);
state.loadState(snapshot, compression);
expect(state.flags.getFieldValue<number>('score')).toBe(5);
expect(state.flags.getFieldValue<number>('stage')).toBe(3);
expect(state.replaySystem.route.length).toBe(2);
expect(state.replaySystem.route.get(0)).toEqual({
command: 1,
params: [],
index: 0
});
expect(state.replaySystem.route.get(1)).toEqual({
command: 2,
params: [7],
index: 1
});
}
});
});