From e766ed0a846a251e0807eb525f29829b56c3e2b0 Mon Sep 17 00:00:00 2001 From: David Snelling Date: Wed, 2 Sep 2026 16:14:24 -0700 Subject: [PATCH 1/2] test(find-connected): close the brain this file leaks, and name the half a short answer came from MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TWO THINGS, both about the same file. THE LEAK, which is a defect of the test. `afterAll` set `brain = null`. That does not close a brain — it only makes it unreachable from here. The instance stayed open and registered with its unref'd cadence timer running, and the gate config runs the whole suite in ONE process (pool: 'forks', singleFork: true — two files report the same process.pid), so a brain leaked in this file goes on narrating its flushes into every file that runs after it. This one holds 151 entities and 30 relations. It is closed now. It is not the only leaker in the suite — a create-versus-close scan turns up 67 files with the same shape, and this is one of them, not the cause of anything on its own. Fixing the file I was already in. THE DIAGNOSTIC. 'walks the vector leg over the neighbours only' went red on the gate box (1 row of a requested 5) while passing here in isolation eight runs out of eight, beside its own box predecessor, and under a perturbed random stream — and it passed on the box one gate earlier behind the IDENTICAL predecessor. So the cause is process state accumulated by the time this file runs, and a bare count mismatch says nothing about which half broke. The case now runs the same query without the vector leg first, as a control, and reports both counts: both short means the neighbour set or the filter, only the vector leg short means the walk — which matters here because every row in this corpus carries an IDENTICAL vector, so the walk is ranking an exact tie and a tie has no defined order to return 5 of. The assertion is unchanged: still exactly 5, still every row a neighbour. --- .../integration/find-connected-order.test.ts | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/integration/find-connected-order.test.ts b/tests/integration/find-connected-order.test.ts index b04e7f99..3b7560e4 100644 --- a/tests/integration/find-connected-order.test.ts +++ b/tests/integration/find-connected-order.test.ts @@ -67,6 +67,13 @@ describe('find({ connected }) is graph-first: neighbours → filter → page', ( }) afterAll(async () => { + // CLOSE IT. Dropping the reference does not close a brain — it only makes + // it unreachable from here. The instance stays open and registered, its + // unref'd cadence timer keeps running, and because the gate config runs the + // whole suite in ONE process (pool: 'forks', singleFork: true) it goes on + // narrating its flushes into every test file that runs after this one. + // A test that leaks a brain is a defect of the test. + await brain?.close() brain = null as any }) @@ -137,13 +144,34 @@ describe('find({ connected }) is graph-first: neighbours → filter → page', ( }) it('walks the vector leg over the neighbours only', async () => { + // The SAME query without the vector leg, first. Both legs draw from the + // one neighbour set, so this is the control: it says whether a short answer + // came from the adjacency/filter (both legs short) or from the vector walk + // alone (only the vector leg short). Cheap, and it turns a bare count + // mismatch into a named half — this case has gone red on the gate box + // while passing in isolation and beside its own predecessor, so the next + // red must arrive already carrying the half it belongs to. + const control = await brain.find({ + connected: { from: anchor, direction: 'out' }, + where: { kind: 'note' }, + limit: 5 + }) + const results = await brain.find({ vector: sharedVector, connected: { from: anchor, direction: 'out' }, where: { kind: 'note' }, limit: 5 }) - expect(results).toHaveLength(5) + + expect( + results.length, + `the vector leg returned ${results.length} of a requested 5. The same query ` + + `WITHOUT the vector returned ${control.length}: if that is also short the ` + + `neighbour set or the filter is the cause, and if it is 5 the vector walk is — ` + + `note every row in this corpus carries an identical vector, so the walk is ` + + `ranking an exact tie.` + ).toBe(5) for (const r of results) expect(neighbourIds.has(r.entity.id)).toBe(true) }) From dadfa61b5fd5baed5a6fcec100dcf54ef3f6dc3e Mon Sep 17 00:00:00 2001 From: David Snelling Date: Wed, 2 Sep 2026 16:17:55 -0700 Subject: [PATCH 2/2] =?UTF-8?q?test(idle):=20capture=20the=20stack=20behin?= =?UTF-8?q?d=20each=20flush=20narration=20=E2=80=94=20the=20line=20alone?= =?UTF-8?q?=20cannot=20name=20its=20brain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The self-diagnosis from the last round worked: the box says this brain's own providers were NOT called, so the flush pairs inside the 90 s window belong to another brain in the same process. It could not say WHICH, and the advice it gave — read the 'stdout | > ' prefix — cannot work here: vitest tags a stdout block with the test that is RUNNING, and these lines are captured by this test's own console hook anyway. Teeing them through would only ever print this test's name. The call stack does name the driver, so it is captured beside each line and the first one is reported: `kickBackgroundFlush('idle')` under `armIdleFlushTimer` is some brain's cadence timer, the deferred-embed worker's commit path is a brain still landing vectors, and a bare `flush()` is an explicit caller. Why that distinction settles it. A flush only narrates PAST the dirty gate, and `_dirtySinceLastFlush` is set in exactly three places — `noteWriteForPersistence()` (both commit paths, and the deferred-embed worker lands its vectors through the single-op one), `clear()`, and `repairIndex()`. So a narrating flush is a flush whose brain really did commit a write; "0 ms" is the flush being cheap, not the flush being empty. That reading rules OUT the re-arming-follow-up theory: the queued follow-up is armed only by a concurrent flush() caller, cleared before promotion, and a promoted run over a clean brain returns at the dirty gate without touching a provider or printing a line. Context the message now carries: the suite runs every file in ONE process, and a create-versus-close scan puts 67 test files above the line — more brains made than closed. This assertion is downstream of that, and the next red arrives with the stack that names which one. --- tests/integration/idle-costs-nothing.test.ts | 26 ++++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/tests/integration/idle-costs-nothing.test.ts b/tests/integration/idle-costs-nothing.test.ts index 2374627e..7e664a28 100644 --- a/tests/integration/idle-costs-nothing.test.ts +++ b/tests/integration/idle-costs-nothing.test.ts @@ -70,8 +70,22 @@ describe('an idle brain costs nothing', () => { await brain.flush() const logged: string[] = [] + // The STACK behind each narration, kept beside the line it belongs to. + // vitest tags a stdout block with the test that is RUNNING, not the brain + // that wrote it, so teeing these lines through would only ever name this + // test. The call stack does name the driver: `kickBackgroundFlush('idle')` + // under `armIdleFlushTimer` is a cadence flush on some brain, the deferred- + // embed worker's commit path is a brain still landing vectors, and a bare + // `flush()` is an explicit caller. That distinction is the whole question. + const stacks: string[] = [] const origLog = console.log - console.log = ((...a: unknown[]) => { logged.push(a.map(String).join(' ')) }) as typeof console.log + console.log = ((...a: unknown[]) => { + const line = a.map(String).join(' ') + logged.push(line) + if (/All indexes flushed to disk|Flushing Brainy indexes/.test(line)) { + stacks.push(new Error('flush narration').stack ?? '(no stack)') + } + }) as typeof console.log // Watch the providers directly: a flush that runs calls all of them. const storage = (brain as unknown as { storage: { flushCounts: () => Promise } }).storage @@ -113,10 +127,12 @@ describe('an idle brain costs nothing', () => { ) expect( flushChatter, - `a flush narrated during the ${IDLE_WATCH_MS}ms idle window. This brain's own ` + - `providers were NOT called (asserted above), so the lines below were printed by ` + - `another brain alive in this process — find it by the 'stdout | > ' ` + - `prefix in the run log:\n${flushChatter.join('\n')}` + `${flushChatter.length} flush line(s) narrated during the ${IDLE_WATCH_MS}ms idle ` + + `window. This brain's own providers were NOT called (asserted above), so another ` + + `brain alive in this process printed them — the suite runs every file in ONE ` + + `process and 67 test files create more brains than they close.\n` + + `${flushChatter.join('\n')}\n\n` + + `The stack behind the first one names the driver:\n${stacks[0] ?? '(none captured)'}` ).toEqual([]) }, 180_000)