2026-08-20 11:51:29 -07:00
|
|
|
/**
|
|
|
|
|
* @module tests/integration/repair-report
|
|
|
|
|
* @description repairIndex() returns the per-family receipt (checked /
|
|
|
|
|
* healed / skipped-with-reason per family) and narrates a summary — the
|
|
|
|
|
* "repair that shows its work" half of the graph-trust program's ask. A
|
|
|
|
|
* repair nobody can audit is a repair nobody can trust.
|
|
|
|
|
*/
|
|
|
|
|
import { describe, it, expect, afterEach } from 'vitest'
|
|
|
|
|
import { mkdtempSync, rmSync } from 'node:fs'
|
|
|
|
|
import { tmpdir } from 'node:os'
|
|
|
|
|
import { join } from 'node:path'
|
|
|
|
|
import { Brainy } from '../../src/index.js'
|
|
|
|
|
import { NounType } from '../../src/types/graphTypes.js'
|
|
|
|
|
|
|
|
|
|
type RawBox = {
|
|
|
|
|
storage: { writeNounRaw(id: string, r: { metadata: unknown; vector: unknown }): Promise<void> }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dirs: string[] = []
|
|
|
|
|
const brains: Brainy[] = []
|
|
|
|
|
afterEach(async () => {
|
|
|
|
|
for (const b of brains.splice(0)) await b.close().catch(() => {})
|
|
|
|
|
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('repairIndex per-family receipt', () => {
|
|
|
|
|
it('a healthy store gets a complete zero-heal receipt — every family accounted, none silent', async () => {
|
|
|
|
|
const dir = mkdtempSync(join(tmpdir(), 'brainy-repair-clean-'))
|
|
|
|
|
dirs.push(dir)
|
|
|
|
|
const brain = new Brainy({ storage: { type: 'filesystem', path: dir }, requireSubtype: false, silent: true })
|
|
|
|
|
await brain.init()
|
|
|
|
|
brains.push(brain)
|
|
|
|
|
await brain.add({ data: 'healthy row', type: NounType.Document, metadata: { n: 1 } })
|
|
|
|
|
await brain.flush()
|
|
|
|
|
|
|
|
|
|
const report = await brain.repairIndex()
|
|
|
|
|
expect(report.families.length, 'every family reports a row').toBeGreaterThanOrEqual(5)
|
|
|
|
|
const names = report.families.map((f) => f.family)
|
|
|
|
|
for (const expected of ['orphaned-containers', 'count-rollups', 'metadata-corruption']) {
|
|
|
|
|
expect(names, `family ${expected} accounted`).toContain(expected)
|
|
|
|
|
}
|
|
|
|
|
// Every row is either checked or carries its skip reason — no silent rows.
|
|
|
|
|
for (const f of report.families) {
|
|
|
|
|
expect(f.checked || !!f.skipped, `${f.family} is checked or explains itself`).toBe(true)
|
|
|
|
|
}
|
|
|
|
|
expect(report.healedTotal).toBe(0)
|
|
|
|
|
expect(report.durationMs).toBeGreaterThanOrEqual(0)
|
|
|
|
|
}, 120000)
|
|
|
|
|
|
|
|
|
|
it('a manufactured ghost container appears in the receipt as a heal', async () => {
|
|
|
|
|
const dir = mkdtempSync(join(tmpdir(), 'brainy-repair-ghost-'))
|
|
|
|
|
dirs.push(dir)
|
|
|
|
|
const brain = new Brainy({ storage: { type: 'filesystem', path: dir }, requireSubtype: false, silent: true })
|
|
|
|
|
await brain.init()
|
|
|
|
|
brains.push(brain)
|
|
|
|
|
await brain.add({ data: 'real row', type: NounType.Document, metadata: { n: 1 } })
|
|
|
|
|
await brain.flush()
|
|
|
|
|
// The pre-8.3.1 ghost shape: a vector leg with no content leg.
|
|
|
|
|
const storage = (brain as unknown as RawBox).storage
|
|
|
|
|
await storage.writeNounRaw('00000000-0000-7000-8000-00000000dead', {
|
|
|
|
|
metadata: null,
|
|
|
|
|
vector: { vector: [0.1, 0.2], noun: 'document' }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const report = await brain.repairIndex()
|
|
|
|
|
const orphans = report.families.find((f) => f.family === 'orphaned-containers')
|
|
|
|
|
expect(orphans?.checked).toBe(true)
|
|
|
|
|
expect(orphans!.healed, 'the ghost was pruned and receipted').toBeGreaterThan(0)
|
|
|
|
|
expect(report.healedTotal).toBeGreaterThan(0)
|
|
|
|
|
}, 120000)
|
2026-08-25 10:47:51 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
it("a heal:'repair' verdict routes to the provider's own repair(), and the re-read decides", async () => {
|
|
|
|
|
// A fake provider report: one failing invariant asking for the INCREMENTAL
|
|
|
|
|
// heal. repairIndex must call repair() (never rebuild()) and count the heal
|
|
|
|
|
// only when the post-repair re-read clears the same verdict.
|
|
|
|
|
const dir = mkdtempSync(join(tmpdir(), 'brainy-repair-route-'))
|
|
|
|
|
const brain: any = new Brainy({ storage: { type: 'filesystem', path: dir }, requireSubtype: false, silent: true })
|
|
|
|
|
await brain.init()
|
|
|
|
|
brains.push(brain)
|
|
|
|
|
let repairCalls = 0
|
|
|
|
|
let rebuildCalls = 0
|
|
|
|
|
let healed = false
|
|
|
|
|
const failing = {
|
|
|
|
|
provider: 'vector', healthy: false, serving: true,
|
|
|
|
|
invariants: [{ name: 'node-coverage', holds: false, detail: 'short 3', heal: 'repair' as const }],
|
|
|
|
|
checkedAt: 1, durationMs: 1
|
|
|
|
|
}
|
|
|
|
|
const clean = {
|
|
|
|
|
provider: 'vector', healthy: true, serving: true,
|
|
|
|
|
invariants: [{ name: 'node-coverage', holds: true, detail: 'ok', heal: 'none' as const }],
|
|
|
|
|
checkedAt: 2, durationMs: 1
|
|
|
|
|
}
|
|
|
|
|
;(brain.index as any).validateInvariants = async () => (healed ? clean : failing)
|
|
|
|
|
;(brain.index as any).repair = async () => { repairCalls++; healed = true; return { repaired: 3 } }
|
|
|
|
|
const origRebuild = (brain.index as any).rebuild
|
|
|
|
|
;(brain.index as any).rebuild = async () => { rebuildCalls++ }
|
|
|
|
|
try {
|
|
|
|
|
const report = await brain.repairIndex()
|
|
|
|
|
const row = report.families.find((f: any) => f.family === 'provider:vector')
|
|
|
|
|
expect(row, 'the provider family is in the receipt').toBeDefined()
|
|
|
|
|
expect(repairCalls, 'repair() ran exactly once').toBe(1)
|
|
|
|
|
expect(rebuildCalls, "a heal:'repair' verdict never runs rebuild()").toBe(0)
|
|
|
|
|
expect(row!.healed, 'the cleared verdict counts as healed').toBe(1)
|
|
|
|
|
expect(String(row!.detail)).toMatch(/incremental repair cleared: node-coverage/)
|
|
|
|
|
} finally {
|
|
|
|
|
delete (brain.index as any).validateInvariants
|
|
|
|
|
delete (brain.index as any).repair
|
|
|
|
|
;(brain.index as any).rebuild = origRebuild
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-20 11:51:29 -07:00
|
|
|
})
|