/** * 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 语义 */ 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(); 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; close: () => Promise; } = { 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 }; } const enc = (s: string) => new Uint8Array(new TextEncoder().encode(s)).buffer; describe('AriaEngine — OPFSBackend v2', () => { beforeEach(() => { createOPFSMock(); }); 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(new TextDecoder().decode(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(new TextDecoder().decode(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(new TextDecoder().decode(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(new TextDecoder().decode(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(new TextDecoder().decode(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(new TextDecoder().decode(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(new TextDecoder().decode(await backend.read('a'))).toBe('AAA'); expect(new TextDecoder().decode(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(); }); });