Files
MetonaSqlark/tests/v042-hardening.test.ts
T
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

316 lines
13 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.
/**
* v0.4.2 深度审计回归测试(第二轮)
* 覆盖:
* - P0-A: compaction 不依赖缓存(缓存未命中不得丢数据/数据不可见)
* - P0-B: flush/compaction 失败不得卡死 flushChain(写路径死锁)
* - P1-A: 重开后二级索引恢复 + createIndex 幂等重建 + WAL 恢复后索引一致
* - P1-B: ALTER TABLE 在 IndexedDB/Hybrid 引擎持久化
* - P1-C: IndexedDB 事务内 DDL(建表/删表)commit 后磁盘一致
*/
import { AriaEngine } from '../src/engine/aria/index';
import { createSchema } from '../src/table/schema';
import { MemoryEngine } from '../src/engine/memory';
import { HybridEngine } from '../src/hybrid/index';
import { MetonaSqlark } from '../src/core';
import { resetOPFSMock } from './helpers/storage-harness';
beforeEach(() => { resetOPFSMock(); });
let idbCounter = 0;
function uniqueDB(): string {
return `audit-${Date.now()}-${++idbCounter}-${Math.random().toString(36).slice(2, 8)}`;
}
// ===================================================================
// P0-A: compaction 不得依赖 SSTable 缓存
// ===================================================================
describe('P0-A — Compaction 缓存独立性', () => {
it('缓存上限小于单个 SSTable 时 compaction 不丢数据(此前数据不可见)', async () => {
const engine = new AriaEngine({
storageBackend: 'memory',
// 缓存上限 4 页 = 16KBmemtable 32KB → 每次 flush 的文件都超出缓存上限被驱逐
bufferPoolPages: 4,
memtableSizeThreshold: 32 * 1024,
checkpointInterval: 100000,
});
await engine.open(uniqueDB(), 1);
await engine.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
v: { type: 'number' },
data: { type: 'string' },
}));
for (let i = 0; i < 400; i++) {
await engine.insert('t', [{ id: `k${String(i).padStart(4, '0')}`, v: i, data: 'x'.repeat(200) }]);
}
// 等待 flush + compaction 链全部完成
await (engine as any).lsm.flush();
await new Promise((r) => setTimeout(r, 100));
// 修复前:compaction 缓存未命中跳过全部文件并从 levels 移除 → 查询为空
expect(await engine.count('t')).toBe(400);
await engine.close();
});
it('多层级 compaction 后数据仍完整且可重开', async () => {
const dbName = uniqueDB();
const engine = new AriaEngine({
storageBackend: 'opfs',
bufferPoolPages: 8, // 32KB 缓存
memtableSizeThreshold: 16 * 1024,
checkpointInterval: 100000,
});
await engine.open(dbName, 1);
await engine.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
v: { type: 'number' },
}));
for (let i = 0; i < 300; i++) {
await engine.insert('t', [{ id: `k${String(i).padStart(4, '0')}`, v: i }]);
}
await (engine as any).lsm.flush();
await new Promise((r) => setTimeout(r, 150));
const before = await engine.count('t');
expect(before).toBe(300);
await engine.close();
// 重开:数据完整(meta/文件未被 compaction 破坏)
const engine2 = new AriaEngine({
storageBackend: 'opfs',
bufferPoolPages: 8,
memtableSizeThreshold: 16 * 1024,
checkpointInterval: 100000,
});
await engine2.open(dbName, 1);
expect(await engine2.count('t')).toBe(300);
await engine2.close();
});
});
// ===================================================================
// P1-A: 二级索引跨重启恢复
// ===================================================================
describe('P1-A — 二级索引恢复', () => {
it('Aria 重开后二级索引可用(索引 LSM 持久化恢复)', async () => {
const dbName = uniqueDB();
const engine = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
await engine.open(dbName, 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', index: true },
}));
await engine.insert('users', [
{ id: '1', email: 'a@x.com' },
{ id: '2', email: 'b@x.com' },
{ id: '3', email: 'a@x.com' },
]);
await (engine as any).lsm.flush();
await engine.close();
// 重开:二级索引 LSM 应自动恢复(修复前为空 → 索引查询回退全表,createIndex 也静默跳过)
const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
await engine2.open(dbName, 1);
// 强断言:索引 LSM 真实存在且数据完整(防止"索引缺失静默回退全表扫描"的假通过)
const idxLsm = (engine2 as any).secondaryIndexes.get('users:idx:email');
expect(idxLsm).toBeDefined();
expect(idxLsm.getStats().sstableCount).toBeGreaterThan(0);
await idxLsm.prefetchRange('', '\uffff');
expect(idxLsm.rangeScan('', '\uffff')).toHaveLength(3);
const byEmail = await engine2.find('users', { table: 'users', where: { email: 'a@x.com' } });
expect(byEmail).toHaveLength(2);
// createIndex 对已持久化的索引列应幂等可重建(不得静默跳过导致索引永久缺失)
await engine2.createIndex('users', 'email');
await engine2.close();
});
it('WAL 恢复后二级索引与主数据一致(崩溃前索引未更新)', async () => {
const dbName = uniqueDB();
const engine = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
await engine.open(dbName, 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
city: { type: 'string', index: true },
}));
await engine.insert('users', [{ id: '1', city: 'Beijing' }]);
await (engine as any).lsm.flush();
// 写入 WAL 但强制不 flush(模拟崩溃:新行只在 WAL,索引 LSM 未更新)
await engine.insert('users', [{ id: '2', city: 'Shanghai' }]);
// 模拟异常退出(不 close
await (engine as any).backend.close();
(engine as any).opened = false;
const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
await engine2.open(dbName, 1);
// 强断言:索引 LSM 已恢复且包含 WAL 回放的行(崩溃前索引未更新,恢复后必须重建)
const idxLsm = (engine2 as any).secondaryIndexes.get('users:idx:city');
expect(idxLsm).toBeDefined();
await idxLsm.prefetchRange('', '\uffff');
expect(idxLsm.rangeScan('', '\uffff')).toHaveLength(2);
// 索引查询应看到 WAL 恢复的行(修复前索引与主数据不一致 → 丢行)
const byCity = await engine2.find('users', { table: 'users', where: { city: 'Shanghai' } });
expect(byCity).toHaveLength(1);
expect(byCity[0].id).toBe('2');
await engine2.close();
});
it('createIndex 重开后仍可新建(schema 标记恢复后不阻塞)', async () => {
const dbName = uniqueDB();
const engine = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
await engine.open(dbName, 1);
await engine.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
}));
await engine.insert('t', [{ id: '1', name: 'A' }]);
await engine.close();
const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
await engine2.open(dbName, 1);
await engine2.createIndex('t', 'name'); // 修复前 schema 无标记时正常;此处验证无标记场景
const byName = await engine2.find('t', { table: 't', where: { name: 'A' } });
expect(byName).toHaveLength(1);
await engine2.close();
});
});
// ===================================================================
// P1-B: ALTER TABLE 跨引擎持久化
// ===================================================================
describe('P1-B — ALTER TABLE 持久化', () => {
it('KVStoreEngine DROP COLUMN 后重启不复活', async () => {
const dbName = uniqueDB();
const db = new MetonaSqlark({ name: dbName, mode: 'disk', diskEngine: 'opfs' });
await db.init();
await db.defineTable('t', {
id: { type: 'string', primaryKey: true },
old_col: { type: 'string' },
keep: { type: 'string' },
});
await db.table('t').insert({ id: '1', old_col: 'x', keep: 'y' });
await db.query('ALTER TABLE t DROP COLUMN old_col');
// 行数据中该列已移除
const rows = await db.table('t').select().execute();
expect(rows[0].old_col).toBeUndefined();
await db.close();
// 重启:schema 不复活,列定义已持久化
const db2 = new MetonaSqlark({ name: dbName, mode: 'disk', diskEngine: 'opfs' });
await db2.init();
const schema2 = await db2.getEngine().getTableSchema('t');
expect(schema2!.columns.old_col).toBeUndefined();
expect(schema2!.columns.keep).toBeDefined();
await db2.close();
});
it('HybridEngine ADD COLUMN 后重启保留', async () => {
const dbName = uniqueDB();
const db = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'opfs' });
await db.init();
await db.defineTable('t', { id: { type: 'string', primaryKey: true } });
await db.query('ALTER TABLE t ADD COLUMN phone STRING');
await db.close();
const db2 = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'opfs' });
await db2.init();
const schema2 = await db2.getEngine().getTableSchema('t');
expect(schema2!.columns.phone).toBeDefined();
await db2.close();
});
});
// ===================================================================
// P1-C: KVStore 事务内 DDL
// ===================================================================
describe('P1-C — KVStore 事务内 DDL', () => {
it('事务内建表 → commit 后磁盘一致(重启表存在且可查)', async () => {
const dbName = uniqueDB();
const db = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'opfs' });
await db.init();
await db.defineTable('base', { id: { type: 'string', primaryKey: true } });
await db.table('base').insert({ id: '1' });
await db.transaction(async (trx) => {
await trx.table('base').insert({ id: '2' });
// 事务内建新表(此前 commit 时 IDB 无对应 store → 事务失败)
await db.defineTable('created_in_tx', { id: { type: 'string', primaryKey: true } });
await db.table('created_in_tx').insert({ id: 'tx1' });
});
expect(await db.table('created_in_tx').count()).toBe(1);
await db.close();
const db2 = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'opfs' });
await db2.init();
expect(await db2.table('created_in_tx').count()).toBe(1);
expect(await db2.table('base').count()).toBe(2);
await db2.close();
});
it('事务内删表 → commit 后磁盘一致(重启无幽灵表)', async () => {
const dbName = uniqueDB();
const db = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'opfs' });
await db.init();
await db.defineTable('ghost', { id: { type: 'string', primaryKey: true } });
await db.table('ghost').insert({ id: '1' });
await db.transaction(async () => {
await db.dropTable('ghost');
});
await db.close();
const db2 = new MetonaSqlark({ name: dbName, mode: 'hybrid', diskEngine: 'opfs' });
await db2.init();
const names = await db2.getTableNames();
expect(names).not.toContain('ghost');
await db2.close();
});
});
// ===================================================================
// 各引擎 clearAll / repair / 元数据 一致性抽查
// ===================================================================
describe('全模式抽查 — 生命周期与元数据', () => {
it('MemoryEngine clearAll/repair/getMeta/setMeta', async () => {
const e = new MemoryEngine();
await e.open('m', 1);
await e.createTable({ name: 't', columns: { id: { type: 'string', primaryKey: true } } });
await e.insert('t', [{ id: '1' }]);
await e.setMeta('k', 'v');
expect(await e.getMeta('k')).toBe('v');
await e.repair();
await e.clearAll();
expect(await e.getTableNames()).toEqual([]);
await e.close();
});
it('HybridEngine getMeta/setMeta 委托磁盘', async () => {
const dbName = uniqueDB();
const e = new HybridEngine('opfs');
await e.open(dbName, 1);
await e.setMeta('__metona_version', '7');
expect(await e.getMeta('__metona_version')).toBe('7');
await e.close();
const e2 = new HybridEngine('opfs');
await e2.open(dbName, 1);
expect(await e2.getMeta('__metona_version')).toBe('7');
await e2.close();
});
it('AriaEngine repair 幂等', async () => {
const dbName = uniqueDB();
const engine = new AriaEngine({ storageBackend: 'opfs' });
await engine.open(dbName, 1);
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
await engine.insert('t', [{ id: '1' }]);
await engine.repair();
await engine.repair();
expect(await engine.count('t')).toBe(1);
await engine.close();
});
});