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)
This commit is contained in:
unanmed 2026-09-14 15:15:14 +08:00
parent dfe14b12ec
commit 8832159f8c
2 changed files with 339 additions and 0 deletions

View File

@ -277,3 +277,200 @@ describe('ReplayArray param codec', () => {
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();
});
});

View File

@ -0,0 +1,142 @@
// 测试 replay 播放安全收集装饰器的组合生命周期与其告警码
import { logger } from '@motajs/common';
import { afterAll, afterEach, describe, expect, it, vi } from 'vitest';
import {
beginReplaySafetyCollection,
endReplaySafetyCollection,
ignoreReplay,
logReplaySafetyDetail,
shouldReplay
} from './func';
import { ReplaySystem } from './system';
vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
});
afterEach(() => {
vi.restoreAllMocks();
});
afterAll(() => {
vi.unstubAllGlobals();
});
// 构造 shouldReplay/ignoreReplay 需要的类方法上下文
function methodContext<This, Return>(
name: string
): ClassMethodDecoratorContext<This, (this: This) => Return> {
return { name } as ClassMethodDecoratorContext<
This,
(this: This) => Return
>;
}
describe('replay safety collection', () => {
// 验证 begin 后嵌套 decorated 调用被收集且 end 触发告警码 161 并输出简化详情
it('collects nested decorated calls and warns code 161', () => {
interface Fixture {
inner(): void;
outer(): void;
}
const inner = shouldReplay('inner')(function (
this: Fixture
): void {}, methodContext<Fixture, void>('inner'));
const outer = shouldReplay('outer')(function (this: Fixture): void {
this.inner();
}, methodContext<Fixture, void>('outer'));
const fixture: Fixture = { inner, outer };
const system = new ReplaySystem();
const group = vi.spyOn(console, 'group').mockImplementation(() => {});
const output = vi.spyOn(console, 'log').mockImplementation(() => {});
const { info } = logger.catch(() => {
beginReplaySafetyCollection(system);
fixture.outer();
fixture.inner();
endReplaySafetyCollection();
});
const detail = info.find(v => v.code === 161);
expect(detail).toBeDefined();
expect(detail!.message).toContain('outer: outer');
expect(detail!.message).toContain('inner: inner');
const match = detail!.message.match(/logReplaySafetyDetail\((\d+)\)/);
expect(match).not.toBeNull();
const detailed = logger.catch(() =>
logReplaySafetyDetail(Number(match![1]))
);
expect(detailed.info.map(v => v.code)).not.toContain(162);
expect(group).toHaveBeenCalled();
expect(output).toHaveBeenCalled();
});
// 验证已有收集进行时再次 begin 触发告警码 159
it('warns code 159 when beginning during another collection', () => {
const system = new ReplaySystem();
const { info } = logger.catch(() => {
beginReplaySafetyCollection(system);
beginReplaySafetyCollection(system);
endReplaySafetyCollection();
});
expect(info.map(v => v.code)).toContain(159);
});
// 验证收集外调用 end 触发告警码 160
it('warns code 160 when ending outside a collection', () => {
const { info } = logger.catch(() => endReplaySafetyCollection());
expect(info.map(v => v.code)).toContain(160);
});
// 验证查询不存在的详情 code 触发告警码 162
it('warns code 162 when the detail code is unknown', () => {
const { info } = logger.catch(() => logReplaySafetyDetail(987654));
expect(info.map(v => v.code)).toContain(162);
});
// 验证 ignoreReplay 跳过收集输出并在结束后完全重置生命周期
it('skips the collection output when ignoreReplay ran', () => {
interface Fixture {
ignored(): void;
}
const ignored = ignoreReplay('async')(function (
this: Fixture
): void {}, methodContext<Fixture, void>('ignored'));
const fixture: Fixture = { ignored };
const system = new ReplaySystem();
const first = logger.catch(() => {
beginReplaySafetyCollection(system);
fixture.ignored();
endReplaySafetyCollection();
});
expect(first.info.map(v => v.code)).not.toContain(161);
const second = logger.catch(() => {
beginReplaySafetyCollection(system);
endReplaySafetyCollection();
});
expect(second.info.map(v => v.code)).not.toContain(159);
});
// 验证收集未开始时 shouldReplay 直接透传原方法的返回值
it('passes through decorated calls when no collection is active', () => {
interface Fixture {
compute(): number;
}
const compute = shouldReplay('compute')(function (
this: Fixture
): number {
return 42;
}, methodContext<Fixture, number>('compute'));
const fixture: Fixture = { compute };
expect(fixture.compute()).toBe(42);
});
});