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
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 测试共享 — 类型化断言辅助(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;
|
||||
}
|
||||
Reference in New Issue
Block a user