template/packages-user/data-state/test/nodeReplay.test.ts
unanmed 704e23fa2d fix(03-04): share verifier harness with Node replay tests
- Keep Vitest imports inside the data-state test package
- Leave the dedicated script as the direct Node process entry
2026-09-10 17:17:40 +08:00

293 lines
8.8 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { IReplayCommand, ReplayParamValue } from '@user/data-common';
import { ReplaySystem } from '../../data-common/src/replay/system';
import {
IReplayVerifierRuntime,
IReplayVerifierSnapshot,
verifyReplay
} from './replayVerifier';
enum TestCommandOutcome {
Success,
False,
Throw
}
interface ITestStep {
readonly code: number;
readonly params: readonly ReplayParamValue[];
}
interface ITestHarnessOptions {
readonly steps: readonly ITestStep[];
readonly commands: ReadonlyMap<number, IReplayCommand>;
readonly actual?: IReplayVerifierSnapshot;
}
interface ITestHarness {
readonly runtime: IReplayVerifierRuntime;
readonly calls: number[];
readonly finished: () => boolean;
}
interface ITestLayerSnapshot {
readonly zIndex: number;
readonly matrix: Uint32Array;
}
interface ITestFloorSnapshot {
readonly floorId: string;
readonly layers: readonly ITestLayerSnapshot[];
}
interface ITestExpectedSnapshot {
readonly hero: {
name: string;
hp: number;
hpmax: number;
atk: number;
def: number;
mdef: number;
mana: number;
manamax: number;
money: number;
exp: number;
};
readonly maps: readonly ITestFloorSnapshot[];
}
interface IVerifierDiagnostic {
readonly index: number;
readonly code: number;
readonly params: readonly ReplayParamValue[];
readonly reason: string;
readonly message: string;
}
function createSnapshot(): ITestExpectedSnapshot {
const layer: ITestLayerSnapshot = {
zIndex: 20,
matrix: new Uint32Array([1, 2])
};
const floor: ITestFloorSnapshot = {
floorId: 'F1',
layers: [layer]
};
return {
hero: {
name: '',
hp: 1,
hpmax: 0,
atk: 0,
def: 0,
mdef: 0,
mana: 0,
manamax: 0,
money: 0,
exp: 0
},
maps: [floor]
};
}
function createCommand(
outcome: TestCommandOutcome,
calls: number[],
message: string = 'test command failure'
): IReplayCommand {
return {
execute: async step => {
calls.push(step.index);
if (outcome === TestCommandOutcome.Throw) {
throw new Error(message);
}
return outcome === TestCommandOutcome.Success;
}
};
}
function createHarness(options: ITestHarnessOptions): ITestHarness {
const replay = new ReplaySystem();
for (const step of options.steps) {
replay.record(step.code, ...step.params);
}
for (const [code, command] of options.commands) {
replay.registerCommand(code, command);
}
const calls: number[] = [];
const sandbox = { ended: false };
let cursor = 0;
const runtime: IReplayVerifierRuntime = {
route: replay.route,
expected: createSnapshot(),
sandbox,
getCommand: code => replay.getCommand(code),
step: async () => {
const step = replay.route.get(cursor++);
const command = replay.getCommand(step.command);
if (!command) return false;
return command.execute(step);
},
finish: async () => {
sandbox.ended = true;
},
snapshot: () => options.actual ?? createSnapshot()
};
return {
runtime,
calls,
finished: () => sandbox.ended
};
}
async function expectDiagnostic(
runtime: IReplayVerifierRuntime
): Promise<IVerifierDiagnostic> {
let failure: unknown = null;
try {
await verifyReplay(runtime);
} catch (error) {
failure = error;
}
expect(failure).not.toBeNull();
expect(failure).toMatchObject({
index: expect.any(Number),
code: expect.any(Number),
params: expect.any(Array),
reason: expect.any(String)
});
return failure as IVerifierDiagnostic;
}
describe('Node replay verifier', () => {
// 验证固定成功路径在正常结束后比较完整勇士属性与所有地图矩阵
it('accepts a successful replay after end-only snapshot comparison', async () => {
const calls: number[] = [];
const command = createCommand(TestCommandOutcome.Success, calls);
const harness = createHarness({
steps: [{ code: 0, params: [] }],
commands: new Map([[0, command]])
});
await verifyReplay(harness.runtime);
expect(calls).toEqual([0]);
expect(harness.finished()).toBe(true);
});
// 验证未知命令在首个索引立即抛出并使用确定性安全参数展示
it('throws the first unknown-command diagnostic without executing a step', async () => {
const harness = createHarness({
steps: [{ code: 99, params: [1, 'safe', true, 2n] }],
commands: new Map()
});
const failure = await expectDiagnostic(harness.runtime);
expect(failure.index).toBe(0);
expect(failure.code).toBe(99);
expect(failure.params).toEqual([1, 'safe', true, 2n]);
expect(failure.reason).toContain('unknown replay command code 99');
expect(failure.message).toContain('params=[1, "safe", true, 2n]');
expect(harness.finished()).toBe(false);
});
// 验证 false 结果在首个索引停止且不会执行后续录像指令
it('throws on a false command result and stops before later commands', async () => {
const calls: number[] = [];
const first = createCommand(TestCommandOutcome.False, calls);
const second = createCommand(TestCommandOutcome.Success, calls);
const harness = createHarness({
steps: [
{ code: 5, params: [7] },
{ code: 6, params: [] }
],
commands: new Map([
[5, first],
[6, second]
])
});
const failure = await expectDiagnostic(harness.runtime);
expect(failure.index).toBe(0);
expect(failure.code).toBe(5);
expect(failure.params).toEqual([7]);
expect(failure.reason).toBe('command returned false');
expect(calls).toEqual([0]);
expect(harness.finished()).toBe(false);
});
// 验证 command throw 在首个索引停止并保留原始参数与可读原因
it('throws on a command exception and does not execute later commands', async () => {
const calls: number[] = [];
const first = createCommand(TestCommandOutcome.Throw, calls, 'boom');
const second = createCommand(TestCommandOutcome.Success, calls);
const harness = createHarness({
steps: [
{ code: 6, params: ['first'] },
{ code: 7, params: ['later'] }
],
commands: new Map([
[6, first],
[7, second]
])
});
const failure = await expectDiagnostic(harness.runtime);
expect(failure.index).toBe(0);
expect(failure.code).toBe(6);
expect(failure.params).toEqual(['first']);
expect(failure.reason).toBe('command threw: boom');
expect(calls).toEqual([0]);
expect(harness.finished()).toBe(false);
});
// 验证勇士完整属性差异只在录像正常结束后报告
it('reports a hero snapshot mismatch only after normal end', async () => {
const calls: number[] = [];
const actual = createSnapshot();
actual.hero.hp = 2;
const harness = createHarness({
steps: [{ code: 0, params: [] }],
commands: new Map([
[0, createCommand(TestCommandOutcome.Success, calls)]
]),
actual
});
const failure = await expectDiagnostic(harness.runtime);
expect(failure.index).toBe(0);
expect(failure.code).toBe(0);
expect(failure.reason).toContain('hero attribute hp differs');
expect(calls).toEqual([0]);
expect(harness.finished()).toBe(true);
});
// 验证所有地图图层矩阵差异只在录像正常结束后报告
it('reports a map matrix mismatch only after normal end', async () => {
const calls: number[] = [];
const actual = createSnapshot();
actual.maps[0].layers[0].matrix[1] = 9;
const harness = createHarness({
steps: [{ code: 0, params: [] }],
commands: new Map([
[0, createCommand(TestCommandOutcome.Success, calls)]
]),
actual
});
const failure = await expectDiagnostic(harness.runtime);
expect(failure.index).toBe(0);
expect(failure.code).toBe(0);
expect(failure.reason).toContain(
'map floor F1 layer 20 index 1 differs'
);
expect(calls).toEqual([0]);
expect(harness.finished()).toBe(true);
});
});