工作流 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
469 lines
17 KiB
TypeScript
469 lines
17 KiB
TypeScript
/**
|
||
* KVStore — 自研 KV 引擎单元测试
|
||
*
|
||
* 覆盖:
|
||
* 1. 基本读写(put/get/delete/exists/listKeys)
|
||
* 2. putMany/deleteMany 原子性(多 key 一次落盘)
|
||
* 3. 跨实例持久化(SharedMemory 全局注册表,close 不清数据)
|
||
* 4. 崩溃恢复:日志重放(未 checkpoint 数据恢复)
|
||
* 5. checkpoint 后恢复(快照 + 水位跳过)
|
||
* 6. 快照损坏 → 全量日志重放自愈
|
||
* 7. 日志损坏 → 截断至损坏处(丢弃未确认尾部)
|
||
* 8. 写入失败原子性(日志失败不更新内存索引)
|
||
* 9. clear / repair
|
||
* 10. 并发写与 checkpoint 串行(无交错丢数据)
|
||
*/
|
||
import { KVStore } from '../../src/engine/kvstore/index';
|
||
import { SharedMemoryBackend } from '../../src/engine/kvstore/shared_memory_medium';
|
||
import { encodeLogRecord, parseLogRecords, KVLogOp } from '../../src/engine/kvstore/log';
|
||
import { encodeSnapshot, decodeSnapshot } from '../../src/engine/kvstore/snapshot';
|
||
|
||
let dbCounter = 0;
|
||
function uniqueDB(): string {
|
||
return `kv-${Date.now()}-${++dbCounter}-${Math.random().toString(36).slice(2, 8)}`;
|
||
}
|
||
|
||
const enc = (s: string) => new TextEncoder().encode(s).buffer as ArrayBuffer;
|
||
const dec = (b: ArrayBuffer | null | undefined) => (b ? new TextDecoder().decode(b) : null);
|
||
|
||
beforeEach(() => {
|
||
SharedMemoryBackend.clearRegistry();
|
||
});
|
||
|
||
describe('KVStore — 基本读写', () => {
|
||
it('put/get/delete/exists/listKeys/size', async () => {
|
||
const kv = new KVStore(new SharedMemoryBackend());
|
||
await kv.open(uniqueDB());
|
||
await kv.put('a', enc('AAA'));
|
||
await kv.put('b', enc('BBB'));
|
||
expect(dec(await kv.get('a'))).toBe('AAA');
|
||
expect(await kv.exists('b')).toBe(true);
|
||
expect(await kv.exists('nope')).toBe(false);
|
||
expect((await kv.listKeys()).sort()).toEqual(['a', 'b']);
|
||
expect(kv.size()).toBe(2);
|
||
|
||
await kv.delete('a');
|
||
expect(await kv.exists('a')).toBe(false);
|
||
expect(kv.size()).toBe(1);
|
||
await kv.close();
|
||
});
|
||
|
||
it('putMany 多 key 原子写入', async () => {
|
||
const kv = new KVStore(new SharedMemoryBackend());
|
||
await kv.open(uniqueDB());
|
||
await kv.putMany({ a: enc('1'), b: enc('2'), c: enc('3') });
|
||
expect(dec(await kv.get('a'))).toBe('1');
|
||
expect(dec(await kv.get('c'))).toBe('3');
|
||
await kv.deleteMany(['a', 'c']);
|
||
expect(await kv.exists('a')).toBe(false);
|
||
expect(await kv.exists('c')).toBe(false);
|
||
expect(await kv.exists('b')).toBe(true);
|
||
await kv.close();
|
||
});
|
||
});
|
||
|
||
describe('KVStore — 持久化与崩溃恢复', () => {
|
||
it('未 checkpoint 的数据:close 后重开经日志重放恢复', async () => {
|
||
const dbName = uniqueDB();
|
||
const medium = new SharedMemoryBackend();
|
||
const kv1 = new KVStore(medium);
|
||
await kv1.open(dbName);
|
||
await kv1.putMany({ a: enc('AAA'), b: enc('BBB') });
|
||
await kv1.put('c', enc('CCC'));
|
||
await kv1.delete('b');
|
||
await kv1.close();
|
||
|
||
// 模拟"崩溃后重开"(新实例,共享介质)
|
||
const kv2 = new KVStore(medium);
|
||
await kv2.open(dbName);
|
||
expect(dec(await kv2.get('a'))).toBe('AAA');
|
||
expect(await kv2.exists('b')).toBe(false);
|
||
expect(dec(await kv2.get('c'))).toBe('CCC');
|
||
await kv2.close();
|
||
});
|
||
|
||
it('checkpoint 后重开:快照加载 + 日志水位跳过(无重复/无丢失)', async () => {
|
||
const dbName = uniqueDB();
|
||
const medium = new SharedMemoryBackend();
|
||
const kv1 = new KVStore(medium, 0); // 关闭自动 checkpoint
|
||
await kv1.open(dbName);
|
||
await kv1.putMany({ a: enc('A1'), b: enc('B1') });
|
||
await kv1.checkpoint();
|
||
// checkpoint 后新写入(进日志)
|
||
await kv1.put('c', enc('C1'));
|
||
await kv1.put('a', enc('A2'));
|
||
await kv1.close();
|
||
|
||
const kv2 = new KVStore(medium, 0);
|
||
await kv2.open(dbName);
|
||
expect(dec(await kv2.get('a'))).toBe('A2'); // 日志重放覆盖快照值
|
||
expect(dec(await kv2.get('b'))).toBe('B1');
|
||
expect(dec(await kv2.get('c'))).toBe('C1');
|
||
await kv2.close();
|
||
});
|
||
|
||
it('多次 checkpoint + 截断日志后重开正确', async () => {
|
||
const dbName = uniqueDB();
|
||
const medium = new SharedMemoryBackend();
|
||
const kv1 = new KVStore(medium, 0);
|
||
await kv1.open(dbName);
|
||
for (let i = 0; i < 10; i++) {
|
||
await kv1.put(`k${i}`, enc(`v${i}`));
|
||
await kv1.checkpoint();
|
||
}
|
||
await kv1.put('last', enc('L'));
|
||
await kv1.close();
|
||
|
||
const kv2 = new KVStore(medium, 0);
|
||
await kv2.open(dbName);
|
||
expect(kv2.size()).toBe(11);
|
||
expect(dec(await kv2.get('k9'))).toBe('v9');
|
||
expect(dec(await kv2.get('last'))).toBe('L');
|
||
await kv2.close();
|
||
});
|
||
|
||
it('快照损坏 → 全量日志重放:日志数据保留,快照唯一副本丢失(自愈不崩)', async () => {
|
||
const dbName = uniqueDB();
|
||
const medium = new SharedMemoryBackend();
|
||
const kv1 = new KVStore(medium, 0);
|
||
await kv1.open(dbName);
|
||
await kv1.putMany({ a: enc('AAA'), b: enc('BBB') });
|
||
await kv1.checkpoint(); // 快照成为 a/b 的唯一副本(日志已截断)
|
||
await kv1.put('c', enc('CCC')); // 日志中(seq=2)
|
||
await kv1.close();
|
||
|
||
// 篡改快照(close 后介质不可用,重新打开访问"磁盘")
|
||
const disk = new SharedMemoryBackend();
|
||
await disk.open(dbName);
|
||
const snap = await disk.read('__kv_snapshot');
|
||
expect(snap).not.toBeNull();
|
||
const corrupted = new Uint8Array(snap as ArrayBuffer);
|
||
corrupted[20] ^= 0xff; // 破坏 entry 长度字段 → CRC/边界校验失败
|
||
await disk.write('__kv_snapshot', corrupted.buffer as ArrayBuffer);
|
||
|
||
const kv2 = new KVStore(medium, 0);
|
||
await kv2.open(dbName);
|
||
// 快照损坏 → 全量日志重放:c(日志中)保留;a/b(快照唯一副本)丢失
|
||
expect(await kv2.exists('a')).toBe(false);
|
||
expect(await kv2.exists('b')).toBe(false);
|
||
expect(dec(await kv2.get('c'))).toBe('CCC');
|
||
await kv2.close();
|
||
});
|
||
|
||
it('回归:快照完好但 meta.seq 超前 → 日志不被 metaSeq 跳过(水位只信任快照)', async () => {
|
||
const dbName = uniqueDB();
|
||
const medium = new SharedMemoryBackend();
|
||
const kv1 = new KVStore(medium, 0);
|
||
await kv1.open(dbName);
|
||
await kv1.put('a', enc('AAA'));
|
||
await kv1.checkpoint(); // 快照 seq=1,meta seq=1,日志截断
|
||
await kv1.put('b', enc('BBB')); // 日志 seq=2
|
||
await kv1.close();
|
||
|
||
// 手工把 meta.seq 改大(模拟异常状态:meta 比日志内容超前)
|
||
const disk = new SharedMemoryBackend();
|
||
await disk.open(dbName);
|
||
await disk.write('__kv_meta', new TextEncoder().encode(JSON.stringify({ seq: 999 })).buffer);
|
||
|
||
const kv2 = new KVStore(medium, 0);
|
||
await kv2.open(dbName);
|
||
// 快照完好(seq=1)→ 水位=1 → 日志 seq2 应用;meta.seq=999 不参与水位判断
|
||
expect(dec(await kv2.get('a'))).toBe('AAA');
|
||
expect(dec(await kv2.get('b'))).toBe('BBB');
|
||
await kv2.close();
|
||
});
|
||
|
||
it('日志损坏尾部 → 截断至损坏处(丢弃未确认尾部,已确认数据保留)', async () => {
|
||
const dbName = uniqueDB();
|
||
const medium = new SharedMemoryBackend();
|
||
const kv1 = new KVStore(medium, 0);
|
||
await kv1.open(dbName);
|
||
await kv1.put('confirmed', enc('KEEP'));
|
||
await kv1.put('confirmed2', enc('KEEP2'));
|
||
await kv1.close();
|
||
|
||
// 追加一条损坏记录(模拟写入中断:半写记录)
|
||
const bad = encodeLogRecord(999, { damaged: enc('X') });
|
||
const corrupted = new Uint8Array(bad);
|
||
corrupted[20] ^= 0xff; // 破坏 CRC
|
||
// close 后介质实例不可用,重新打开访问"磁盘"
|
||
const disk = new SharedMemoryBackend();
|
||
await disk.open(dbName);
|
||
const log = await disk.read('__kv_log');
|
||
const combined = new Uint8Array((log as ArrayBuffer).byteLength + corrupted.byteLength);
|
||
combined.set(new Uint8Array(log as ArrayBuffer), 0);
|
||
combined.set(corrupted, (log as ArrayBuffer).byteLength);
|
||
await disk.write('__kv_log', combined.buffer as ArrayBuffer);
|
||
|
||
const kv2 = new KVStore(medium, 0);
|
||
await kv2.open(dbName);
|
||
expect(dec(await kv2.get('confirmed'))).toBe('KEEP');
|
||
expect(dec(await kv2.get('confirmed2'))).toBe('KEEP2');
|
||
expect(await kv2.exists('damaged')).toBe(false);
|
||
// 损坏日志已被截断(重开后再写正常)
|
||
await kv2.put('after', enc('OK'));
|
||
await kv2.close();
|
||
const kv3 = new KVStore(medium, 0);
|
||
await kv3.open(dbName);
|
||
expect(dec(await kv3.get('after'))).toBe('OK');
|
||
await kv3.close();
|
||
});
|
||
|
||
it('写入失败 → 内存索引不更新(原子性)', async () => {
|
||
const dbName = uniqueDB();
|
||
const medium = new SharedMemoryBackend();
|
||
const kv = new KVStore(medium, 0);
|
||
await kv.open(dbName);
|
||
|
||
// 注入日志追加失败
|
||
const origAppend = medium.append!.bind(medium);
|
||
medium.append = async () => { throw new Error('disk full'); };
|
||
await expect(kv.put('x', enc('X'))).rejects.toMatchObject({ code: 'KV_LOG_ERROR' });
|
||
expect(await kv.exists('x')).toBe(false);
|
||
|
||
medium.append = origAppend;
|
||
await kv.put('y', enc('Y'));
|
||
expect(dec(await kv.get('y'))).toBe('Y');
|
||
await kv.close();
|
||
});
|
||
});
|
||
|
||
describe('KVStore — 维护与并发', () => {
|
||
it('clear 清空全部(保留库),重开为空', async () => {
|
||
const dbName = uniqueDB();
|
||
const medium = new SharedMemoryBackend();
|
||
const kv1 = new KVStore(medium, 0);
|
||
await kv1.open(dbName);
|
||
await kv1.putMany({ a: enc('1'), b: enc('2') });
|
||
await kv1.checkpoint();
|
||
await kv1.put('c', enc('3'));
|
||
await kv1.clear();
|
||
expect(kv1.size()).toBe(0);
|
||
await kv1.close();
|
||
|
||
const kv2 = new KVStore(medium, 0);
|
||
await kv2.open(dbName);
|
||
expect(kv2.size()).toBe(0);
|
||
await kv2.close();
|
||
});
|
||
|
||
it('repair 清理损坏快照与日志(损坏快照数据无法恢复,日志数据保留)', async () => {
|
||
const dbName = uniqueDB();
|
||
const medium = new SharedMemoryBackend();
|
||
const kv1 = new KVStore(medium, 0);
|
||
await kv1.open(dbName);
|
||
await kv1.put('a', enc('A'));
|
||
await kv1.checkpoint(); // 快照成为 a 的唯一副本(日志已截断)
|
||
await kv1.put('b', enc('B')); // 日志中(seq=2)
|
||
await kv1.close();
|
||
|
||
// 快照损坏 + 日志尾部损坏
|
||
const disk = new SharedMemoryBackend();
|
||
await disk.open(dbName);
|
||
const snap = new Uint8Array(await disk.read('__kv_snapshot') as ArrayBuffer);
|
||
snap[10] ^= 0xff;
|
||
await disk.write('__kv_snapshot', snap.buffer as ArrayBuffer);
|
||
const bad = encodeLogRecord(500, { junk: enc('J') });
|
||
bad[15] ^= 0xff;
|
||
const log = new Uint8Array(await disk.read('__kv_log') as ArrayBuffer);
|
||
const combined = new Uint8Array(log.byteLength + bad.byteLength);
|
||
combined.set(log, 0); combined.set(bad, log.byteLength);
|
||
await disk.write('__kv_log', combined.buffer as ArrayBuffer);
|
||
|
||
const kv2 = new KVStore(medium, 0);
|
||
await kv2.open(dbName);
|
||
const discarded = await kv2.repair();
|
||
expect(discarded).toBeGreaterThan(0);
|
||
// 快照损坏(a 的唯一副本丢失)+ 损坏日志尾部被截断 → 日志数据 b 完整
|
||
expect(await kv2.exists('a')).toBe(false);
|
||
expect(dec(await kv2.get('b'))).toBe('B');
|
||
// repair 后介质上的损坏快照已清理
|
||
expect(await medium.exists('__kv_snapshot')).toBe(false);
|
||
await kv2.close();
|
||
});
|
||
|
||
it('并发写 + checkpoint 串行(无交错丢数据)', async () => {
|
||
const dbName = uniqueDB();
|
||
const medium = new SharedMemoryBackend();
|
||
const kv = new KVStore(medium, 0);
|
||
await kv.open(dbName);
|
||
|
||
// 并发发起大量写入 + 中途 checkpoint
|
||
const writes = [] as Promise<void>[];
|
||
for (let i = 0; i < 200; i++) {
|
||
writes.push(kv.put(`k${i}`, enc(`v${i}`)));
|
||
if (i === 100) writes.push(kv.checkpoint());
|
||
}
|
||
await Promise.all(writes);
|
||
|
||
expect(kv.size()).toBe(200);
|
||
await kv.close();
|
||
|
||
// 重开验证全部恢复
|
||
const kv2 = new KVStore(medium, 0);
|
||
await kv2.open(dbName);
|
||
expect(kv2.size()).toBe(200);
|
||
expect(dec(await kv2.get('k150'))).toBe('v150');
|
||
await kv2.close();
|
||
});
|
||
|
||
it('自动 checkpoint 阈值触发(日志不无限增长)', async () => {
|
||
const dbName = uniqueDB();
|
||
const medium = new SharedMemoryBackend();
|
||
const kv = new KVStore(medium, 256); // 小阈值
|
||
await kv.open(dbName);
|
||
for (let i = 0; i < 50; i++) {
|
||
await kv.put(`k${i}`, enc(`value-${i}-`.repeat(10)));
|
||
}
|
||
// 自动 checkpoint 后日志应被截断
|
||
const log = await medium.read('__kv_log');
|
||
expect((log as ArrayBuffer).byteLength).toBeLessThan(4096);
|
||
await kv.close();
|
||
|
||
const kv2 = new KVStore(medium, 256);
|
||
await kv2.open(dbName);
|
||
expect(kv2.size()).toBe(50);
|
||
await kv2.close();
|
||
});
|
||
});
|
||
|
||
describe('KVStore — 编解码单元', () => {
|
||
it('encodeLogRecord / parseLogRecords 往返', () => {
|
||
const rec = encodeLogRecord(1, { a: enc('A'), b: enc('BB') }, ['del']);
|
||
const records: { seq: number; entries: { op: KVLogOp; key: string; value: ArrayBuffer }[] }[] = [];
|
||
parseLogRecords(rec, (r) => records.push(r));
|
||
expect(records).toHaveLength(1);
|
||
expect(records[0].seq).toBe(1);
|
||
expect(records[0].entries).toHaveLength(3);
|
||
expect(dec(records[0].entries[0].value)).toBe('A');
|
||
expect(records[0].entries[2].op).toBe(KVLogOp.DELETE);
|
||
});
|
||
|
||
it('多记录日志顺序解析', () => {
|
||
const r1 = encodeLogRecord(1, { a: enc('1') });
|
||
const r2 = encodeLogRecord(2, { b: enc('2') });
|
||
const combined = new Uint8Array(r1.byteLength + r2.byteLength);
|
||
combined.set(r1, 0); combined.set(r2, r1.byteLength);
|
||
const seqs: number[] = [];
|
||
parseLogRecords(combined, (r) => seqs.push(r.seq));
|
||
expect(seqs).toEqual([1, 2]);
|
||
});
|
||
|
||
it('encodeSnapshot / decodeSnapshot 往返', () => {
|
||
const entries = new Map<string, ArrayBuffer>([['a', enc('A')], ['b', enc('BB')]]);
|
||
const bytes = encodeSnapshot(42, entries);
|
||
const snap = decodeSnapshot(bytes);
|
||
expect(snap).not.toBeNull();
|
||
expect(snap!.seq).toBe(42);
|
||
expect(dec(snap!.entries.get('a'))).toBe('A');
|
||
expect(dec(snap!.entries.get('b'))).toBe('BB');
|
||
});
|
||
|
||
it('损坏快照 decode 返回 null', () => {
|
||
const entries = new Map<string, ArrayBuffer>([['a', enc('A')]]);
|
||
const bytes = encodeSnapshot(1, entries);
|
||
bytes[10] ^= 0xff;
|
||
expect(decodeSnapshot(bytes)).toBeNull();
|
||
});
|
||
});
|
||
|
||
// ===================================================================
|
||
// APPEND 追加写入(v0.6.1)
|
||
// ===================================================================
|
||
describe('KVStore — APPEND 追加写入', () => {
|
||
it('appendValue 拼接 + 恢复完整(跨实例)', async () => {
|
||
const dbName = uniqueDB();
|
||
const medium = new SharedMemoryBackend();
|
||
const kv = new KVStore(medium, 0);
|
||
await kv.open(dbName);
|
||
await kv.appendValue('log', enc('AAA'));
|
||
await kv.appendValue('log', enc('BBB'));
|
||
await kv.appendValue('log', enc('CCC'));
|
||
expect(dec(await kv.get('log'))).toBe('AAABBBCCC');
|
||
await kv.close();
|
||
|
||
const kv2 = new KVStore(medium, 0);
|
||
await kv2.open(dbName);
|
||
expect(dec(await kv2.get('log'))).toBe('AAABBBCCC');
|
||
await kv2.close();
|
||
});
|
||
|
||
it('append 与 put/delete 混合 + checkpoint 后恢复', async () => {
|
||
const dbName = uniqueDB();
|
||
const medium = new SharedMemoryBackend();
|
||
const kv = new KVStore(medium, 0);
|
||
await kv.open(dbName);
|
||
await kv.appendValue('log', enc('A'));
|
||
await kv.put('meta', enc('M'));
|
||
await kv.appendValue('log', enc('B'));
|
||
await kv.checkpoint();
|
||
await kv.appendValue('log', enc('C'));
|
||
await kv.put('meta2', enc('M2'));
|
||
await kv.close();
|
||
|
||
const kv2 = new KVStore(medium, 0);
|
||
await kv2.open(dbName);
|
||
expect(dec(await kv2.get('log'))).toBe('ABC');
|
||
expect(dec(await kv2.get('meta'))).toBe('M');
|
||
expect(dec(await kv2.get('meta2'))).toBe('M2');
|
||
await kv2.close();
|
||
});
|
||
|
||
it('append 覆盖语义:先 put 后 append 拼接', async () => {
|
||
const dbName = uniqueDB();
|
||
const medium = new SharedMemoryBackend();
|
||
const kv = new KVStore(medium, 0);
|
||
await kv.open(dbName);
|
||
await kv.put('f', enc('HEAD-'));
|
||
await kv.appendValue('f', enc('BODY'));
|
||
expect(dec(await kv.get('f'))).toBe('HEAD-BODY');
|
||
await kv.close();
|
||
|
||
const kv2 = new KVStore(medium, 0);
|
||
await kv2.open(dbName);
|
||
expect(dec(await kv2.get('f'))).toBe('HEAD-BODY');
|
||
await kv2.close();
|
||
});
|
||
|
||
it('append 后 delete → 删除生效(索引与恢复一致)', async () => {
|
||
const dbName = uniqueDB();
|
||
const medium = new SharedMemoryBackend();
|
||
const kv = new KVStore(medium, 0);
|
||
await kv.open(dbName);
|
||
await kv.appendValue('log', enc('X'));
|
||
await kv.delete('log');
|
||
expect(await kv.exists('log')).toBe(false);
|
||
await kv.close();
|
||
|
||
const kv2 = new KVStore(medium, 0);
|
||
await kv2.open(dbName);
|
||
expect(await kv2.exists('log')).toBe(false);
|
||
await kv2.close();
|
||
});
|
||
|
||
it('日志损坏截断后已确认的 append 数据保留', async () => {
|
||
const dbName = uniqueDB();
|
||
const medium = new SharedMemoryBackend();
|
||
const kv = new KVStore(medium, 0);
|
||
await kv.open(dbName);
|
||
await kv.appendValue('log', enc('KEEP'));
|
||
await kv.close();
|
||
|
||
// 追加损坏记录
|
||
const disk = new SharedMemoryBackend();
|
||
await disk.open(dbName);
|
||
const log = await disk.read('__kv_log');
|
||
const bad = new Uint8Array(encodeLogRecord(999, { x: enc('X') }));
|
||
bad[15] ^= 0xff;
|
||
const combined = new Uint8Array((log as ArrayBuffer).byteLength + bad.byteLength);
|
||
combined.set(new Uint8Array(log as ArrayBuffer), 0);
|
||
combined.set(bad, (log as ArrayBuffer).byteLength);
|
||
await disk.write('__kv_log', combined.buffer as ArrayBuffer);
|
||
|
||
const kv2 = new KVStore(medium, 0);
|
||
await kv2.open(dbName);
|
||
expect(dec(await kv2.get('log'))).toBe('KEEP');
|
||
await kv2.close();
|
||
});
|
||
});
|