删除: - 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 的详细表格(门禁逐条验收、交付物清单、未修复项表)。
243 lines
8.3 KiB
TypeScript
243 lines
8.3 KiB
TypeScript
/**
|
||
* 测试共享 — 故障注入后端包装器(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();
|
||
}
|
||
}
|