工作流 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
140 lines
5.8 KiB
TypeScript
140 lines
5.8 KiB
TypeScript
/**
|
||
* v0.5.1 — CRUD 生命周期钩子真实接线测试
|
||
*
|
||
* 此前 beforeInsert/afterInsert/beforeUpdate/afterUpdate/beforeDelete/afterDelete
|
||
* 六个钩子虽在 HookName 类型与文档中宣称("14 种钩子"),但从未被任何路径触发。
|
||
* 本次验证 Table API 与 SQL query 两条路径均真实触发全部 14 个钩子。
|
||
*/
|
||
import { MetonaSqlark } from '../src/core';
|
||
|
||
async function createDb(name: string): Promise<MetonaSqlark> {
|
||
const db = new MetonaSqlark({ name: `hooks-${name}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, mode: 'memory' });
|
||
await db.init();
|
||
await db.defineTable('users', {
|
||
id: { type: 'string', primaryKey: true },
|
||
name: { type: 'string' },
|
||
age: { type: 'number' },
|
||
});
|
||
return db;
|
||
}
|
||
|
||
describe('v0.5.1 — CRUD hooks 真实接线', () => {
|
||
it('Table.insert / insertMany 触发 beforeInsert + afterInsert', async () => {
|
||
const db = await createDb('table-insert');
|
||
const events: string[] = [];
|
||
db.on('beforeInsert', (rows) => { events.push(`before:${(rows as unknown[]).length}`); });
|
||
db.on('afterInsert', (rows, pks) => { events.push(`after:${(rows as unknown[]).length}:${(pks as string[]).length}`); });
|
||
|
||
await db.table('users').insert({ id: '1', name: 'A' });
|
||
await db.table('users').insertMany([{ id: '2', name: 'B' }, { id: '3', name: 'C' }]);
|
||
|
||
expect(events).toEqual([
|
||
'before:1', 'after:1:1',
|
||
'before:2', 'after:2:2',
|
||
]);
|
||
await db.close();
|
||
});
|
||
|
||
it('Table.update().execute() 触发 beforeUpdate + afterUpdate(含影响行数)', async () => {
|
||
const db = await createDb('table-update');
|
||
await db.table('users').insertMany([{ id: '1', name: 'A', age: 1 }, { id: '2', name: 'B', age: 2 }]);
|
||
const events: string[] = [];
|
||
db.on('beforeUpdate', (query, updates) => { events.push(`before:${(updates as Record<string, unknown>).name}`); });
|
||
db.on('afterUpdate', (_query, _updates, count) => { events.push(`after:${count}`); });
|
||
|
||
await db.table('users').update({ name: 'X' }).where({ age: { $gt: 1 } }).execute();
|
||
|
||
expect(events).toEqual(['before:X', 'after:1']);
|
||
await db.close();
|
||
});
|
||
|
||
it('Table.delete().execute() 触发 beforeDelete + afterDelete(含影响行数)', async () => {
|
||
const db = await createDb('table-delete');
|
||
await db.table('users').insertMany([{ id: '1', name: 'A' }, { id: '2', name: 'B' }]);
|
||
const events: string[] = [];
|
||
db.on('beforeDelete', (query) => { events.push(`before:${(query as { where: Record<string, unknown> }).where.id}`); });
|
||
db.on('afterDelete', (_query, count) => { events.push(`after:${count}`); });
|
||
|
||
await db.table('users').delete().where({ id: '1' }).execute();
|
||
|
||
expect(events).toEqual(['before:1', 'after:1']);
|
||
await db.close();
|
||
});
|
||
|
||
it('SQL query 路径触发全部 CRUD hooks(INSERT/UPDATE/DELETE)', async () => {
|
||
const db = await createDb('sql-crud');
|
||
await db.query("INSERT INTO users VALUES ('1', 'Alice', 30)");
|
||
const events: string[] = [];
|
||
db.on('beforeInsert', () => { events.push('beforeInsert'); });
|
||
db.on('afterInsert', () => { events.push('afterInsert'); });
|
||
db.on('beforeUpdate', () => { events.push('beforeUpdate'); });
|
||
db.on('afterUpdate', () => { events.push('afterUpdate'); });
|
||
db.on('beforeDelete', () => { events.push('beforeDelete'); });
|
||
db.on('afterDelete', () => { events.push('afterDelete'); });
|
||
|
||
await db.query("INSERT INTO users VALUES ('2', 'Bob', 25)");
|
||
await db.query("UPDATE users SET age = 26 WHERE id = '2'");
|
||
await db.query("DELETE FROM users WHERE id = '1'");
|
||
|
||
expect(events).toEqual([
|
||
'beforeInsert', 'afterInsert',
|
||
'beforeUpdate', 'afterUpdate',
|
||
'beforeDelete', 'afterDelete',
|
||
]);
|
||
await db.close();
|
||
});
|
||
|
||
it('SQL INSERT 多行语句按列名映射触发 afterInsert', async () => {
|
||
const db = await createDb('sql-insert-cols');
|
||
const seen: unknown[] = [];
|
||
db.on('afterInsert', (_rows, pks) => { seen.push(pks); });
|
||
|
||
await db.query("INSERT INTO users (id, name) VALUES ('9', 'Zed')");
|
||
|
||
expect(seen).toEqual([['9']]);
|
||
await db.close();
|
||
});
|
||
|
||
it('全部 14 个钩子均可注册并真实触发', async () => {
|
||
const db = await createDb('all-hooks');
|
||
const fired = new Set<string>();
|
||
const hooks = [
|
||
'beforeCreateTable', 'afterCreateTable',
|
||
'beforeDropTable', 'afterDropTable',
|
||
'beforeInsert', 'afterInsert',
|
||
'beforeUpdate', 'afterUpdate',
|
||
'beforeDelete', 'afterDelete',
|
||
'beforeQuery', 'afterQuery',
|
||
'beforeTransaction', 'afterTransaction',
|
||
] as const;
|
||
for (const h of hooks) db.on(h, () => { fired.add(h); });
|
||
|
||
await db.defineTable('t2', { id: { type: 'string', primaryKey: true } }); // createTable hooks
|
||
await db.table('t2').insert({ id: '1' }); // insert hooks
|
||
await db.table('t2').update({}).where({ id: '1' }).execute(); // update hooks
|
||
await db.table('t2').delete().where({ id: '1' }).execute(); // delete hooks
|
||
await db.query('SELECT * FROM t2'); // query hooks
|
||
await db.transaction(async () => { /* noop */ }); // transaction hooks
|
||
await db.dropTable('t2'); // dropTable hooks
|
||
|
||
for (const h of hooks) {
|
||
expect(fired.has(h)).toBe(true);
|
||
if (!fired.has(h)) throw new Error(`hook ${h} 应被触发`);
|
||
}
|
||
await db.close();
|
||
});
|
||
|
||
it('事务路径不触发 CRUD hooks(Table 无 hooks 回调)', async () => {
|
||
const db = await createDb('tx-hooks');
|
||
let fired = false;
|
||
db.on('beforeInsert', () => { fired = true; });
|
||
|
||
await db.transaction(async (trx) => {
|
||
await trx.table('users').insert({ id: '1', name: 'Tx' });
|
||
});
|
||
|
||
expect(fired).toBe(false);
|
||
await db.close();
|
||
});
|
||
});
|