Files
MetonaSqlark/tests/v043-hardening.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

270 lines
11 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.4.2 生产就绪审计(第三轮)
* 覆盖:
* - P0: Aria 事务进行中 checkpoint 截断 WAL → 崩溃恢复丢事务数据
* - P1: Aria dropTable / ALTER DROP 索引列 的二级索引清理
* - P1: OPFS 并发写乱序丢更新 + 空表/schema/索引持久化
* - P2: Aria memtable 阈值衰减、红黑树随机压力、onUpdate 级联、事务 DDL 显式拒绝
*/
import { AriaEngine } from '../src/engine/aria/index';
import { createSchema } from '../src/table/schema';
import { MemTable } from '../src/engine/aria/index/memtable';
import { MetonaSqlark } from '../src/core';
import { resetOPFSMock } from './helpers/storage-harness';
beforeEach(() => { resetOPFSMock(); });
let idbCounter = 0;
function uniqueDB(): string {
return `prod-${Date.now()}-${++idbCounter}-${Math.random().toString(36).slice(2, 8)}`;
}
// ===================================================================
// OPFS mock(模拟真实 I/OgetFileHandle 延迟 + 写入按内容差异化耗时)
// ===================================================================
// ===================================================================
// P0: 事务与 checkpoint 冲突
// ===================================================================
describe('P0 — 事务进行中 checkpoint 不得截断 WAL', () => {
it('事务中触发 checkpoint → 崩溃恢复不丢事务数据', async () => {
const dbName = uniqueDB();
const engine = new AriaEngine({
storageBackend: 'opfs',
checkpointInterval: 2, // 每 2 次操作即触发 checkpoint(事务中途)
});
await engine.open(dbName, 1);
await engine.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
v: { type: 'number' },
}));
await engine.beginTransaction();
await engine.insert('t', [{ id: '1', v: 1 }]);
await engine.insert('t', [{ id: '2', v: 2 }]); // opCounter=2 → tick → checkpoint(修复前截断 WAL
await engine.commitTransaction();
// 模拟异常退出(不 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);
// 修复前:checkpoint 截断了事务的 WAL 记录 → 恢复后数据丢失
expect(await engine2.count('t')).toBe(2);
await engine2.close();
});
it('事务回滚后再 checkpoint 正常截断', async () => {
const dbName = uniqueDB();
const engine = new AriaEngine({
storageBackend: 'opfs',
checkpointInterval: 1,
});
await engine.open(dbName, 1);
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
await engine.insert('t', [{ id: 'keep' }]);
await engine.beginTransaction();
await engine.insert('t', [{ id: 'tx1' }]);
await engine.rollbackTransaction();
// 事务结束后 checkpoint 可正常截断
await (engine as any).checkpointManager.checkpoint();
await (engine as any).backend.close();
(engine as any).opened = false;
const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
await engine2.open(dbName, 1);
expect(await engine2.count('t')).toBe(1);
await engine2.close();
});
});
// ===================================================================
// P1: Aria DDL 的二级索引清理
// ===================================================================
describe('P1 — Aria DDL 索引清理', () => {
it('dropTable 清理二级索引(重建同名表索引不脏)', async () => {
const engine = new AriaEngine({ storageBackend: 'memory' });
await engine.open(uniqueDB(), 1);
await engine.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', index: true },
}));
await engine.insert('t', [{ id: '1', email: 'a@x.com' }]);
await engine.dropTable('t');
// 索引 LSM 必须清理(修复前残留)
expect((engine as any).secondaryIndexes.size).toBe(0);
// 重建同名表 + 相同 id:旧索引残留会返回错误行
await engine.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', index: true },
}));
await engine.insert('t', [{ id: '1', email: 'b@x.com' }]);
// 旧索引残留场景:按旧 email 查询不得命中新行(主 LSM 有 t:1 → 修复前返回错误结果)
const byOld = await engine.find('t', { table: 't', where: { email: 'a@x.com' } });
expect(byOld).toHaveLength(0);
const byNew = await engine.find('t', { table: 't', where: { email: 'b@x.com' } });
expect(byNew).toHaveLength(1);
await engine.close();
});
it('ALTER TABLE DROP 索引列清理索引 LSM', async () => {
const engine = new AriaEngine({ storageBackend: 'memory' });
await engine.open(uniqueDB(), 1);
await engine.createTable(createSchema('t', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', index: true },
}));
await engine.insert('t', [{ id: '1', email: 'a@x.com' }]);
await engine.alterTable('t', 'DROP', { name: 'email', type: 'string' });
expect((engine as any).secondaryIndexes.size).toBe(0);
await engine.close();
});
it('DROP_TABLE 崩溃恢复同样清理索引', 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 },
email: { type: 'string', index: true },
}));
await engine.insert('t', [{ id: '1', email: 'a@x.com' }]);
await engine.dropTable('t');
// 模拟崩溃(不 closeWAL 含 DROP_TABLE
await (engine as any).backend.close();
(engine as any).opened = false;
const engine2 = new AriaEngine({ storageBackend: 'opfs', checkpointInterval: 100000 });
await engine2.open(dbName, 1);
// 表不存在,且无索引残留
expect(await engine2.hasTable('t')).toBe(false);
expect((engine2 as any).secondaryIndexes.size).toBe(0);
await engine2.close();
});
});
// ===================================================================
// P2: Aria 内部状态
// ===================================================================
describe('P2 — Aria 内部状态加固', () => {
it('freezeMemtable 后阈值不衰减', async () => {
const engine = new AriaEngine({
storageBackend: 'memory',
memtableSizeThreshold: 1024 * 1024,
});
await engine.open(uniqueDB(), 1);
await engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
// 少量写入 + 手动 flushfreezeMemtable 用旧表已用大小当新阈值 → 衰减)
for (let i = 0; i < 10; i++) await engine.insert('t', [{ id: `${i}` }]);
await (engine as any).lsm.flush();
// 修复前:新 memtable maxSize ≈ 已用字节(远小于配置)
const memtable = (engine as any).lsm.memtable;
expect((memtable as any).maxSize).toBeGreaterThanOrEqual(1024 * 1024);
await engine.close();
});
it('红黑树随机 insert/delete 5000 次保持有序且无丢失', () => {
const mem = new MemTable(1 << 30);
const reference = new Set<string>();
let seed = 42;
const rnd = () => {
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
return seed / 0x7fffffff;
};
for (let i = 0; i < 5000; i++) {
const k = `k${Math.floor(rnd() * 800)}`;
if (rnd() < 0.3) {
reference.delete(k);
mem.delete(k);
} else {
reference.add(k);
mem.put(k, { v: i });
}
}
const entries = mem.getAllEntries();
// 有序
for (let i = 1; i < entries.length; i++) {
expect(entries[i][0] > entries[i - 1][0]).toBe(true);
}
// 与引用集合完全一致(无丢失/无残留)
expect(entries.map(([k]) => k)).toEqual([...reference].sort());
expect(mem.getEntryCount()).toBe(reference.size);
});
it('onUpdate CASCADE:更新父表主键级联更新子表外键', async () => {
const db = new MetonaSqlark({ name: uniqueDB(), mode: 'memory' });
await db.init();
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string', references: 'users.id', onUpdate: 'CASCADE' },
});
await db.table('users').insert({ id: '1' });
await db.table('orders').insert({ id: 'o1', user_id: '1' });
// 更新父表主键 1 → 2
await db.table('users').update({ id: '2' }).where({ id: '1' }).execute();
const orders = await db.table('orders').select().execute();
expect(orders[0].user_id).toBe('2');
// 旧 id 不可再被引用
expect(await db.table('orders').count({ user_id: '1' })).toBe(0);
await db.close();
});
it('onUpdate RESTRICT:存在引用行时禁止更新主键', async () => {
const db = new MetonaSqlark({ name: uniqueDB(), mode: 'memory' });
await db.init();
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string', references: 'users.id', onUpdate: 'RESTRICT' },
});
await db.table('users').insert({ id: '1' });
await db.table('orders').insert({ id: 'o1', user_id: '1' });
await expect(
db.table('users').update({ id: '2' }).where({ id: '1' }).execute(),
).rejects.toThrow();
// 主键未被修改
const users = await db.table('users').select().execute();
expect(users[0].id).toBe('1');
await db.close();
});
it('onUpdate CASCADE:更新父表主键级联更新子表外键(Aria)', async () => {
const db = new MetonaSqlark({ name: uniqueDB(), mode: 'aria', diskEngine: 'memory' });
await db.init();
await db.defineTable('users', { id: { type: 'string', primaryKey: true } });
await db.defineTable('orders', {
id: { type: 'string', primaryKey: true },
user_id: { type: 'string', references: 'users.id', onUpdate: 'CASCADE' },
});
await db.table('users').insert({ id: '1' });
await db.table('orders').insert({ id: 'o1', user_id: '1' });
await db.table('users').update({ id: '2' }).where({ id: '1' }).execute();
const orders = await db.table('orders').select().execute();
expect(orders[0].user_id).toBe('2');
expect(await db.table('orders').count({ user_id: '1' })).toBe(0);
await db.close();
});
it('Aria 事务中 DDL 显式拒绝(NOT_SUPPORTED)而非静默不一致', async () => {
const engine = new AriaEngine({ storageBackend: 'memory' });
await engine.open(uniqueDB(), 1);
await engine.beginTransaction();
await expect(
engine.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } })),
).rejects.toMatchObject({ code: 'NOT_SUPPORTED' });
await engine.rollbackTransaction();
// 表未创建
expect(await engine.hasTable('t')).toBe(false);
await engine.close();
});
});