/** * 测试共享 — 故障注入后端包装器(v0.8.0 工作流 C-1) * * ============================================================================ * 为什么需要它(v0.8.0 审计根因 5) * ============================================================================ * 项目所有"崩溃恢复"测试用的都是 `await engine.backend.close()`,而 OPFSBackend.close() * 会 `await this.writeQueue` 把在途写**全部刷完** —— 那是优雅停机,不是崩溃。 * 加上 mock 永远原子、绝不撕裂,崩溃相关的声称在结构上无法被验证。 * * 本包装器让任意 IStorageBackend 具备可编程故障: * - failNextWrite(n) / failNextAppend(n) / failNextDelete(n) 抛错注入 * - truncateNextAppendTo(n) 撕裂写(只落前 n 字节) * - dropNextAppend(n) / dropNextWrite(n) 静默丢弃(模拟掉电丢失) * - corruptNextWrite(mutator) 写入落盘后篡改字节 * - crash() 丢弃未提交写(委托介质) * * 用法: * const faulty = new FaultyBackend(new OPFSBackend()); * await faulty.open('db'); * faulty.failNextAppend(); // 下一次追加失败 * ... 触发写入 ... * await expect(...).rejects.toThrow(); * faulty.clearFaults(); */ import type { IStorageBackend } from '../../src/engine/aria/store/backend'; type Mutator = (bytes: Uint8Array) => void; interface Faults { failWrite: number; failAppend: number; failDelete: number; dropWrite: number; dropAppend: number; truncateAppendTo: number; corruptWrite: Mutator | null; } /** 支持崩溃模拟的介质(OPFS mock / TransactionalFileStore 等) */ export interface CrashableMedium { crashPending(): void; hasPending(): boolean; } /** 支持崩溃模拟的后端(OPFSBackend 等在 v0.8.0 提供了 simulateCrash) */ export interface CrashableBackend { simulateCrash(): void; } function isCrashableMedium(m: unknown): m is CrashableMedium { return typeof (m as CrashableMedium)?.crashPending === 'function'; } function isCrashableBackend(m: unknown): m is CrashableBackend { return typeof (m as CrashableBackend)?.simulateCrash === 'function'; } export class FaultyBackend implements IStorageBackend { private readonly inner: IStorageBackend; private faults: Faults = { failWrite: 0, failAppend: 0, failDelete: 0, dropWrite: 0, dropAppend: 0, truncateAppendTo: -1, corruptWrite: null, }; /** 注入统计(测试可断言注入真的生效了,避免"注入了但没走到"的假绿) */ readonly injected = { write: 0, append: 0, delete: 0, dropped: 0, truncated: 0, corrupted: 0 }; constructor(inner: IStorageBackend) { this.inner = inner; } /** 暴露内层(需要访问具体 backing store 时使用,例如 crash()) */ unwrap(): T { return this.inner as T; } // ---- 故障注入 API ---- failNextWrite(n = 1): void { this.faults.failWrite = n; } failNextAppend(n = 1): void { this.faults.failAppend = n; } failNextDelete(n = 1): void { this.faults.failDelete = n; } /** 静默丢弃接下来 n 次 write(不抛错、"看起来成功",模拟掉电丢失) */ dropNextWrite(n = 1): void { this.faults.dropWrite = n; } /** 静默丢弃接下来 n 次 append */ dropNextAppend(n = 1): void { this.faults.dropAppend = n; } /** 下一次 append 只落前 n 字节(撕裂写) */ truncateNextAppendTo(n: number): void { this.faults.truncateAppendTo = n; } /** 下一次写入的字节落盘后被就地篡改(模拟 bit flip) */ corruptNextWrite(mutator: Mutator): void { this.faults.corruptWrite = mutator; } clearFaults(): void { this.faults = { failWrite: 0, failAppend: 0, failDelete: 0, dropWrite: 0, dropAppend: 0, truncateAppendTo: -1, corruptWrite: null, }; } /** * 模拟崩溃:优先让内层后端自己处理(OPFSBackend.simulateCrash 会丢弃未提交的 swap 写入), * 否则若内层介质本身可崩溃(TransactionalFileStore 等)则直接委托。 * * 注意:**不要**用 inner.close() 代替崩溃 —— 那会把写队列刷完(优雅停机)。 * 返回 false 表示"该后端无法模拟崩溃",调用方应据此改用丢写注入表达崩溃。 */ crash(): boolean { if (isCrashableBackend(this.inner)) { this.inner.simulateCrash(); return true; } if (isCrashableMedium(this.inner)) { this.inner.crashPending(); return true; } return false; } // ---- IStorageBackend 委托 ---- open(name: string): Promise { return this.inner.open(name); } close(): Promise { return this.inner.close(); } isOpen(): boolean { return this.inner.isOpen(); } read(key: string): Promise { return this.inner.read(key); } async write(key: string, data: ArrayBuffer): Promise { if (this.faults.dropWrite > 0) { this.faults.dropWrite--; this.injected.dropped++; return; // 静默成功但没落盘 } if (this.faults.failWrite > 0) { this.faults.failWrite--; this.injected.write++; throw new Error(`[FaultyBackend] injected write failure: ${key}`); } await this.inner.write(key, data); if (this.faults.corruptWrite) { const mutator = this.faults.corruptWrite; this.faults.corruptWrite = null; const stored = await this.inner.read(key); if (stored) { mutator(new Uint8Array(stored)); await this.inner.write(key, stored); this.injected.corrupted++; } } } async append(key: string, data: ArrayBuffer): Promise { if (this.faults.dropAppend > 0) { this.faults.dropAppend--; this.injected.dropped++; return; } if (this.faults.failAppend > 0) { this.faults.failAppend--; this.injected.append++; throw new Error(`[FaultyBackend] injected append failure: ${key}`); } if (this.faults.truncateAppendTo >= 0) { const n = this.faults.truncateAppendTo; this.faults.truncateAppendTo = -1; this.injected.truncated++; // 撕裂写:只把前 n 字节交给介质 // (不用 ArrayBuffer.slice —— jsdom 下可能被 Blob.slice 语义遮蔽,见 storage-harness 注释) const len = Math.max(0, Math.min(n, data.byteLength)); const torn = new Uint8Array(len); torn.set(new Uint8Array(data, 0, len)); const tornBuf = torn.buffer as ArrayBuffer; if (typeof this.inner.append === 'function') { await this.inner.append(key, tornBuf); } else { const existing = await this.inner.read(key); const merged = new Uint8Array((existing?.byteLength ?? 0) + tornBuf.byteLength); if (existing) merged.set(new Uint8Array(existing), 0); merged.set(new Uint8Array(tornBuf), existing?.byteLength ?? 0); await this.inner.write(key, merged.buffer as ArrayBuffer); } return; } if (typeof this.inner.append === 'function') { await this.inner.append(key, data); } else { const existing = await this.inner.read(key); const merged = new Uint8Array((existing?.byteLength ?? 0) + data.byteLength); if (existing) merged.set(new Uint8Array(existing), 0); merged.set(new Uint8Array(data), existing?.byteLength ?? 0); await this.inner.write(key, merged.buffer as ArrayBuffer); } } async writeMany(entries: Record): Promise { // 逐 key 走出本包装的 write,使注入对批量写同样生效 for (const [key, data] of Object.entries(entries)) { await this.write(key, data); } } async delete(key: string): Promise { if (this.faults.failDelete > 0) { this.faults.failDelete--; this.injected.delete++; throw new Error(`[FaultyBackend] injected delete failure: ${key}`); } await this.inner.delete(key); } async deleteMany(keys: string[]): Promise { for (const key of keys) { await this.delete(key); } } listKeys(): Promise { return this.inner.listKeys(); } exists(key: string): Promise { return this.inner.exists(key); } clear(): Promise { return this.inner.clear(); } }