fix: warm() metadata surface routes through the active provider (warm hook added to the metadata contract); add maintenanceDebt() observability surface
A production deployment's warm report showed metadata: 'unavailable' under a native metadata provider. brain.warm()'s metadata leg only duck-typed the built-in JS manager's hydrateAll() method, which a native provider has no reason to implement. - MetadataIndexProvider (src/plugin.ts) gains an optional warm?(): Promise<void> hook, mirroring the existing vector and graph provider hooks. brain.warm() now checks the active provider's own warm() FIRST, falls back to the JS manager's hydrateAll() when absent, and reports 'unavailable' only when neither exists -- never init() as a stand-in, since a native provider's init() may be a cheap verify rather than a real warm. - Tests (tests/unit/brainy/warm.test.ts): a live provider instance shaped to have warm() reports 'warmed' and the hook called with no hydrateAll fallback; shaped to have neither hook reports 'unavailable' (pins the honest branch); the unmodified built-in JS manager still reports 'warmed' via hydrateAll(), unchanged. Additive scope agreed mid-flight with the native-provider team: a maintenance-debt observability seam so an operator sees a grind coming instead of discovering it as a CPU storm. - New optional maintenanceDebt?(): Promise<ProviderMaintenanceDebt> hook on all three provider contracts (vector, metadata, graph -- the same three warm?() lives on). ProviderMaintenanceDebt is fields-all-optional: a provider reports only what it truly measures (pendingBytes, pendingItems, lastPassCompletedAt, lastPassOutcome, converging), never an estimate dressed as fact. - New public brain.maintenanceDebt(): a pure passthrough -- for each surface it calls only the active provider's own hook and reports the payload verbatim, or 'unavailable' when absent. No thresholds, no polling, no JS-side estimation; the provider owns the numbers, the operator owns the policy. - ProviderMaintenanceDebt, MaintenanceDebtReport, and MaintenanceDebtOutcome are exported from the package root. - Tests (tests/unit/brainy/maintenance-debt.test.ts): hook present reports 'reported' with the exact payload passed through; hook absent reports 'unavailable' on every surface; mixed surfaces resolve independently of each other. RELEASES.md gains the 8.10.1 entry covering both fixes above and this feature, including the no-hot-retry contract from the prior commit.
This commit is contained in:
parent
003e2a74ea
commit
5b2cbf74e5
6 changed files with 471 additions and 6 deletions
122
tests/unit/brainy/maintenance-debt.test.ts
Normal file
122
tests/unit/brainy/maintenance-debt.test.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
/**
|
||||
* @module tests/unit/brainy/maintenance-debt
|
||||
* @description Coverage for `brain.maintenanceDebt()` (8.10.1) — the
|
||||
* observability seam so an operator sees a provider's outstanding background
|
||||
* maintenance work (compaction, deferred writes, a build-new→verify→swap in
|
||||
* flight, ...) BEFORE it grinds a transaction into a budget-busting op, the
|
||||
* same failure class documented on `TransactionTimeoutError`
|
||||
* (src/transaction/errors.ts). Sibling to tests/unit/brainy/warm.test.ts,
|
||||
* which establishes this file's technique: shape the probe points brain.ts
|
||||
* reads (`typeof provider.maintenanceDebt === 'function'`) directly on the
|
||||
* REAL, live provider instances rather than hand-rolling full fakes for the
|
||||
* larger `MetadataIndexProvider` / `GraphIndexProvider` interfaces.
|
||||
*
|
||||
* `brain.maintenanceDebt()` is a PURE PASSTHROUGH: no thresholds, no
|
||||
* polling, no JS-side estimation — these tests pin exactly that by asserting
|
||||
* the returned payload is the provider's object, verbatim.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { Brainy } from '../../../src/brainy.js'
|
||||
import { NounType } from '../../../src/types/graphTypes.js'
|
||||
import type { ProviderMaintenanceDebt } from '../../../src/plugin.js'
|
||||
|
||||
// Brainy's ValidationConfig fixes vectors at exactly 384 dimensions
|
||||
// (src/utils/paramValidation.ts) — match it so add() doesn't reject test data.
|
||||
const DIM = 384
|
||||
const V = (seed = 1): number[] => Array.from({ length: DIM }, (_, i) => Math.sin(seed + i))
|
||||
|
||||
async function freshBrain(): Promise<Brainy<any>> {
|
||||
const brain = new Brainy({
|
||||
requireSubtype: false,
|
||||
storage: { type: 'memory' },
|
||||
silent: true
|
||||
})
|
||||
await brain.init()
|
||||
await brain.add({ data: 'a', type: NounType.Thing, vector: V(1) })
|
||||
return brain
|
||||
}
|
||||
|
||||
describe('brain.maintenanceDebt()', () => {
|
||||
it('reports "unavailable" for every surface when no active provider implements maintenanceDebt() (the built-in JS stack today)', async () => {
|
||||
const brain = await freshBrain()
|
||||
|
||||
const report = await brain.maintenanceDebt()
|
||||
|
||||
expect(report.vector.outcome).toBe('unavailable')
|
||||
expect(report.vector.debt).toBeUndefined()
|
||||
expect(report.metadata.outcome).toBe('unavailable')
|
||||
expect(report.metadata.debt).toBeUndefined()
|
||||
expect(report.graph.outcome).toBe('unavailable')
|
||||
expect(report.graph.debt).toBeUndefined()
|
||||
|
||||
await brain.close()
|
||||
})
|
||||
|
||||
it('reports "reported" + the exact payload when the active provider implements maintenanceDebt() (verbatim passthrough, no thresholding)', async () => {
|
||||
const brain = await freshBrain()
|
||||
|
||||
const vectorDebt: ProviderMaintenanceDebt = {
|
||||
pendingBytes: 4_096,
|
||||
pendingItems: 12,
|
||||
lastPassCompletedAt: 1_700_000_000_000,
|
||||
lastPassOutcome: 'completed',
|
||||
converging: true
|
||||
}
|
||||
;(brain as any).index.maintenanceDebt = async () => vectorDebt
|
||||
|
||||
const report = await brain.maintenanceDebt()
|
||||
|
||||
expect(report.vector.outcome).toBe('reported')
|
||||
// Verbatim passthrough — the exact object, not a re-derived copy.
|
||||
expect(report.vector.debt).toBe(vectorDebt)
|
||||
// Untouched surfaces stay honestly 'unavailable'.
|
||||
expect(report.metadata.outcome).toBe('unavailable')
|
||||
expect(report.graph.outcome).toBe('unavailable')
|
||||
|
||||
await brain.close()
|
||||
})
|
||||
|
||||
it('mixed surfaces: each surface\'s outcome depends ONLY on its OWN active provider — one surface reporting never leaks into another', async () => {
|
||||
const brain = await freshBrain()
|
||||
|
||||
const metadataDebt: ProviderMaintenanceDebt = {
|
||||
pendingItems: 3,
|
||||
lastPassOutcome: 'partial',
|
||||
converging: false
|
||||
}
|
||||
const graphDebt: ProviderMaintenanceDebt = {
|
||||
pendingBytes: 0,
|
||||
converging: true
|
||||
}
|
||||
;(brain as any).metadataIndex.maintenanceDebt = async () => metadataDebt
|
||||
;(brain as any).graphIndex.maintenanceDebt = async () => graphDebt
|
||||
// Vector is deliberately left unpatched.
|
||||
|
||||
const report = await brain.maintenanceDebt()
|
||||
|
||||
expect(report.vector.outcome).toBe('unavailable')
|
||||
expect(report.vector.debt).toBeUndefined()
|
||||
|
||||
expect(report.metadata.outcome).toBe('reported')
|
||||
expect(report.metadata.debt).toBe(metadataDebt)
|
||||
|
||||
expect(report.graph.outcome).toBe('reported')
|
||||
expect(report.graph.debt).toBe(graphDebt)
|
||||
|
||||
await brain.close()
|
||||
})
|
||||
|
||||
it('an empty ProviderMaintenanceDebt object (every field omitted) is still honestly "reported" — presence of the hook, not the payload\'s richness, drives the outcome', async () => {
|
||||
const brain = await freshBrain()
|
||||
|
||||
const emptyDebt: ProviderMaintenanceDebt = {}
|
||||
;(brain as any).graphIndex.maintenanceDebt = async () => emptyDebt
|
||||
|
||||
const report = await brain.maintenanceDebt()
|
||||
|
||||
expect(report.graph.outcome).toBe('reported')
|
||||
expect(report.graph.debt).toEqual({})
|
||||
|
||||
await brain.close()
|
||||
})
|
||||
})
|
||||
|
|
@ -307,4 +307,92 @@ describe('brain.warm()', () => {
|
|||
expect(report.totalDurationMs).toBeGreaterThanOrEqual(0)
|
||||
await brain.close()
|
||||
})
|
||||
|
||||
// --- Metadata leg routes through the ACTIVE provider (8.10.1) -----------
|
||||
//
|
||||
// `MetadataIndexProvider` is a ~50-method interface (src/plugin.ts) — far
|
||||
// too large to hand-write a compliant fake class the way `FakeVectorProvider`
|
||||
// fakes the ~8-method `VectorIndexProvider` above. Test (c) already
|
||||
// establishes this file's pattern for the metadata leg: exercise the REAL
|
||||
// `MetadataIndexManager` instance and shape just the probe points brain.ts
|
||||
// reads (`typeof provider.warm === 'function'` /
|
||||
// `typeof provider.hydrateAll === 'function'`) directly on that instance.
|
||||
// Shadowing an own property on the live object stands in for "a different
|
||||
// provider implementation" without needing a hand-rolled full fake — the
|
||||
// rest of the real manager (used by add()/init() above) is untouched.
|
||||
describe('metadata leg — warm() routes through the active provider', () => {
|
||||
it('(f) calls the ACTIVE metadata provider\'s warm() when present and reports "warmed", never falling back to hydrateAll', async () => {
|
||||
const brain = new Brainy({
|
||||
requireSubtype: false,
|
||||
storage: { type: 'memory' },
|
||||
silent: true
|
||||
})
|
||||
await brain.init()
|
||||
await brain.add({ data: 'a', type: NounType.Thing, vector: V(1) })
|
||||
|
||||
const metadataIndex = (brain as any).metadataIndex
|
||||
let warmCalls = 0
|
||||
let hydrateAllCalls = 0
|
||||
const origHydrateAll = metadataIndex.hydrateAll.bind(metadataIndex)
|
||||
metadataIndex.hydrateAll = async (...args: unknown[]) => {
|
||||
hydrateAllCalls++
|
||||
return origHydrateAll(...args)
|
||||
}
|
||||
// Simulates a native metadata provider declaring the optional `warm()`
|
||||
// hook added to `MetadataIndexProvider` (src/plugin.ts) in 8.10.1.
|
||||
metadataIndex.warm = async () => {
|
||||
warmCalls++
|
||||
}
|
||||
|
||||
const report = await brain.warm()
|
||||
|
||||
expect(warmCalls).toBe(1)
|
||||
expect(hydrateAllCalls).toBe(0) // warm() ran — no hydrateAll fallback
|
||||
expect(report.metadata.outcome).toBe('warmed')
|
||||
await brain.close()
|
||||
})
|
||||
|
||||
it('reports "unavailable" when the active metadata provider implements neither warm() nor hydrateAll() (the honest branch a native provider without either hook must hit)', async () => {
|
||||
const brain = new Brainy({
|
||||
requireSubtype: false,
|
||||
storage: { type: 'memory' },
|
||||
silent: true
|
||||
})
|
||||
await brain.init()
|
||||
await brain.add({ data: 'a', type: NounType.Thing, vector: V(1) })
|
||||
|
||||
const metadataIndex = (brain as any).metadataIndex
|
||||
// Shadow away BOTH optional hooks — models a genuinely native provider
|
||||
// that (unlike the built-in JS manager) offers neither seam. This must
|
||||
// never fall back to calling init() as a stand-in for warmth.
|
||||
metadataIndex.warm = undefined
|
||||
metadataIndex.hydrateAll = undefined
|
||||
|
||||
const report = await brain.warm()
|
||||
|
||||
expect(report.metadata.outcome).toBe('unavailable')
|
||||
await brain.close()
|
||||
})
|
||||
|
||||
it('the built-in JS manager (no warm()) still reports "warmed" via its existing hydrateAll() duck-type — unchanged by the new provider hook', async () => {
|
||||
const brain = new Brainy({
|
||||
requireSubtype: false,
|
||||
storage: { type: 'memory' },
|
||||
silent: true
|
||||
})
|
||||
await brain.init()
|
||||
await brain.add({ data: 'a', type: NounType.Thing, vector: V(1) })
|
||||
|
||||
// No patching at all — the default built-in MetadataIndexManager has
|
||||
// hydrateAll() but no warm(), exactly as it did before this change.
|
||||
const metadataIndex = (brain as any).metadataIndex
|
||||
expect(typeof metadataIndex.warm).not.toBe('function')
|
||||
expect(typeof metadataIndex.hydrateAll).toBe('function')
|
||||
|
||||
const report = await brain.warm()
|
||||
|
||||
expect(report.metadata.outcome).toBe('warmed')
|
||||
await brain.close()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue