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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user