template/packages-user/data-common/src/replay/array.test.ts
unanmed 8832159f8c test(06-04): cover ReplayArray pipeline and replay safety decorators (stage 2)
- 读流/缓冲区组合:createReadStream / setReplayArray / rebuildIndexArray / 位宽切换
- 触发告警码 149 / 150 / 154 / 155
- func.ts 播放安全装饰器:begin/nested/end/detail、ignoreReplay、收集外 end
- 触发告警码 159 / 160 / 161 / 162
- insert 带参数组合按正确预期 it.skip(#06-04-4)
2026-09-14 15:15:14 +08:00

477 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 测试 ReplayArray 构件级行为:单类型参数编解码与单数组操作单次读回
import { logger } from '@motajs/common';
import { afterAll, describe, expect, it, vi } from 'vitest';
import { ReplayArray } from './array';
import { ReplayCommandWidth, ReplayParamValue } from './types';
vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
Map.prototype.getOrInsertComputed ??= function <K, V>(
this: Map<K, V>,
key: K,
callback: (key: K) => V
): V {
const existing = this.get(key);
if (existing !== undefined) return existing;
const value = callback(key);
this.set(key, value);
return value;
};
});
afterAll(() => {
vi.unstubAllGlobals();
});
interface IArrayOverrides {
initCommandLength?: number;
initParamLength?: number;
commandExpandMultiplier?: number;
paramExpandMultiplier?: number;
commandWidth?: ReplayCommandWidth;
commandMaxLength?: number;
paramMaxLength?: number;
}
// 与 system.ts 初始化保持一致的构造入口,默认给足初始容量避免无关扩容
function createArray(overrides: IArrayOverrides = {}): ReplayArray {
return new ReplayArray({
initCommandLength: 32,
initParamLength: 128,
commandExpandMultiplier: 2,
paramExpandMultiplier: 2,
commandWidth: ReplayCommandWidth.Uint8,
commandMaxLength: 64,
paramMaxLength: 512,
...overrides
});
}
// 读取第一个参数的类型 token,用于断言编解码与位宽选择一致
function firstParamToken(array: ReplayArray): number {
return new Uint8Array(array.getParamArray())[0];
}
// 整数与浮点参数用例:值 → 期望参数类型 token(int64 见 #06-04-1 跳过)
const paramCases: ReadonlyArray<readonly [ReplayParamValue, number]> = [
[-128, 1],
[127, 1],
[128, 2],
[-129, 2],
[-32768, 2],
[32767, 2],
[32768, 3],
[-32769, 3],
[-2147483648, 3],
[2147483647, 3],
[1.5, 5],
[-1.5, 5]
];
describe('ReplayArray single operations', () => {
// 验证 add 追加一条录像步并在原索引读回指令与参数
it('appends one step and reads it back at the original index', () => {
const array = createArray();
array.add(7, [1, true]);
expect(array.length).toBe(1);
expect(array.get(0)).toEqual({
command: 7,
params: [1, true],
index: 0
});
});
// 验证 insert 在指定索引插入录像步并将既有步骤整体后移
it('inserts a step at the given index and shifts later steps', () => {
const array = createArray();
array.add(1, []);
array.add(3, []);
array.insert(1, 2, [9]);
expect(array.length).toBe(3);
expect(array.get(0)).toEqual({ command: 1, params: [], index: 0 });
expect(array.get(1)).toEqual({ command: 2, params: [9], index: 1 });
expect(array.get(2)).toEqual({ command: 3, params: [], index: 2 });
});
// 验证 delete 删除首步后不产生空槽且后续步骤索引前移
it('deletes the first step without leaving a hole', () => {
const array = createArray();
array.add(1, [10]);
array.add(2, [20]);
array.add(3, [30]);
array.delete(0);
expect(array.length).toBe(2);
expect(array.get(0)).toEqual({ command: 2, params: [20], index: 0 });
expect(array.get(1)).toEqual({ command: 3, params: [30], index: 1 });
});
// 疑似 bug:delete 中间步骤后索引数组未按删除位置回退,导致后续步骤读到错误参数,详见 06-TEST-FINDINGS.md #06-04-3,修复后取消 skip
it.skip('deletes a middle step and shifts later param indexes', () => {
const array = createArray();
array.add(1, [10]);
array.add(2, [20]);
array.add(3, [30]);
array.delete(1);
expect(array.length).toBe(2);
expect(array.get(0)).toEqual({ command: 1, params: [10], index: 0 });
expect(array.get(1)).toEqual({ command: 3, params: [30], index: 1 });
});
// 验证 set 覆盖指定步骤的指令与全部参数且不影响相邻步骤
it('overwrites the command and params of a step', () => {
const array = createArray();
array.add(1, [1]);
array.add(2, [2]);
array.add(3, [30]);
array.set(1, 5, [true]);
expect(array.get(1)).toEqual({ command: 5, params: [true], index: 1 });
expect(array.get(0)).toEqual({ command: 1, params: [1], index: 0 });
expect(array.get(2)).toEqual({ command: 3, params: [30], index: 2 });
});
// 验证 get 返回的录像步携带其在录像中的索引
it('returns the step index together with command and params', () => {
const array = createArray();
array.add(1, []);
array.add(2, []);
array.add(3, []);
expect(array.get(2).index).toBe(2);
expect(array.get(2).command).toBe(3);
});
});
describe('ReplayArray param codec', () => {
// 验证 boolean 参数按 type 0 写入并读回布尔值
it('round-trips boolean parameters', () => {
const array = createArray();
array.add(0, [true, false]);
expect(array.get(0).params).toEqual([true, false]);
expect(new Uint8Array(array.getParamArray())[0]).toBe(0);
});
// 验证各整数位宽与浮点参数按类型写入并原样读回
it('round-trips every integer width and float', () => {
for (const [value, token] of paramCases) {
const array = createArray();
array.add(0, [value]);
expect(array.get(0).params).toEqual([value]);
expect(firstParamToken(array)).toBe(token);
}
});
// 验证单字节 bigint 参数写入后读回一致
it('round-trips a single byte bigint', () => {
const array = createArray();
array.add(0, [100n]);
expect(array.get(0).params).toEqual([100n]);
expect(firstParamToken(array)).toBe(6);
});
// 疑似 bug:bigint 编码循环缺少按字节右移,多字节 bigint 只能还原最低字节,详见 06-TEST-FINDINGS.md #06-04-2,修复后取消 skip
it.skip('round-trips a multi-byte bigint', () => {
const array = createArray();
const value = 0x0102030405060708n;
array.add(0, [value]);
expect(array.get(0).params).toEqual([value]);
});
// 疑似 bug:int64 解码乘数误用 2147483647,导致 int64 参数无法按写入值读回,详见 06-TEST-FINDINGS.md #06-04-1,修复后取消 skip
it.skip('round-trips int64 values above the int32 range', () => {
const array = createArray();
array.add(0, [2147483648]);
expect(array.get(0).params).toEqual([2147483648]);
});
// 验证短字符串参数使用内联类型 token 并读回一致
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(9);
});
// 验证超过内联长度的字符串参数使用 type 7 并读回一致
it('round-trips a long string through the length-prefixed type', () => {
const array = createArray();
const value = 'a'.repeat(300);
array.add(0, [value]);
expect(array.get(0).params).toEqual([value]);
expect(firstParamToken(array)).toBe(7);
});
// 验证空字符串参数回退到 type 7 并读回为空串
it('round-trips an empty string', () => {
const array = createArray();
array.add(0, ['']);
expect(array.get(0).params).toEqual(['']);
expect(firstParamToken(array)).toBe(7);
});
// 验证单条录像步的多个不同类型参数按顺序完整读回
it('round-trips a boolean, integer, string and bigint in one step', () => {
const array = createArray();
array.add(9, [true, 42, 'x', 100n]);
expect(array.get(0).params).toEqual([true, 42, 'x', 100n]);
});
// 验证未知参数类型触发告警码 148 且写入流程不中断
it('warns code 148 for an unknown param type', () => {
const array = createArray();
const { info } = logger.catch(() => array.add(1, [undefined!]));
expect(info.map(v => v.code)).toContain(148);
expect(array.length).toBe(1);
});
// 验证 boolean 原始字节非法时触发告警码 151 并回退为 false
it('warns code 151 for an out-of-range boolean byte', () => {
const array = createArray();
const commands = new ArrayBuffer(2);
new DataView(commands).setUint8(0, 1);
new DataView(commands).setUint8(1, 5);
const params = new ArrayBuffer(2);
new DataView(params).setUint8(0, 0);
new DataView(params).setUint8(1, 2);
const { ret, info } = logger.catch(() => {
array.setReplayArray(ReplayCommandWidth.Uint8, commands, params, 1);
return array.get(0);
});
expect(info.map(v => v.code)).toContain(151);
expect(ret.params).toEqual([false]);
});
// 验证超出范围的 bigint 触发告警码 152
it('warns code 152 for an out-of-range bigint', () => {
const array = createArray();
const { info } = logger.catch(() => array.add(0, [2n ** 2048n]));
expect(info.map(v => v.code)).toContain(152);
});
// 验证单条命令参数超过 255 个时触发告警码 153 并忽略溢出参数
it('warns code 153 when a command exceeds 255 params', () => {
const array = createArray({
initParamLength: 1024,
paramMaxLength: 4096
});
const params = new Array<number>(256).fill(0);
const { info } = logger.catch(() => array.add(0, params));
expect(info.map(v => v.code)).toContain(153);
});
});
describe('ReplayArray stream and buffer combination', () => {
// 验证 createReadStream 从起始索引顺序读回多步并在末尾返回 null
it('reads a sequence of steps through a read stream', () => {
const array = createArray();
array.add(1, [10]);
array.add(2, [20]);
array.add(3, [30]);
const stream = array.createReadStream(0);
expect(stream.index).toBe(0);
expect(stream.length).toBe(3);
expect(stream.read()).toEqual({ command: 1, params: [10], index: 1 });
expect(stream.read()).toEqual({ command: 2, params: [20], index: 2 });
expect(stream.read()).toEqual({ command: 3, params: [30], index: 3 });
expect(stream.read()).toBeNull();
});
// 验证 createReadStream 可从指定起始索引开始读取
it('starts a read stream at the given index', () => {
const array = createArray();
array.add(1, []);
array.add(2, []);
array.add(3, []);
const stream = array.createReadStream(2);
expect(stream.read()).toEqual({ command: 3, params: [], index: 3 });
expect(stream.read()).toBeNull();
});
// 疑似 bug:insert 的参数缓冲区位移方向相反,已有参数时后续步骤读到错误参数,详见 06-TEST-FINDINGS.md #06-04-4,修复后取消 skip
it.skip('reads the new order after inserting a step', () => {
const array = createArray();
array.add(1, [10]);
array.add(3, [30]);
array.insert(1, 2, [20]);
const stream = array.createReadStream(0);
expect(stream.read()).toEqual({ command: 1, params: [10], index: 1 });
expect(stream.read()).toEqual({ command: 2, params: [20], index: 2 });
expect(stream.read()).toEqual({ command: 3, params: [30], index: 3 });
});
// 验证 getCommandArray 与 getParamArray 暴露内部缓冲区的直接内容
it('exposes the underlying command and param buffers', () => {
const array = createArray();
array.add(3, [1]);
const commands = new Uint8Array(array.getCommandArray());
const params = new Uint8Array(array.getParamArray());
expect(array.getCommandArray()).toBeInstanceOf(ArrayBuffer);
expect(commands[0]).toBe(1);
expect(commands[1]).toBe(3);
expect(params[0]).toBe(1);
expect(params[1]).toBe(1);
});
// 验证 setReplayArray 装载原始缓冲区并重建索引后可读回步骤
it('loads raw buffers through setReplayArray and reads them back', () => {
const source = createArray();
source.add(1, [10]);
source.add(2, [20]);
const loaded = createArray();
loaded.setReplayArray(
ReplayCommandWidth.Uint8,
source.getCommandArray().slice(0, 4),
source.getParamArray().slice(0, 4),
2
);
expect(loaded.length).toBe(2);
expect(loaded.get(0)).toEqual({ command: 1, params: [10], index: 0 });
expect(loaded.get(1)).toEqual({ command: 2, params: [20], index: 1 });
});
// 验证 rebuildIndexArray 幂等,重复重建后读回结果不变
it('rebuilds the index array idempotently', () => {
const array = createArray();
array.add(5, ['a', 7]);
array.add(6, []);
const before = array.get(1);
array.rebuildIndexArray();
expect(array.get(1)).toEqual(before);
expect(array.get(0)).toEqual({
command: 5,
params: ['a', 7],
index: 0
});
});
// 验证 setCommandWidth 加宽到 uint16 后指令与参数仍能读回
it('re-encodes commands when widening to uint16', () => {
const array = createArray();
array.add(1, [10]);
array.add(200, ['x']);
array.setCommandWidth(ReplayCommandWidth.Uint16);
expect(array.commandWidth).toBe(ReplayCommandWidth.Uint16);
expect(array.get(0)).toEqual({ command: 1, params: [10], index: 0 });
expect(array.get(1)).toEqual({ command: 200, params: ['x'], index: 1 });
});
// 验证 setCommandWidth 可从 uint16 缩回 uint8 且不影响合法指令
it('narrows commands back to uint8', () => {
const array = createArray({
commandWidth: ReplayCommandWidth.Uint16
});
array.add(200, [7]);
array.setCommandWidth(ReplayCommandWidth.Uint8);
expect(array.commandWidth).toBe(ReplayCommandWidth.Uint8);
expect(array.get(0)).toEqual({ command: 200, params: [7], index: 0 });
});
});
describe('ReplayArray expand and width warnings', () => {
// 验证扩容乘数小于 1 时触发告警码 149 并回退为默认倍率
it('warns code 149 for an illegal expand multiplier', () => {
const { info } = logger.catch(() =>
createArray({
commandExpandMultiplier: 0,
paramExpandMultiplier: 0
})
);
expect(info.map(v => v.code)).toEqual([149, 149]);
});
// 验证指令数组达到上限后触发告警码 150
it('warns code 150 when the command array is full', () => {
const array = createArray({
initCommandLength: 10,
commandMaxLength: 10
});
const { info } = logger.catch(() => {
array.add(1, []);
array.add(2, []);
array.add(3, []);
});
expect(info.map(v => v.code)).toContain(150);
});
// 验证从 uint16 缩回 uint8 时超出 255 的指令触发告警码 154
it('warns code 154 when narrowing a command above 255', () => {
const array = createArray({
commandWidth: ReplayCommandWidth.Uint16
});
array.add(300, []);
const { info } = logger.catch(() =>
array.setCommandWidth(ReplayCommandWidth.Uint8)
);
expect(info.map(v => v.code)).toContain(154);
expect(array.commandWidth).toBe(ReplayCommandWidth.Uint8);
});
// 验证录像被修改后读取流过期并触发告警码 155
it('warns code 155 when reading an expired stream', () => {
const array = createArray();
array.add(1, []);
const stream = array.createReadStream(0);
array.add(2, []);
const { info } = logger.catch(() => stream.read());
expect(stream.expired).toBe(true);
expect(info.map(v => v.code)).toContain(155);
});
// 验证初始容量不足时自动扩容且扩容后仍能顺序读回每一步
it('expands buffers and still reads every step back', () => {
const array = createArray({
initCommandLength: 2,
initParamLength: 8
});
for (let i = 0; i < 15; i++) {
array.add(i, [i]);
}
expect(array.length).toBe(15);
const stream = array.createReadStream(0);
for (let i = 0; i < 15; i++) {
expect(stream.read()).toEqual({
command: i,
params: [i],
index: i + 1
});
}
expect(stream.read()).toBeNull();
});
});