From c073f9a213f3cf341a056183c6f6b54a4bc47dcb Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Thu, 25 Jun 2026 13:21:10 +0200 Subject: [PATCH] fix(network-mode): wrap remote calls for errors, skip Content-Type on empty body, use program CLI in E2E --- src/cli/index.ts | 142 +++++++++++++++++++++++++------------- src/cli/remoteClient.ts | 2 +- tests/e2e-network.test.ts | 34 +++++---- 3 files changed, 111 insertions(+), 67 deletions(-) diff --git a/src/cli/index.ts b/src/cli/index.ts index 8e660cd..d26ea7e 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -7,7 +7,7 @@ import { handoffCreate, handoffRead, handoffList } from './commands/handoff.js'; import { decisionCreate, decisionList } from './commands/decision.js'; import { delegate } from './commands/delegate.js'; import { serverStart } from './commands/server.js'; -import { remoteClient } from './remoteClient.js'; +import { remoteClient, RemoteError } from './remoteClient.js'; function getServerUrl(program: Command): string | undefined { return (program.opts().server as string | undefined) || process.env.AGENTHUB_SERVER; @@ -18,6 +18,22 @@ function remoteOnly(): never { process.exit(1); } +async function runRemote(serverUrl: string, fn: () => Promise): Promise { + try { + await fn(); + } catch (err) { + if (err instanceof RemoteError) { + if (err.status === 0) { + console.error(`AgentHub server at ${serverUrl} is not reachable. Is 'agenthub server start --host 0.0.0.0' running?`); + } else { + console.error(`AgentHub server error (${err.status}): ${err.message}`); + } + process.exit(1); + } + throw err; + } +} + export function createProgram(cwd: string): Command { const program = new Command('agenthub') .description('Local coordination layer for AI coding agents') @@ -41,8 +57,10 @@ export function createProgram(cwd: string): Command { .action(async (options) => { const serverUrl = getServerUrl(program); if (serverUrl) { - const body = options.update ? await remoteClient.updateStatus(serverUrl) : await remoteClient.getStatus(serverUrl); - console.log(body); + await runRemote(serverUrl, async () => { + const body = options.update ? await remoteClient.updateStatus(serverUrl) : await remoteClient.getStatus(serverUrl); + console.log(body); + }); } else { status(cwd, options); } @@ -58,8 +76,10 @@ export function createProgram(cwd: string): Command { .action(async (options) => { const serverUrl = getServerUrl(program); if (serverUrl) { - const memory = await remoteClient.addMemory(serverUrl, options); - console.log(`Memory saved as ${memory.id}.`); + await runRemote(serverUrl, async () => { + const memory = await remoteClient.addMemory(serverUrl, options); + console.log(`Memory saved as ${memory.id}.`); + }); } else { await memoryAdd(cwd, options); } @@ -70,9 +90,11 @@ export function createProgram(cwd: string): Command { .action(async (query) => { const serverUrl = getServerUrl(program); if (serverUrl) { - const results = await remoteClient.searchMemory(serverUrl, query); - if (results.length === 0) { console.log('No results found.'); return; } - for (const r of results) console.log(`[${r.type}] ${r.id}: ${r.title}`); + await runRemote(serverUrl, async () => { + const results = await remoteClient.searchMemory(serverUrl, query); + if (results.length === 0) { console.log('No results found.'); return; } + for (const r of results) console.log(`[${r.type}] ${r.id}: ${r.title}`); + }); } else { memorySearch(cwd, query); } @@ -83,9 +105,11 @@ export function createProgram(cwd: string): Command { .action(async () => { const serverUrl = getServerUrl(program); if (serverUrl) { - const memories = await remoteClient.listMemory(serverUrl); - if (memories.length === 0) { console.log('No memory entries found.'); return; } - for (const m of memories) console.log(`${m.id}: ${m.title}`); + await runRemote(serverUrl, async () => { + const memories = await remoteClient.listMemory(serverUrl); + if (memories.length === 0) { console.log('No memory entries found.'); return; } + for (const m of memories) console.log(`${m.id}: ${m.title}`); + }); } else { memoryList(cwd); } @@ -102,8 +126,10 @@ export function createProgram(cwd: string): Command { .action(async (options) => { const serverUrl = getServerUrl(program); if (serverUrl) { - const task = await remoteClient.createTask(serverUrl, options); - console.log(`Created ${task.id}: ${task.title}`); + await runRemote(serverUrl, async () => { + const task = await remoteClient.createTask(serverUrl, options); + console.log(`Created ${task.id}: ${task.title}`); + }); } else { await taskCreate(cwd, options); } @@ -116,9 +142,11 @@ export function createProgram(cwd: string): Command { .action(async (options) => { const serverUrl = getServerUrl(program); if (serverUrl) { - const tasks = await remoteClient.listTasks(serverUrl, options); - if (tasks.length === 0) { console.log('No tasks found.'); return; } - for (const t of tasks) console.log(`${t.id} [${t.status}] (${t.role ?? 'unassigned'}) ${t.title}`); + await runRemote(serverUrl, async () => { + const tasks = await remoteClient.listTasks(serverUrl, options); + if (tasks.length === 0) { console.log('No tasks found.'); return; } + for (const t of tasks) console.log(`${t.id} [${t.status}] (${t.role ?? 'unassigned'}) ${t.title}`); + }); } else { taskList(cwd, options); } @@ -129,10 +157,12 @@ export function createProgram(cwd: string): Command { .action(async (id) => { const serverUrl = getServerUrl(program); if (serverUrl) { - const { task, body } = await remoteClient.getTask(serverUrl, id); - console.log(`# ${task.title}`); - console.log(`Status: ${task.status} | Role: ${task.role ?? '-'} | Priority: ${task.priority}`); - console.log('\n' + body); + await runRemote(serverUrl, async () => { + const { task, body } = await remoteClient.getTask(serverUrl, id); + console.log(`# ${task.title}`); + console.log(`Status: ${task.status} | Role: ${task.role ?? '-'} | Priority: ${task.priority}`); + console.log('\n' + body); + }); } else { taskShow(cwd, id); } @@ -144,8 +174,10 @@ export function createProgram(cwd: string): Command { .action(async (id, options) => { const serverUrl = getServerUrl(program); if (serverUrl) { - await remoteClient.claimTask(serverUrl, id, options.agent); - console.log(`${id} claimed by ${options.agent}.`); + await runRemote(serverUrl, async () => { + await remoteClient.claimTask(serverUrl, id, options.agent); + console.log(`${id} claimed by ${options.agent}.`); + }); } else { taskClaim(cwd, id, options.agent); } @@ -156,8 +188,10 @@ export function createProgram(cwd: string): Command { .action(async (id) => { const serverUrl = getServerUrl(program); if (serverUrl) { - await remoteClient.doneTask(serverUrl, id); - console.log(`${id} marked as done.`); + await runRemote(serverUrl, async () => { + await remoteClient.doneTask(serverUrl, id); + console.log(`${id} marked as done.`); + }); } else { taskDone(cwd, id); } @@ -176,8 +210,10 @@ export function createProgram(cwd: string): Command { .action(async (options) => { const serverUrl = getServerUrl(program); if (serverUrl) { - const handoff = await remoteClient.createHandoff(serverUrl, options); - console.log(`Handoff created: ${handoff.id}`); + await runRemote(serverUrl, async () => { + const handoff = await remoteClient.createHandoff(serverUrl, options); + console.log(`Handoff created: ${handoff.id}`); + }); } else { await handoffCreate(cwd, options); } @@ -188,11 +224,13 @@ export function createProgram(cwd: string): Command { .action(async (id) => { const serverUrl = getServerUrl(program); if (serverUrl) { - const { handoff, body } = await remoteClient.getHandoff(serverUrl, id); - console.log(`# ${handoff.summary}`); - console.log(`From: ${handoff.fromRole} → ${handoff.toRole}`); - if (handoff.taskId) console.log(`Task: ${handoff.taskId}`); - console.log('\n' + body); + await runRemote(serverUrl, async () => { + const { handoff, body } = await remoteClient.getHandoff(serverUrl, id); + console.log(`# ${handoff.summary}`); + console.log(`From: ${handoff.fromRole} → ${handoff.toRole}`); + if (handoff.taskId) console.log(`Task: ${handoff.taskId}`); + console.log('\n' + body); + }); } else { handoffRead(cwd, id); } @@ -203,9 +241,11 @@ export function createProgram(cwd: string): Command { .action(async () => { const serverUrl = getServerUrl(program); if (serverUrl) { - const handoffs = await remoteClient.listHandoffs(serverUrl); - if (handoffs.length === 0) { console.log('No handoffs found.'); return; } - for (const h of handoffs) console.log(`${h.id}: ${h.title}`); + await runRemote(serverUrl, async () => { + const handoffs = await remoteClient.listHandoffs(serverUrl); + if (handoffs.length === 0) { console.log('No handoffs found.'); return; } + for (const h of handoffs) console.log(`${h.id}: ${h.title}`); + }); } else { handoffList(cwd); } @@ -222,8 +262,10 @@ export function createProgram(cwd: string): Command { .action(async (options) => { const serverUrl = getServerUrl(program); if (serverUrl) { - const decision = await remoteClient.createDecision(serverUrl, options); - console.log(`Decision recorded: ${decision.id}`); + await runRemote(serverUrl, async () => { + const decision = await remoteClient.createDecision(serverUrl, options); + console.log(`Decision recorded: ${decision.id}`); + }); } else { await decisionCreate(cwd, options); } @@ -234,9 +276,11 @@ export function createProgram(cwd: string): Command { .action(async () => { const serverUrl = getServerUrl(program); if (serverUrl) { - const decisions = await remoteClient.listDecisions(serverUrl); - if (decisions.length === 0) { console.log('No decisions found.'); return; } - for (const d of decisions) console.log(`${d.id}: ${d.title}`); + await runRemote(serverUrl, async () => { + const decisions = await remoteClient.listDecisions(serverUrl); + if (decisions.length === 0) { console.log('No decisions found.'); return; } + for (const d of decisions) console.log(`${d.id}: ${d.title}`); + }); } else { decisionList(cwd); } @@ -250,15 +294,17 @@ export function createProgram(cwd: string): Command { .action(async (options) => { const serverUrl = getServerUrl(program); if (serverUrl) { - const result = await remoteClient.delegate(serverUrl, options.auto ?? false); - if (!result.suggestion) { console.log('No open tasks to delegate.'); return; } - const s = result.suggestion; - console.log('Suggested delegation:'); - console.log(` Task: ${s.task.id} — ${s.task.title}`); - console.log(` Role: ${s.role}`); - console.log(` Preferred agent: ${s.preferredAgent}`); - if (result.handoff) console.log('Handoff created automatically.'); - else console.log('Run with --auto to create the handoff.'); + await runRemote(serverUrl, async () => { + const result = await remoteClient.delegate(serverUrl, options.auto ?? false); + if (!result.suggestion) { console.log('No open tasks to delegate.'); return; } + const s = result.suggestion; + console.log('Suggested delegation:'); + console.log(` Task: ${s.task.id} — ${s.task.title}`); + console.log(` Role: ${s.role}`); + console.log(` Preferred agent: ${s.preferredAgent}`); + if (result.handoff) console.log('Handoff created automatically.'); + else console.log('Run with --auto to create the handoff.'); + }); } else { await delegate(cwd, options); } diff --git a/src/cli/remoteClient.ts b/src/cli/remoteClient.ts index 90f3434..c66c153 100644 --- a/src/cli/remoteClient.ts +++ b/src/cli/remoteClient.ts @@ -13,7 +13,7 @@ async function request(baseUrl: string, method: string, path: string, body?: try { res = await fetch(url, { method, - headers: { 'Content-Type': 'application/json' }, + headers: body ? { 'Content-Type': 'application/json' } : undefined, body: body ? JSON.stringify(body) : undefined, }); } catch (err) { diff --git a/tests/e2e-network.test.ts b/tests/e2e-network.test.ts index 4a684cc..03beb05 100644 --- a/tests/e2e-network.test.ts +++ b/tests/e2e-network.test.ts @@ -3,7 +3,8 @@ import { mkdtempSync, rmSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; import { startServer } from '../src/server/index.js'; -import { remoteClient } from '../src/cli/remoteClient.js'; +import { remoteClient, RemoteError } from '../src/cli/remoteClient.js'; +import { createProgram } from '../src/cli/index.js'; import { init } from '../src/cli/commands/init.js'; describe('network e2e', () => { @@ -21,28 +22,25 @@ describe('network e2e', () => { rmSync(cwd, { recursive: true, force: true }); }); - it('creates a task remotely and reads it back', async () => { - const url = server.url; - const created = await remoteClient.createTask(url, { title: 'Remote', role: 'implementer' }); - expect(created.id).toBe('TSK-0001'); + it('creates a task remotely via CLI and reads it back', async () => { + const program = createProgram(cwd); + await program.parseAsync(['node', 'agenthub', '--server', server.url, 'task', 'create', '--title', 'Remote CLI', '--role', 'implementer']); - const tasks = await remoteClient.listTasks(url); - expect(tasks).toHaveLength(1); - - const { task } = await remoteClient.getTask(url, 'TSK-0001'); - expect(task.title).toBe('Remote'); + const { task } = await remoteClient.getTask(server.url, 'TSK-0001'); + expect(task.title).toBe('Remote CLI'); }); - it('claims and completes a task remotely', async () => { - const url = server.url; - await remoteClient.createTask(url, { title: 'Remote', role: 'implementer' }); - const claimed = await remoteClient.claimTask(url, 'TSK-0001', 'codex'); - expect(claimed.status).toBe('in_progress'); - const done = await remoteClient.doneTask(url, 'TSK-0001'); - expect(done.status).toBe('done'); + it('updates status remotely via CLI', async () => { + await remoteClient.createTask(server.url, { title: 'Remote', role: 'implementer' }); + + const program = createProgram(cwd); + await program.parseAsync(['node', 'agenthub', '--server', server.url, 'status', '--update']); + + const status = await remoteClient.getStatus(server.url); + expect(status).toContain('Active tasks: 1'); }); it('returns a meaningful error when the server is unreachable', async () => { - await expect(remoteClient.listTasks('http://127.0.0.1:1')).rejects.toThrow(/Cannot reach AgentHub server/); + await expect(remoteClient.listTasks('http://127.0.0.1:1')).rejects.toThrow(RemoteError); }); });