Files
MetonaSqlark/tests/engine/aria-matrix-audit.test.ts
thzxx 0dba1abf2a 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
2026-09-14 21:03:06 +08:00

318 lines
13 KiB
TypeScript
Raw Permalink 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.
/**
* 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 → b1tag/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);
});