mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
Merge PR #2630: feat(autopilot): wire the nightly conversation-parser probe into the loop
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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).
|
||||
@@ -1063,6 +1066,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));
|
||||
}
|
||||
|
||||
+71
-13
@@ -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 }>,
|
||||
@@ -4972,19 +5020,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/`
|
||||
|
||||
@@ -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<ParserProbeAuditEvent>({
|
||||
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;
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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> = {}): 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);
|
||||
});
|
||||
});
|
||||
@@ -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:`);
|
||||
});
|
||||
});
|
||||
@@ -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)');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user