Files
MetonaSqlark/tests/engine/aria-opfs-backend.test.ts
T
thzxx 334067d89e
CI / test (18.x) (push) Successful in 10m10s
CI / test (20.x) (push) Successful in 10m10s
CI / test (22.x) (push) Successful in 10m6s
CI / e2e (push) Successful in 9m51s
CI / test (24.x) (push) Successful in 10m28s
release: v0.5.1 — 存储后端生产级硬化(CRC-32/全库加密/WAL分片/页面化存储/多标签页锁/e2e)+ 深度审查修复(假实现接线/死代码清理)
2026-08-10 12:07:00 +08:00

229 lines
8.5 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 语义
*/
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();
});
});