release: v0.5.1 — 存储后端生产级硬化(CRC-32/全库加密/WAL分片/页面化存储/多标签页锁/e2e)+ 深度审查修复(假实现接线/死代码清理)
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* 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<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 };
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user