mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
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:
co-authored by
Claude Fable 5
parent
76ac3af529
commit
62d54056c0
+2
-1
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user