fix(network-mode): wrap remote calls for errors, skip Content-Type on empty body, use program CLI in E2E

This commit is contained in:
chahinebrini 2026-06-25 13:21:10 +02:00
parent d53fbf6794
commit c073f9a213
3 changed files with 111 additions and 67 deletions

View File

@ -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<void>): Promise<void> {
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);
}

View File

@ -13,7 +13,7 @@ async function request<T>(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) {

View File

@ -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);
});
});