Files
MetonaSqlark/tests/helpers/assertions.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

82 lines
3.4 KiB
TypeScript

/**
* 测试共享 — 类型化断言辅助(v0.8.0)
*
* 背景:v0.8.0 起 tests/ 纳入类型检查(tsconfig.test.json)。测试里最常见的两类
* 类型错误是:
* 1. `backend.read(key)` 返回 `ArrayBuffer | null` → 直接喂给 TextDecoder 报错;
* 2. `db.query()` 返回 `unknown` → 取 `.length` / `[0].field` 报错。
* 旧做法是 `as any`,那等于把类型检查关掉;这里提供**断言 + 收窄**的辅助函数:
* 断言失败会明确报出"期望非空",而不是让后续断言落空。
*
* 用法:
* expect(decode(await backend.read('k'))).toBe('v');
* const rows = rows<{ id: string }>(await db.query('SELECT * FROM t'));
*/
/** 断言非空并收窄(失败信息指向调用点,便于定位) */
export function nonNull<T>(value: T | null | undefined, what = 'value'): T {
if (value === null || value === undefined) {
throw new Error(`expected ${what} to be non-null, got ${String(value)}`);
}
return value;
}
/** 解码字节为字符串(自动处理 null → 明确失败) */
export function decode(value: ArrayBuffer | Uint8Array | null | undefined, what = 'buffer'): string {
if (value === null || value === undefined) {
throw new Error(`expected ${what} to be non-null for decoding`);
}
return new TextDecoder().decode(value instanceof Uint8Array ? value : new Uint8Array(value));
}
/** 把 query/executor 的 unknown 结果收窄为行数组 */
export function rows<T = Record<string, unknown>>(value: unknown): T[] {
if (!Array.isArray(value)) {
throw new Error(`expected query result to be an array, got ${typeof value}`);
}
return value as T[];
}
/** 把 query 结果收窄为单行(断言至少一行并返回首行) */
export function firstRow<T = Record<string, unknown>>(value: unknown): T {
const list = rows<T>(value);
if (list.length === 0) throw new Error('expected at least one row, got empty result');
return list[0];
}
/** 把 query 结果收窄为对象(EXPLAIN 等返回单对象的场景) */
export function object<T = Record<string, unknown>>(value: unknown): T {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new Error(`expected an object result, got ${Array.isArray(value) ? 'array' : typeof value}`);
}
return value as T;
}
/** 断言并收窄错误码(替代 `catch { threw = true }` 这种无法区分原因的写法) */
export async function expectCode(promise: Promise<unknown>, code: string): Promise<void> {
try {
await promise;
} catch (error) {
const actual = (error as { code?: string }).code;
if (actual !== code) {
throw new Error(`expected error code "${code}", got "${actual ?? '(none)'}": ${(error as Error).message}`);
}
return;
}
throw new Error(`expected rejection with code "${code}", but the promise resolved`);
}
/**
* 取用引擎上的**可选**方法(如 clearAll / backup / repair / createIndex)。
*
* `IStorageEngine` 把这些方法声明为可选,直接 `engine.clearAll()` 在类型上不成立。
* 测试里需要它们时用本函数:缺失即明确失败,而不是运行时 TypeError。
*/
export function engineMethod<M>(engine: unknown, name: string): M {
const fn = (engine as Record<string, unknown> | null)?.[name];
if (typeof fn !== 'function') {
throw new Error(`engine is missing required test method "${name}"`);
}
return (fn as (...a: unknown[]) => unknown).bind(engine) as M;
}