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
+24 -78
View File
@@ -10,82 +10,28 @@
* 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';
// ===================================================================
// 真实语义 OPFS mock:记录文件内容、支持 keepExistingData+position
// ===================================================================
interface MockFile {
content: ArrayBuffer;
writeCalls: { position?: number; data: ArrayBuffer; keepExistingData?: boolean }[];
}
function createOPFSMock() {
const files = new Map<string, MockFile>();
const getFileHandle = async (name: string, opts?: { create?: boolean }) => {
if (!files.has(name)) {
if (!opts?.create) throw new Error(`NotFoundError: ${name}`);
files.set(name, { content: new ArrayBuffer(0), writeCalls: [] });
}
const entry = files.get(name)!;
return {
getFile: async () => ({ size: entry.content.byteLength, arrayBuffer: async () => entry.content }),
createWritable: async (wOpts?: { keepExistingData?: boolean }) => {
const w: {
write: (arg: ArrayBuffer | { type: string; position: number; data: ArrayBuffer }) => Promise<void>;
close: () => Promise<void>;
} = {
write: async (arg) => {
const keepExisting = wOpts?.keepExistingData ?? false;
const isChunk = typeof arg !== 'object' || !('type' in (arg as object)) || (arg as { type?: string }).type === undefined
? { data: arg as ArrayBuffer, position: keepExisting ? entry.content.byteLength : 0 }
: { data: (arg as { data: ArrayBuffer }).data, position: (arg as { position: number }).position };
entry.writeCalls.push({ position: isChunk.position, data: isChunk.data, keepExistingData: keepExisting });
const merged = new Uint8Array(isChunk.position + isChunk.data.byteLength);
if (keepExisting || isChunk.position > 0) {
merged.set(new Uint8Array(entry.content.subarray ? entry.content : entry.content), 0);
}
merged.set(new Uint8Array(isChunk.data), isChunk.position);
entry.content = merged.buffer;
},
close: async () => { /* no-op */ },
};
return w;
},
};
};
const dir = {
getFileHandle,
entries: async function* () {
for (const [name] of files) yield [name];
},
removeEntry: async (name: string) => {
files.delete(name);
},
};
Object.defineProperty(globalThis, 'navigator', {
value: {
storage: {
getDirectory: async () => ({
getDirectoryHandle: async (_name: string, _opts?: unknown) => dir,
}),
},
},
configurable: true,
writable: true,
});
return { files, dir };
}
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(() => {
createOPFSMock();
resetOPFSMock();
});
it('read/write/exists/delete 基本语义', async () => {
@@ -95,7 +41,7 @@ describe('AriaEngine — OPFSBackend v2', () => {
await backend.write('k1', enc('hello'));
expect(await backend.exists('k1')).toBe(true);
expect(new TextDecoder().decode(await backend.read('k1'))).toBe('hello');
expect(decodeBytes(await backend.read('k1'))).toBe('hello');
expect(await backend.read('missing')).toBeNull();
await backend.delete('k1');
@@ -111,7 +57,7 @@ describe('AriaEngine — OPFSBackend v2', () => {
await backend.append('wal', enc('BBB'));
await backend.append('wal', enc('CCC'));
const all = await backend.read('wal');
expect(new TextDecoder().decode(all)).toBe('AAABBBCCC');
expect(decodeBytes(all)).toBe('AAABBBCCC');
await backend.close();
});
@@ -120,7 +66,7 @@ describe('AriaEngine — OPFSBackend v2', () => {
await backend.open('opfs-test-3');
await backend.write('f', enc('OLD-CONTENT'));
await backend.write('f', enc('NEW'));
expect(new TextDecoder().decode(await backend.read('f'))).toBe('NEW');
expect(decodeBytes(await backend.read('f'))).toBe('NEW');
await backend.close();
});
@@ -132,7 +78,7 @@ describe('AriaEngine — OPFSBackend v2', () => {
const p2 = backend.append('log', enc('B'));
const p3 = backend.append('log', enc('C'));
await Promise.all([p1, p2, p3]);
expect(new TextDecoder().decode(await backend.read('log'))).toBe('ABC');
expect(decodeBytes(await backend.read('log'))).toBe('ABC');
await backend.close();
});
@@ -157,7 +103,7 @@ describe('AriaEngine — OPFSBackend v2', () => {
await expect(origWrite('boom', enc('x'))).rejects.toThrow('Injected write failure');
// 队列链恢复:后续写成功
await backend.write('ok', enc('fine'));
expect(new TextDecoder().decode(await backend.read('ok'))).toBe('fine');
expect(decodeBytes(await backend.read('ok'))).toBe('fine');
await backend.close();
});
@@ -189,7 +135,7 @@ describe('AriaEngine — OPFSBackend v2', () => {
expect(keys).toEqual(['data']);
expect(keys.some((k) => k.endsWith('.crswap') || k.endsWith('.tmp'))).toBe(false);
// 正常数据不受影响
expect(new TextDecoder().decode(await backend2.read('data'))).toBe('real');
expect(decodeBytes(await backend2.read('data'))).toBe('real');
await backend2.close();
});
@@ -197,8 +143,8 @@ describe('AriaEngine — OPFSBackend v2', () => {
const backend = new OPFSBackend();
await backend.open('opfs-test-8');
await backend.writeMany({ a: enc('AAA'), b: enc('BBB') });
expect(new TextDecoder().decode(await backend.read('a'))).toBe('AAA');
expect(new TextDecoder().decode(await backend.read('b'))).toBe('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);