Files
thzxx 0b44620721 fix(v0.8.0): 全量回归审查 —— 1 处 P0 数据丢失 + 4 处 P1 + 9 处 P2 根因修复
方法:四个对抗性子代理分头审查(数据正确性 / 文档宣称 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 已重建。
2026-09-15 16:33:40 +08:00

133 lines
5.6 KiB
TypeScript
Raw Permalink 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.
/**
* 行所有权(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);
});
});