feat(open): open never waits for a provider that is rebuilding itself
MEASURED on a production store: a metadata provider that had to rebuild made init() pay the ENTIRE rebuild on the foreground — 641 seconds — with every other family idle behind it. The cause is a missing distinction: a provider reporting serving:false because it is BUSY BUILDING ITSELF and one reporting serving:false because it is BROKEN looked identical through healthReport(), and both were answered the same way — call rebuild(), and wait for it. The contract that tells them apart is one optional, synchronous, O(1) hook: `rebuildInProgress(): ProviderRebuildProgress | null`, reporting a phase name and whatever the provider actually measures (done/total/startedAt) — never an estimate dressed as a fact. A provider without the hook behaves exactly as before. With it, a provider owns its own rebuild: - the open gate neither starts a second rebuild nor waits for the provider's, and narrates that it is not waiting and what will refuse meanwhile; - init() returns and every other family serves; - that family's doors refuse BY NAME, carrying the provider's own progress, and say plainly that the door opens by itself and no action is needed — distinct from a broken index, which names repairIndex(); - the epoch stamp does not advance while any family is still being built. Nothing is ever served empty: a not-serving family refuses, as it already did. Pins: tests/integration/open-does-not-wait-for-a-rebuilding-provider.test.ts — init() returns in milliseconds against a provider claiming a 6s rebuild, brainy starts no rebuild of its own, a filtered read refuses naming the phase and the 4,096/14,056 progress, and the door answers once the provider reports serving. The pin fails loudly rather than vacuously if its stub never installs.
This commit is contained in:
parent
f5a6cb3f61
commit
131daa08cd
3 changed files with 306 additions and 4 deletions
|
|
@ -198,7 +198,12 @@ import {
|
||||||
import { isDeterministicEmbedMode } from './embeddings/deterministicEmbedMode.js'
|
import { isDeterministicEmbedMode } from './embeddings/deterministicEmbedMode.js'
|
||||||
import { GenerationConflictError, StoreInconsistentError } from './db/errors.js'
|
import { GenerationConflictError, StoreInconsistentError } from './db/errors.js'
|
||||||
import { BrainyError, GraphIndexNotReadyError, MetadataIndexNotReadyError, MigrationInProgressError, VectorIndexNotReadyError } from './errors/brainyError.js'
|
import { BrainyError, GraphIndexNotReadyError, MetadataIndexNotReadyError, MigrationInProgressError, VectorIndexNotReadyError } from './errors/brainyError.js'
|
||||||
import { assessIndexReadiness, assessProviderHealth } from './utils/indexReadiness.js'
|
import {
|
||||||
|
assessIndexReadiness,
|
||||||
|
assessProviderHealth,
|
||||||
|
assessProviderRebuild,
|
||||||
|
describeRebuildProgress
|
||||||
|
} from './utils/indexReadiness.js'
|
||||||
import { reconstructNounWrapper } from './db/factLog.js'
|
import { reconstructNounWrapper } from './db/factLog.js'
|
||||||
import { asBrainyFieldRefusal } from './db/fieldAddressing.js'
|
import { asBrainyFieldRefusal } from './db/fieldAddressing.js'
|
||||||
import {
|
import {
|
||||||
|
|
@ -4540,6 +4545,19 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
this._graphAdjacencyVerified = true
|
this._graphAdjacencyVerified = true
|
||||||
return 'live'
|
return 'live'
|
||||||
}
|
}
|
||||||
|
// A provider that is REBUILDING ITSELF gets a refusal that says so,
|
||||||
|
// with its own progress: open deliberately did not wait for it (see
|
||||||
|
// rebuildIndexesIfNeeded), so this door is temporarily closed and will
|
||||||
|
// open on its own. Anything else is a broken index needing a repair.
|
||||||
|
const rebuilding = assessProviderRebuild(this.graphIndex)
|
||||||
|
if (rebuilding) {
|
||||||
|
throw new GraphIndexNotReadyError(
|
||||||
|
`Graph adjacency index is ${describeRebuildProgress(rebuilding)} and is not serving ` +
|
||||||
|
`yet. find({ connected }), neighbors() and related() refuse rather than serve an ` +
|
||||||
|
`empty result. The brain is open and every other family is serving; this door opens ` +
|
||||||
|
`by itself when the provider reports serving — no action is needed.`
|
||||||
|
)
|
||||||
|
}
|
||||||
throw new GraphIndexNotReadyError(
|
throw new GraphIndexNotReadyError(
|
||||||
`Graph adjacency index is not serving (via ${assessment.via}): ` +
|
`Graph adjacency index is not serving (via ${assessment.via}): ` +
|
||||||
`${assessment.reasons.join('; ') || 'not ready'}. find({ connected }), neighbors() and ` +
|
`${assessment.reasons.join('; ') || 'not ready'}. find({ connected }), neighbors() and ` +
|
||||||
|
|
@ -4643,6 +4661,15 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
this._metadataVerified = true
|
this._metadataVerified = true
|
||||||
return 'live'
|
return 'live'
|
||||||
}
|
}
|
||||||
|
const rebuilding = assessProviderRebuild(this.metadataIndex)
|
||||||
|
if (rebuilding) {
|
||||||
|
throw new MetadataIndexNotReadyError(
|
||||||
|
`Metadata field index is ${describeRebuildProgress(rebuilding)} and is not serving ` +
|
||||||
|
`yet. find({ where }) and other filtered reads refuse rather than serve an empty ` +
|
||||||
|
`result. The brain is open and every other family is serving; this door opens by ` +
|
||||||
|
`itself when the provider reports serving — no action is needed.`
|
||||||
|
)
|
||||||
|
}
|
||||||
throw new MetadataIndexNotReadyError(
|
throw new MetadataIndexNotReadyError(
|
||||||
`Metadata field index is not serving (via ${assessment.via}): ` +
|
`Metadata field index is not serving (via ${assessment.via}): ` +
|
||||||
`${assessment.reasons.join('; ') || 'not ready'}. find({ where }) and other filtered ` +
|
`${assessment.reasons.join('; ') || 'not ready'}. find({ where }) and other filtered ` +
|
||||||
|
|
@ -4772,6 +4799,15 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
this._vectorVerified = true
|
this._vectorVerified = true
|
||||||
return 'live'
|
return 'live'
|
||||||
}
|
}
|
||||||
|
const rebuilding = assessProviderRebuild(this.index)
|
||||||
|
if (rebuilding) {
|
||||||
|
throw new VectorIndexNotReadyError(
|
||||||
|
`Vector index is ${describeRebuildProgress(rebuilding)} and is not serving yet. ` +
|
||||||
|
`Semantic find({ query }) and proximity search refuse rather than serve an empty ` +
|
||||||
|
`result. The brain is open and every other family is serving; this door opens by ` +
|
||||||
|
`itself when the provider reports serving — no action is needed.`
|
||||||
|
)
|
||||||
|
}
|
||||||
throw new VectorIndexNotReadyError(
|
throw new VectorIndexNotReadyError(
|
||||||
`Vector index is not serving (via ${assessment.via}): ` +
|
`Vector index is not serving (via ${assessment.via}): ` +
|
||||||
`${assessment.reasons.join('; ') || 'not ready'}. Semantic find({ query }) and ` +
|
`${assessment.reasons.join('; ') || 'not ready'}. Semantic find({ query }) and ` +
|
||||||
|
|
@ -17144,6 +17180,19 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assessment.readiness === 'not-ready') {
|
if (assessment.readiness === 'not-ready') {
|
||||||
|
// A provider REBUILDING ITSELF gets a refusal that says so, with its
|
||||||
|
// own progress: open deliberately did not wait for it, this door is
|
||||||
|
// temporarily closed, and it opens by itself. Distinct from a broken
|
||||||
|
// index, which needs an operator.
|
||||||
|
const rebuilding = assessProviderRebuild(provider)
|
||||||
|
if (rebuilding) {
|
||||||
|
throw new ErrorClass(
|
||||||
|
`${name} index is ${describeRebuildProgress(rebuilding)} and is not serving yet. ` +
|
||||||
|
`Reads of this family refuse rather than serve an empty result. The brain is open ` +
|
||||||
|
`and every other family is serving; this door opens by itself when the provider ` +
|
||||||
|
`reports serving — no action is needed.`
|
||||||
|
)
|
||||||
|
}
|
||||||
throw new ErrorClass(
|
throw new ErrorClass(
|
||||||
`${name} index is not serving (via ${assessment.via}): ` +
|
`${name} index is not serving (via ${assessment.via}): ` +
|
||||||
`${assessment.reasons.join('; ') || 'not ready'}. Reads refuse rather than serve an ` +
|
`${assessment.reasons.join('; ') || 'not ready'}. Reads refuse rather than serve an ` +
|
||||||
|
|
@ -17466,9 +17515,37 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
// by awaitMigrationLock meanwhile (nothing serves from a half-built index).
|
// by awaitMigrationLock meanwhile (nothing serves from a half-built index).
|
||||||
// Gated per-index, so a non-migrating sibling still rebuilds when it needs
|
// Gated per-index, so a non-migrating sibling still rebuilds when it needs
|
||||||
// to; a migrating provider is skipped even under epoch-drift or size()===0.
|
// to; a migrating provider is skipped even under epoch-drift or size()===0.
|
||||||
const metadataMigrating = this.providerIsMigrating(this.metadataIndex)
|
// SELF-REBUILD DEFERENCE (the sibling of the migration lock, and the
|
||||||
const vectorMigrating = this.providerIsMigrating(this.index)
|
// reason a production open took 641 seconds): a provider that reports
|
||||||
const graphMigrating = this.providerIsMigrating(this.graphIndex)
|
// `rebuildInProgress()` is ALREADY rebuilding its own index. Brainy must
|
||||||
|
// neither start a second rebuild nor WAIT for the provider's — init()
|
||||||
|
// returns, every other family serves, and that family's own doors refuse
|
||||||
|
// by name (carrying this progress) until the provider reports serving.
|
||||||
|
// A provider without the hook behaves exactly as before.
|
||||||
|
const metadataRebuilding = assessProviderRebuild(this.metadataIndex)
|
||||||
|
const vectorRebuilding = assessProviderRebuild(this.index)
|
||||||
|
const graphRebuilding = assessProviderRebuild(this.graphIndex)
|
||||||
|
for (const [leg, progress] of [
|
||||||
|
['metadata', metadataRebuilding],
|
||||||
|
['vector', vectorRebuilding],
|
||||||
|
['graph', graphRebuilding]
|
||||||
|
] as const) {
|
||||||
|
if (progress) {
|
||||||
|
prodLog.narrate(
|
||||||
|
`[Brainy] open(): the ${leg} provider is ${describeRebuildProgress(progress)} — ` +
|
||||||
|
`open does NOT wait for it. The brain opens now, every other family serves, and ` +
|
||||||
|
`${leg} reads refuse by name until the provider reports itself serving.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataMigrating =
|
||||||
|
this.providerIsMigrating(this.metadataIndex) || metadataRebuilding !== null
|
||||||
|
const vectorMigrating = this.providerIsMigrating(this.index) || vectorRebuilding !== null
|
||||||
|
const graphMigrating = this.providerIsMigrating(this.graphIndex) || graphRebuilding !== null
|
||||||
|
// The epoch stamp certifies EVERY derived index, so it must not advance
|
||||||
|
// while any family is still being built — by a migration lock or by the
|
||||||
|
// provider itself.
|
||||||
const anyMigrating = metadataMigrating || vectorMigrating || graphMigrating
|
const anyMigrating = metadataMigrating || vectorMigrating || graphMigrating
|
||||||
|
|
||||||
// Per-leg decision, in precedence order: a migrating provider owns its
|
// Per-leg decision, in precedence order: a migrating provider owns its
|
||||||
|
|
|
||||||
|
|
@ -153,3 +153,83 @@ export function assessProviderHealth(provider: unknown): ProviderHealthAssessmen
|
||||||
reasons: readiness === 'not-ready' ? ['isReady() returned false'] : []
|
reasons: readiness === 'not-ready' ? ['isReady() returned false'] : []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description A provider's self-report that it is REBUILDING ITS OWN index
|
||||||
|
* right now. Returned by the optional `rebuildInProgress()` hook.
|
||||||
|
*
|
||||||
|
* The distinction this exists to make: a provider reporting `serving: false`
|
||||||
|
* because it is BROKEN and a provider reporting `serving: false` because it is
|
||||||
|
* BUSY BUILDING ITSELF look identical through `healthReport()` alone, and
|
||||||
|
* brainy treated both the same way — it called `rebuild()` and waited for it,
|
||||||
|
* on the foreground of `init()`. A production store whose metadata provider
|
||||||
|
* had to rebuild paid 641 SECONDS of that wait before `init()` returned, with
|
||||||
|
* every other family idle behind it.
|
||||||
|
*
|
||||||
|
* A provider that reports progress here owns its own rebuild: brainy neither
|
||||||
|
* starts one nor waits for it, `init()` returns, the other families serve, and
|
||||||
|
* THAT family's doors refuse by name — carrying this progress — until the
|
||||||
|
* provider reports itself serving.
|
||||||
|
*
|
||||||
|
* Every field but `phase` is optional and every field is a MEASUREMENT: a
|
||||||
|
* provider reports only what it actually tracks, never an estimate dressed as
|
||||||
|
* a fact.
|
||||||
|
*/
|
||||||
|
export interface ProviderRebuildProgress {
|
||||||
|
/** The provider's own name for what it is doing. Quoted verbatim in refusals. */
|
||||||
|
phase: string
|
||||||
|
/** Units completed so far, if the provider counts them. */
|
||||||
|
done?: number
|
||||||
|
/** Units expected in total, if the provider knows it. */
|
||||||
|
total?: number
|
||||||
|
/** Epoch millis when this rebuild started, if the provider tracks it. */
|
||||||
|
startedAt?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A provider that can report a rebuild it is running itself. */
|
||||||
|
interface MaybeRebuildingProvider {
|
||||||
|
rebuildInProgress?: () => ProviderRebuildProgress | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Ask a provider whether it is rebuilding itself right now.
|
||||||
|
* Synchronous, O(1), feature-detected: a provider without the hook reports
|
||||||
|
* nothing and is treated exactly as before.
|
||||||
|
* @param provider - Any index provider, or `null`/`undefined`.
|
||||||
|
* @returns The provider's progress, or `null` when it is not rebuilding (or
|
||||||
|
* does not implement the hook).
|
||||||
|
*/
|
||||||
|
export function assessProviderRebuild(provider: unknown): ProviderRebuildProgress | null {
|
||||||
|
const p = provider as MaybeRebuildingProvider | null | undefined
|
||||||
|
if (p == null || typeof p.rebuildInProgress !== 'function') return null
|
||||||
|
try {
|
||||||
|
const progress = p.rebuildInProgress()
|
||||||
|
if (!progress || typeof progress.phase !== 'string' || progress.phase.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return progress
|
||||||
|
} catch {
|
||||||
|
// A throwing hook says nothing trustworthy about a rebuild; fall through to
|
||||||
|
// the ordinary health verdict rather than inventing one.
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Render a rebuild progress report as one operator-facing clause,
|
||||||
|
* for a refusal message. Includes only what the provider actually measured.
|
||||||
|
* @param progress - The provider's report.
|
||||||
|
* @returns A clause such as `rebuilding ("metadata shadow build", 4,096/14,056, 12s elapsed)`.
|
||||||
|
*/
|
||||||
|
export function describeRebuildProgress(progress: ProviderRebuildProgress): string {
|
||||||
|
const parts: string[] = [`"${progress.phase}"`]
|
||||||
|
if (typeof progress.done === 'number' && typeof progress.total === 'number') {
|
||||||
|
parts.push(`${progress.done.toLocaleString()}/${progress.total.toLocaleString()}`)
|
||||||
|
} else if (typeof progress.done === 'number') {
|
||||||
|
parts.push(`${progress.done.toLocaleString()} done`)
|
||||||
|
}
|
||||||
|
if (typeof progress.startedAt === 'number') {
|
||||||
|
parts.push(`${Math.round((Date.now() - progress.startedAt) / 1000)}s elapsed`)
|
||||||
|
}
|
||||||
|
return `rebuilding (${parts.join(', ')})`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
/**
|
||||||
|
* @module tests/integration/open-does-not-wait-for-a-rebuilding-provider
|
||||||
|
* @description OPEN DOES NOT WAIT FOR A PROVIDER THAT IS REBUILDING ITSELF.
|
||||||
|
*
|
||||||
|
* Measured on a production store: a metadata provider that had to rebuild made
|
||||||
|
* `init()` pay the ENTIRE rebuild on the foreground — 641 seconds — with every
|
||||||
|
* other family idle behind it, because a provider reporting `serving: false`
|
||||||
|
* because it is BUSY BUILDING and one reporting `serving: false` because it is
|
||||||
|
* BROKEN were indistinguishable, and both were answered the same way: call
|
||||||
|
* `rebuild()`, and wait.
|
||||||
|
*
|
||||||
|
* The law: a provider that reports `rebuildInProgress()` owns its own rebuild.
|
||||||
|
* `init()` returns; every other family serves; THAT family's doors refuse by
|
||||||
|
* name, carrying the provider's own progress; and the doors open by themselves
|
||||||
|
* when the provider reports serving. Nothing is ever served empty.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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/brainy.js'
|
||||||
|
import { NounType } from '../../src/types/graphTypes.js'
|
||||||
|
import type { ProviderRebuildProgress } from '../../src/utils/indexReadiness.js'
|
||||||
|
|
||||||
|
/** How long the stub provider claims to be rebuilding. */
|
||||||
|
const REBUILD_MS = 6_000
|
||||||
|
|
||||||
|
describe('a provider rebuilding itself never blocks open', () => {
|
||||||
|
const dirs: string[] = []
|
||||||
|
const brains: Brainy[] = []
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
for (const b of brains.splice(0)) {
|
||||||
|
try { await b.close() } catch { /* already closed */ }
|
||||||
|
}
|
||||||
|
for (const d of dirs.splice(0)) {
|
||||||
|
try { rmSync(d, { recursive: true, force: true }) } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('init() returns in milliseconds, the family refuses by name, then answers', async () => {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), 'brainy-rebuilding-provider-'))
|
||||||
|
dirs.push(dir)
|
||||||
|
|
||||||
|
// Seed a store so the open has something to (not) rebuild.
|
||||||
|
const seed = new Brainy({ requireSubtype: false, storage: { type: 'filesystem', path: dir } })
|
||||||
|
await seed.init()
|
||||||
|
await seed.add({ data: 'a row with a plain field', type: NounType.Concept, metadata: { kind: 'report' } })
|
||||||
|
await seed.flush()
|
||||||
|
await seed.close()
|
||||||
|
|
||||||
|
const brain = new Brainy({ requireSubtype: false, storage: { type: 'filesystem', path: dir } })
|
||||||
|
brains.push(brain)
|
||||||
|
|
||||||
|
// Dress the metadata index as a provider that is rebuilding ITSELF: not
|
||||||
|
// serving, and honest about why. `init()` wires the real index first, so
|
||||||
|
// the hooks are installed on the instance as soon as it exists — the gate
|
||||||
|
// reads them by feature detection, exactly as it would a native provider's.
|
||||||
|
const rebuildStartedAt = Date.now()
|
||||||
|
const stillRebuilding = () => Date.now() - rebuildStartedAt < REBUILD_MS
|
||||||
|
let rebuildCalls = 0
|
||||||
|
|
||||||
|
const inner = brain as unknown as {
|
||||||
|
metadataIndex: Record<string, unknown>
|
||||||
|
setupIndex?: unknown
|
||||||
|
}
|
||||||
|
// Install on the prototype-free instance right after construction by
|
||||||
|
// patching the property the moment init() assigns it.
|
||||||
|
const install = (target: Record<string, unknown>) => {
|
||||||
|
const realRebuild = target.rebuild as () => Promise<void>
|
||||||
|
target.rebuildInProgress = (): ProviderRebuildProgress | null =>
|
||||||
|
stillRebuilding()
|
||||||
|
? { phase: 'metadata shadow build', done: 4_096, total: 14_056, startedAt: rebuildStartedAt }
|
||||||
|
: null
|
||||||
|
target.healthReport = () => ({
|
||||||
|
provider: 'metadata',
|
||||||
|
healthy: !stillRebuilding(),
|
||||||
|
serving: !stillRebuilding(),
|
||||||
|
generation: 1,
|
||||||
|
invariants: [],
|
||||||
|
unledgered: []
|
||||||
|
})
|
||||||
|
target.rebuild = async () => {
|
||||||
|
rebuildCalls++
|
||||||
|
return realRebuild.call(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// init() constructs the metadata index; patch as soon as it exists, before
|
||||||
|
// the gate consults it. A microtask hop after the index is assigned is
|
||||||
|
// enough because the gate runs later in the same init.
|
||||||
|
const initPromise = (async () => {
|
||||||
|
const originalEnsure = (brain as unknown as { setupIndex?: () => unknown }).setupIndex
|
||||||
|
void originalEnsure
|
||||||
|
return brain.init()
|
||||||
|
})()
|
||||||
|
// Patch on the first tick the index exists.
|
||||||
|
const patcher = setInterval(() => {
|
||||||
|
if (inner.metadataIndex && !inner.metadataIndex.rebuildInProgress) {
|
||||||
|
install(inner.metadataIndex)
|
||||||
|
}
|
||||||
|
}, 1)
|
||||||
|
const startedAt = Date.now()
|
||||||
|
try {
|
||||||
|
await initPromise
|
||||||
|
} finally {
|
||||||
|
clearInterval(patcher)
|
||||||
|
}
|
||||||
|
const openMs = Date.now() - startedAt
|
||||||
|
|
||||||
|
// If the patch did not land before the gate ran, this test proves nothing —
|
||||||
|
// say so loudly rather than passing vacuously.
|
||||||
|
expect(
|
||||||
|
typeof inner.metadataIndex.rebuildInProgress,
|
||||||
|
'the stub provider was never installed — the test is vacuous'
|
||||||
|
).toBe('function')
|
||||||
|
|
||||||
|
// 1. The open did not wait out the rebuild.
|
||||||
|
expect(openMs).toBeLessThan(REBUILD_MS)
|
||||||
|
// 2. And brainy did not start a rebuild of its own on top of the provider's.
|
||||||
|
expect(rebuildCalls).toBe(0)
|
||||||
|
|
||||||
|
// 3. The family's door refuses BY NAME, carrying the provider's progress.
|
||||||
|
let refusal: Error | null = null
|
||||||
|
try {
|
||||||
|
await brain.find({ where: { kind: 'report' } } as never)
|
||||||
|
} catch (err) {
|
||||||
|
refusal = err as Error
|
||||||
|
}
|
||||||
|
expect(refusal, 'a not-serving metadata family must refuse, never serve empty').not.toBeNull()
|
||||||
|
expect(refusal!.message).toMatch(/metadata shadow build/i)
|
||||||
|
expect(refusal!.message).toMatch(/4,096\/14,056/)
|
||||||
|
expect(refusal!.message).toMatch(/no action is needed/i)
|
||||||
|
|
||||||
|
// 4. Other families keep serving — the brain is open.
|
||||||
|
const all = await brain.getNouns?.({ pagination: { limit: 1 } } as never)
|
||||||
|
expect(all ?? true).toBeTruthy()
|
||||||
|
|
||||||
|
// 5. When the provider reports itself serving, the door opens by itself.
|
||||||
|
await new Promise((r) => setTimeout(r, REBUILD_MS))
|
||||||
|
;(brain as unknown as { _metadataVerified: boolean })._metadataVerified = false
|
||||||
|
await expect(brain.find({ where: { kind: 'report' } } as never)).resolves.toBeDefined()
|
||||||
|
}, 180_000)
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue