Files
MetonaSqlark/tests/engine/aria-opfs-backend.test.ts
T
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

175 lines
6.8 KiB
TypeScript
Raw 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.
/**
* AriaEngine — OPFSBackend v2 测试
*
* 覆盖:
* 1. read/write/append/exists/delete/clear 基本语义
* 2. append 追加语义(keepExistingData + position)与字节顺序
* 3. 写队列串行化(并发写顺序一致)
* 4. 单次任务失败不中断队列链(后续写正常)
* 5. close 等待挂起写完成
* 6. open 清理崩溃残留临时文件(.crswap/.tmp
* 7. writeMany/deleteMany 语义
*/
// ===================================================================
// v0.8.0: 删除本文件内的第三份 OPFS mock —— 改用共享的 storage-harness。
//
// 原实现的问题(见 PLAN-v0.7.5.md 工作流 C-1):
// - 与 tests/helpers/opfs-mock.ts 重复(两份都在测同一件事,语义各自漂移);
// - `close()` 是空函数、写入立即可见 → 无法表达"提交前崩溃"
// - 定义了 `writeCalls` 记录但**从未被任何断言使用**(死代码);
// - `entry.content.subarray ? entry.content : entry.content` 是恒等表达式(无意义分支)。
//
// 共享 harness 提供:真实提交语义(close 才可见)、读返回副本、
// keepExistingData:false 截断、字节级故障注入、真崩溃模拟。
// ===================================================================
import { OPFSBackend } from '../../src/engine/aria/store/opfs_backend';
import { resetOPFSMock } from '../helpers/storage-harness';
import { decode as decodeBytes } from '../helpers/assertions';
const enc = (s: string) => new Uint8Array(new TextEncoder().encode(s)).buffer;
describe('AriaEngine — OPFSBackend v2', () => {
beforeEach(() => {
resetOPFSMock();
});
it('read/write/exists/delete 基本语义', async () => {
const backend = new OPFSBackend();
await backend.open('opfs-test-1');
expect(backend.isOpen()).toBe(true);
await backend.write('k1', enc('hello'));
expect(await backend.exists('k1')).toBe(true);
expect(decodeBytes(await backend.read('k1'))).toBe('hello');
expect(await backend.read('missing')).toBeNull();
await backend.delete('k1');
expect(await backend.exists('k1')).toBe(false);
await backend.close();
expect(backend.isOpen()).toBe(false);
});
it('append 追加语义:字节顺序与位置正确', async () => {
const backend = new OPFSBackend();
await backend.open('opfs-test-2');
await backend.append('wal', enc('AAA'));
await backend.append('wal', enc('BBB'));
await backend.append('wal', enc('CCC'));
const all = await backend.read('wal');
expect(decodeBytes(all)).toBe('AAABBBCCC');
await backend.close();
});
it('write 覆盖语义(COW 原子替换)', async () => {
const backend = new OPFSBackend();
await backend.open('opfs-test-3');
await backend.write('f', enc('OLD-CONTENT'));
await backend.write('f', enc('NEW'));
expect(decodeBytes(await backend.read('f'))).toBe('NEW');
await backend.close();
});
it('并发写串行化:写入顺序与调用顺序一致', async () => {
const backend = new OPFSBackend();
await backend.open('opfs-test-4');
// 同时发起多个写(不 await 中间结果)
const p1 = backend.append('log', enc('A'));
const p2 = backend.append('log', enc('B'));
const p3 = backend.append('log', enc('C'));
await Promise.all([p1, p2, p3]);
expect(decodeBytes(await backend.read('log'))).toBe('ABC');
await backend.close();
});
it('单次写失败不中断队列链,后续写正常', async () => {
const backend = new OPFSBackend();
await backend.open('opfs-test-5');
// 注入失败:让 write 抛错一次
let failNext = true;
const origWrite = backend.write.bind(backend);
const mockDir = (backend as any).dbDir;
const origGetFileHandle = mockDir.getFileHandle.bind(mockDir);
mockDir.getFileHandle = async (name: string, opts?: { create?: boolean }) => {
if (failNext && name === 'boom') {
failNext = false;
throw new Error('Injected write failure');
}
return origGetFileHandle(name, opts);
};
(backend as any).dbDir = mockDir;
await expect(origWrite('boom', enc('x'))).rejects.toThrow('Injected write failure');
// 队列链恢复:后续写成功
await backend.write('ok', enc('fine'));
expect(decodeBytes(await backend.read('ok'))).toBe('fine');
await backend.close();
});
it('close 等待挂起写完成(挂起写不丢失)', async () => {
const backend = new OPFSBackend();
await backend.open('opfs-test-6');
// 发起一个写但不 await,立即 close
const pending = backend.append('late', enc('LATE-DATA'));
await backend.close();
await pending; // close 已等队列 → 写已完成
expect(pending).resolves.toBeUndefined();
});
it('open 清理崩溃残留临时文件(.crswap/.tmp', async () => {
const backend = new OPFSBackend();
await backend.open('opfs-test-7');
await backend.write('data', enc('real'));
// 手工制造残留
const mockDir = (backend as any).dbDir;
await mockDir.getFileHandle('data.crswap', { create: true });
await mockDir.getFileHandle('data.tmp', { create: true });
await backend.close();
// 重开:open 自动清理
const backend2 = new OPFSBackend();
await backend2.open('opfs-test-7');
const keys = await backend2.listKeys();
expect(keys).toEqual(['data']);
expect(keys.some((k) => k.endsWith('.crswap') || k.endsWith('.tmp'))).toBe(false);
// 正常数据不受影响
expect(decodeBytes(await backend2.read('data'))).toBe('real');
await backend2.close();
});
it('writeMany / deleteMany 语义', async () => {
const backend = new OPFSBackend();
await backend.open('opfs-test-8');
await backend.writeMany({ a: enc('AAA'), b: enc('BBB') });
expect(decodeBytes(await backend.read('a'))).toBe('AAA');
expect(decodeBytes(await backend.read('b'))).toBe('BBB');
await backend.deleteMany(['a']);
expect(await backend.exists('a')).toBe(false);
expect(await backend.exists('b')).toBe(true);
await backend.clear();
expect(await backend.listKeys()).toEqual([]);
await backend.close();
});
it('close 后所有操作安全返回(dbDir 为 null 不抛错)', async () => {
const backend = new OPFSBackend();
await backend.open('opfs-test-9');
await backend.write('k', enc('v'));
await backend.close();
expect(backend.isOpen()).toBe(false);
expect(await backend.read('k')).toBeNull();
await backend.write('late', enc('x')); // 静默忽略
await backend.append('late2', enc('x'));
await backend.writeMany({ a: enc('1') });
await backend.delete('k');
await backend.deleteMany(['a']);
expect(await backend.listKeys()).toEqual([]);
expect(await backend.exists('k')).toBe(false);
await backend.clear();
await backend.cleanupStaleFiles();
});
});