Files
MetonaSqlark/tests/helpers/storage-harness.test.ts
thzxx 0dba1abf2a test(P0): v0.8.0 验证基座与工程门禁根治
工作流 C-1 / C-3 前半 + 测试代码类型检查。

【故障注入基座】新增 tests/helpers/storage-harness.ts + faulty-backend.ts
- TransactionalFileStore:忠实 OPFS 提交语义(close 才可见)+ 字节级故障注入
  (failNextWrite/Append/Delete、truncateAppendTo 撕裂写、crashPending 真崩溃)
- 删除旧 opfs-mock:读返回内部引用、keepExistingData:false 不截断、close 空实现
  导致"提交前可见"等真实缺陷无法被测出(31 个测试文件迁移至新 harness)
- 删除 aria-opfs-backend 内的第三份重复 mock(含从未被断言使用的 writeCalls 死代码
  与 entry.content.subarray 恒等分支)
- FaultyBackend:包装任意 IStorageBackend 注入故障;crash() 明确区别于 close()
  (后者是优雅停机,会刷完写队列 —— 这正是此前所有"崩溃恢复"测试的真相)
- 16 条基座自测证明注入真的生效(含 close 不能当崩溃的对照组)

【覆盖率口径】jest.config.cjs
- 移除 '!src/**/index.ts'(该 glob 把 AriaEngine 主实现等 15 个实现文件整体
  排除出统计,与 v0.2.6 曾承认过的问题同源),改为只排除纯类型声明文件并附理由
- 新增 coverageThreshold 门禁(此前完全不存在)
- 真实基线:语句 90.66% / 分支 82.94% / 函数 94.36% / 行 93.43%
- 修正 testMatch 使 tests/helpers 下的测试可被发现

【测试代码类型检查】tsconfig.test.json + npm run typecheck:tests
- 修复 103 个测试代码类型错误(此前 babel 剥离类型 + tsconfig 排除 tests,全部隐藏)
- 新增 tests/helpers/assertions.ts:nonNull/decode/rows/object/engineMethod/expectCode
  以断言收窄替代 as any
- 消除 21 个 lint warning(含 v043-hardening 中定义后从未调用的 mockOPFS 死代码)
- parser.test.ts 12 处 toBeDefined() 空断言升级为结构断言(并新增 AND/OR 优先级用例,
  当前红灯,对应总账第 11 项,将在工作流 A 修复)

【版本契约】新增 tests/version-contract.test.ts
- 校验 src VERSION / package.json / dist 三者一致,替代两处硬编码版本字面量

【CI 门禁】.gitea/workflows/ci.yml
- lint 去掉 continue-on-error(此前永远不让 CI 变红)
- 新增 tests 类型检查、--coverage 覆盖率门禁、dist 与源码同步校验
- 版本 0.7.4 升至 0.8.0
2026-09-14 21:03:06 +08:00

226 lines
9.0 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.
/**
* 故障注入基座自测(v0.8.0 工作流 C-1)
*
* 这些测试的作用不是测产品代码,而是**证明测试基座本身是有效的**:
* 如果故障注入不能稳定复现"崩溃丢数据",那么后面所有崩溃相关测试都是空头支票。
*/
import { TransactionalFileStore, installOPFSMock, clearRegistry, CrashableStoreBackend } from './storage-harness';
import { FaultyBackend } from './faulty-backend';
import { OPFSBackend } from '../../src/engine/aria/store/opfs_backend';
import { decode as decodeBytes } from './assertions';
describe('[v0.8.0] 测试基座:TransactionalFileStore', () => {
let store: TransactionalFileStore;
beforeEach(() => {
clearRegistry();
store = new TransactionalFileStore({ dbName: 'unit' });
});
test('提交语义:未提交的写入对读不可见(真实 OPFS 是 close 才原子替换)', async () => {
await store.write('k', new TextEncoder().encode('v1').buffer as ArrayBuffer);
expect(await store.read('k')).toBeNull(); // 未 commit → 读不到
store.commitAll();
expect(decodeBytes(await store.read('k'))).toBe('v1');
});
test('崩溃语义:丢弃未提交写入,已提交内容保持', async () => {
await store.write('k', new TextEncoder().encode('committed').buffer as ArrayBuffer);
store.commitAll();
await store.write('k', new TextEncoder().encode('pending').buffer as ArrayBuffer);
expect(store.hasPending()).toBe(true);
store.crashPending(); // 崩溃:丢掉 pending
expect(decodeBytes(await store.read('k'))).toBe('committed');
expect(store.hasPending()).toBe(false);
});
test('读返回副本:调用方原地修改不会污染"磁盘"(旧 mock 的缺陷 1', async () => {
await store.write('k', new TextEncoder().encode('original').buffer as ArrayBuffer);
store.commitAll();
const first = await store.read('k');
new Uint8Array(first!)[0] = 0x58; // 'X'
const second = await store.read('k');
expect(decodeBytes(second)).toBe('original');
});
test('故障注入:failNextWrite / failNextAppend / failNextDelete 精确计数', async () => {
store.failNextWrite();
await expect(store.write('k', new ArrayBuffer(4))).rejects.toThrow(/injected write failure/);
await expect(store.write('k', new ArrayBuffer(4))).resolves.toBeUndefined();
store.failNextAppend(2);
await expect(store.append('k', new ArrayBuffer(2))).rejects.toThrow(/injected append failure/);
await expect(store.append('k', new ArrayBuffer(2))).rejects.toThrow(/injected append failure/);
await expect(store.append('k', new ArrayBuffer(2))).resolves.toBeUndefined();
store.failNextDelete();
await expect(store.delete('k')).rejects.toThrow(/injected delete failure/);
});
test('撕裂写:truncateNextAppendTo 只落前 n 字节', async () => {
const text = new TextEncoder();
await store.write('log', text.encode('AAAA').buffer as ArrayBuffer);
store.commitAll();
store.truncateNextAppendTo(2);
await store.append('log', text.encode('BBBB').buffer as ArrayBuffer);
store.commitAll();
// 已提交的 'AAAA' 保留 + 撕裂后只追加了 'BB' → 'AAAABB'
expect(decodeBytes(await store.read('log'))).toBe('AAAABB');
});
test('篡改已提交字节:corruptCommitted 用于 bit-flip 场景', async () => {
await store.write('k', new TextEncoder().encode('hello').buffer as ArrayBuffer);
store.commitAll();
const ok = store.corruptCommitted('k', (bytes) => { bytes[0] ^= 0xff; });
expect(ok).toBe(true);
const after = new Uint8Array((await store.read('k'))!);
expect(after[0]).not.toBe('h'.charCodeAt(0));
});
test('提交原子性:commitAll 一次性生效全部 pending', async () => {
await store.write('a', new ArrayBuffer(1));
await store.write('b', new ArrayBuffer(1));
expect(await store.listKeys()).toEqual(['a', 'b']);
store.commitAll();
expect((await store.listKeys()).sort()).toEqual(['a', 'b']);
});
});
describe('[v0.8.0] 测试基座:installOPFSMock 真实语义', () => {
beforeEach(() => clearRegistry());
test('keepExistingData:false 会截断(旧 mock 的缺陷 2', async () => {
installOPFSMock('trunc');
const backend = new OPFSBackend();
await backend.open('trunc');
await backend.write('f', new TextEncoder().encode('LONGCONTENT').buffer as ArrayBuffer);
// 覆盖写更短内容 → 真实 OPFS 应截断为 5 字节
await backend.write('f', new TextEncoder().encode('short').buffer as ArrayBuffer);
const read = await backend.read('f');
expect(decodeBytes(read)).toBe('short');
});
test('append 是真追加且不破坏已有内容', async () => {
installOPFSMock('app');
const backend = new OPFSBackend();
await backend.open('app');
await backend.write('log', new TextEncoder().encode('AAA').buffer as ArrayBuffer);
await backend.append('log', new TextEncoder().encode('BBB').buffer as ArrayBuffer);
expect(decodeBytes(await backend.read('log'))).toBe('AAABBB');
});
test('读返回副本,跨 backend 实例隔离', async () => {
installOPFSMock('iso');
const b1 = new OPFSBackend();
await b1.open('iso');
await b1.write('k', new TextEncoder().encode('v').buffer as ArrayBuffer);
const b2 = new OPFSBackend();
await b2.open('iso');
const buf = await b2.read('k');
new Uint8Array(buf!)[0] = 0x5a;
expect(decodeBytes(await b1.read('k'))).toBe('v');
});
test('崩溃后重开:已提交数据在,未提交的不在', async () => {
const installed = installOPFSMock('crash');
const store = installed.store;
store.restoreCommitted(new Map()); // 清空
await store.write('committed', new TextEncoder().encode('yes').buffer as ArrayBuffer);
store.commitAll();
await store.write('pending', new TextEncoder().encode('no').buffer as ArrayBuffer);
store.crashPending();
expect(decodeBytes(await store.read('committed'))).toBe('yes');
expect(await store.read('pending')).toBeNull();
});
});
describe('[v0.8.0] 测试基座:FaultyBackend', () => {
beforeEach(() => clearRegistry());
test('注入的写失败必须真的让调用方拿到 rejection', async () => {
installOPFSMock('faulty');
const faulty = new FaultyBackend(new OPFSBackend());
await faulty.open('faulty');
faulty.failNextWrite();
await expect(faulty.write('k', new ArrayBuffer(4))).rejects.toThrow(/injected write failure/);
expect(faulty.injected.write).toBe(1);
faulty.clearFaults();
await expect(faulty.write('k', new ArrayBuffer(4))).resolves.toBeUndefined();
});
test('静默丢弃写:调用方看到成功,但介质上没有(掉电场景)', async () => {
installOPFSMock('drop');
const faulty = new FaultyBackend(new OPFSBackend());
await faulty.open('drop');
faulty.dropNextWrite();
await expect(faulty.write('k', new ArrayBuffer(4))).resolves.toBeUndefined();
expect(await faulty.read('k')).toBeNull(); // 静默丢失
expect(faulty.injected.dropped).toBe(1);
});
test('撕裂追加:写到一半的 WAL 记录可被构造', async () => {
installOPFSMock('torn');
const faulty = new FaultyBackend(new OPFSBackend());
await faulty.open('torn');
const record = new Uint8Array(100).fill(7);
faulty.truncateNextAppendTo(37);
await faulty.append('__wal_000000.bin', record.buffer as ArrayBuffer);
const stored = await faulty.read('__wal_000000.bin');
expect(stored!.byteLength).toBe(37);
expect(faulty.injected.truncated).toBe(1);
});
test('crash() 丢弃未提交写(这才是崩溃,不是 close 优雅停机)', async () => {
const store = new TransactionalFileStore({ dbName: 'crash2' });
const backend = new CrashableStoreBackend(store);
const faulty = new FaultyBackend(backend);
const text = new TextEncoder();
await faulty.write('saved', text.encode('KEEP').buffer as ArrayBuffer);
backend.commit(); // 模拟 createWritable.close() 完成
await faulty.write('unsaved', text.encode('LOST').buffer as ArrayBuffer);
expect(store.hasPending()).toBe(true); // 存在真实的"提交前崩溃窗口"
expect(faulty.crash()).toBe(true);
expect(store.hasPending()).toBe(false);
expect(await store.read('unsaved')).toBeNull();
expect(decodeBytes(await store.read('saved'))).toBe('KEEP');
});
test('对照组:close() 是优雅停机,会把未提交写刷完 —— 因此不能用来模拟崩溃', async () => {
const store = new TransactionalFileStore({ dbName: 'graceful' });
const backend = new CrashableStoreBackend(store);
await backend.write('k', new TextEncoder().encode('PENDING').buffer as ArrayBuffer);
expect(store.hasPending()).toBe(true);
// close() 不丢弃 pending(真实 OPFSBackend.close 还会等写队列排空)
await backend.close();
expect(store.hasPending()).toBe(true);
// 提交后才可见 —— 证明"用 close 当崩溃"会掩盖崩溃窗口
backend.commit();
expect(decodeBytes(await store.read('k'))).toBe('PENDING');
});
});