test: close the remaining plan-audit gaps — formatResult rendering + watch SIGINT

formatResult exported for tests (same import-safety contract as cliAliases);
test/cli-format-volunteer.test.ts pins the pointer lines, empty-gate message,
and approximate stats summary. test/watch-command.test.ts gains a real
subprocess SIGINT test: piped stdin that never reaches EOF, SIGINT mid-stream,
assert exit 0 with no force-exit banner — the drain-then-exit lifecycle under
the actual signal, not just the shared exit path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-06-12 06:32:42 -07:00
co-authored by Claude Fable 5
parent 76ac3af529
commit 62d54056c0
3 changed files with 114 additions and 1 deletions
+2 -1
View File
@@ -881,7 +881,8 @@ async function makeContext(engine: BrainEngine, params: Record<string, unknown>)
};
}
function formatResult(opName: string, result: unknown): string {
// Exported for tests (same import-safety contract as cliAliases/printOpHelp).
export function formatResult(opName: string, result: unknown): string {
switch (opName) {
case 'volunteer_context': {
const r = result as any;
+67
View File
@@ -0,0 +1,67 @@
/**
* v0.43 (#2095) — formatResult's volunteer_context human rendering
* (ship coverage G3): pointer lines with confidence/arm/rationale,
* the empty-result message, and the approximate stats summary.
*/
import { describe, test, expect } from 'bun:test';
import { formatResult } from '../src/cli.ts';
describe('formatResult — volunteer_context', () => {
test('renders pointer lines with confidence, arm, rationale, and synopsis', () => {
const out = formatResult('volunteer_context', {
pages: [
{
slug: 'people/alice-example',
source_id: 'default',
display: 'Alice Example',
confidence: 0.85,
arm: 'title',
rationale: 'exact title match "Alice Example"; mentioned in the newest turn',
synopsis: 'Alice is an early founder.',
},
],
count: 1,
window_turns: 3,
});
expect(out).toContain('Alice Example → people/alice-example (0.85, title)');
expect(out).toContain('exact title match');
expect(out).toContain('Alice is an early founder.');
});
test('empty result explains the confidence gate', () => {
const out = formatResult('volunteer_context', { pages: [], count: 0, window_turns: 1 });
expect(out).toContain('Nothing volunteered');
expect(out).toContain('confidence gate');
});
test('stats mode renders totals, per-arm precision, and the approximate note', () => {
const out = formatResult('volunteer_context', {
days: 30,
approximate: true,
note: 'approximate: "used" = pages.last_retrieved_at > volunteered_at.',
total_volunteered: 4,
total_used: 3,
by_arm: [
{ match_arm: 'alias', channel: 'reflex', volunteered: 2, used: 2, precision: 1 },
{ match_arm: 'title', channel: 'op', volunteered: 2, used: 1, precision: 0.5 },
],
});
expect(out).toContain('last 30 day(s)');
expect(out).toContain('approximate');
expect(out).toContain('total: 4 volunteered, 3 used');
expect(out).toContain('alias/reflex: 2/2 used (precision 1)');
expect(out).toContain('title/op: 1/2 used (precision 0.5)');
});
test('stats mode with zero events prints the empty-window line', () => {
const out = formatResult('volunteer_context', {
days: 7,
approximate: true,
note: 'approximate',
total_volunteered: 0,
total_used: 0,
by_arm: [],
});
expect(out).toContain('no volunteer events in the window');
});
});
+45
View File
@@ -153,3 +153,48 @@ describe('gbrain watch — window + cap flags (ship coverage G4)', () => {
expect(out.length).toBe(1);
});
});
describe('gbrain watch — SIGINT lifecycle (real subprocess)', () => {
test('SIGINT mid-stream closes the stream and exits cleanly (drain path, exit 0)', async () => {
const { mkdtempSync, mkdirSync, writeFileSync, rmSync } = await import('fs');
const { join, resolve } = await import('path');
const { tmpdir } = await import('os');
const REPO = resolve(import.meta.dir, '..');
const home = mkdtempSync(join(tmpdir(), 'gbrain-watch-sigint-'));
try {
mkdirSync(join(home, '.gbrain'), { recursive: true });
writeFileSync(
join(home, '.gbrain', 'config.json'),
JSON.stringify({
engine: 'pglite',
database_path: join(home, '.gbrain', 'brain.pglite'),
embedding_dimensions: 1536,
}) + '\n',
);
// Piped stdin that NEVER reaches EOF — only SIGINT can end the stream.
const proc = Bun.spawn(['bun', 'run', join(REPO, 'src', 'cli.ts'), 'watch'], {
cwd: REPO,
env: { ...process.env, HOME: home, GBRAIN_HOME: home, GBRAIN_SKIP_STARTUP_HOOKS: '1' },
stdin: 'pipe',
stdout: 'pipe',
stderr: 'pipe',
});
proc.stdin.write('user: nothing relevant here\n');
await proc.stdin.flush();
// Give the brain time to init + process the turn, then interrupt.
await new Promise((r) => setTimeout(r, 15_000));
proc.kill('SIGINT');
const killer = setTimeout(() => {
try { proc.kill('SIGKILL'); } catch { /* already dead */ }
}, 30_000);
const exitCode = await proc.exited;
clearTimeout(killer);
const stderr = await new Response(proc.stderr).text();
// Clean drain-then-exit: no force-exit banner, no SIGKILL (137), exit 0.
expect(stderr).not.toContain('force-exiting');
expect(exitCode).toBe(0);
} finally {
rmSync(home, { recursive: true, force: true });
}
}, 120_000);
});