Files
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

180 lines
6.5 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.
/**
* AriaEngine — 多标签页独占锁(Web Locks)测试
*
* 覆盖:
* 1. 第一个引擎持锁 → 第二个引擎打开抛 ARIA_LOCKED
* 2. 关闭后锁释放 → 可再次打开
* 3. 环境不支持 Web Locks → 降级打开(返回 false 语义 + 警告)
* 4. open 失败(如加密密码错误)→ 锁释放(其他标签页可打开)
* 5. 锁名按库隔离(不同库不互斥)
*/
import { AriaEngine } from '../../src/engine/aria/index';
import { DatabaseLock, lockName } from '../../src/engine/aria/locks';
import { createSchema } from '../../src/table/schema';
import { resetOPFSMock } from '../helpers/storage-harness';
beforeEach(() => { resetOPFSMock(); });
let idbCounter = 0;
function uniqueDB(): string {
return `lock-${Date.now()}-${++idbCounter}-${Math.random().toString(36).slice(2, 8)}`;
}
// ===================================================================
// Web Locks mock:真实语义(ifAvailable / 回调结束锁自动释放)
// ===================================================================
let lockHolders: Map<string, true>;
function installLocksMock() {
lockHolders = new Map();
// 保留 storageOPFS mock 设置过),与 locks 共存
const existingNav = (globalThis as { navigator?: { storage?: unknown } }).navigator;
const storage = existingNav?.storage;
Object.defineProperty(globalThis, 'navigator', {
value: {
storage,
locks: {
request: async (_name: string, opts: { ifAvailable?: boolean }, cb: (lock: { name: string } | null) => Promise<void> | void) => {
const name = _name;
if (lockHolders.has(name)) {
if (opts?.ifAvailable) {
await cb(null);
return;
}
throw new Error('unreachable: non-ifAvailable request not used');
}
lockHolders.set(name, true);
try {
// 真实 Web Locks 语义:回调结束 → 锁自动释放
await cb({ name });
} finally {
lockHolders.delete(name);
}
},
},
},
configurable: true,
writable: true,
});
}
function removeLocksMock() {
// 只移除 locks,保留 storageOPFS mock 需要)
const existingNav = (globalThis as { navigator?: { storage?: unknown } }).navigator;
Object.defineProperty(globalThis, 'navigator', {
value: { storage: existingNav?.storage },
configurable: true,
writable: true,
});
}
// ===================================================================
// DatabaseLock 单元
// ===================================================================
describe('AriaEngine — DatabaseLock 单元', () => {
beforeEach(installLocksMock);
it('acquire 持锁 → 二次 acquire 抛 ARIA_LOCKED → release 后恢复', async () => {
const lock1 = new DatabaseLock();
expect(await lock1.acquire('db-a')).toBe(true);
expect(lock1.isAcquired()).toBe(true);
expect(lock1.isSupported()).toBe(true);
expect(lockHolders.has(lockName('db-a'))).toBe(true);
const lock2 = new DatabaseLock();
await expect(lock2.acquire('db-a')).rejects.toMatchObject({ code: 'ARIA_LOCKED' });
expect(lock2.isAcquired()).toBe(false);
await lock1.release();
expect(lock1.isAcquired()).toBe(false);
expect(lockHolders.has(lockName('db-a'))).toBe(false);
const lock3 = new DatabaseLock();
expect(await lock3.acquire('db-a')).toBe(true);
await lock3.release();
});
it('不同库名互不阻塞', async () => {
const l1 = new DatabaseLock();
const l2 = new DatabaseLock();
expect(await l1.acquire('db-x')).toBe(true);
expect(await l2.acquire('db-y')).toBe(true);
await l1.release();
await l2.release();
});
it('无 Web Locks 环境 → acquire 返回 false(降级)', async () => {
removeLocksMock();
const lock = new DatabaseLock();
expect(await lock.acquire('db-z')).toBe(false);
expect(lock.isSupported()).toBe(false);
expect(lock.isAcquired()).toBe(false);
});
it('重复 release 幂等', async () => {
const lock = new DatabaseLock();
await lock.acquire('db-r');
await lock.release();
await lock.release();
expect(lock.isAcquired()).toBe(false);
});
});
// ===================================================================
// AriaEngine 集成 — 多标签页锁
// ===================================================================
describe('AriaEngine — 多标签页锁(集成)', () => {
beforeEach(installLocksMock);
it('第二个标签页打开同一库 → ARIA_LOCKED', async () => {
const dbName = uniqueDB();
const engine1 = new AriaEngine({ storageBackend: 'opfs' });
await engine1.open(dbName, 1);
await engine1.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
const engine2 = new AriaEngine({ storageBackend: 'opfs' });
await expect(engine2.open(dbName, 1)).rejects.toMatchObject({ code: 'ARIA_LOCKED' });
// engine2 未持锁、未 opened
expect((engine2 as any).opened).toBe(false);
await engine1.close();
// 释放后可打开
const engine3 = new AriaEngine({ storageBackend: 'opfs' });
await engine3.open(dbName, 1);
expect(await engine3.count('t')).toBe(0);
await engine3.close();
});
it('open 失败(错误密码)→ 锁释放,其他标签页可打开', async () => {
const dbName = uniqueDB();
const engine1 = new AriaEngine({ storageBackend: 'opfs', encryption: { password: 'pw' } });
await engine1.open(dbName, 1);
await engine1.createTable(createSchema('t', { id: { type: 'string', primaryKey: true } }));
await engine1.close();
// 错误密码 → open 失败
const engineBad = new AriaEngine({ storageBackend: 'opfs', encryption: { password: 'wrong' } });
await expect(engineBad.open(dbName, 1)).rejects.toMatchObject({ code: 'ARIA_DECRYPT_ERROR' });
// 失败后锁已释放(未泄漏)
expect(lockHolders.has(lockName(dbName))).toBe(false);
// 正确密码可打开
const engineOk = new AriaEngine({ storageBackend: 'opfs', encryption: { password: 'pw' } });
await engineOk.open(dbName, 1);
await engineOk.close();
});
it('无 Web Locks 环境 → 降级打开(不抛错)', async () => {
removeLocksMock();
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' }]);
expect(await engine.count('t')).toBe(1);
await engine.close();
});
});