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:
@@ -8,11 +8,10 @@ import { WAL, type WALStore } from '../../src/engine/aria/wal/log';
|
||||
import { WALRecordType } from '../../src/engine/aria/types';
|
||||
import { BufferPool } from '../../src/engine/aria/buffer/pool';
|
||||
import { MemoryBackend } from '../../src/engine/aria/store/backend';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
beforeEach(() => { resetOPFSMock(); });
|
||||
|
||||
describe('AriaEngine — Bloom + WAL + BufferPool', () => {
|
||||
// ---- BloomFilter 完整测试 ----
|
||||
|
||||
@@ -5,9 +5,9 @@ import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
import { MemoryBackend } from '../../src/engine/aria/store/backend';
|
||||
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
beforeEach(() => { resetOPFSMock(); });
|
||||
|
||||
describe('AriaEngine — 批量扩展测试', () => {
|
||||
let engine: AriaEngine;
|
||||
|
||||
@@ -98,8 +98,8 @@ describe('AriaEngine — EvictionManager', () => {
|
||||
}
|
||||
|
||||
it('access 更新 LRU', async () => {
|
||||
let evicted = -1;
|
||||
const em = new EvictionManager(3, async (p) => { evicted = p.pageId; });
|
||||
let _evicted = -1;
|
||||
const em = new EvictionManager(3, async (p) => { _evicted = p.pageId; });
|
||||
const p = makePage(1);
|
||||
em.add(p);
|
||||
em.access(p);
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
beforeEach(() => { resetOPFSMock(); });
|
||||
|
||||
/** 构造小缓存 + 小 MemTable 阈值的引擎,快速产生多个 SSTable */
|
||||
function createSmallCacheEngine(bufferPoolPages = 2) {
|
||||
@@ -61,7 +61,7 @@ describe('AriaEngine SSTable 缓存内存上限', () => {
|
||||
for (let round = 0; round < 5; round++) {
|
||||
const rows = await engine.find('users', { table: 'users', where: { age: 25 } });
|
||||
expect(rows.length).toBe(10);
|
||||
expect(lsm.cacheSize).toBeLessThanOrEqual(lsm.cacheLimitBytes);
|
||||
expect(lsm.getCacheSize()).toBeLessThanOrEqual(lsm.getCacheLimit());
|
||||
}
|
||||
|
||||
await engine.close();
|
||||
@@ -82,7 +82,7 @@ describe('AriaEngine SSTable 缓存内存上限', () => {
|
||||
expect(all.length).toBe(300);
|
||||
|
||||
const lsm = (engine as any).lsm;
|
||||
expect(lsm.cacheSize).toBeLessThanOrEqual(lsm.cacheLimitBytes);
|
||||
expect(lsm.getCacheSize()).toBeLessThanOrEqual(lsm.getCacheLimit());
|
||||
await engine.close();
|
||||
});
|
||||
|
||||
@@ -173,7 +173,7 @@ describe('AriaEngine SSTable 缓存内存上限', () => {
|
||||
for (let batch = 0; batch < 10; batch++) {
|
||||
await engine.insert('users', makeRows(30).map((r, i) => ({ ...r, id: `b${batch}_u${i}` })));
|
||||
const lsm = (engine as any).lsm;
|
||||
expect(lsm.cacheSize).toBeLessThanOrEqual(lsm.cacheLimitBytes);
|
||||
expect(lsm.getCacheSize()).toBeLessThanOrEqual(lsm.getCacheLimit());
|
||||
}
|
||||
|
||||
const all = await engine.find('users', { table: 'users' });
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
beforeEach(() => { resetOPFSMock(); });
|
||||
|
||||
let idbCounter = 0;
|
||||
function uniqueDB(): string {
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
beforeEach(() => { resetOPFSMock(); });
|
||||
|
||||
describe('AriaEngine — 扩展边缘测试', () => {
|
||||
let engine: AriaEngine;
|
||||
@@ -69,7 +69,7 @@ describe('AriaEngine — 扩展边缘测试', () => {
|
||||
});
|
||||
|
||||
it('多列 ORDER BY', async () => {
|
||||
const rows = await engine.find('users', { table: 'users', orderBy: [{ column: 'age', direction: 'asc' }, { column: 'score', direction: 'desc' }] });
|
||||
const rows = await engine.find('users', { table: 'users', orderBy: [{ column: 'age', direction: 'asc' }, { column: 'score', direction: 'desc' }] }) as Array<{ age: number }>;
|
||||
expect(rows[0].age).toBeLessThanOrEqual(rows[4].age);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@ import { EncryptedBackend } from '../../src/engine/aria/store/encrypted_backend'
|
||||
import { MemoryBackend } from '../../src/engine/aria/store/backend';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
import { decode as decodeBytes } from '../helpers/assertions';
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
beforeEach(() => { resetOPFSMock(); });
|
||||
|
||||
let idbCounter = 0;
|
||||
function uniqueDB(): string {
|
||||
@@ -48,11 +49,11 @@ describe('AriaEngine — EncryptedBackend 单元', () => {
|
||||
await backend.write('k1', payload);
|
||||
const back = await backend.read('k1');
|
||||
expect(back).not.toBeNull();
|
||||
expect(new TextDecoder().decode(back)).toBe('hello encrypted world');
|
||||
expect(decodeBytes(back)).toBe('hello encrypted world');
|
||||
|
||||
// 底层是密文(非明文)
|
||||
const raw = await inner.read('k1');
|
||||
const rawStr = new TextDecoder().decode(raw);
|
||||
const rawStr = decodeBytes(raw, 'ciphertext');
|
||||
expect(rawStr).not.toContain('hello encrypted world');
|
||||
|
||||
await backend.close();
|
||||
@@ -65,8 +66,8 @@ describe('AriaEngine — EncryptedBackend 单元', () => {
|
||||
|
||||
const enc = (s: string) => new TextEncoder().encode(s).buffer;
|
||||
await backend.writeMany({ a: enc('AAA'), b: enc('BBB'), c: enc('CCC') });
|
||||
expect(new TextDecoder().decode(await backend.read('a'))).toBe('AAA');
|
||||
expect(new TextDecoder().decode(await backend.read('c'))).toBe('CCC');
|
||||
expect(decodeBytes(await backend.read('a'))).toBe('AAA');
|
||||
expect(decodeBytes(await backend.read('c'))).toBe('CCC');
|
||||
|
||||
await backend.deleteMany(['a', 'c']);
|
||||
expect(await backend.exists('a')).toBe(false);
|
||||
@@ -94,8 +95,8 @@ describe('AriaEngine — EncryptedBackend 单元', () => {
|
||||
expect((rawX as ArrayBuffer).byteLength).toBe((rawY as ArrayBuffer).byteLength);
|
||||
|
||||
// 但解密一致
|
||||
expect(new TextDecoder().decode(await backend.read('x'))).toBe('same plaintext');
|
||||
expect(new TextDecoder().decode(await backend.read('y'))).toBe('same plaintext');
|
||||
expect(decodeBytes(await backend.read('x'))).toBe('same plaintext');
|
||||
expect(decodeBytes(await backend.read('y'))).toBe('same plaintext');
|
||||
await backend.close();
|
||||
});
|
||||
|
||||
@@ -115,7 +116,7 @@ describe('AriaEngine — EncryptedBackend 单元', () => {
|
||||
const backend2 = new EncryptedBackend(inner, 'pw');
|
||||
await backend2.open('enc-unit-4');
|
||||
await backend2.write('k2', new TextEncoder().encode('v2').buffer);
|
||||
expect(new TextDecoder().decode(await backend2.read('k2'))).toBe('v2');
|
||||
expect(decodeBytes(await backend2.read('k2'))).toBe('v2');
|
||||
|
||||
// 换密码被拒(keymeta 与旧密码绑定)——注意:MemoryBackend close 清空 store,
|
||||
// 因此在 close 前验证(backend2 仍持有 inner)
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
beforeEach(() => { resetOPFSMock(); });
|
||||
|
||||
describe('AriaEngine — 补充测试', () => {
|
||||
let engine: AriaEngine;
|
||||
|
||||
@@ -7,9 +7,9 @@ import { createSchema } from '../../src/table/schema';
|
||||
import { MemoryBackend } from '../../src/engine/aria/store/backend';
|
||||
import { checkFieldType } from '../../src/table/schema';
|
||||
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
beforeEach(() => { resetOPFSMock(); });
|
||||
|
||||
describe('AriaEngine — 最终扩展测试', () => {
|
||||
let engine: AriaEngine;
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
*/
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
|
||||
let counter = 0;
|
||||
function uniqueDB(): string {
|
||||
return `idxrace-${Date.now()}-${++counter}-${Math.random().toString(36).slice(2, 6)}`;
|
||||
}
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
beforeEach(() => { resetOPFSMock(); });
|
||||
|
||||
describe('AriaEngine — 二级索引完整性(P0 回归)', () => {
|
||||
it('5 万行写入:索引查询与主表一致(修复前丢 106~771 条)', async () => {
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
beforeEach(() => { resetOPFSMock(); });
|
||||
|
||||
describe('AriaEngine — 二级索引查询', () => {
|
||||
let engine: AriaEngine;
|
||||
|
||||
@@ -275,7 +275,7 @@ describe('MetonaSqlark + diskEngine: kv(高层 API)', () => {
|
||||
});
|
||||
|
||||
// 事务回滚
|
||||
await expect(db.transaction(async (trx) => {
|
||||
await expect(db.transaction(async (trx: { table: (n: string) => any }) => {
|
||||
await trx.table('users').insert({ id: '1' });
|
||||
throw new Error('boom');
|
||||
})).rejects.toThrow('boom');
|
||||
|
||||
@@ -12,9 +12,9 @@ import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { DatabaseLock, lockName } from '../../src/engine/aria/locks';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
beforeEach(() => { resetOPFSMock(); });
|
||||
|
||||
let idbCounter = 0;
|
||||
function uniqueDB(): string {
|
||||
|
||||
@@ -7,9 +7,10 @@ import { createSchema } from '../../src/table/schema';
|
||||
import { QueryExecutor } from '../../src/query/executor';
|
||||
import { parse } from '../../src/sql/parser';
|
||||
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
import { object } from '../helpers/assertions';
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
beforeEach(() => { resetOPFSMock(); });
|
||||
|
||||
describe('AriaEngine — ANALYZE/VACUUM/REINDEX + EXPLAIN', () => {
|
||||
let engine: AriaEngine;
|
||||
@@ -78,7 +79,9 @@ describe('AriaEngine — ANALYZE/VACUUM/REINDEX + EXPLAIN', () => {
|
||||
const executor = new QueryExecutor(engine);
|
||||
const selectStmt = parse('SELECT * FROM users WHERE id = \'1\'');
|
||||
const explainStmt: any = { type: 'EXPLAIN', query: selectStmt };
|
||||
const plan = await executor.execute(explainStmt);
|
||||
const plan = object<{ type: string; table: string; usingIndex?: unknown; actualTimeMs: number }>(
|
||||
await executor.execute(explainStmt),
|
||||
);
|
||||
expect(plan.type).toBe('SELECT');
|
||||
expect(plan.table).toBe('users');
|
||||
expect(plan.usingIndex).toBeDefined();
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
import { SharedMemoryBackend } from '../../src/engine/kvstore/shared_memory_medium';
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
|
||||
let counter = 0;
|
||||
function uniqueDB(): string {
|
||||
@@ -56,7 +56,7 @@ async function reopen(dbName: string, config: Record<string, unknown>): Promise<
|
||||
|
||||
beforeEach(() => {
|
||||
SharedMemoryBackend.clearRegistry();
|
||||
installOPFSMock(new Map());
|
||||
resetOPFSMock();
|
||||
});
|
||||
|
||||
describe('生产矩阵审计 — 后端 × 核心功能', () => {
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
beforeEach(() => { resetOPFSMock(); });
|
||||
|
||||
describe('AriaEngine — MVCC 事务 + Savepoint', () => {
|
||||
let engine: AriaEngine;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -20,7 +20,8 @@ import { PAGE_SIZE } from '../../src/engine/aria/types';
|
||||
// ===================================================================
|
||||
// OPFS mock(共享工具)
|
||||
// ===================================================================
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
import { decode as decodeBytes } from '../helpers/assertions';
|
||||
|
||||
const SCHEMA = () => createSchema('users', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
@@ -47,7 +48,7 @@ describe('AriaEngine — PageSSTableStore 单元', () => {
|
||||
|
||||
const back = await store.load(1, pageIds, data.byteLength);
|
||||
expect(back).not.toBeNull();
|
||||
expect(new TextDecoder().decode(back)).toBe('hello page store');
|
||||
expect(decodeBytes(back)).toBe('hello page store');
|
||||
|
||||
// 页面已落盘(backend 有 pg_ 键)
|
||||
expect(await backend.exists('pg_1')).toBe(true);
|
||||
@@ -130,7 +131,7 @@ describe('AriaEngine — PageSSTableStore 单元', () => {
|
||||
// ===================================================================
|
||||
describe('AriaEngine — 页面化 SSTable 集成(OPFS)', () => {
|
||||
it('写入 → flush → close → reopen 数据完整(页面模式默认启用)', async () => {
|
||||
const files = installOPFSMock(new Map());
|
||||
resetOPFSMock();
|
||||
const engine = new AriaEngine({
|
||||
storageBackend: 'opfs',
|
||||
memtableSizeThreshold: 64 * 1024 * 1024,
|
||||
@@ -173,7 +174,7 @@ describe('AriaEngine — 页面化 SSTable 集成(OPFS)', () => {
|
||||
});
|
||||
|
||||
it('多级 compaction 后页面化数据仍完整', async () => {
|
||||
const files = installOPFSMock(new Map());
|
||||
resetOPFSMock();
|
||||
const engine = new AriaEngine({
|
||||
storageBackend: 'opfs',
|
||||
memtableSizeThreshold: 16 * 1024,
|
||||
@@ -204,7 +205,7 @@ describe('AriaEngine — 页面化 SSTable 集成(OPFS)', () => {
|
||||
});
|
||||
|
||||
it('页面损坏(篡改 pg_ 文件)→ 打开自愈清理,其余数据可读', async () => {
|
||||
const files = installOPFSMock(new Map());
|
||||
resetOPFSMock();
|
||||
const engine = new AriaEngine({
|
||||
storageBackend: 'opfs',
|
||||
memtableSizeThreshold: 64 * 1024 * 1024,
|
||||
@@ -245,7 +246,7 @@ describe('AriaEngine — 页面化 SSTable 集成(OPFS)', () => {
|
||||
});
|
||||
|
||||
it('页面模式与非页面模式混合兼容(pageIds 缺失 → 整 value 读取)', async () => {
|
||||
const { files } = installOPFSMock(new Map());
|
||||
const opfs = resetOPFSMock();
|
||||
// 阶段 1:非页面模式写入(pageStorage: false → 整 value SSTable)
|
||||
const engine = new AriaEngine({
|
||||
storageBackend: 'opfs',
|
||||
@@ -259,7 +260,7 @@ describe('AriaEngine — 页面化 SSTable 集成(OPFS)', () => {
|
||||
await (engine as any).lsm.flush();
|
||||
await engine.close();
|
||||
|
||||
const backendKeys1 = Array.from(files.keys());
|
||||
const backendKeys1 = await opfs.listKeys();
|
||||
expect(backendKeys1.some((k) => k.startsWith('sst_'))).toBe(true);
|
||||
|
||||
// 阶段 2:页面模式打开(默认 opfs → 启用),读旧数据 + 写新数据
|
||||
@@ -285,7 +286,7 @@ describe('AriaEngine — 页面化 SSTable 集成(OPFS)', () => {
|
||||
});
|
||||
|
||||
it('页面化 + 加密 + 压缩组合:往返完整', async () => {
|
||||
const files = installOPFSMock(new Map());
|
||||
resetOPFSMock();
|
||||
const engine = new AriaEngine({
|
||||
storageBackend: 'opfs',
|
||||
compression: true,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
import { SharedMemoryBackend } from '../../src/engine/kvstore/shared_memory_medium';
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
|
||||
let counter = 0;
|
||||
function uniqueDB(): string {
|
||||
@@ -30,7 +30,7 @@ const SCHEMA = () => createSchema('big', {
|
||||
|
||||
beforeEach(() => {
|
||||
SharedMemoryBackend.clearRegistry();
|
||||
installOPFSMock(new Map());
|
||||
resetOPFSMock();
|
||||
});
|
||||
|
||||
describe('AriaEngine — 生产负载验证', () => {
|
||||
@@ -317,6 +317,7 @@ describe('AriaEngine — 生产负载验证', () => {
|
||||
// 修复后本机 ~12.5s。CI(debian runner 慢 2~3 倍、重型套件串行)下
|
||||
// 健康耗时约 30~80s;护栏放宽到 240s —— 仍能拦截性能悬崖回归(353s >> 240s),
|
||||
// 不误报健康慢环境。
|
||||
// eslint-disable-next-line no-console -- 性能护栏需要输出实测耗时
|
||||
console.log(`10万行 kv 插入耗时: ${insertMs}ms`);
|
||||
expect(insertMs).toBeLessThan(240000);
|
||||
expect(await engine.count('big')).toBe(TOTAL);
|
||||
@@ -364,6 +365,7 @@ describe('AriaEngine — 生产负载验证', () => {
|
||||
}
|
||||
const insertMs = Date.now() - t0;
|
||||
// 同上:CI 慢环境护栏放宽(本机 ~25s;悬崖回归仍会被拦截)
|
||||
// eslint-disable-next-line no-console -- 性能护栏需要输出实测耗时
|
||||
console.log(`10万行 opfs 插入耗时: ${insertMs}ms`);
|
||||
expect(insertMs).toBeLessThan(300000);
|
||||
expect(await engine.count('big')).toBe(TOTAL);
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
*/
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
beforeEach(() => { resetOPFSMock(); });
|
||||
|
||||
let idbCounter = 0;
|
||||
function uniqueDB(): string {
|
||||
@@ -65,20 +65,20 @@ describe('AriaEngine — repair 自愈增强', () => {
|
||||
|
||||
it('清理 OPFS 残留临时文件(.crswap/.tmp)', async () => {
|
||||
// 用 OPFS mock 后端验证 cleanupStaleFiles 被调用
|
||||
const { files, dir } = installOPFSMock(new Map());
|
||||
const opfs = resetOPFSMock();
|
||||
const engine = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
|
||||
await engine.open('repair-opfs-1', 1);
|
||||
await engine.createTable(SCHEMA());
|
||||
await engine.insert('items', [{ id: 'a', val: 1, tag: 'x' }]);
|
||||
|
||||
// 制造残留
|
||||
await dir.getFileHandle('junk.crswap', { create: true });
|
||||
await dir.getFileHandle('junk2.tmp', { create: true });
|
||||
expect(Array.from(files.keys()).some((k) => k.endsWith('.crswap'))).toBe(true);
|
||||
// 制造残留(模拟 createWritable 中断留下的临时文件)
|
||||
await opfs.createFile('junk.crswap');
|
||||
await opfs.createFile('junk2.tmp');
|
||||
expect((await opfs.listKeys()).some((k) => k.endsWith('.crswap'))).toBe(true);
|
||||
|
||||
await (engine as any).repair();
|
||||
|
||||
const after = Array.from(files.keys());
|
||||
const after = await opfs.listKeys();
|
||||
expect(after.some((k) => k.endsWith('.crswap'))).toBe(false);
|
||||
expect(after.some((k) => k.endsWith('.tmp'))).toBe(false);
|
||||
expect(await engine.count('items')).toBe(1);
|
||||
@@ -193,7 +193,7 @@ describe('AriaEngine — 随机操作压力 + 模拟崩溃', () => {
|
||||
});
|
||||
|
||||
it('随机操作 + 页面化 + 模拟崩溃 → 重开验证', async () => {
|
||||
const files = installOPFSMock(new Map());
|
||||
resetOPFSMock();
|
||||
const dbName = `rand-opfs-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
||||
const engine = new AriaEngine({
|
||||
storageBackend: 'opfs',
|
||||
|
||||
@@ -14,9 +14,9 @@ import { MemoryBackend } from '../../src/engine/aria/store/backend';
|
||||
import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
beforeEach(() => { resetOPFSMock(); });
|
||||
|
||||
let idbCounter = 0;
|
||||
function uniqueDB(): string {
|
||||
|
||||
@@ -9,9 +9,9 @@ import { AriaEngine } from '../../src/engine/aria/index';
|
||||
import { createSchema } from '../../src/table/schema';
|
||||
import { MetonaSqlark } from '../../src/core';
|
||||
|
||||
import { installOPFSMock } from '../helpers/opfs-mock';
|
||||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||||
|
||||
beforeEach(() => { installOPFSMock(new Map()); });
|
||||
beforeEach(() => { resetOPFSMock(); });
|
||||
|
||||
// ===================================================================
|
||||
// AriaEngine 引擎级测试 (Memory Backend)
|
||||
|
||||
@@ -24,7 +24,7 @@ function uniqueDB(): string {
|
||||
}
|
||||
|
||||
const enc = (s: string) => new TextEncoder().encode(s).buffer as ArrayBuffer;
|
||||
const dec = (b: ArrayBuffer | null) => (b ? new TextDecoder().decode(b) : null);
|
||||
const dec = (b: ArrayBuffer | null | undefined) => (b ? new TextDecoder().decode(b) : null);
|
||||
|
||||
beforeEach(() => {
|
||||
SharedMemoryBackend.clearRegistry();
|
||||
|
||||
Reference in New Issue
Block a user