工作流 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
318 lines
13 KiB
TypeScript
318 lines
13 KiB
TypeScript
/**
|
||
* v0.6.1 生产矩阵审计 — 模式 × 功能全组合验收
|
||
*
|
||
* 后端(opfs/kv/memory)× 特性(页面化/加密/压缩/WAL开关/同步模式)×
|
||
* 功能(CRUD/二级索引/事务/崩溃恢复/主键变更一致性)
|
||
*
|
||
* 目标:全部组合生产级可用。任一组合同等断言 —— 数据完整、索引一致、崩溃恢复不丢。
|
||
*/
|
||
import { AriaEngine } from '../../src/engine/aria/index';
|
||
import { createSchema } from '../../src/table/schema';
|
||
import { SharedMemoryBackend } from '../../src/engine/kvstore/shared_memory_medium';
|
||
import { resetOPFSMock } from '../helpers/storage-harness';
|
||
|
||
let counter = 0;
|
||
function uniqueDB(): string {
|
||
return `mx-${Date.now()}-${++counter}-${Math.random().toString(36).slice(2, 6)}`;
|
||
}
|
||
|
||
const SCHEMA = () => createSchema('big', {
|
||
id: { type: 'string', primaryKey: true },
|
||
val: { type: 'number' },
|
||
tag: { type: 'string', index: true },
|
||
grp: { type: 'number', index: true },
|
||
name: { type: 'string' },
|
||
});
|
||
|
||
/** 写入 total 行(tag/grp 双索引) */
|
||
async function seed(engine: AriaEngine, table: string, total: number): Promise<void> {
|
||
for (let batch = 0; batch < total / 1000; batch++) {
|
||
const rows = [] as Record<string, unknown>[];
|
||
for (let i = 0; i < 1000; i++) {
|
||
const idx = batch * 1000 + i;
|
||
rows.push({ id: `k${idx}`, val: idx, tag: `t${idx % 10}`, grp: idx % 5, name: `U${idx}` });
|
||
}
|
||
await engine.insert(table, rows);
|
||
}
|
||
}
|
||
|
||
/** 双索引完整性断言 */
|
||
async function assertIndexes(engine: AriaEngine, table: string, total: number): Promise<void> {
|
||
expect(await engine.count(table)).toBe(total);
|
||
for (let t = 0; t < 10; t++) {
|
||
expect(await engine.find(table, { table, where: { tag: `t${t}` } })).toHaveLength(total / 10);
|
||
}
|
||
for (let g = 0; g < 5; g++) {
|
||
expect(await engine.find(table, { table, where: { grp: g } })).toHaveLength(total / 5);
|
||
}
|
||
}
|
||
|
||
/** 崩溃恢复:关闭后端 → 新引擎重开同库 */
|
||
async function reopen(dbName: string, config: Record<string, unknown>): Promise<AriaEngine> {
|
||
const engine2 = new AriaEngine(config as never);
|
||
await engine2.open(dbName, 1);
|
||
return engine2;
|
||
}
|
||
|
||
beforeEach(() => {
|
||
SharedMemoryBackend.clearRegistry();
|
||
resetOPFSMock();
|
||
});
|
||
|
||
describe('生产矩阵审计 — 后端 × 核心功能', () => {
|
||
const coreConfig = (backend: string): Record<string, unknown> => ({
|
||
storageBackend: backend,
|
||
memtableSizeThreshold: 256 * 1024,
|
||
checkpointInterval: 5000,
|
||
walSyncMode: 'full',
|
||
});
|
||
|
||
it('kv × 3 万行 + 双索引 + 事务 + 崩溃恢复', async () => {
|
||
const dbName = uniqueDB();
|
||
const engine = new AriaEngine(coreConfig('kv') as never);
|
||
await engine.open(dbName, 1);
|
||
await engine.createTable(SCHEMA());
|
||
await seed(engine, 'big', 30000);
|
||
// 事务:提交一批更新
|
||
await engine.beginTransaction();
|
||
for (let i = 0; i < 100; i++) {
|
||
await engine.update('big', { table: 'big', where: { id: `k${i}` } }, { name: `Tx${i}` });
|
||
}
|
||
await engine.commitTransaction();
|
||
await assertIndexes(engine, 'big', 30000);
|
||
const row = await engine.find('big', { table: 'big', where: { id: 'k50' } });
|
||
expect(row[0].name).toBe('Tx50');
|
||
// 事务回滚不产生残留
|
||
await engine.beginTransaction();
|
||
for (let i = 1000; i < 1100; i++) {
|
||
await engine.update('big', { table: 'big', where: { id: `k${i}` } }, { name: `Rx${i}` });
|
||
}
|
||
await engine.rollbackTransaction();
|
||
expect((await engine.find('big', { table: 'big', where: { id: 'k1050' } }))[0].name).toBe('U1050');
|
||
// 崩溃恢复
|
||
await (engine as any).backend.close();
|
||
(engine as any).opened = false;
|
||
const engine2 = await reopen(dbName, coreConfig('kv'));
|
||
await assertIndexes(engine2, 'big', 30000);
|
||
expect((await engine2.find('big', { table: 'big', where: { id: 'k50' } }))[0].name).toBe('Tx50');
|
||
await engine2.close();
|
||
}, 600000);
|
||
|
||
it('opfs × 3 万行 + 双索引 + 崩溃恢复', async () => {
|
||
const dbName = uniqueDB();
|
||
const engine = new AriaEngine(coreConfig('opfs') as never);
|
||
await engine.open(dbName, 1);
|
||
await engine.createTable(SCHEMA());
|
||
await seed(engine, 'big', 30000);
|
||
await assertIndexes(engine, 'big', 30000);
|
||
await (engine as any).backend.close();
|
||
(engine as any).opened = false;
|
||
const engine2 = await reopen(dbName, coreConfig('opfs'));
|
||
await assertIndexes(engine2, 'big', 30000);
|
||
await engine2.close();
|
||
}, 600000);
|
||
|
||
it('memory × 2 万行 + 双索引 + 事务回滚', async () => {
|
||
const engine = new AriaEngine(coreConfig('memory') as never);
|
||
await engine.open(uniqueDB(), 1);
|
||
await engine.createTable(SCHEMA());
|
||
await seed(engine, 'big', 20000);
|
||
await engine.beginTransaction();
|
||
await engine.update('big', { table: 'big', where: { id: 'k7' } }, { tag: 't9' });
|
||
await engine.rollbackTransaction();
|
||
await assertIndexes(engine, 'big', 20000);
|
||
// 更新后索引一致(无脏数据)——k7 从 t7 移到 t9
|
||
await engine.update('big', { table: 'big', where: { id: 'k7' } }, { tag: 't9' });
|
||
expect(await engine.find('big', { table: 'big', where: { tag: 't9' } })).toHaveLength(2001);
|
||
expect(await engine.find('big', { table: 'big', where: { tag: 't7' } })).toHaveLength(1999);
|
||
await engine.close();
|
||
}, 600000);
|
||
});
|
||
|
||
describe('生产矩阵审计 — 特性组合', () => {
|
||
it('kv × 加密 × 压缩 × 页面化(默认开)→ 2 万行 + 崩溃恢复', async () => {
|
||
const dbName = uniqueDB();
|
||
const cfg = {
|
||
storageBackend: 'kv', memtableSizeThreshold: 256 * 1024, checkpointInterval: 5000,
|
||
walSyncMode: 'full', compression: true, encryption: { password: 'matrix-secret' },
|
||
};
|
||
const engine = new AriaEngine(cfg as never);
|
||
await engine.open(dbName, 1);
|
||
await engine.createTable(SCHEMA());
|
||
await seed(engine, 'big', 20000);
|
||
await assertIndexes(engine, 'big', 20000);
|
||
await (engine as any).backend.close();
|
||
(engine as any).opened = false;
|
||
const engine2 = await reopen(dbName, cfg);
|
||
await assertIndexes(engine2, 'big', 20000);
|
||
await engine2.close();
|
||
// 错误密码必须拒绝打开
|
||
const bad = new AriaEngine({ ...cfg, encryption: { password: 'wrong' } } as never);
|
||
await expect(bad.open(dbName, 1)).rejects.toThrow();
|
||
}, 600000);
|
||
|
||
it('kv × pageStorage:false(整 value)→ 2 万行 + 崩溃恢复', async () => {
|
||
const dbName = uniqueDB();
|
||
const cfg = {
|
||
storageBackend: 'kv', memtableSizeThreshold: 256 * 1024, checkpointInterval: 5000,
|
||
walSyncMode: 'full', pageStorage: false,
|
||
};
|
||
const engine = new AriaEngine(cfg as never);
|
||
await engine.open(dbName, 1);
|
||
await engine.createTable(SCHEMA());
|
||
await seed(engine, 'big', 20000);
|
||
await assertIndexes(engine, 'big', 20000);
|
||
await (engine as any).backend.close();
|
||
(engine as any).opened = false;
|
||
const engine2 = await reopen(dbName, cfg);
|
||
await assertIndexes(engine2, 'big', 20000);
|
||
await engine2.close();
|
||
}, 600000);
|
||
|
||
it('opfs × 加密 × 压缩 × 页面化全开 → 2 万行 + 崩溃恢复', async () => {
|
||
const dbName = uniqueDB();
|
||
const cfg = {
|
||
storageBackend: 'opfs', memtableSizeThreshold: 256 * 1024, checkpointInterval: 5000,
|
||
walSyncMode: 'full', compression: true, encryption: { password: 'matrix-secret' },
|
||
pageStorage: true,
|
||
};
|
||
const engine = new AriaEngine(cfg as never);
|
||
await engine.open(dbName, 1);
|
||
await engine.createTable(SCHEMA());
|
||
await seed(engine, 'big', 20000);
|
||
await assertIndexes(engine, 'big', 20000);
|
||
await (engine as any).backend.close();
|
||
(engine as any).opened = false;
|
||
const engine2 = await reopen(dbName, cfg);
|
||
await assertIndexes(engine2, 'big', 20000);
|
||
await engine2.close();
|
||
}, 600000);
|
||
|
||
it('opfs × walEnabled:false → 写入 + 优雅关闭后重开完整', async () => {
|
||
const dbName = uniqueDB();
|
||
const cfg = {
|
||
storageBackend: 'opfs', memtableSizeThreshold: 128 * 1024, checkpointInterval: 2000,
|
||
walEnabled: false, pageStorage: false,
|
||
};
|
||
const engine = new AriaEngine(cfg as never);
|
||
await engine.open(dbName, 1);
|
||
await engine.createTable(SCHEMA());
|
||
await seed(engine, 'big', 20000);
|
||
await assertIndexes(engine, 'big', 20000);
|
||
// 优雅关闭:close 等待后台链 + checkpoint 落盘 → 重开完整
|
||
await engine.close();
|
||
const engine2 = await reopen(dbName, cfg);
|
||
await assertIndexes(engine2, 'big', 20000);
|
||
await engine2.close();
|
||
}, 600000);
|
||
|
||
it('kv × walEnabled:false → 写入 + 优雅关闭后重开完整', async () => {
|
||
const dbName = uniqueDB();
|
||
const cfg = {
|
||
storageBackend: 'kv', memtableSizeThreshold: 128 * 1024, checkpointInterval: 2000,
|
||
walEnabled: false, pageStorage: false,
|
||
};
|
||
const engine = new AriaEngine(cfg as never);
|
||
await engine.open(dbName, 1);
|
||
await engine.createTable(SCHEMA());
|
||
await seed(engine, 'big', 20000);
|
||
await assertIndexes(engine, 'big', 20000);
|
||
await engine.close();
|
||
const engine2 = await reopen(dbName, cfg);
|
||
await assertIndexes(engine2, 'big', 20000);
|
||
await engine2.close();
|
||
}, 600000);
|
||
|
||
it('kv × walSyncMode:batch → 优雅关闭后重开完整(崩溃保底已 checkpoint 数据)', async () => {
|
||
const dbName = uniqueDB();
|
||
const cfg = {
|
||
storageBackend: 'kv', memtableSizeThreshold: 256 * 1024, checkpointInterval: 3000,
|
||
walSyncMode: 'batch',
|
||
};
|
||
const engine = new AriaEngine(cfg as never);
|
||
await engine.open(dbName, 1);
|
||
await engine.createTable(SCHEMA());
|
||
await seed(engine, 'big', 20000);
|
||
await assertIndexes(engine, 'big', 20000);
|
||
await engine.close();
|
||
const engine2 = await reopen(dbName, cfg);
|
||
await assertIndexes(engine2, 'big', 20000);
|
||
await engine2.close();
|
||
}, 600000);
|
||
|
||
it('opfs × walSyncMode:none → 优雅关闭后重开完整(checkpoint 兜底)', async () => {
|
||
const dbName = uniqueDB();
|
||
const cfg = {
|
||
storageBackend: 'opfs', memtableSizeThreshold: 128 * 1024, checkpointInterval: 3000,
|
||
walSyncMode: 'none', pageStorage: false,
|
||
};
|
||
const engine = new AriaEngine(cfg as never);
|
||
await engine.open(dbName, 1);
|
||
await engine.createTable(SCHEMA());
|
||
await seed(engine, 'big', 20000);
|
||
await assertIndexes(engine, 'big', 20000);
|
||
await engine.close();
|
||
const engine2 = await reopen(dbName, cfg);
|
||
await assertIndexes(engine2, 'big', 20000);
|
||
await engine2.close();
|
||
}, 600000);
|
||
});
|
||
|
||
describe('生产矩阵审计 — 主键变更索引一致性(三后端)', () => {
|
||
it('kv × 主键变更:新旧索引无脏数据', async () => {
|
||
const engine = new AriaEngine({ storageBackend: 'kv', memtableSizeThreshold: 256 * 1024, checkpointInterval: 100000 } as never);
|
||
await engine.open(uniqueDB(), 1);
|
||
await engine.createTable(SCHEMA());
|
||
await engine.insert('big', [{ id: 'a1', val: 1, tag: 'x', grp: 1, name: 'A' }]);
|
||
// 主键 a1 → b1(tag/grp 不变)
|
||
await engine.update('big', { table: 'big', where: { id: 'a1' } }, { id: 'b1' });
|
||
expect(await engine.count('big')).toBe(1);
|
||
expect(await engine.find('big', { table: 'big', where: { tag: 'x' } })).toHaveLength(1);
|
||
expect(await engine.find('big', { table: 'big', where: { id: 'a1' } })).toHaveLength(0);
|
||
await engine.close();
|
||
}, 300000);
|
||
|
||
it('opfs × 主键变更 + 崩溃恢复:索引一致', async () => {
|
||
const dbName = uniqueDB();
|
||
const engine = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 256 * 1024, checkpointInterval: 100000, pageStorage: false } as never);
|
||
await engine.open(dbName, 1);
|
||
await engine.createTable(SCHEMA());
|
||
for (let i = 0; i < 5000; i++) {
|
||
await engine.insert('big', [{ id: `k${i}`, val: i, tag: `t${i % 10}`, grp: i % 5, name: `U${i}` }]);
|
||
}
|
||
for (let i = 0; i < 5000; i += 10) {
|
||
await engine.update('big', { table: 'big', where: { id: `k${i}` } }, { id: `p${i}` });
|
||
}
|
||
await (engine as any).backend.close();
|
||
(engine as any).opened = false;
|
||
const engine2 = new AriaEngine({ storageBackend: 'opfs', memtableSizeThreshold: 256 * 1024, checkpointInterval: 100000, pageStorage: false } as never);
|
||
await engine2.open(dbName, 1);
|
||
expect(await engine2.count('big')).toBe(5000);
|
||
for (let t = 0; t < 10; t++) {
|
||
expect(await engine2.find('big', { table: 'big', where: { tag: `t${t}` } })).toHaveLength(500);
|
||
}
|
||
await engine2.close();
|
||
}, 600000);
|
||
|
||
it('memory × 级联删除 + 索引清理', async () => {
|
||
const engine = new AriaEngine({ storageBackend: 'memory', memtableSizeThreshold: 256 * 1024 } as never);
|
||
await engine.open(uniqueDB(), 1);
|
||
await engine.createTable(createSchema('parent', {
|
||
id: { type: 'string', primaryKey: true },
|
||
tag: { type: 'string', index: true },
|
||
}));
|
||
await engine.createTable(createSchema('child', {
|
||
id: { type: 'string', primaryKey: true },
|
||
pid: { type: 'string', references: 'parent.id', onDelete: 'CASCADE', index: true },
|
||
}));
|
||
for (let i = 0; i < 2000; i++) {
|
||
await engine.insert('parent', [{ id: `p${i}`, tag: `t${i % 10}` }]);
|
||
await engine.insert('child', [{ id: `c${i}`, pid: `p${i}` }]);
|
||
}
|
||
await engine.delete('parent', { table: 'parent', where: { id: 'p5' } });
|
||
expect(await engine.count('child')).toBe(1999);
|
||
expect(await engine.find('child', { table: 'child', where: { pid: 'p5' } })).toHaveLength(0);
|
||
await engine.close();
|
||
}, 300000);
|
||
});
|