fix(07-10): CR-01 ReplayArray.set 索引回退起点与末步终点对齐

- set() 改用 getParamRange(index) 取 [start, end),末步终点取 paramUsed,不再直读未初始化的 indexArray[index + 1]
- 索引回退循环起点由参数字节偏移 paramStart + 1 改为命令索引 index + 1,与 delete() 同口径
- types.ts 的 IReplayArray.set jsDoc 写明允许参数编码长度变化及其后步骤索引平移
- array.test.ts 新增 6 条 CR-01 回归:中间步增长/缩短、末步增长/缩短、读流遍历(IN-03 场景)
This commit is contained in:
unanmed 2026-09-17 12:42:48 +08:00
parent b299d348a7
commit 9ae28c68cc
3 changed files with 120 additions and 5 deletions

View File

@ -245,6 +245,118 @@ describe('ReplayArray single operations', () => {
});
});
describe('ReplayArray set index maintenance', () => {
// 验证中间步 set 增长参数编码长度后其后步骤的索引同步平移,逐步读回命令与参数均正确(CR-01)
it('keeps later steps readable after a middle set grows the param length', () => {
const array = createArray();
array.add(1, [1]);
array.add(2, [2]);
array.add(3, [30]);
array.set(1, 5, [true, true, true]);
expect(array.length).toBe(3);
expect(array.get(0)).toEqual({ command: 1, params: [1], index: 0 });
expect(array.get(1)).toEqual({
command: 5,
params: [true, true, true],
index: 1
});
expect(array.get(2)).toEqual({ command: 3, params: [30], index: 2 });
});
// 验证中间步 set 缩短参数编码长度后后续参数字节前移,且末位残留字节被清零
it('keeps later steps readable after a middle set shrinks the param length', () => {
const array = createArray();
array.add(1, [1]);
array.add(2, [2]);
array.add(3, [30]);
array.set(1, 5, []);
expect(array.length).toBe(3);
expect(array.get(0)).toEqual({ command: 1, params: [1], index: 0 });
expect(array.get(1)).toEqual({ command: 5, params: [], index: 1 });
expect(array.get(2)).toEqual({ command: 3, params: [30], index: 2 });
});
// 验证末步 set 增长参数时以 paramUsed 作为参数终点,不产生位移,且之后仍能正确追加
it('sets the last step with more params and appends afterwards', () => {
const array = createArray();
array.add(1, [1]);
array.add(2, [2]);
array.set(1, 5, [true, true, true]);
expect(array.length).toBe(2);
expect(array.get(0)).toEqual({ command: 1, params: [1], index: 0 });
expect(array.get(1)).toEqual({
command: 5,
params: [true, true, true],
index: 1
});
array.add(3, [30]);
expect(array.get(2)).toEqual({ command: 3, params: [30], index: 2 });
});
// 验证末步 set 缩短参数后 paramUsed 相应减少,后续追加的参数落在正确偏移上
it('sets the last step with fewer params and appends afterwards', () => {
const array = createArray();
array.add(1, [1]);
array.add(2, [300, 400, 500]);
array.set(1, 5, [7]);
expect(array.get(0)).toEqual({ command: 1, params: [1], index: 0 });
expect(array.get(1)).toEqual({ command: 5, params: [7], index: 1 });
array.add(3, [30]);
expect(array.get(2)).toEqual({ command: 3, params: [30], index: 2 });
});
// 验证中间步 set 增长参数后,读取流逐条读回与按索引读回结果一致
it('reads every step through a read stream after a growing set', () => {
const array = createArray();
array.add(1, [10]);
array.add(2, [20]);
array.add(3, [30]);
array.set(1, 5, [true, 'ab', 300]);
const expected: ReadonlyArray<readonly [number, ReplayParamValue[]]> = [
[1, [10]],
[5, [true, 'ab', 300]],
[3, [30]]
];
const stream = array.createReadStream(0);
expected.forEach(([command, params], i) => {
expectStepTyped(stream.read()!, command, params, i + 1);
const step = array.get(i);
expect(step.command).toBe(command);
params.forEach((param, j) =>
expectParamTyped(step.params[j], param)
);
});
expect(stream.read()).toBeNull();
expect(stream.index).toBe(3);
});
// 验证末步 set 改变参数长度后,读取流仍能读完两步并在末尾返回 null
it('reads every step through a read stream after setting the last step', () => {
const array = createArray();
array.add(1, [10]);
array.add(2, [20]);
array.set(1, 5, [true, true, true, true, true]);
const stream = array.createReadStream(0);
expectStepTyped(stream.read()!, 1, [10], 1);
expectStepTyped(stream.read()!, 5, [true, true, true, true, true], 2);
expect(stream.read()).toBeNull();
expect(stream.index).toBe(2);
});
});
describe('ReplayArray param codec', () => {
// 验证 boolean 参数按 type 0 写入并读回布尔值
it('round-trips boolean parameters', () => {

View File

@ -503,8 +503,10 @@ export class ReplayArray implements IReplayArray {
if (this.disabled > 0) return;
const normalized = this.normalizeParamList(params);
const length = this.calculateParamsLength(normalized);
const paramStart = this.indexArray[index];
const nextParam = this.indexArray[index + 1];
// 末步的参数终点取 paramUsed,与 delete 同口径,避免读到未初始化的 indexArray[index + 1]
const range = this.getParamRange(index);
const paramStart = range.start;
const nextParam = range.end;
const paramLength = nextParam - paramStart;
const deltaLength = length - paramLength;
if (deltaLength > 0) {
@ -530,8 +532,8 @@ export class ReplayArray implements IReplayArray {
// 然后设置参数数组
this.setParamArray(paramStart, normalized);
// 最后调整索引数组
for (let i = paramStart + 1; i < this.length; i++) {
// 最后调整索引数组,从本命令的下一条起平移:本命令的起始索引自身不变
for (let i = index + 1; i < this.length; i++) {
this.indexArray[i] += deltaLength;
}

View File

@ -211,7 +211,8 @@ export interface IReplayArray {
delete(index: number): void;
/**
* 修改指定索引处的录像步。
* 修改指定索引处的录像步。新的参数列表允许与原有参数编码长度不同,
* 此情形下其后所有录像步的参数起始索引会同步平移。
* 由于此操作会涉及大量的内存迁移,耗时较长,因此不建议频繁调用。
* @param index 要修改的步索引
* @param command 新的指令标识