open-brainy/tests/unit/storage/torn-record-loud.test.ts
David Snelling 214c98b4d5
All checks were successful
CI / Node 22 (push) Successful in 12m16s
CI / Node 24 (push) Successful in 12m13s
CI / Bun (latest) (push) Successful in 12m20s
feat(log): log authority is the fleet default — adopt-at-open, oracle-gated; plus the power-cut throw-site cures and the loud torn-record contract
THE DEFAULT FLIP (ruled on proven evidence — at-ack survived 301/301
acked-writes-through-power-cut in block-layer fault injection; deferred
tree authority demonstrably loses flush-covered acks): a brain with NO
stored authority artifact now ADOPTS LOG AUTHORITY AT OPEN. The oracle
gates the flip exactly as the guarded adoption path always did — curable
divergences baseline-backfilled, the flip lands ONLY on a green verdict —
and a brain that cannot verify STAYS tree-authoritative loudly, with the
refusal recorded on the switch artifact so subsequent opens are cheap.
config logAuthority: 'defer' is the explicit documented opt-out (no
automatic adoption; declared flush-window loss; adoptLogAuthority() flips
later). A stored artifact always wins. RELEASES.md carries the posture.

Two standing .fails debt pins FLIP TO HOLDING under the default: the
at-ack crash-survival gap and the ack-at-log durability target — both now
permanent asserted truths, not aspirations.

POWER-CUT THROW SITES (fault-injection findings, brainy-alone config):
- A manifest-listed-but-unloadable column segment QUARANTINES at
  discovery (loud once, counted always, quarantinedSegments() exposed for
  the heal) and the field serves its remaining segments DEGRADED — never
  a raw throw killing every query on the field. Real storage faults still
  propagate untouched.
- Torn generation artifacts (NaN/garbage in manifest or counter) DISCARD
  with narration at the store's open and recovery re-derives — plus a
  defensive finite-integer guard at the init consumer. Never a RangeError
  killing an open.

THE LOUD TORN-RECORD CONTRACT: an existing-but-unparseable stored record
now surfaces as a typed, counted TornRecordError on every entity-read
surface (including fifteen previously-blind per-item batch catches);
ENOENT stays clean-absent; artifact readers with designed absent-recovery
keep null-tolerance behind the loud floor. Disk corruption can no longer
read as silent data invisibility.

Suite migration: the default's pins inverted deliberately, generation
baselines made relative, quarantine-contract pins rewritten to the ruled
behavior.

Gates: tsc 0 · unit 2065/2065 (159 files) · integration 826 (93 files) ·
conformance 31/31 · kill-matrix 15/15 · torn-open guards 2/2.
2026-08-11 08:37:38 -07:00

251 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @module tests/unit/storage/torn-record-loud
* @description Torn records must be LOUD, never silent. A file that EXISTS but
* cannot be decoded (truncated/garbled JSON, undecodable gzip) is disk
* corruption, not absence — the old behavior logged "gracefully skipping" and
* returned `null`, so a consumer could not distinguish "never existed" from
* "exists but torn" and nothing ever healed it. Pins the cured contract:
*
* - ENTITY read paths (get/getBatch/pagination) throw a typed
* `TornRecordError` ({path, cause}, code `TORN_RECORD`) — NEVER a silent null.
* - Genuine absence (ENOENT) still reads as clean `null` — no error, no gauge.
* - EVERY torn encounter increments the per-process torn-record gauge and
* records the path, whatever the caller surface decides.
* - SYSTEM-ARTIFACT reads (`readRawObject`: manifests/markers with recovery
* paths) map torn → `null` BY DESIGN — but only after the encounter was
* logged and counted (loud degrade, not a quiet loss).
* - Legacy dual-format recovery: a torn `.gz` with a decodable uncompressed
* fallback returns the recovered object AND still counts the torn `.gz`.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { FileSystemStorage } from '../../../src/storage/adapters/fileSystemStorage.js'
import {
TornRecordError,
isTornRecordError,
getTornRecordGauge,
resetTornRecordGauge
} from '../../../src/storage/tornRecordError.js'
import type { NounMetadata } from '../../../src/coreTypes.js'
const VEC = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]
/** Release a raw FileSystemStorage so a subsequent open of the dir is unblocked. */
async function teardown(s: any): Promise<void> {
try { await s.flush?.() } catch { /* best effort */ }
try { s.stopFlushRequestWatcher?.() } catch { /* best effort */ }
try { await s.releaseWriterLock?.() } catch { /* best effort */ }
}
/** Save one noun (metadata + vector) through the adapter's real write path. */
async function seedOne(s: any, id: string): Promise<void> {
await s.saveNounMetadata(id, {
noun: 'thing',
createdAt: Date.now(),
updatedAt: Date.now()
} as NounMetadata)
await s.saveNoun({ id, vector: VEC, connections: new Map(), level: 0 })
}
/** Find the on-disk file(s) for an entity leg (metadata.json / vectors.json), .gz or plain. */
function findEntityFiles(dir: string, id: string, leg: 'metadata' | 'vectors'): string[] {
const found: string[] = []
const walk = (d: string): void => {
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
const p = path.join(d, entry.name)
if (entry.isDirectory()) walk(p)
else if (
p.includes(`${path.sep}${id}${path.sep}`) &&
(entry.name === `${leg}.json` || entry.name === `${leg}.json.gz`)
) {
found.push(p)
}
}
}
walk(path.join(dir, 'entities'))
return found
}
/** Overwrite a file with bytes that can never decode as gzip or JSON. */
function corruptFile(filePath: string): void {
fs.writeFileSync(filePath, Buffer.from('{"noun":"thing","creaGARBAGE'))
}
describe('torn records are loud: typed on entity reads, counted everywhere (never a silent null)', () => {
let dirs: string[] = []
beforeEach(() => {
resetTornRecordGauge()
})
afterEach(() => {
for (const d of dirs.splice(0)) fs.rmSync(d, { recursive: true, force: true })
})
function tmpDir(): string {
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-torn-'))
dirs.push(d)
return d
}
/** Seed one entity, release the writer, corrupt the chosen leg on disk, reopen cold. */
async function seedCorruptReopen(
leg: 'metadata' | 'vectors',
options?: { compression?: boolean }
): Promise<{ dir: string; storage: any; id: string; corrupted: string[] }> {
const dir = tmpDir()
const id = 'aaaaaaaa-1111-4222-8333-444444444444'
const writer = new FileSystemStorage(dir, options) as any
await writer.init()
await seedOne(writer, id)
await teardown(writer)
const files = findEntityFiles(dir, id, leg)
expect(files.length).toBeGreaterThan(0) // the record really landed on disk
for (const f of files) corruptFile(f)
const storage = new FileSystemStorage(dir, options) as any
await storage.init()
return { dir, storage, id, corrupted: files }
}
it('entity metadata read on a torn (uncompressed) record throws TornRecordError — never null', async () => {
const { storage, id } = await seedCorruptReopen('metadata', { compression: false })
try {
// Capture the outcome without letting a non-throw masquerade as a pass:
// if the read RESOLVES, the outlawed shape is back (torn read as value/absent).
const outcome = await storage.getNounMetadata(id).then(
(value: unknown) => ({ threw: false as const, value }),
(error: unknown) => ({ threw: true as const, error })
)
expect(outcome.threw).toBe(true)
const caught = (outcome as { error: unknown }).error
expect(isTornRecordError(caught)).toBe(true)
expect((caught as TornRecordError).name).toBe('TornRecordError')
expect((caught as TornRecordError).code).toBe('TORN_RECORD')
expect((caught as TornRecordError).path).toContain(id)
expect((caught as TornRecordError).cause).toBeTruthy()
const gauge = getTornRecordGauge()
expect(gauge.count).toBeGreaterThanOrEqual(1)
expect(gauge.lastPath).toContain(id)
} finally {
await teardown(storage)
}
})
it('entity vector read (getNoun) on a torn record throws typed — a row is never silently dropped', async () => {
const { storage, id } = await seedCorruptReopen('vectors', { compression: false })
try {
await expect(storage.getNoun(id)).rejects.toSatisfy((e: unknown) => isTornRecordError(e))
expect(getTornRecordGauge().count).toBeGreaterThanOrEqual(1)
} finally {
await teardown(storage)
}
})
it('batch hydration (getNounMetadataBatch) throws typed instead of silently omitting the torn row', async () => {
const { storage, id } = await seedCorruptReopen('metadata', { compression: false })
try {
await expect(storage.getNounMetadataBatch([id])).rejects.toSatisfy((e: unknown) =>
isTornRecordError(e)
)
} finally {
await teardown(storage)
}
})
it('pagination/enumeration (getNounsWithPagination) surfaces the torn row typed instead of skipping it', async () => {
const { storage, id } = await seedCorruptReopen('vectors', { compression: false })
void id
try {
await expect(storage.getNounsWithPagination({ limit: 10 })).rejects.toSatisfy((e: unknown) =>
isTornRecordError(e)
)
} finally {
await teardown(storage)
}
})
it('ENOENT still reads as clean absent: null result, no error, gauge untouched', async () => {
const dir = tmpDir()
const storage = new FileSystemStorage(dir, { compression: false }) as any
await storage.init()
try {
const before = getTornRecordGauge().count
await expect(
storage.getNounMetadata('bbbbbbbb-1111-4222-8333-444444444444')
).resolves.toBeNull()
await expect(
storage.getNoun('bbbbbbbb-1111-4222-8333-444444444444')
).resolves.toBeNull()
await expect(storage.readRawObject('_system/never-written.json')).resolves.toBeNull()
expect(getTornRecordGauge().count).toBe(before) // absence is not corruption
} finally {
await teardown(storage)
}
})
it('system-artifact surface (readRawObject) maps torn → null BY DESIGN, but the encounter is counted', async () => {
const dir = tmpDir()
const storage = new FileSystemStorage(dir, { compression: false }) as any
await storage.init()
try {
await storage.writeRawObject('_system/some-manifest.json', { version: 1 })
corruptFile(path.join(dir, '_system/some-manifest.json'))
const before = getTornRecordGauge().count
// Artifact readers (manifests with recovery paths, markers whose verdict
// is "rescan") are designed for absent-artifact degradation: torn maps to
// their existing degrade — AFTER the loud floor (error log + gauge).
await expect(storage.readRawObject('_system/some-manifest.json')).resolves.toBeNull()
const gauge = getTornRecordGauge()
expect(gauge.count).toBe(before + 1)
expect(gauge.lastPath).toContain('some-manifest.json')
} finally {
await teardown(storage)
}
})
it('compressed installs: a torn .gz with no fallback throws typed and names the .gz path', async () => {
const { storage, id } = await seedCorruptReopen('metadata', { compression: true })
try {
let caught: unknown = null
try {
await storage.getNounMetadata(id)
} catch (e) {
caught = e
}
expect(isTornRecordError(caught)).toBe(true)
expect((caught as TornRecordError).path).toMatch(/metadata\.json\.gz$/)
expect(getTornRecordGauge().count).toBeGreaterThanOrEqual(1)
} finally {
await teardown(storage)
}
})
it('legacy dual-format: torn .gz with a decodable uncompressed fallback recovers the object AND counts the torn file', async () => {
const { storage, id, corrupted } = await seedCorruptReopen('metadata', { compression: true })
try {
// Recreate the legacy state: the corrupt .gz sits next to a valid plain file.
const gzPath = corrupted.find((f) => f.endsWith('.gz'))!
const plainPath = gzPath.replace(/\.gz$/, '')
fs.writeFileSync(
plainPath,
JSON.stringify({ noun: 'thing', createdAt: 1, updatedAt: 1 })
)
const before = getTornRecordGauge().count
const recovered = await storage.getNounMetadata(id)
expect(recovered).toBeTruthy()
expect(recovered.noun).toBe('thing') // loud recovery, not a silent skip
const gauge = getTornRecordGauge()
expect(gauge.count).toBe(before + 1) // the torn .gz was still surfaced
expect(gauge.lastPath).toMatch(/metadata\.json\.gz$/)
} finally {
await teardown(storage)
}
})
})