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
This commit is contained in:
thzxx
2026-09-14 21:03:06 +08:00
parent c8b59bd16f
commit 0dba1abf2a
65 changed files with 1533 additions and 454 deletions
+225
View File
@@ -0,0 +1,225 @@
/**
* 故障注入基座自测(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');
});
});