工作流 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
226 lines
9.0 KiB
TypeScript
226 lines
9.0 KiB
TypeScript
/**
|
||
* 故障注入基座自测(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');
|
||
});
|
||
});
|