- 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/
193 lines
4.8 KiB
TypeScript
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 };
|
|
}
|
|
}
|