fix(health): one contract for a throwing probe — heal is none, serving is not withheld; repair report gains missing/rebuilt/reason
Some checks are pending
CI / Node 22 (push) Waiting to run
CI / Node 24 (push) Waiting to run
CI / Integration + conformance (Node 22) (push) Waiting to run
CI / Bun (latest) (push) Waiting to run

A validateInvariants() that threw was re-synthesized by the host's catch as
heal:'rebuild' with serving:false — a rebuild lever one transient exception
away, while the native provider's own composer reports the same event as
heal:'none'. Two components disagreeing on what a thrown check means is how
a flaky probe becomes an outage. Both now agree: the report is named
('validate-invariants-threw'), loud (healthy:false, the error in detail),
unverified — and it never buys a rebuild and never withholds serving; the
provider's serving verdict is the provider's to compose, not inferred from a
probe that failed to run. The synthesized report also carries the provider's
name instead of 'unknown'.

RepairFamilyReport gains `missing: {count, sample}` (an exact count plus a
capped id sample — a verdict, not a dump), `rebuilt` (a full generational
rebuild ran, as opposed to an incremental heal) and `reason`, aligning the
receipt's shape with the provider-side health report.

Pinned in tests/unit/validate-invariants-delegation.test.ts.
This commit is contained in:
David Snelling 2026-08-24 09:54:25 -07:00
parent 7c8c8be30c
commit 116550eb16
3 changed files with 54 additions and 3 deletions

View file

@ -77,6 +77,31 @@ describe('validateIndexConsistency delegates to provider validateInvariants() (P
delete brain.index.validateInvariants
})
it('ONE CONTRACT FOR A THROWING PROBE: heal is none (flakiness never buys a rebuild) and serving is not withheld', async () => {
// The probe that fails to RUN must never be read as "the index is broken,
// rebuild it" — that synthesized heal:'rebuild' was the dark-rebuild lever
// one transient exception away, and the native composer already said
// 'none' for the same event. Both engines now agree: named, loud,
// unverified — and never a rebuild, never a withheld serve.
brain.index.validateInvariants = async () => { throw new Error('transient: mmap window busy') }
const v = await brain.validateIndexConsistency()
const thrown = v.providers?.find((p: ProviderInvariantReport) =>
p.invariants.some((i) => i.name === 'validate-invariants-threw')
)
expect(thrown).toBeDefined()
expect(thrown!.healthy).toBe(false)
expect(thrown!.serving).toBe(true)
const inv = thrown!.invariants.find((i) => i.name === 'validate-invariants-threw')!
expect(inv.holds).toBe(false)
expect(inv.heal).toBe('none')
expect(inv.detail).toMatch(/transient: mmap window busy/)
// No provider report in the set recommends a rebuild for this event.
expect(
v.providers!.flatMap((p: ProviderInvariantReport) => p.invariants).some((i) => i.heal === 'rebuild')
).toBe(false)
delete brain.index.validateInvariants
})
it('providers without validateInvariants() are omitted (JS baseline unchanged)', async () => {
const v = await brain.validateIndexConsistency()
expect(v.providers).toBeUndefined()