Files
gbrain/test/audit-parser-probe.serial.test.ts
T
Paolo BelcastroandClaude Fable 5 7639493e8c feat(autopilot): wire the nightly conversation-parser probe into the loop
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FQgByq4aqQq2PP8UHCdnfk
2026-07-06 14:11:16 +02:00

103 lines
3.4 KiB
TypeScript

/**
* 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);
});
});