test(03-15): cover independent replay command ownership

- Verify distinct command classes, fresh instances, stable order, and extension registration
- Add structural ownership regressions without changing replay or user-owned decorator boundaries
This commit is contained in:
unanmed 2026-09-11 14:28:24 +08:00
parent 13162b2406
commit f5c6b06c59
3 changed files with 97 additions and 13 deletions

View File

@ -1,10 +1,10 @@
---
schema_version: 1
open_count: 9
open_count: 10
waived_count: 0
fixed_count: 8
total_count: 17
last_updated: 2026-09-11T06:11:49.370Z
total_count: 18
last_updated: 2026-09-11T06:27:22.412Z
---
# Broken Windows Ledger
@ -32,6 +32,7 @@ last_updated: 2026-09-11T06:11:49.370Z
| 15 | 03 | deviation | packages-user/data-common/src/replay/array.ts | | Repaired replay parameter encoding so diagnostic params remain original and deterministic. | open | | 2026-09-10T09:18:58.279Z | |
| 16 | 03 | deviation | packages-user/data-state/test/replayVerifier.ts | | Added a package-local verifier harness shared by Vitest and the Node runner to avoid composite-script import resolution. | open | | 2026-09-10T09:18:58.946Z | |
| 17 | 03 | deviation | packages-user/data-state/test/fixtures/closed-loop.ts | | Applied repository Prettier/CRLF formatting to the fixed replay fixture and runner files. | open | | 2026-09-10T09:18:59.589Z | |
| 18 | 03 | deviation | script/check-data-circular.ts | | Plan 03-15 scoped circular gate reports seven pre-existing legacy/render boundary cycles through data-state/src/legacy/move.ts; replay command class changes do not touch those imports. | open | | 2026-09-11T06:27:22.412Z | |
````json
[
@ -238,6 +239,18 @@ last_updated: 2026-09-11T06:11:49.370Z
"reason": "",
"recorded_at": "2026-09-10T09:18:59.589Z",
"resolved_at": null
},
{
"id": 18,
"kind": "deviation",
"phase": "03",
"file": "script/check-data-circular.ts",
"line": null,
"description": "Plan 03-15 scoped circular gate reports seven pre-existing legacy/render boundary cycles through data-state/src/legacy/move.ts; replay command class changes do not touch those imports.",
"status": "open",
"reason": "",
"recorded_at": "2026-09-11T06:27:22.412Z",
"resolved_at": null
}
]
````

View File

@ -1,3 +1,4 @@
# Deferred Items — Phase 03
- `pnpm check:type` remains blocked by pre-existing render/legacy diagnostics outside Plan 03-06. The Tile events contract diagnostics from the earlier baseline were resolved by Plan 03-06.
- Plan 03-15's scoped circular gate reports seven pre-existing legacy/render boundary cycles through `data-state/src/legacy/move.ts`; the independent replay command class changes do not touch those imports.

View File

@ -48,6 +48,16 @@ describe('replay commands', () => {
expect(items.map(item => item.code)).toEqual(REPLAY_COMMAND_ORDER);
expect(items).toHaveLength(8);
expect(items.map(item => item.command.execute)).toHaveLength(8);
expect(items.map(item => item.command.constructor.name)).toEqual([
'ReplayUpCommand',
'ReplayRightCommand',
'ReplayDownCommand',
'ReplayLeftCommand',
'ReplayAutoPathfindCommand',
'ReplayUseItemCommand',
'ReplayEquipCommand',
'ReplayUnequipCommand'
]);
});
// 验证顶层注册器按稳定顺序注册并拒绝重复 code
@ -72,18 +82,18 @@ describe('replay commands', () => {
it('assembles an independent top-level registry for every CoreState', () => {
const first = createCoreState();
const second = createCoreState();
const firstCommands = REPLAY_COMMAND_ORDER.map(
code => first.replaySystem.getCommand(code)!
);
const secondCommands = REPLAY_COMMAND_ORDER.map(
code => second.replaySystem.getCommand(code)!
);
expect(first.replaySystem).not.toBe(second.replaySystem);
expect(first.pathfinding).not.toBe(second.pathfinding);
expect(
REPLAY_COMMAND_ORDER.map(code =>
first.replaySystem.getCommand(code)
).length
).toBe(8);
expect(
REPLAY_COMMAND_ORDER.map(code =>
second.replaySystem.getCommand(code)
).length
).toBe(8);
expect(firstCommands).toHaveLength(8);
expect(secondCommands).toHaveLength(8);
expect(new Set(firstCommands).size).toBe(8);
expect(new Set(secondCommands).size).toBe(8);
expect(
REPLAY_COMMAND_ORDER.every(
code => first.replaySystem.getCommand(code) !== null
@ -95,6 +105,32 @@ describe('replay commands', () => {
)
).toBe(true);
expect(first.replaySystem.route).not.toBe(second.replaySystem.route);
for (let index = 0; index < firstCommands.length; index++) {
expect(firstCommands[index]).not.toBe(secondCommands[index]);
}
});
// 验证现有 command item 扩展边界可以替换单个实现而不改变注册器
it('registers an existing custom command item through the current interface', () => {
const state = createCoreState();
const replay = new ReplaySystem();
const defaultItems = createReplayCommandItems(state);
const customItem: IReplayCommandItem = {
code: ReplayCommandCode.Up,
command: {
execute: () => Promise.resolve(true)
}
};
registerReplayCommandItems(replay, [
customItem,
...defaultItems.slice(1)
]);
expect(replay.getCommand(ReplayCommandCode.Up)).toBe(
customItem.command
);
expect(replay.getCommand(ReplayCommandCode.Right)).toBe(
defaultItems[1].command
);
});
// 验证四向移动不等待 controller.onEnd 即完成 command
@ -390,4 +426,38 @@ describe('replay safety decorators', () => {
);
expect(source).not.toContain('shouldReplay');
});
// 验证八个 command 类各自拥有 execute 且不存在共享入口或跨类调用
it('keeps replay command ownership isolated in the command module', () => {
const source = readFileSync(
new URL('./commands.ts', import.meta.url),
'utf8'
);
const classes = [
'ReplayUpCommand',
'ReplayRightCommand',
'ReplayDownCommand',
'ReplayLeftCommand',
'ReplayAutoPathfindCommand',
'ReplayUseItemCommand',
'ReplayEquipCommand',
'ReplayUnequipCommand'
];
expect(source).not.toContain('ReplayCommandEntrances');
expect(source).not.toContain('createMoveCommand');
expect(source).not.toMatch(/\bentries\./);
for (const className of classes) {
const body = source.match(
new RegExp(
`class\\s+${className}\\b[\\s\\S]*?(?=\\r?\\nclass\\s|\\r?\\nfunction\\s|\\r?\\nexport function\\s|\\r?\\n/\\*\\*/)`
)
)?.[0];
expect(body).toBeDefined();
expect(body).toMatch(/execute\s*\(/);
for (const otherClass of classes) {
if (otherClass === className) continue;
expect(body).not.toContain(otherClass);
}
}
});
});