From 7639493e8c27e5ca584fa874994c56188274856c Mon Sep 17 00:00:00 2001 From: Paolo Belcastro Date: Mon, 6 Jul 2026 11:57:36 +0200 Subject: [PATCH 1/2] feat(autopilot): wire the nightly conversation-parser probe into the loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v0.41.16.0 probe module (runConversationParserNightlyProbe) shipped as a callable phase with its scheduler wire-up deferred; the follow-up never landed. Result: zero call sites — the enable flag did nothing, the D10 tokenmax default-on never fired, and doctor's conversation_parser_probe_health was a hardcoded 'Skipped' stub that ignored config and mode. This adds the deferred wiring as autopilot step 4.6, mirroring the nightly quality probe (4.5): - Flag reads dual-plane (DB row from gbrain config set wins; file plane fallback); search.mode=tokenmax keeps the probe default-on per D10. - Fixtures resolve from the gbrain package root (the committed test/fixtures/conversation-formats/), NOT the brain repoPath. Compiled binaries carry no source tree: skip quietly with a once-per-process stderr note instead of writing failure rows that would flip doctor to WARN on every binary install. - New parser-probe audit trail (audit-parser-probe.ts, shared audit-writer primitive, parser-probe-YYYY-Www.jsonl). rate_limited outcomes are NOT logged: the loop ticks every few minutes, so auditing each skip would flood the file. The 24h gate (parserProbeRanWithin) reads real outcomes. - Doctor stub replaced with computeConversationParserProbeHealthCheck: enable hint when off+silent, WARN on any non-pass run in 7 days. - try/catch posture identical to 4.5 — a probe failure never crashes the loop and never bumps consecutiveErrors. Tests: audit round-trip + rate-limit gate, doctor check branches, and source-shape pins for the wiring protections. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FQgByq4aqQq2PP8UHCdnfk --- src/commands/autopilot.ts | 59 ++++++++++ src/commands/doctor.ts | 84 ++++++++++++--- src/core/audit-parser-probe.ts | 63 +++++++++++ src/core/config.ts | 10 ++ src/core/conversation-parser/nightly-probe.ts | 10 +- test/audit-parser-probe.serial.test.ts | 102 ++++++++++++++++++ test/autopilot-parser-probe-wiring.test.ts | 77 +++++++++++++ test/doctor-parser-probe-check.test.ts | 51 +++++++++ 8 files changed, 438 insertions(+), 18 deletions(-) create mode 100644 src/core/audit-parser-probe.ts create mode 100644 test/audit-parser-probe.serial.test.ts create mode 100644 test/autopilot-parser-probe-wiring.test.ts create mode 100644 test/doctor-parser-probe-check.test.ts diff --git a/src/commands/autopilot.ts b/src/commands/autopilot.ts index 927b571e5..785b16ffa 100644 --- a/src/commands/autopilot.ts +++ b/src/commands/autopilot.ts @@ -476,6 +476,9 @@ export async function runAutopilot(engine: BrainEngine, args: string[]) { process.on('SIGINT', () => { void shutdown('SIGINT'); }); let consecutiveErrors = 0; + // Parser-probe fixture warning is once-per-process, not once-per-cycle + // (compiled-binary installs have no source tree; don't spam the log). + let parserProbeFixtureWarned = false; // v0.37.7.0 #1162 — counter for consecutive reconnect failures. // Reset on every successful health probe or reconnect. Threshold // controlled by GBRAIN_AUTOPILOT_MAX_RECONNECT_FAILS env (default 30). @@ -1044,6 +1047,62 @@ export async function runAutopilot(engine: BrainEngine, args: string[]) { // informational; autopilot loop continues. } + // 4.6 — Nightly conversation-parser probe (v0.41.16.0 phase module; + // the scheduler wire-up was deferred at ship and is added here). Same + // posture as 4.5: the phase owns its gates (enabled/mode-gate, LLM + // key), the wiring owns invocation + the audit row, and a probe + // failure NEVER crashes the autopilot loop. Per D10 the probe is + // default-ON for search.mode=tokenmax, opt-in otherwise. + try { + const { runConversationParserNightlyProbe } = await import('../core/conversation-parser/nightly-probe.ts'); + const { logParserProbeEvent, parserProbeRanWithin } = await import('../core/audit-parser-probe.ts'); + const { isAvailable } = await import('../core/ai/gateway.ts'); + const { existsSync } = await import('node:fs'); + const { fileURLToPath } = await import('node:url'); + const { join } = await import('node:path'); + // Flag reads dual-plane: the DB row (`gbrain config set …`) wins, + // ~/.gbrain/config.json is the fallback. search.mode lives on the + // DB plane only (mode.ts owns it). + let parserDbEnabled: string | null = null; + let dbSearchMode: string | null = null; + try { + parserDbEnabled = await engine.getConfig('autopilot.conversation_parser_probe.enabled'); + dbSearchMode = await engine.getConfig('search.mode'); + } catch { /* DB unavailable → file plane only */ } + const parserEnabled = parserDbEnabled != null + ? parserDbEnabled === 'true' + : cfg?.autopilot?.conversation_parser_probe?.enabled === true; + const searchMode = dbSearchMode ?? ''; + // Fixtures are committed in the gbrain package (test/fixtures/…), + // NOT the brain repo — resolve from the module location. Compiled + // binaries carry no source tree: skip quietly instead of writing + // failure rows that would flip doctor to WARN on every binary install. + const pkgRoot = fileURLToPath(new URL('../..', import.meta.url)); + const fixturePath = join(pkgRoot, 'test', 'fixtures', 'conversation-formats', 'all.jsonl'); + const adversarialPath = join(pkgRoot, 'test', 'fixtures', 'conversation-formats', 'adversarial.jsonl'); + const shouldInvoke = parserEnabled || searchMode === 'tokenmax'; + if (shouldInvoke && existsSync(fixturePath) && existsSync(adversarialPath)) { + const result = await runConversationParserNightlyProbe({ + isEnabled: () => parserEnabled, + searchMode: () => searchMode, + hasLlmKey: () => isAvailable('chat'), + resolveFixturePath: () => fixturePath, + resolveAdversarialPath: () => adversarialPath, + now: () => new Date(), + shouldSkipForRateLimit: () => parserProbeRanWithin(24 * 60 * 60 * 1000), + }); + // rate_limited is a non-run: the loop ticks every few minutes, so + // logging every skip would flood the audit file with no-signal rows. + if (result.outcome !== 'rate_limited') logParserProbeEvent(result); + } else if (shouldInvoke && !parserProbeFixtureWarned) { + parserProbeFixtureWarned = true; + console.error(`[parser-probe] fixtures not found under ${pkgRoot}; skipping (probe needs a source-checkout install)`); + } + } catch (e) { + logError('autopilot.parser_probe', e); + // Informational, like 4.5: do NOT bump consecutiveErrors. + } + // Wait for next cycle await new Promise(r => setTimeout(r, interval * 1000)); } diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 9b0dc3a4b..9d4970f87 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -2896,6 +2896,54 @@ function _resolveSyncFreshnessHours(varName: string, fallback: number): number { * branch (disabled / enabled-no-events / enabled-all-pass / enabled-with-failures) * without spinning up the audit JSONL or a real config file. */ +/** + * Pure function form of the conversation_parser_probe_health check. + * Mirrors computeNightlyQualityProbeHealthCheck: skip-with-hint when the + * probe is off and silent, surface the last 7 days of audit events when + * it has run, WARN on any non-pass outcome. + * + * `effectiveEnabled` folds the D10 mode-gate in: explicitly enabled OR + * search.mode=tokenmax (where the probe is default-on). + */ +export function computeConversationParserProbeHealthCheck( + effectiveEnabled: boolean, + events: ReadonlyArray<{ outcome: string; ts: string; reason?: string }>, +): Check { + const name = 'conversation_parser_probe_health'; + if (!effectiveEnabled && events.length === 0) { + return { + name, + status: 'ok', + message: + 'disabled (opt-in; default-on only for search.mode=tokenmax). Enable with: ' + + '`gbrain config set autopilot.conversation_parser_probe.enabled true`', + }; + } + if (events.length === 0) { + return { + name, + status: 'ok', + message: 'enabled but no probe events in the last 7 days (next run by autopilot; fixtures require a source-checkout install).', + }; + } + const bad = events.filter(e => e.outcome !== 'pass'); + const latest = events[events.length - 1]!; + if (bad.length > 0) { + return { + name, + status: 'warn', + message: + `${bad.length}/${events.length} probe run(s) in the last 7 days did not pass; ` + + `latest: ${latest.outcome}${latest.reason ? ` (${latest.reason})` : ''}`, + }; + } + return { + name, + status: 'ok', + message: `${events.length} probe run(s) in the last 7 days, all pass (latest ${latest.ts}).`, + }; +} + export function computeNightlyQualityProbeHealthCheck( probeEnabled: boolean, events: ReadonlyArray<{ outcome: string; ts: string; detail?: string }>, @@ -4965,19 +5013,29 @@ export async function buildChecks( // 3d.5 v0.41.13.0 — conversation_parser_probe_health. Mode-gated // per D10: ON when search.mode=tokenmax, opt-in for other modes. - // Surface the last 7 days of nightly-probe events; warn on FAIL / - // BUDGET_EXCEEDED / adversarial_false_positive. - // - // v0.41.13.0 ships the probe as opt-in (autopilot wiring deferred - // to T7 in the cathedral plan); this check skips with an enable - // hint until the probe has at least one audit event written. - checks.push({ - name: 'conversation_parser_probe_health', - status: 'ok', - message: - 'Skipped (nightly probe is opt-in; enable with ' + - '`gbrain config set autopilot.conversation_parser_probe.enabled true`)', - }); + // Surfaces the last 7 days of nightly-probe audit events; warn on any + // non-pass outcome (fail / budget_exceeded / adversarial_false_positive). + // (Until the autopilot wire-up this was a hardcoded "Skipped" stub.) + try { + const { readRecentParserProbeEvents } = await import('../core/audit-parser-probe.ts'); + let parserProbeEnabled = false; + try { + let dbVal: string | null = null; + let dbMode: string | null = null; + try { + dbVal = engine ? await engine.getConfig('autopilot.conversation_parser_probe.enabled') : null; + dbMode = engine ? await engine.getConfig('search.mode') : null; + } catch { /* DB unavailable → file plane only */ } + const { loadConfig } = await import('../core/config.ts'); + const fileVal = (loadConfig() as any)?.autopilot?.conversation_parser_probe?.enabled; + const flagOn = dbVal != null ? dbVal === 'true' : fileVal === true; + parserProbeEnabled = flagOn || dbMode === 'tokenmax'; + } catch { /* config unavailable → treat as disabled */ } + const parserEvents = readRecentParserProbeEvents(7); + checks.push(computeConversationParserProbeHealthCheck(parserProbeEnabled, parserEvents)); + } catch { + // Best-effort; audit-log read failure shouldn't stop doctor. + } // 3e. home_dir_in_worktree (v0.35.8.0). Walks up from `gbrainPath()` // looking for a `.git` directory OR file. If found, warns: `~/.gbrain/` diff --git a/src/core/audit-parser-probe.ts b/src/core/audit-parser-probe.ts new file mode 100644 index 000000000..ac83d985d --- /dev/null +++ b/src/core/audit-parser-probe.ts @@ -0,0 +1,63 @@ +/** + * Nightly conversation-parser probe audit trail. + * + * One event per REAL probe run lands in + * `~/.gbrain/audit/parser-probe-YYYY-Www.jsonl` (ISO-week rotation via the + * shared audit-writer primitive; honors `GBRAIN_AUDIT_DIR`). + * Scheduler-cadence skips (`rate_limited`) are NOT logged — the autopilot + * loop ticks every few minutes, so logging every skip would flood the + * audit file with rows that carry no signal. + * + * Read by `gbrain doctor`'s `conversation_parser_probe_health` check and + * by the autopilot wiring's 24h rate-limit gate (`parserProbeRanWithin`). + */ + +import { createAuditWriter } from './audit/audit-writer.ts'; +import type { NightlyProbeResult } from './conversation-parser/nightly-probe.ts'; + +export type ParserProbeAuditEvent = NightlyProbeResult; + +const writer = createAuditWriter({ + featureName: 'parser-probe', + errorLabel: 'gbrain', + errorMessagePrefix: 'parser-probe audit ', + errorTrailer: '; probe continues', +}); + +/** Append one parser-probe event. Best-effort; never throws. */ +export function logParserProbeEvent(event: ParserProbeAuditEvent): void { + writer.log(event); +} + +/** + * Read recent parser-probe events (current + previous ISO week, filtered + * to the window). Missing files and corrupt rows are skipped silently. + */ +export function readRecentParserProbeEvents( + days = 7, + now: Date = new Date(), +): ParserProbeAuditEvent[] { + return writer.readRecent(days, now); +} + +/** Exposed for tests pinning the rotation edge cases. */ +export function computeParserProbeAuditFilename(now: Date = new Date()): string { + return writer.computeFilename(now); +} + +/** + * 24h rate-limit gate for the autopilot wiring: true when any audited run + * happened within `windowMs` of `now`. Only REAL outcomes are audited (see + * module header), so a pass/fail today blocks re-runs until tomorrow while + * scheduler-cadence skips never extend the window. + */ +export function parserProbeRanWithin( + windowMs: number, + now: Date = new Date(), +): boolean { + const cutoff = now.getTime() - windowMs; + return readRecentParserProbeEvents(2, now).some((ev) => { + const ts = Date.parse(ev.ts); + return Number.isFinite(ts) && ts >= cutoff; + }); +} diff --git a/src/core/config.ts b/src/core/config.ts index 8a79b0e06..8b067e4ee 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -96,6 +96,16 @@ export interface GBrainConfig { */ max_usd?: number; }; + /** + * v0.41.16.0 — nightly conversation-parser probe. Per D10: default ON + * for `search.mode=tokenmax` brains, opt-in for conservative/balanced. + * ~$0.05/night with the committed fixtures × Haiku polish. Gated + * INSIDE the autopilot tick body, like nightly_quality_probe. + */ + conversation_parser_probe?: { + /** Enable for non-tokenmax modes. Defaults to false. */ + enabled?: boolean; + }; /** * v0.42.x (#1685 GAP D) — extract_atoms backlog auto-drain. Default ON so a * pack-gated silent backlog never piles up unseen; daily-spend-capped so the diff --git a/src/core/conversation-parser/nightly-probe.ts b/src/core/conversation-parser/nightly-probe.ts index a386650a1..84f19eecd 100644 --- a/src/core/conversation-parser/nightly-probe.ts +++ b/src/core/conversation-parser/nightly-probe.ts @@ -17,11 +17,11 @@ * Cost: ~$0.05/night with default fixtures × Haiku polish. Bounded * by the active BudgetTracker the autopilot loop creates per-tick. * - * **Wiring into the autopilot loop is deferred to a follow-up** - * (filed in TODOS.md). v0.41.16.0 ships the phase as a callable - * module so doctor + future cron drivers can invoke it; the - * scheduler wire-up follows the same shape as - * `src/core/cycle/nightly-quality-probe.ts` (v0.40.1.0 Track D / T6). + * Wired into the autopilot loop (step 4.6 in autopilot.ts), following + * the same shape as `src/core/cycle/nightly-quality-probe.ts` + * (v0.40.1.0 Track D / T6): the wiring resolves fixtures from the + * gbrain package root, writes real outcomes to the parser-probe audit + * trail (`audit-parser-probe.ts`), and never crashes the loop. * * Test seam: all dependencies are injected via NightlyProbeDeps so * unit tests don't touch real LLMs or real fixtures. diff --git a/test/audit-parser-probe.serial.test.ts b/test/audit-parser-probe.serial.test.ts new file mode 100644 index 000000000..9625c80e3 --- /dev/null +++ b/test/audit-parser-probe.serial.test.ts @@ -0,0 +1,102 @@ +/** + * Tests for the parser-probe audit trail + the 24h rate-limit gate. + * + * Uses GBRAIN_AUDIT_DIR override pointed at a tmpdir for hermeticity + * (same pattern as audit-slug-fallback.serial.test.ts). Serial because + * the env override is process-global. + */ +import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; +import { mkdtempSync, rmSync, readdirSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { + computeParserProbeAuditFilename, + logParserProbeEvent, + parserProbeRanWithin, + readRecentParserProbeEvents, + type ParserProbeAuditEvent, +} from '../src/core/audit-parser-probe.ts'; + +let auditDir: string; +let savedEnv: string | undefined; + +beforeEach(() => { + auditDir = mkdtempSync(join(tmpdir(), 'parser-probe-audit-')); + savedEnv = process.env.GBRAIN_AUDIT_DIR; + process.env.GBRAIN_AUDIT_DIR = auditDir; +}); + +afterEach(() => { + if (savedEnv === undefined) delete process.env.GBRAIN_AUDIT_DIR; + else process.env.GBRAIN_AUDIT_DIR = savedEnv; + rmSync(auditDir, { recursive: true, force: true }); +}); + +function makeEvent(overrides: Partial = {}): ParserProbeAuditEvent { + return { + schema_version: 1, + ts: new Date().toISOString(), + outcome: 'pass', + fixtures_total: 12, + fixtures_passed: 12, + recall_mean: 0.98, + participants_recall_mean: 0.97, + adversarial_false_positives: 0, + failed_fixture_ids: [], + ...overrides, + }; +} + +describe('parser-probe audit trail', () => { + test('log + readRecent round-trip', () => { + logParserProbeEvent(makeEvent({ outcome: 'fail', reason: '2 fixture(s) failed' })); + const events = readRecentParserProbeEvents(7); + expect(events.length).toBe(1); + expect(events[0]!.outcome).toBe('fail'); + expect(events[0]!.reason).toBe('2 fixture(s) failed'); + const files = readdirSync(auditDir); + expect(files.length).toBe(1); + expect(files[0]).toMatch(/^parser-probe-\d{4}-W\d{2}\.jsonl$/); + }); + + test('filename uses ISO-week rotation with the parser-probe prefix', () => { + // Year-boundary edge pinned by the shared writer's own tests; here we + // pin the prefix wiring. + expect(computeParserProbeAuditFilename(new Date('2026-07-06T12:00:00Z'))).toBe( + 'parser-probe-2026-W28.jsonl', + ); + }); + + test('readRecent filters by window', () => { + const old = new Date(Date.now() - 10 * 86400000).toISOString(); + logParserProbeEvent(makeEvent({ ts: old })); + expect(readRecentParserProbeEvents(7).length).toBe(0); + }); +}); + +describe('parserProbeRanWithin — 24h rate-limit gate', () => { + const DAY_MS = 24 * 60 * 60 * 1000; + + test('false when no runs are audited', () => { + expect(parserProbeRanWithin(DAY_MS)).toBe(false); + }); + + test('true when a run landed within the window', () => { + logParserProbeEvent(makeEvent({ ts: new Date(Date.now() - 60_000).toISOString() })); + expect(parserProbeRanWithin(DAY_MS)).toBe(true); + }); + + test('false when the last run is older than the window', () => { + logParserProbeEvent(makeEvent({ ts: new Date(Date.now() - 25 * 3600_000).toISOString() })); + expect(parserProbeRanWithin(DAY_MS)).toBe(false); + }); + + test('non-pass outcomes also hold the window (mirrors quality-probe semantics)', () => { + logParserProbeEvent(makeEvent({ + outcome: 'no_embedding_key', + ts: new Date(Date.now() - 3600_000).toISOString(), + })); + expect(parserProbeRanWithin(DAY_MS)).toBe(true); + }); +}); diff --git a/test/autopilot-parser-probe-wiring.test.ts b/test/autopilot-parser-probe-wiring.test.ts new file mode 100644 index 000000000..9ff408c7a --- /dev/null +++ b/test/autopilot-parser-probe-wiring.test.ts @@ -0,0 +1,77 @@ +/** + * Source-shape regression tests for the autopilot wiring of + * `runConversationParserNightlyProbe` (step 4.6). + * + * Same rationale as autopilot-nightly-probe-wiring.test.ts: the loop is + * hard to drive end-to-end, so these pin the structural protections — + * the dual-plane flag read, the D10 tokenmax mode-gate, the package-root + * fixture resolution, the audit-flood guard, and the try/catch posture. + * + * The probe's own gate/scoring logic is pinned by the module's unit + * tests; the audit trail by audit-parser-probe.serial.test.ts. + */ + +import { describe, test, expect } from 'bun:test'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const AUTOPILOT_SRC = resolve('src/commands/autopilot.ts'); +const SOURCE = readFileSync(AUTOPILOT_SRC, 'utf-8'); + +describe('autopilot wiring: conversation-parser probe', () => { + test('invokes the phase module and the audit trail', () => { + expect(SOURCE).toContain(`runConversationParserNightlyProbe`); + expect(SOURCE).toContain(`conversation-parser/nightly-probe`); + expect(SOURCE).toContain(`logParserProbeEvent`); + expect(SOURCE).toContain(`audit-parser-probe`); + }); + + test('flag reads dual-plane: DB row (gbrain config set) wins, file plane fallback', () => { + expect(SOURCE).toContain(`getConfig('autopilot.conversation_parser_probe.enabled')`); + expect(SOURCE).toContain(`cfg?.autopilot?.conversation_parser_probe?.enabled === true`); + }); + + test('D10 mode-gate present: tokenmax brains run the probe by default', () => { + expect(SOURCE).toMatch(/parserEnabled \|\| searchMode === 'tokenmax'/); + }); + + test('fixtures resolve from the gbrain package root, NOT the brain repoPath', () => { + // The committed fixtures live in the gbrain source tree; resolving + // them against sync.repo_path would point into the user's brain repo. + expect(SOURCE).toMatch(/fileURLToPath\(new URL\('\.\.\/\.\.', import\.meta\.url\)\)/); + expect(SOURCE).toContain(`'conversation-formats', 'all.jsonl'`); + expect(SOURCE).toContain(`'conversation-formats', 'adversarial.jsonl'`); + }); + + test('missing fixtures skip quietly (no audit row, once-per-process stderr note)', () => { + // Compiled-binary installs carry no source tree; writing failure rows + // would flip doctor to WARN on every binary install. + expect(SOURCE).toContain(`parserProbeFixtureWarned`); + }); + + test('rate_limited outcomes are NOT audit-logged (flood guard)', () => { + expect(SOURCE).toMatch(/outcome !== 'rate_limited'\) logParserProbeEvent\(result\)/); + }); + + test('rate-limit gate delegates to the audit module, not inline event reads', () => { + expect(SOURCE).toContain(`parserProbeRanWithin(24 * 60 * 60 * 1000)`); + }); + + test('LLM-key gate reads gateway.isAvailable("chat") in-process', () => { + expect(SOURCE).toContain(`isAvailable('chat')`); + }); + + test('probe call wrapped in try/catch that does NOT bump consecutiveErrors', () => { + expect(SOURCE).toMatch(/catch[\s\S]*?autopilot\.parser_probe[\s\S]*?do NOT bump consecutiveErrors/); + }); + + test('DI shape: the exact 7 fields of the parser probe NightlyProbeDeps', () => { + expect(SOURCE).toContain(`isEnabled:`); + expect(SOURCE).toContain(`searchMode:`); + expect(SOURCE).toContain(`hasLlmKey:`); + expect(SOURCE).toContain(`resolveFixturePath:`); + expect(SOURCE).toContain(`resolveAdversarialPath:`); + expect(SOURCE).toContain(`shouldSkipForRateLimit:`); + expect(SOURCE).toContain(`now:`); + }); +}); diff --git a/test/doctor-parser-probe-check.test.ts b/test/doctor-parser-probe-check.test.ts new file mode 100644 index 000000000..c35bf6945 --- /dev/null +++ b/test/doctor-parser-probe-check.test.ts @@ -0,0 +1,51 @@ +/** + * Tests for computeConversationParserProbeHealthCheck — the pure function + * behind doctor's conversation_parser_probe_health check, which replaced + * the v0.41.13.0 hardcoded "Skipped" stub when the autopilot wiring + * landed. Mirrors the branch coverage style of the quality-probe check. + */ +import { describe, expect, test } from 'bun:test'; + +import { computeConversationParserProbeHealthCheck } from '../src/commands/doctor.ts'; + +const ev = (outcome: string, reason?: string, ts = new Date().toISOString()) => ({ + outcome, + ts, + ...(reason !== undefined ? { reason } : {}), +}); + +describe('computeConversationParserProbeHealthCheck', () => { + test('disabled + no events → ok with paste-ready enable hint', () => { + const check = computeConversationParserProbeHealthCheck(false, []); + expect(check.status).toBe('ok'); + expect(check.message).toContain('gbrain config set autopilot.conversation_parser_probe.enabled true'); + }); + + test('enabled + no events yet → ok, next run by autopilot', () => { + const check = computeConversationParserProbeHealthCheck(true, []); + expect(check.status).toBe('ok'); + expect(check.message).toContain('no probe events'); + }); + + test('disabled flag but events exist (tokenmax mode-gate ran it) → events win over the hint', () => { + const check = computeConversationParserProbeHealthCheck(false, [ev('pass')]); + expect(check.status).toBe('ok'); + expect(check.message).toContain('all pass'); + }); + + test('any non-pass outcome in the window → warn, latest surfaced with reason', () => { + const check = computeConversationParserProbeHealthCheck(true, [ + ev('pass'), + ev('adversarial_false_positive', '1 adversarial fixture(s) parsed to non-empty'), + ]); + expect(check.status).toBe('warn'); + expect(check.message).toContain('adversarial_false_positive'); + expect(check.message).toContain('parsed to non-empty'); + }); + + test('all pass → ok with run count', () => { + const check = computeConversationParserProbeHealthCheck(true, [ev('pass'), ev('pass')]); + expect(check.status).toBe('ok'); + expect(check.message).toContain('2 probe run(s)'); + }); +}); From f2e7a87910106db47739a54b5df070d4b165e8b8 Mon Sep 17 00:00:00 2001 From: Paolo Belcastro Date: Mon, 6 Jul 2026 14:11:00 +0200 Subject: [PATCH 2/2] =?UTF-8?q?test(find-trajectory):=20pin=20embedding=20?= =?UTF-8?q?dims=20=E2=80=94=20kill=20the=20shard-order=201280/1536=20flake?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit engine-find-trajectory.test.ts hardcodes Float32Array(1536) vectors but let initSchema size its vector columns from process-global gateway state (getEmbeddingDimensions(), default 1280). Whether the file passed depended on which test files ran before it in the shard: any predecessor leaving the gateway configured without dims (or a bare CI env) yields vector(1280) and every insert dies with 'expected 1280 dimensions, not 1536'. Adding test files to the repo reshuffles the weight-packed shards, so unrelated PRs trip it (seen on #2629/#2630 CI, test (5)). Same fix + rationale as cosine-rescore-column.test.ts, which documents this exact class: configureGateway(1536) in beforeAll, resetGateway in afterAll. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FQgByq4aqQq2PP8UHCdnfk --- test/engine-find-trajectory.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/engine-find-trajectory.test.ts b/test/engine-find-trajectory.test.ts index ab34ef61d..524cefeb7 100644 --- a/test/engine-find-trajectory.test.ts +++ b/test/engine-find-trajectory.test.ts @@ -15,6 +15,7 @@ import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test'; import { PGLiteEngine } from '../src/core/pglite-engine.ts'; +import { configureGateway, resetGateway } from '../src/core/ai/gateway.ts'; import { detectRegressions, computeDriftScore, @@ -26,6 +27,20 @@ import type { TrajectoryPoint } from '../src/core/engine.ts'; let engine: PGLiteEngine; beforeAll(async () => { + // Pin the embedding dim to 1536 BEFORE initSchema. This file hardcodes + // Float32Array(1536) vectors, but initSchema sizes vector columns from + // process-global gateway state (getEmbeddingDimensions(), default 1280 = + // zeroentropyai). Whether this file passes therefore depended on which + // test files happened to run before it in the shard: a predecessor that + // leaves the gateway configured without dims (or a bare CI env) yields + // vector(1280) and every insert here dies with "expected 1280 dimensions, + // not 1536". Same fix + rationale as cosine-rescore-column.test.ts, which + // documents this exact class. + configureGateway({ + embedding_model: 'openai:text-embedding-3-large', + embedding_dimensions: 1536, + env: { OPENAI_API_KEY: 'sk-test-find-trajectory' }, + }); engine = new PGLiteEngine(); await engine.connect({}); await engine.initSchema(); @@ -33,6 +48,7 @@ beforeAll(async () => { afterAll(async () => { await engine.disconnect(); + resetGateway(); }); beforeEach(async () => {