Files
MetonaSqlark/tests/helpers/faulty-backend.ts
T
thzxx 50b1864145 chore(docs): 删除 v0.7.4 审计与 v0.7.5 计划共 4 个 md 文档
删除:
- AUDIT-aria-lsm-v0.7.4.md(50KB)
- AUDIT-query-layer-v0.7.4.md(39KB)
- AUDIT-storage-engines-v0.7.4.md(39KB)
- PLAN-v0.7.5.md(98KB,含附录 G/H/I)

删除前先清除引用面,避免留下断链(共 18 处):
- 源码注释 7 处(change-notifier / kvstore index / column-value / expression /
  sql-compare / where-matcher / validation):保留设计意图,引用改为"v0.8.0 审计根因 N"
- 测试注释 9 处(opfs.spec / aria-opfs-backend / faulty-backend / storage-harness /
  v080-b6 / v080-kvstore / v080-query-layer / v080-sql-three-valued /
  v080-unified-validation / parser):同上
- CHANGELOG 3 处:改为不依赖已删除文档的自洽表述(B-6 交付物见各条;门禁订正三处
  按内容重写),并把变异数量同步为 42
- 校验:三个 md 之间无断链;仓库内已无 PLAN-v0.7.5/AUDIT-* 的任何引用
  (git 历史仍可追溯,需要时可 `git show <commit>:PLAN-v0.7.5.md` 找回)

验证:93 套件 / 1985 用例全绿;覆盖率 90.59 / 82.61 / 94.14 / 93.50(阈值 90/82/94/93);
e2e 14/14;lint + 两份 tsc 干净;dist 已重建(注释只影响非压缩产物,min 产物
251,731 B / gzip 63,431 B 不变)。

说明:审查记录的核心内容仍在 CHANGELOG.md("全量回归审查"与"现场失败修复"两节),
随 PLAN 一起删除的是附录 G/H/I 的详细表格(门禁逐条验收、交付物清单、未修复项表)。
2026-09-15 17:17:24 +08:00

243 lines
8.3 KiB
TypeScript
Raw 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.
/**
* 测试共享 — 故障注入后端包装器(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 extends IStorageBackend>(): 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<void> {
return this.inner.open(name);
}
close(): Promise<void> {
return this.inner.close();
}
isOpen(): boolean {
return this.inner.isOpen();
}
read(key: string): Promise<ArrayBuffer | null> {
return this.inner.read(key);
}
async write(key: string, data: ArrayBuffer): Promise<void> {
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<void> {
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<string, ArrayBuffer>): Promise<void> {
// 逐 key 走出本包装的 write,使注入对批量写同样生效
for (const [key, data] of Object.entries(entries)) {
await this.write(key, data);
}
}
async delete(key: string): Promise<void> {
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<void> {
for (const key of keys) {
await this.delete(key);
}
}
listKeys(): Promise<string[]> {
return this.inner.listKeys();
}
exists(key: string): Promise<boolean> {
return this.inner.exists(key);
}
clear(): Promise<void> {
return this.inner.clear();
}
}