agent-orchestrator/tests/e2e/daemon/agent-bridge.ts
Hibryda d7dd7722ab feat(e2e): add test daemon CLI with ANSI dashboard and Agent SDK bridge
- index.ts: CLI entry point (--full, --spec, --watch, --agent flags)
- runner.ts: programmatic WDIO launcher with result streaming
- dashboard.ts: ANSI terminal UI (pass/fail/skip/running icons, summary)
- agent-bridge.ts: NDJSON stdin/stdout for Agent SDK queries
  (status, rerun, failures, reset-cache)
- Standalone package at tests/e2e/daemon/
2026-03-18 05:17:17 +01:00

193 lines
4.8 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, TestEntry } from './dashboard.ts';
import { runSpecs, clearCache, type RunOptions } from './runner.ts';
// ── Query/Response types ──
interface StatusQuery {
type: 'status';
}
interface RerunQuery {
type: 'rerun';
pattern?: string;
}
interface FailuresQuery {
type: 'failures';
}
interface ResetCacheQuery {
type: 'reset-cache';
}
type Query = StatusQuery | RerunQuery | FailuresQuery | ResetCacheQuery;
interface StatusResponse {
type: 'status';
running: boolean;
passed: number;
failed: number;
skipped: number;
pending: number;
total: number;
failures: Array<{ name: string; error?: string }>;
}
interface RerunResponse {
type: 'rerun';
specsQueued: number;
}
interface FailuresResponse {
type: 'failures';
failures: Array<{ name: string; specFile: string; error?: string }>;
}
interface ResetCacheResponse {
type: 'reset-cache';
ok: true;
}
interface ErrorResponse {
type: 'error';
message: string;
}
type Response = StatusResponse | RerunResponse | FailuresResponse | ResetCacheResponse | ErrorResponse;
// ── 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(): StatusResponse {
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: RerunQuery): RerunResponse {
if (this.running) {
return { type: 'rerun', specsQueued: 0 };
}
const opts: RunOptions = {};
if (query.pattern) opts.pattern = query.pattern;
opts.full = true; // rerun ignores cache
if (this.triggerRerun) {
this.triggerRerun(opts);
}
return { type: 'rerun', specsQueued: 1 };
}
private handleFailures(): FailuresResponse {
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(): ResetCacheResponse {
clearCache();
return { type: 'reset-cache', ok: true };
}
}