open-brainy/tests/unit/db/fault-injection-shim.test.ts
David Snelling 3484107462 feat(log): fact-log format v2 codec — record envelope, type registry, genesis, sector seals; fault-injection shim
The two-implementation contract surface as one pure module (no I/O):
segment header v2 (formatVersion 2 + sealSize in the reserved bytes),
per-record [type u8, version u8] envelope killing the unknown-kind
misclassification trap, the 12-type registry (after-images with minted
ints, tombstones, batch.meta, embed.pending/landed, blob.manifest,
projection.note, bootstrap.baseline, log.genesis with id-space width and
TYPED width-mismatch refusal), vectorLeg inline|{sameAsGeneration} with
writer-enforced single-hop, sector-sealed groups with pad frames, torn-tail
discipline, and GOLDEN BYTE VECTORS pinned so a second (native) reader
implementation can conform byte-for-byte. 50 format pins + a
fault-injecting storage wrapper (tear/drop-sync/fail-append) with 13
self-tests. v1 segments remain readable; nothing writes v2 yet — the
live-format cutover is its own commit.
2026-08-10 09:29:06 -07:00

231 lines
9 KiB
TypeScript

/**
* @module tests/unit/db/fault-injection-shim
* @description The fault-injection storage wrapper proven in isolation: a
* torn write persists a decodable prefix (the crash shape durability tests
* replay), a dropped sync is observable (armed → the inner adapter never sees
* it; journaled), a failed append throws without writing a byte, knobs are
* one-shot, and unarmed operation is a transparent passthrough. The full
* commit-path fault matrix lives with the log's ack work — this file proves
* the SHIM itself.
*/
import { describe, it, expect, beforeEach } from 'vitest'
import { MemoryStorage } from '../../../src/storage/adapters/memoryStorage.js'
import {
FactLog,
storageSupportsFactLog,
type CommitFact,
type FactLogStorage
} from '../../../src/db/factLog.js'
import {
FaultInjectionStorage,
FaultInjectedError
} from '../../../src/db/faultInjectionStorage.js'
import {
encodeFactV2,
encodeSegmentHeaderV2,
decodeGroupV2,
parseSegmentHeader,
SEGMENT_HEADER_BYTES,
type CommitFactV2
} from '../../../src/db/factLogFormat.js'
const UUID = (n: number): string =>
`00000000-0000-4000-8000-${String(n).padStart(12, '0')}`
const factV2 = (generation: number): CommitFactV2 => ({
generation,
timestamp: 1_700_000_000_000 + generation,
records: [{ type: 'noun.tombstone', id: UUID(generation) }]
})
const factV1 = (generation: number): CommitFact => ({
generation,
timestamp: 1_700_000_000_000 + generation,
ops: [
{
kind: 'noun',
id: UUID(generation),
record: { metadata: { noun: 'document' }, vector: null }
}
]
})
describe('fault-injection storage wrapper', () => {
let inner: FactLogStorage & { syncRawObjects: (paths: string[]) => Promise<void> }
let shim: FaultInjectionStorage
let innerSyncCalls: string[][]
beforeEach(async () => {
const mem: any = new MemoryStorage()
await mem.init()
innerSyncCalls = []
const realSync = mem.syncRawObjects.bind(mem)
mem.syncRawObjects = async (paths: string[]) => {
innerSyncCalls.push([...paths])
return realSync(paths)
}
inner = mem
shim = new FaultInjectionStorage(inner)
})
it('satisfies the fact-log storage surface (drop-in wrapper)', () => {
expect(storageSupportsFactLog(shim)).toBe(true)
})
it('unarmed, every operation is a transparent passthrough', async () => {
await shim.writeRawBytes('seg', new Uint8Array([1, 2, 3]))
await shim.appendRawBytes('seg', new Uint8Array([4, 5]))
expect(Array.from((await shim.readRawBytes('seg'))!)).toEqual([1, 2, 3, 4, 5])
expect(await shim.rawByteSize('seg')).toBe(5)
expect(Array.from((await inner.readRawBytes('seg'))!)).toEqual([1, 2, 3, 4, 5])
await shim.writeRawObject('obj.json', { a: 1 })
expect(await shim.readRawObject('obj.json')).toEqual({ a: 1 })
await shim.deleteRawObject('obj.json')
expect(await shim.readRawObject('obj.json')).toBeNull()
await shim.syncRawObjects(['seg'])
expect(innerSyncCalls).toEqual([['seg']])
expect(shim.injectedFaults).toEqual([])
})
describe('tearWriteAtByte — a torn write produces a decodable-prefix segment', () => {
it('persists only the first N bytes of the next append; the prefix decodes intact', async () => {
const path = 'facts/seg-test.bfl'
const frame1 = encodeFactV2(factV2(1))
const frame2 = encodeFactV2(factV2(2))
await shim.appendRawBytes(path, encodeSegmentHeaderV2(1, 4096))
await shim.appendRawBytes(path, frame1)
shim.tearWriteAtByte(frame2.length - 5) // crash 5 bytes before the frame lands
await shim.appendRawBytes(path, frame2) // reports success — the tear is silent
const bytes = (await inner.readRawBytes(path))!
expect(bytes.length).toBe(SEGMENT_HEADER_BYTES + frame1.length + frame2.length - 5)
// The "crash": reopen from storage and read what actually survived.
const header = parseSegmentHeader(bytes)
expect(header).toStrictEqual({ formatVersion: 2, firstGeneration: 1, sealSize: 4096 })
const { facts, validBytes } = decodeGroupV2(bytes.subarray(SEGMENT_HEADER_BYTES))
expect(facts.map((f) => f.generation)).toEqual([1]) // fact 2's torn frame is invisible
expect(validBytes).toBe(frame1.length)
expect(shim.injectedFaults).toEqual([
{
kind: 'torn-write',
path,
requestedBytes: frame2.length,
writtenBytes: frame2.length - 5
}
])
})
it('a tear inside the frame prefix (first bytes) leaves the earlier facts intact too', async () => {
const path = 'facts/seg-prefix.bfl'
const frame1 = encodeFactV2(factV2(1))
await shim.appendRawBytes(path, encodeSegmentHeaderV2(1, 4096))
await shim.appendRawBytes(path, frame1)
shim.tearWriteAtByte(3)
await shim.appendRawBytes(path, encodeFactV2(factV2(2)))
const bytes = (await inner.readRawBytes(path))!
const { facts } = decodeGroupV2(bytes.subarray(SEGMENT_HEADER_BYTES))
expect(facts.map((f) => f.generation)).toEqual([1])
})
it('a tear at byte 0 writes nothing at all', async () => {
shim.tearWriteAtByte(0)
await shim.appendRawBytes('empty.bfl', new Uint8Array([1, 2, 3]))
expect(await inner.readRawBytes('empty.bfl')).toBeNull()
expect(shim.injectedFaults[0]).toMatchObject({ kind: 'torn-write', writtenBytes: 0 })
})
it('is one-shot: the append after the torn one lands whole', async () => {
shim.tearWriteAtByte(1)
await shim.appendRawBytes('seg', new Uint8Array([1, 2, 3, 4]))
await shim.appendRawBytes('seg', new Uint8Array([5, 6]))
expect(Array.from((await inner.readRawBytes('seg'))!)).toEqual([1, 5, 6])
})
it('refuses a negative tear offset', () => {
expect(() => shim.tearWriteAtByte(-1)).toThrow(/non-negative/)
})
})
describe('dropNextSync — a dropped sync is observable', () => {
it('the armed sync never reaches the inner adapter and is journaled', async () => {
shim.dropNextSync()
await shim.syncRawObjects(['a.bfl', 'b.bfl'])
expect(innerSyncCalls).toEqual([]) // the device never saw it
expect(shim.injectedFaults).toEqual([{ kind: 'dropped-sync', paths: ['a.bfl', 'b.bfl'] }])
})
it('is one-shot: the following sync passes through', async () => {
shim.dropNextSync()
await shim.syncRawObjects(['x'])
await shim.syncRawObjects(['y'])
expect(innerSyncCalls).toEqual([['y']])
})
})
describe('failNextAppend — a failed append throws without writing a byte', () => {
it('throws the typed error, writes nothing, and journals the fault', async () => {
await shim.appendRawBytes('seg', new Uint8Array([1]))
shim.failNextAppend()
await expect(shim.appendRawBytes('seg', new Uint8Array([2, 3]))).rejects.toThrow(
FaultInjectedError
)
expect(Array.from((await inner.readRawBytes('seg'))!)).toEqual([1]) // untouched
expect(shim.injectedFaults).toEqual([{ kind: 'failed-append', path: 'seg' }])
// one-shot: the next append succeeds
await shim.appendRawBytes('seg', new Uint8Array([4]))
expect(Array.from((await inner.readRawBytes('seg'))!)).toEqual([1, 4])
})
it('carries the operation and path for programmatic assertions', async () => {
shim.failNextAppend()
try {
await shim.appendRawBytes('some/path.bfl', new Uint8Array([1]))
expect.unreachable('append must throw')
} catch (error) {
const typed = error as FaultInjectedError
expect(typed).toBeInstanceOf(FaultInjectedError)
expect(typed.operation).toBe('append')
expect(typed.path).toBe('some/path.bfl')
}
})
it('wins over a simultaneously-armed tear; the tear stays pending for the next append', async () => {
shim.failNextAppend()
shim.tearWriteAtByte(2)
await expect(shim.appendRawBytes('seg', new Uint8Array([1, 2, 3]))).rejects.toThrow(
FaultInjectedError
)
expect(await inner.readRawBytes('seg')).toBeNull()
await shim.appendRawBytes('seg', new Uint8Array([9, 8, 7]))
expect(Array.from((await inner.readRawBytes('seg'))!)).toEqual([9, 8]) // torn at 2
expect(shim.injectedFaults.map((f) => f.kind)).toEqual(['failed-append', 'torn-write'])
})
})
describe('composed with the real fact log (v1 surface)', () => {
it('a torn append is truncated away on reopen — the log heals to the intact prefix', async () => {
const log = new FactLog(shim)
await log.open(0)
await log.append(factV1(1))
await log.sync()
shim.tearWriteAtByte(10) // fact 2's frame lands 10 bytes long — torn
await log.append(factV1(2))
await log.sync()
// The crash: abandon the instance, reopen from what storage actually holds.
const reopened = new FactLog(inner)
await reopened.open(2) // generation 2 committed elsewhere — but its fact is torn
expect(reopened.headGeneration()).toBe(1)
const all: CommitFact[] = []
for await (const batch of reopened.scanFacts().batches()) all.push(...batch.facts)
expect(all.map((f) => f.generation)).toEqual([1])
})
})
})