方法:四个对抗性子代理分头审查(数据正确性 / 文档宣称 vs 实现 / 公共 API 契约 / 测试质量),每条结论要求可复现证据;逐条复核 + 探针确认 + 变异验证(40 项全部 被对应用例拦住)。 P0:事务活跃期间 repair()/close()/周期 checkpoint 推进 WAL 水位 → 已 COMMIT 的 事务整批消失且恢复报告"干净"。根因 hasPendingFlushData()/computeDurableLsn() 不看 txnSnapshot;守卫此前只在 CheckpointManager 两个回调里。修复:守卫下沉到 computeDurableLsn() 与 advanceWalCheckpoint() 入口(唯一实现)。 P1: - WAL 前缀缺失丢弃整段活分片(回退上一代 manifest 时 kept 为空)→ 前缀缺失单独 记录,后缀照常重放;仅 fromLsn === 0 时才算真异常 - 孤儿回收门槛只看引擎层 dataLossSuspected,漏掉 LSM 层被丢的 SSTable → 统一 describeRecoveryDamage() 聚合判定(损坏时绝不删"引用不到"的文件) - vacuum() 逐层压缩绕过维护链 → vacuumLevels() 每层作为维护链任务执行 - reclaimRetiredNow() 无视在途读者(读者把"已退休"读成"文件损坏")→ 有读者时 退化为延迟回收 P2:WAL 记录级 CRC 损坏不计数不上报;旧格式表结构记录形状损坏静默当空库; bloomFilterBitsPerKey 配置被接受却完全不生效(构建器写死默认值,实现缺陷); 幽灵 meta;介质读故障等于文件损坏的语义无用例;manifest 回读校验两条守卫无用例; 文件名≠载荷世代判定无用例;pageIdWatermark 单调性无用例;分片号两条真实不变量 无用例。 覆盖率口径(第二处漏洞):interface.ts 混着三个运行时函数(cloneRow 等)却被 描述为"纯类型、不纳入统计" → 实现搬到 src/engine/row_clone.ts;搬完门禁真的 失败(functions 93.84% < 94%),补测退化路径后通过。 测试质量:3 条空壳用例改值级断言;1 条"全损坏"用例实际只走缓存 → 拆成两条真 用例;5 秒墙钟 race 改门控 + 失败上限;setTimeout 改 whenIdle();<= 收紧为 <。 变异脚本加固:正控(干净基线必须全绿)、编译失败/0 用例单独归类、300s 超时、 逐字节 sha256 恢复校验、O_EXCL 进程锁、锚点唯一性;变异 22 → 40 项。 文档两轮订正(16 + 11 条不成立宣称):MVCC 快照隔离、backup 一致性快照、 "空洞检测截断"、体积(251,109 B / gzip 63,145 B)、测试与覆盖率数字、 "5 种存储引擎"、Tree-shakable、错误码表补 16 个码、恢复报告字段、已知限制 (回退单向 / 多实例依赖 Web Locks / manifest 体积 / 尾部 WAL 分片不可识别)。 验证:常规套件 92 套件 / 1980 用例全绿;覆盖率 90.59 / 82.59 / 94.14 / 93.50 (阈值 90/82/94/93);e2e 14/14(真实 Chromium + OPFS + CDP 崩溃); 重型套件 4 套件 / 27 用例;变异 40/40;lint + 两份 tsc 干净;dist 已重建。
133 lines
5.6 KiB
TypeScript
133 lines
5.6 KiB
TypeScript
/**
|
||
* 行所有权(row ownership)工具测试
|
||
*
|
||
* 背景(审计 A 类缺陷):Memory/KVStore/Hybrid 曾把内部行对象直接交给调用方,
|
||
* 调用方一次原地修改就能改写存储、让索引与行失配(改过的行再也查不出来)。
|
||
* `cloneRow`/`cloneRows` 是这条不变量的唯一实现。
|
||
*
|
||
* v0.8.0 审查:这三个函数原本放在 `engine/interface.ts`(被描述为"纯类型、
|
||
* 不纳入覆盖率")→ 真实实现代码逃过覆盖率统计。搬到 `engine/row_clone.ts` 后
|
||
* 覆盖率立刻暴露:退化路径(`cloneRowFallback`,即 `structuredClone` 不可用/
|
||
* 抛错时的逐层复制)**从未被测试**。本文件补上这两条路径。
|
||
*/
|
||
import { describe, it, expect, afterEach } from '@jest/globals';
|
||
import { cloneRow, cloneRows } from '../../src/engine/row_clone';
|
||
|
||
const originalStructuredClone = (globalThis as { structuredClone?: unknown }).structuredClone;
|
||
|
||
afterEach(() => {
|
||
(globalThis as { structuredClone?: unknown }).structuredClone = originalStructuredClone;
|
||
});
|
||
|
||
describe('cloneRow — structuredClone 可用', () => {
|
||
it('深拷贝普通对象与嵌套结构,修改副本不影响源对象', () => {
|
||
const source = { id: 'a', v: 1, nested: { deep: [1, 2, { x: true }] } };
|
||
const copy = cloneRow(source);
|
||
expect(copy).toEqual(source);
|
||
expect(copy).not.toBe(source);
|
||
expect(copy.nested).not.toBe(source.nested);
|
||
copy.nested.deep.push(99);
|
||
copy.v = 42;
|
||
expect(source.v).toBe(1);
|
||
expect(source.nested.deep).toHaveLength(3);
|
||
});
|
||
|
||
it('JSON 安全值深拷贝且逐层独立(行的实际契约)', () => {
|
||
// 引擎的读路径只承载 validateRow 之后的 JSON 安全值 —— `date` 列在 schema 层
|
||
// 就要求**字符串**(`src/table/validation.ts` 的 'date' 分支),因此行里不会
|
||
// 出现 Date 实例。这里断言的是真实契约:值相等 + 逐层不共享引用。
|
||
// (不在此处断言 Date 语义:jsdom 的 structuredClone 是 JSON 化的 polyfill,
|
||
// 会把 Date 变成字符串;真实浏览器/Node 的 structuredClone 保留 Date,
|
||
// 而退化路径的实现也显式保留 —— 见下方 fallback 用例。)
|
||
const source = { id: 'a', when: '2026-09-15T00:00:00.000Z', nested: { k: [1, 2] } };
|
||
const copy = cloneRow(source);
|
||
expect(copy).toEqual(source);
|
||
expect(copy.nested.k).not.toBe(source.nested.k);
|
||
copy.nested.k.push(3);
|
||
expect(source.nested.k).toEqual([1, 2]);
|
||
});
|
||
|
||
it('非对象输入原样返回(null / 数字 / 字符串不参与拷贝)', () => {
|
||
expect(cloneRow(null as never)).toBeNull();
|
||
expect(cloneRow(7 as never)).toBe(7);
|
||
expect(cloneRow('x' as never)).toBe('x');
|
||
});
|
||
});
|
||
|
||
describe('cloneRow — structuredClone 不可用(退化路径)', () => {
|
||
it('无 structuredClone 时逐层复制:嵌套对象 / 数组 / Date / TypedArray / ArrayBuffer', () => {
|
||
(globalThis as { structuredClone?: unknown }).structuredClone = undefined;
|
||
const source = {
|
||
id: 'a',
|
||
nested: { list: [1, { k: 'v' }] },
|
||
when: new Date('2026-01-02T03:04:05.000Z'),
|
||
bytes: new Uint8Array([1, 2, 3]),
|
||
raw: new Uint8Array([9, 8]).buffer,
|
||
plain: 'str',
|
||
num: 5,
|
||
flag: false,
|
||
nil: null,
|
||
};
|
||
const copy = cloneRow(source);
|
||
|
||
expect(copy).toEqual(source);
|
||
expect(copy).not.toBe(source);
|
||
expect(copy.nested).not.toBe(source.nested);
|
||
expect(copy.nested.list).not.toBe(source.nested.list);
|
||
expect(copy.when).toBeInstanceOf(Date);
|
||
expect(copy.when.getTime()).toBe(source.when.getTime());
|
||
expect(copy.bytes).toBeInstanceOf(Uint8Array);
|
||
expect(Array.from(copy.bytes)).toEqual([1, 2, 3]);
|
||
expect(copy.bytes).not.toBe(source.bytes);
|
||
expect(copy.raw).toBeInstanceOf(ArrayBuffer);
|
||
expect(copy.raw).not.toBe(source.raw);
|
||
expect(new Uint8Array(copy.raw as ArrayBuffer)).toEqual(new Uint8Array([9, 8]));
|
||
|
||
// 深拷贝语义:改副本不影响源
|
||
copy.nested.list.push({ k: 'mutated' });
|
||
(copy.bytes as Uint8Array)[0] = 99;
|
||
expect(source.nested.list).toHaveLength(2);
|
||
expect(source.bytes[0]).toBe(1);
|
||
});
|
||
|
||
it('structuredClone 抛错(含不可克隆值)时也退化到逐层复制', () => {
|
||
(globalThis as { structuredClone?: unknown }).structuredClone = () => {
|
||
throw new Error('could not be cloned');
|
||
};
|
||
const fn = (): number => 1;
|
||
const source = { id: 'a', fn, nested: { k: 1 } };
|
||
const copy = cloneRow(source);
|
||
expect(copy.id).toBe('a');
|
||
expect(copy.fn).toBe(fn); // 函数按引用保留(不可克隆值的合理退化)
|
||
expect(copy.nested).toEqual({ k: 1 });
|
||
expect(copy.nested).not.toBe(source.nested);
|
||
});
|
||
|
||
it('退化路径对原始值同样原样返回', () => {
|
||
(globalThis as { structuredClone?: unknown }).structuredClone = undefined;
|
||
expect(cloneRow(null as never)).toBeNull();
|
||
expect(cloneRow(3 as never)).toBe(3);
|
||
});
|
||
});
|
||
|
||
describe('cloneRows — 批量拷贝', () => {
|
||
it('每行都是独立副本(改一行不影响其它行,也不影响源数组)', () => {
|
||
const rows = [{ id: 'a', tags: ['x'] }, { id: 'b', tags: ['y'] }];
|
||
const copies = cloneRows(rows);
|
||
expect(copies).toEqual(rows);
|
||
expect(copies).toHaveLength(2);
|
||
copies[0].tags.push('mutated');
|
||
expect(rows[0].tags).toEqual(['x']);
|
||
expect(copies[0]).not.toBe(rows[0]);
|
||
expect(copies[1]).not.toBe(rows[1]);
|
||
});
|
||
|
||
it('退化路径同样逐行深拷贝', () => {
|
||
(globalThis as { structuredClone?: unknown }).structuredClone = undefined;
|
||
const rows = [{ id: 'a', nested: { k: 1 } }];
|
||
const copies = cloneRows(rows);
|
||
copies[0].nested.k = 2;
|
||
expect(rows[0].nested.k).toBe(1);
|
||
});
|
||
});
|