test(08-02): align data-common tests to shipped replay/face interfaces

Remove addHook().load() chains; align ReplaySandbox command stubs to ReplayCommandType.Active + ReplayCommandResult; retarget failure assertions to error 72; fix short-string param token base to 32; expect FaceDirection.Unknown for unknown blocks; onRecordCommand index 0; retire utils.test.ts. Confirmed production fix: restore first-step pre-read in ReplaySandbox play()/step() (75a39ad omission). Focused: 147/147 green.
This commit is contained in:
unanmed 2026-10-04 18:32:21 +08:00
parent b053c509c7
commit 70d530d71c
6 changed files with 93 additions and 285 deletions

View File

@ -78,7 +78,7 @@ describe('RoleFaceBinder malloc and bind', () => {
expect(result.info.map(info => info.code)).toContain(43);
expect(binder.getMainFace(2)).toBeNull();
expect(binder.getFaceDirection(2)).toBeUndefined();
expect(binder.getFaceDirection(2)).toBe(FaceDirection.Unknown);
});
// 验证绑定与主朝向相同朝向时经 logger 观测错误码 44 且不建立绑定
@ -92,7 +92,7 @@ describe('RoleFaceBinder malloc and bind', () => {
expect(result.info.map(info => info.code)).toContain(44);
expect(binder.getMainFace(2)).toBeNull();
expect(binder.getFaceDirection(2)).toBeUndefined();
expect(binder.getFaceDirection(2)).toBe(FaceDirection.Unknown);
});
});
@ -106,11 +106,11 @@ describe('RoleFaceBinder queries', () => {
expect(binder.getFaceOf(99, FaceDirection.Down)).toBeNull();
});
// 验证 getFaceDirection 对未注册图块返回 undefined
it('returns undefined direction for an unknown block', () => {
// 验证 getFaceDirection 对未注册图块返回 FaceDirection.Unknown
it('returns Unknown direction for an unknown block', () => {
const binder = createBinder();
expect(binder.getFaceDirection(42)).toBeUndefined();
expect(binder.getFaceDirection(42)).toBe(FaceDirection.Unknown);
});
// 验证 getMainFace 对未注册图块返回 null

View File

@ -1,203 +0,0 @@
// 测试 L0 common 朝向工具纯函数:坐标偏移、降级、旋转与字符串解析
import { describe, expect, it } from 'vitest';
import { FaceDirection } from '../types';
import {
degradeFace,
fromDirectionString,
getFaceMovement,
nextFaceDirection
} from '../utils';
/** 全部朝向枚举值,用于校验方向表的完整覆盖 */
const ALL_DIRECTIONS: readonly FaceDirection[] = [
FaceDirection.Unknown,
FaceDirection.Left,
FaceDirection.Up,
FaceDirection.Right,
FaceDirection.Down,
FaceDirection.LeftUp,
FaceDirection.RightUp,
FaceDirection.LeftDown,
FaceDirection.RightDown
];
/** 每个朝向对应的单步坐标偏移量 */
const FACE_MOVEMENTS: readonly [FaceDirection, Loc][] = [
[FaceDirection.Unknown, { x: 0, y: 0 }],
[FaceDirection.Left, { x: -1, y: 0 }],
[FaceDirection.Up, { x: 0, y: -1 }],
[FaceDirection.Right, { x: 1, y: 0 }],
[FaceDirection.Down, { x: 0, y: 1 }],
[FaceDirection.LeftUp, { x: -1, y: -1 }],
[FaceDirection.RightUp, { x: 1, y: -1 }],
[FaceDirection.LeftDown, { x: -1, y: 1 }],
[FaceDirection.RightDown, { x: 1, y: 1 }]
];
describe('getFaceMovement', () => {
// 验证全部朝向(含未知)都返回唯一且正确的坐标偏移量
it('returns the coordinate offset for every face direction', () => {
for (const [dir, movement] of FACE_MOVEMENTS) {
expect(getFaceMovement(dir)).toEqual(movement);
}
expect(FACE_MOVEMENTS.map(item => item[0]).sort()).toEqual(
[...ALL_DIRECTIONS].sort()
);
});
});
describe('degradeFace', () => {
// 验证对角朝向降级为对应的左/右,正交朝向原样返回
it('degrades diagonals into their orthogonal direction', () => {
expect(degradeFace(FaceDirection.LeftUp)).toBe(FaceDirection.Left);
expect(degradeFace(FaceDirection.LeftDown)).toBe(FaceDirection.Left);
expect(degradeFace(FaceDirection.RightUp)).toBe(FaceDirection.Right);
expect(degradeFace(FaceDirection.RightDown)).toBe(FaceDirection.Right);
expect(degradeFace(FaceDirection.Left)).toBe(FaceDirection.Left);
expect(degradeFace(FaceDirection.Right)).toBe(FaceDirection.Right);
expect(degradeFace(FaceDirection.Up)).toBe(FaceDirection.Up);
expect(degradeFace(FaceDirection.Down)).toBe(FaceDirection.Down);
});
// 验证未知朝向默认返回未知,且可经 unknown 参数自定义返回值
it('maps unknown to the configurable fallback', () => {
expect(degradeFace(FaceDirection.Unknown)).toBe(FaceDirection.Unknown);
expect(degradeFace(FaceDirection.Unknown, FaceDirection.Down)).toBe(
FaceDirection.Down
);
});
});
describe('nextFaceDirection', () => {
// 验证四方向顺时针旋转顺序为上→右→下→左
it('rotates four directions clockwise', () => {
expect(nextFaceDirection(FaceDirection.Up)).toBe(FaceDirection.Right);
expect(nextFaceDirection(FaceDirection.Right)).toBe(FaceDirection.Down);
expect(nextFaceDirection(FaceDirection.Down)).toBe(FaceDirection.Left);
expect(nextFaceDirection(FaceDirection.Left)).toBe(FaceDirection.Up);
});
// 验证四方向逆时针旋转顺序为上→左→下→右
it('rotates four directions anticlockwise', () => {
expect(nextFaceDirection(FaceDirection.Up, true)).toBe(
FaceDirection.Left
);
expect(nextFaceDirection(FaceDirection.Left, true)).toBe(
FaceDirection.Down
);
expect(nextFaceDirection(FaceDirection.Down, true)).toBe(
FaceDirection.Right
);
expect(nextFaceDirection(FaceDirection.Right, true)).toBe(
FaceDirection.Up
);
});
// 验证四方向模式下对角朝向同样按九十度旋转
it('rotates diagonal directions in four-way mode', () => {
expect(nextFaceDirection(FaceDirection.LeftUp)).toBe(
FaceDirection.RightUp
);
expect(nextFaceDirection(FaceDirection.RightUp)).toBe(
FaceDirection.RightDown
);
expect(nextFaceDirection(FaceDirection.RightDown)).toBe(
FaceDirection.LeftDown
);
expect(nextFaceDirection(FaceDirection.LeftDown)).toBe(
FaceDirection.LeftUp
);
});
// 验证八方向顺时针按四十五度依次推进一整圈
it('rotates eight directions clockwise', () => {
expect(nextFaceDirection(FaceDirection.Up, false, true)).toBe(
FaceDirection.RightUp
);
expect(nextFaceDirection(FaceDirection.RightUp, false, true)).toBe(
FaceDirection.Right
);
expect(nextFaceDirection(FaceDirection.Right, false, true)).toBe(
FaceDirection.RightDown
);
expect(nextFaceDirection(FaceDirection.RightDown, false, true)).toBe(
FaceDirection.Down
);
expect(nextFaceDirection(FaceDirection.Down, false, true)).toBe(
FaceDirection.LeftDown
);
expect(nextFaceDirection(FaceDirection.LeftDown, false, true)).toBe(
FaceDirection.Left
);
expect(nextFaceDirection(FaceDirection.Left, false, true)).toBe(
FaceDirection.LeftUp
);
expect(nextFaceDirection(FaceDirection.LeftUp, false, true)).toBe(
FaceDirection.Up
);
});
// 验证八方向逆时针按四十五度依次回退一整圈
it('rotates eight directions anticlockwise', () => {
expect(nextFaceDirection(FaceDirection.Up, true, true)).toBe(
FaceDirection.LeftUp
);
expect(nextFaceDirection(FaceDirection.LeftUp, true, true)).toBe(
FaceDirection.Left
);
expect(nextFaceDirection(FaceDirection.Left, true, true)).toBe(
FaceDirection.LeftDown
);
expect(nextFaceDirection(FaceDirection.LeftDown, true, true)).toBe(
FaceDirection.Down
);
expect(nextFaceDirection(FaceDirection.Down, true, true)).toBe(
FaceDirection.RightDown
);
expect(nextFaceDirection(FaceDirection.RightDown, true, true)).toBe(
FaceDirection.Right
);
expect(nextFaceDirection(FaceDirection.Right, true, true)).toBe(
FaceDirection.RightUp
);
expect(nextFaceDirection(FaceDirection.RightUp, true, true)).toBe(
FaceDirection.Up
);
});
// 验证未知朝向在所有旋转模式下都原样透传
it('passes unknown through every rotation mode', () => {
expect(nextFaceDirection(FaceDirection.Unknown)).toBe(
FaceDirection.Unknown
);
expect(nextFaceDirection(FaceDirection.Unknown, true)).toBe(
FaceDirection.Unknown
);
expect(nextFaceDirection(FaceDirection.Unknown, false, true)).toBe(
FaceDirection.Unknown
);
expect(nextFaceDirection(FaceDirection.Unknown, true, true)).toBe(
FaceDirection.Unknown
);
});
});
describe('fromDirectionString', () => {
// 验证每个合法朝向字符串都解析为对应枚举值
it('parses every direction string', () => {
expect(fromDirectionString('left')).toBe(FaceDirection.Left);
expect(fromDirectionString('right')).toBe(FaceDirection.Right);
expect(fromDirectionString('up')).toBe(FaceDirection.Up);
expect(fromDirectionString('down')).toBe(FaceDirection.Down);
expect(fromDirectionString('leftup')).toBe(FaceDirection.LeftUp);
expect(fromDirectionString('rightup')).toBe(FaceDirection.RightUp);
expect(fromDirectionString('leftdown')).toBe(FaceDirection.LeftDown);
expect(fromDirectionString('rightdown')).toBe(FaceDirection.RightDown);
});
// 验证无法识别的字符串回退为未知朝向
it('falls back to unknown for an unrecognized string', () => {
const invalid = 'sideways' as Dir2;
expect(fromDirectionString(invalid)).toBe(FaceDirection.Unknown);
});
});

View File

@ -572,13 +572,13 @@ describe('ReplayArray param codec', () => {
]);
});
// 验证短字符串参数使用内联类型 token(长度 + 9)并读回一致
// 验证短字符串参数使用内联类型 token(长度 + 32)并读回一致
it('round-trips a short string with an inline type token', () => {
const array = createArray();
array.add(0, ['hi']);
expect(array.get(0).params).toEqual(['hi']);
expect(firstParamToken(array)).toBe(11);
expect(firstParamToken(array)).toBe(34);
});
// 验证超过内联长度的字符串参数使用 type 9 并读回一致
@ -591,13 +591,13 @@ describe('ReplayArray param codec', () => {
expect(firstParamToken(array)).toBe(9);
});
// 验证空字符串参数回退到 type 9 并读回为空串
// 验证空字符串参数按内联类型码 32(长度 0)并读回为空串
it('round-trips an empty string', () => {
const array = createArray();
array.add(0, ['']);
expect(array.get(0).params).toEqual(['']);
expect(firstParamToken(array)).toBe(9);
expect(firstParamToken(array)).toBe(32);
});
// 验证单条录像步的多个不同类型参数按顺序完整读回
@ -791,7 +791,7 @@ describe('ReplayArray param codec', () => {
});
describe('ReplayArray param type table', () => {
// 参数类型码表只增不改:锚定 float=6、非负 bigint=7、负 bigint=8、短字符串基址 9、负 int64=5
// 参数类型码表只增不改:锚定 float=6、非负 bigint=7、负 bigint=8、短字符串基址 32、负 int64=5
it('keeps the param type code mapping stable', () => {
const floatArray = createArray();
floatArray.add(0, [1.5]);
@ -811,7 +811,7 @@ describe('ReplayArray param type table', () => {
const shortStringArray = createArray();
shortStringArray.add(0, ['hi']);
expect(firstParamToken(shortStringArray)).toBe(11);
expect(firstParamToken(shortStringArray)).toBe(34);
const longStringArray = createArray();
longStringArray.add(0, ['a'.repeat(300)]);

View File

@ -2,7 +2,13 @@
import { logger } from '@motajs/common';
import { afterAll, afterEach, describe, expect, it, vi } from 'vitest';
import { ReplaySystem } from '../system';
import { IReplayCommand, IReplaySandbox, IReplayStepHandler } from '../types';
import {
IReplayCommand,
IReplaySandbox,
IReplayStepHandler,
ReplayCommandResult,
ReplayCommandType
} from '../types';
vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
@ -22,12 +28,14 @@ interface IManualReplaySandbox extends IReplaySandbox {
pausing: boolean;
}
// 构造一个可注入执行与收尾行为的录像命令
// 构造一个可注入执行与收尾行为的主动录像命令
function createCommand(
execute: (step: IReplayStepHandler) => Promise<boolean>,
notExecuted?: () => Promise<boolean>
execute: (step: IReplayStepHandler) => Promise<ReplayCommandResult>,
finalize?: () => Promise<ReplayCommandResult>
): IReplayCommand {
return notExecuted ? { execute, finalize: notExecuted } : { execute };
return finalize
? { type: ReplayCommandType.Active, execute, finalize }
: { type: ReplayCommandType.Active, execute };
}
// 用录像系统构造一个可手动驱动的录像沙箱
@ -53,7 +61,7 @@ async function waitForEnded(sandbox: IReplaySandbox): Promise<void> {
// 生成一个可外部兑现的异步结果
function createDeferred() {
return Promise.withResolvers<boolean>();
return Promise.withResolvers<ReplayCommandResult>();
}
describe('ReplaySandbox stepping', () => {
@ -65,19 +73,17 @@ describe('ReplaySandbox stepping', () => {
1,
createCommand(async step => {
executed.push(step.index);
return true;
return ReplayCommandResult.Success;
})
);
system.record(1, 5);
const sandbox = createSandbox(system);
const stepped: IReplayStepHandler[] = [];
sandbox
.addHook({
onStep: async step => {
stepped.push(step);
}
})
.load();
sandbox.addHook({
onStep: async step => {
stepped.push(step);
}
});
start(sandbox);
await expect(sandbox.step()).resolves.toBe(true);
@ -88,23 +94,23 @@ describe('ReplaySandbox stepping', () => {
expect(sandbox.getReplayed()).toBe(1);
});
// 验证执行失败的命令触发告警码 158 并停止后续步骤
it('warns code 158 and stops when a command returns false', async () => {
const warn = vi.spyOn(logger, 'warn').mockImplementation(() => {});
// 验证执行失败的命令触发错误码 72 并停止后续步骤
it('errors code 72 and stops when a command returns false', async () => {
const error = vi.spyOn(logger, 'error').mockImplementation(() => {});
const executed: number[] = [];
const system = new ReplaySystem();
system.registerCommand(
1,
createCommand(async () => {
executed.push(1);
return false;
return ReplayCommandResult.Failed;
})
);
system.registerCommand(
2,
createCommand(async () => {
executed.push(2);
return true;
return ReplayCommandResult.Success;
})
);
system.record(1);
@ -115,8 +121,8 @@ describe('ReplaySandbox stepping', () => {
await expect(sandbox.step()).resolves.toBe(false);
expect(executed).toEqual([1]);
expect(warn).toHaveBeenCalledWith(158, '1', '[]');
expect(sandbox.getReplayed()).toBe(1);
expect(error).toHaveBeenCalledWith(72, 'execute');
expect(sandbox.getReplayed()).toBe(2);
});
// 验证未知指令触发告警码 157 并返回 false
@ -141,11 +147,11 @@ describe('ReplaySandbox stepping', () => {
createCommand(
async () => {
order.push('exec1');
return true;
return ReplayCommandResult.Success;
},
async () => {
order.push('fin1');
return true;
return ReplayCommandResult.Success;
}
)
);
@ -153,7 +159,7 @@ describe('ReplaySandbox stepping', () => {
2,
createCommand(async () => {
order.push('exec2');
return true;
return ReplayCommandResult.Success;
})
);
system.record(1);
@ -167,20 +173,20 @@ describe('ReplaySandbox stepping', () => {
expect(order).toEqual(['exec1', 'fin1', 'exec2']);
});
// 验证 notExecuted 收尾失败触发告警码 175 并停止
it('warns code 175 when notExecuted fails', async () => {
const warn = vi.spyOn(logger, 'warn').mockImplementation(() => {});
// 验证收尾失败触发错误码 72 并停止
it('errors code 72 when finalize fails', async () => {
const error = vi.spyOn(logger, 'error').mockImplementation(() => {});
const system = new ReplaySystem();
system.registerCommand(
1,
createCommand(
async () => true,
async () => false
async () => ReplayCommandResult.Success,
async () => ReplayCommandResult.Failed
)
);
system.registerCommand(
2,
createCommand(async () => true)
createCommand(async () => ReplayCommandResult.Success)
);
system.record(1);
system.record(2);
@ -190,7 +196,7 @@ describe('ReplaySandbox stepping', () => {
await expect(sandbox.step()).resolves.toBe(true);
await expect(sandbox.step()).resolves.toBe(false);
expect(warn).toHaveBeenCalledWith(175, '1');
expect(error).toHaveBeenCalledWith(72, 'finalize');
});
// 验证读取流过期时触发告警码 156 并返回 false
@ -199,7 +205,7 @@ describe('ReplaySandbox stepping', () => {
const system = new ReplaySystem();
system.registerCommand(
1,
createCommand(async () => true)
createCommand(async () => ReplayCommandResult.Success)
);
system.record(1);
const sandbox = createSandbox(system);
@ -219,7 +225,7 @@ describe('ReplaySandbox stepping', () => {
1,
createCommand(async step => {
executed.push(step.index);
return true;
return ReplayCommandResult.Success;
})
);
system.record(1);
@ -247,14 +253,14 @@ describe('ReplaySandbox playback control', () => {
1,
createCommand(async step => {
order.push(step.index);
return true;
return ReplayCommandResult.Success;
})
);
system.record(1);
system.record(1);
const sandbox = createSandbox(system);
const startHook = vi.fn();
sandbox.addHook({ onStartReplay: startHook }).load();
sandbox.addHook({ onStartReplay: startHook });
sandbox.play();
await waitForEnded(sandbox);
@ -276,11 +282,11 @@ describe('ReplaySandbox playback control', () => {
system.record(1);
const sandbox = createSandbox(system);
const startHook = vi.fn();
sandbox.addHook({ onStartReplay: startHook }).load();
sandbox.addHook({ onStartReplay: startHook });
sandbox.play();
sandbox.play();
deferred.resolve(true);
deferred.resolve(ReplayCommandResult.Success);
await waitForEnded(sandbox);
expect(startHook).toHaveBeenCalledTimes(1);
@ -291,12 +297,12 @@ describe('ReplaySandbox playback control', () => {
const system = new ReplaySystem();
system.registerCommand(
1,
createCommand(async () => true)
createCommand(async () => ReplayCommandResult.Success)
);
system.record(1);
const sandbox = createSandbox(system);
const startHook = vi.fn();
sandbox.addHook({ onStartReplay: startHook }).load();
sandbox.addHook({ onStartReplay: startHook });
sandbox.play();
await waitForEnded(sandbox);
@ -315,7 +321,9 @@ describe('ReplaySandbox playback control', () => {
1,
createCommand(step => {
order.push(step.index);
return step.index === 1 ? first.promise : Promise.resolve(true);
return step.index === 1
? first.promise
: Promise.resolve(ReplayCommandResult.Success);
})
);
system.record(1);
@ -323,16 +331,14 @@ describe('ReplaySandbox playback control', () => {
const sandbox = createSandbox(system);
const pauseHook = vi.fn();
const resumeHook = vi.fn();
sandbox
.addHook({
onPauseReplay: pauseHook,
onResumeReplay: resumeHook
})
.load();
sandbox.addHook({
onPauseReplay: pauseHook,
onResumeReplay: resumeHook
});
sandbox.play();
const paused = sandbox.pause();
first.resolve(true);
first.resolve(ReplayCommandResult.Success);
await paused;
expect(pauseHook).toHaveBeenCalledTimes(1);
@ -359,16 +365,14 @@ describe('ReplaySandbox playback control', () => {
const sandbox = createSandbox(system);
const pauseHook = vi.fn();
const stopHook = vi.fn();
sandbox
.addHook({
onPauseReplay: pauseHook,
onStopReplay: stopHook
})
.load();
sandbox.addHook({
onPauseReplay: pauseHook,
onStopReplay: stopHook
});
sandbox.play();
const stopped = sandbox.stop();
first.resolve(true);
first.resolve(ReplayCommandResult.Success);
await stopped;
expect(pauseHook).toHaveBeenCalledTimes(1);
@ -382,7 +386,7 @@ describe('ReplaySandbox playback control', () => {
const system = new ReplaySystem();
system.registerCommand(
1,
createCommand(async () => true)
createCommand(async () => ReplayCommandResult.Success)
);
system.record(1);
const sandbox = createSandbox(system);
@ -400,7 +404,7 @@ describe('ReplaySandbox playback control', () => {
const system = new ReplaySystem();
const sandbox = createSandbox(system);
const speedHook = vi.fn();
sandbox.addHook({ onSpeedSet: speedHook }).load();
sandbox.addHook({ onSpeedSet: speedHook });
sandbox.setSpeed(2);

View File

@ -75,13 +75,11 @@ describe('ReplaySystem registration and recording', () => {
const recorded: Array<
readonly [number, number, readonly ReplayParamValue[]]
> = [];
system
.addHook({
onRecordCommand: (code, index, params) => {
recorded.push([code, index, params]);
}
})
.load();
system.addHook({
onRecordCommand: (code, index, params) => {
recorded.push([code, index, params]);
}
});
system.record(5, 1, true);
@ -91,7 +89,7 @@ describe('ReplaySystem registration and recording', () => {
params: [1, true],
index: 0
});
expect(recorded).toEqual([[5, 1, [1, true]]]);
expect(recorded).toEqual([[5, 0, [1, true]]]);
});
// 验证每个 ReplaySystem 实例的注册与录像相互独立
@ -113,13 +111,11 @@ describe('ReplaySystem sandbox lifecycle', () => {
const system = new ReplaySystem();
const { calls, reseter } = createReseter();
const created: IReplaySandbox[] = [];
system
.addHook({
onCreateSandbox: sandbox => {
created.push(sandbox);
}
})
.load();
system.addHook({
onCreateSandbox: sandbox => {
created.push(sandbox);
}
});
const save = new Map<string, unknown>([['hp', 10]]);
const sandbox = system.createReplaySandbox({

View File

@ -264,6 +264,9 @@ export class ReplaySandbox
if (this.playing || this.ended) return;
this.pausing = false;
this.playing = true;
if (!this.appendingStep && !this.reader.expired) {
this.appendingStep = this.reader.read();
}
this.forEachHook(hook => hook.onStartReplay?.());
this.startReplayLoop();
}
@ -292,6 +295,14 @@ export class ReplaySandbox
}
async step(): Promise<boolean> {
if (
this.playing &&
!this.appendingStep &&
!this.reader.expired &&
!this.ended
) {
this.appendingStep = this.reader.read();
}
const res = await this.replayStep();
if (res.success) {
await Promise.all(