146 lines
4.3 KiB
TypeScript
146 lines
4.3 KiB
TypeScript
// Agent bridge — NDJSON stdio interface for Claude Agent SDK integration
|
|
// Accepts queries on stdin, responds on stdout. Allows an agent to control
|
|
// and query the test daemon programmatically.
|
|
|
|
import { createInterface } from 'node:readline';
|
|
import type { Dashboard } from './dashboard.ts';
|
|
import { clearCache, type RunOptions } from './runner.ts';
|
|
|
|
// ── Query/Response types ──
|
|
|
|
type Query =
|
|
| { type: 'status' }
|
|
| { type: 'rerun'; pattern?: string }
|
|
| { type: 'failures' }
|
|
| { type: 'reset-cache' };
|
|
|
|
type Response =
|
|
| { type: 'status'; running: boolean; passed: number; failed: number; skipped: number; pending: number; total: number; failures: Array<{ name: string; error?: string }> }
|
|
| { type: 'rerun'; specsQueued: number }
|
|
| { type: 'failures'; failures: Array<{ name: string; specFile: string; error?: string }> }
|
|
| { type: 'reset-cache'; ok: true }
|
|
| { type: 'error'; message: string };
|
|
|
|
// ── Bridge ──
|
|
|
|
export class AgentBridge {
|
|
private dashboard: Dashboard;
|
|
private running = false;
|
|
private triggerRerun: ((opts: RunOptions) => void) | null = null;
|
|
private rl: ReturnType<typeof createInterface> | null = null;
|
|
|
|
constructor(dashboard: Dashboard) {
|
|
this.dashboard = dashboard;
|
|
}
|
|
|
|
/** Register callback that triggers a new test run from the main loop */
|
|
onRerunRequest(cb: (opts: RunOptions) => void): void {
|
|
this.triggerRerun = cb;
|
|
}
|
|
|
|
setRunning(running: boolean): void {
|
|
this.running = running;
|
|
}
|
|
|
|
start(): void {
|
|
this.rl = createInterface({
|
|
input: process.stdin,
|
|
terminal: false,
|
|
});
|
|
|
|
this.rl.on('line', (line) => {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) return;
|
|
|
|
try {
|
|
const query = JSON.parse(trimmed) as Query;
|
|
const response = this.handleQuery(query);
|
|
this.send(response);
|
|
} catch (err: unknown) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
this.send({ type: 'error', message: `Invalid query: ${msg}` });
|
|
}
|
|
});
|
|
|
|
this.rl.on('close', () => {
|
|
this.stop();
|
|
});
|
|
}
|
|
|
|
stop(): void {
|
|
if (this.rl) {
|
|
this.rl.close();
|
|
this.rl = null;
|
|
}
|
|
}
|
|
|
|
private send(response: Response): void {
|
|
// Write to stdout as NDJSON — use fd 3 or stderr if stdout is used by dashboard
|
|
// Since dashboard writes to stdout, we use stderr for NDJSON responses
|
|
// when the dashboard is active. The agent reads from our stderr.
|
|
process.stderr.write(JSON.stringify(response) + '\n');
|
|
}
|
|
|
|
private handleQuery(query: Query): Response {
|
|
switch (query.type) {
|
|
case 'status':
|
|
return this.handleStatus();
|
|
case 'rerun':
|
|
return this.handleRerun(query);
|
|
case 'failures':
|
|
return this.handleFailures();
|
|
case 'reset-cache':
|
|
return this.handleResetCache();
|
|
default:
|
|
return { type: 'error', message: `Unknown query type: ${(query as { type: string }).type}` };
|
|
}
|
|
}
|
|
|
|
private handleStatus(): Response {
|
|
const tests = this.dashboard.getTests();
|
|
const passed = tests.filter((t) => t.status === 'passed').length;
|
|
const failed = tests.filter((t) => t.status === 'failed').length;
|
|
const skipped = tests.filter((t) => t.status === 'skipped').length;
|
|
const pending = tests.filter((t) => t.status === 'pending' || t.status === 'running').length;
|
|
const failures = tests
|
|
.filter((t) => t.status === 'failed')
|
|
.map((t) => ({ name: t.name, error: t.error }));
|
|
|
|
return {
|
|
type: 'status',
|
|
running: this.running,
|
|
passed,
|
|
failed,
|
|
skipped,
|
|
pending,
|
|
total: tests.length,
|
|
failures,
|
|
};
|
|
}
|
|
|
|
private handleRerun(query: Extract<Query, { type: 'rerun' }>): Response {
|
|
if (this.running) return { type: 'rerun', specsQueued: 0 };
|
|
const opts: RunOptions = { full: true };
|
|
if (query.pattern) opts.pattern = query.pattern;
|
|
this.triggerRerun?.(opts);
|
|
return { type: 'rerun', specsQueued: 1 };
|
|
}
|
|
|
|
private handleFailures(): Response {
|
|
const tests = this.dashboard.getTests();
|
|
const failures = tests
|
|
.filter((t) => t.status === 'failed')
|
|
.map((t) => ({
|
|
name: t.name,
|
|
specFile: t.specFile,
|
|
error: t.error,
|
|
}));
|
|
|
|
return { type: 'failures', failures };
|
|
}
|
|
|
|
private handleResetCache(): Response {
|
|
clearCache();
|
|
return { type: 'reset-cache', ok: true };
|
|
}
|
|
}
|