diff --git a/packages/cli/src/commands/daemon/lifecycle.e2e.test.ts b/packages/cli/src/commands/daemon/lifecycle.e2e.test.ts index dfffe1f61b..cb42c0b0a5 100644 --- a/packages/cli/src/commands/daemon/lifecycle.e2e.test.ts +++ b/packages/cli/src/commands/daemon/lifecycle.e2e.test.ts @@ -146,7 +146,7 @@ test("managed two-home restart retains its supervisor and never routes ordinary expect(await f.ok(["project", "ls", "--home", a])).toEqual([]); expect((await f.ok(["project", "ls", "--home", b], poisoned)).length).toBe(1); const stream = await f.run(["logs", "missing-agent", "--follow", "--home", b], poisoned); - expect(stream.stderr).toContain("Agent not found: missing-agent"); + expect(stream.stderr).toContain("No agent found matching: missing-agent"); const restart = await f.ok(["restart", "--home", b, "--timeout", "30"], poisoned); expect(restart.supervisorPid).toBe(launchB.pid); expect(restart.workerPid).not.toBe(beforeB.workerPid); @@ -503,7 +503,7 @@ test("raw log following subscribes to a stored agent that exists only in B", asy const launchA = await f.ok(["start", "--home", a]); await f.ok(["start", "--home", b]); const absent = await f.run(["logs", agentId, "--home", a]); - expect(absent.stderr).toContain("Agent not found"); + expect(absent.stderr).toContain(`No agent found matching: ${agentId}`); follower = spawn( process.execPath, [cli, "logs", agentId, "--follow", "--tail", "0", "--home", b], @@ -522,7 +522,7 @@ test("raw log following subscribes to a stored agent that exists only in B", asy errors += data; }); await expect.poll(() => output, { timeout: 15_000 }).toContain("Following logs (no history;"); - expect(errors).not.toContain("Agent not found"); + expect(errors).not.toContain("No agent found matching"); } finally { if (follower && follower.exitCode === null) { const exited = new Promise((resolve) => follower!.once("exit", resolve)); diff --git a/packages/cli/tests/07-agent-stop.test.ts b/packages/cli/tests/07-agent-stop.test.ts index 81b0cba66c..9416884a7b 100644 --- a/packages/cli/tests/07-agent-stop.test.ts +++ b/packages/cli/tests/07-agent-stop.test.ts @@ -19,6 +19,7 @@ import assert from "node:assert"; import { getAvailablePort } from "./helpers/network.ts"; +import { runPaseoCli, startTestDaemon } from "./helpers/test-daemon.ts"; import { $ } from "zx"; import { mkdtemp, rm } from "fs/promises"; import { tmpdir } from "os"; @@ -129,6 +130,28 @@ try { assert(!output.includes("error: option"), "should not have option parsing error"); console.log("✓ -q (quiet) flag is accepted with stop\n"); } + + { + console.log("Test 9: stop reports AGENT_NOT_FOUND for an unknown ID"); + const daemon = await startTestDaemon(); + try { + const result = await runPaseoCli(daemon, [ + "agent", + "stop", + "does-not-exist", + "--host", + `127.0.0.1:${daemon.port}`, + "--json", + ]); + assert.notStrictEqual(result.exitCode, 0, "stop should fail for an unknown ID"); + const { error } = JSON.parse(result.stderr); + assert.strictEqual(error.code, "AGENT_NOT_FOUND", result.stderr); + assert.match(error.details, /paseo ls/); + } finally { + await daemon.stop(); + } + console.log("✓ stop reports AGENT_NOT_FOUND for an unknown ID\n"); + } } finally { // Clean up temp directory await rm(paseoHome, { recursive: true, force: true }); diff --git a/packages/cli/tests/27-agent-delete.test.ts b/packages/cli/tests/27-agent-delete.test.ts index bec523f9ca..349125109b 100644 --- a/packages/cli/tests/27-agent-delete.test.ts +++ b/packages/cli/tests/27-agent-delete.test.ts @@ -13,6 +13,7 @@ import assert from "node:assert"; import { runLocalPaseo } from "./helpers/local-cli.ts"; import { getAvailablePort } from "./helpers/network.ts"; +import { runPaseoCli, startTestDaemon } from "./helpers/test-daemon.ts"; import { mkdtemp, rm } from "fs/promises"; import { tmpdir } from "os"; import { join } from "path"; @@ -112,6 +113,28 @@ try { assert(!output.includes("error: option"), "should not have option parsing error"); console.log("✓ -q (quiet) flag is accepted with delete\n"); } + + { + console.log("Test 9: delete reports AGENT_NOT_FOUND for an unknown ID"); + const daemon = await startTestDaemon(); + try { + const result = await runPaseoCli(daemon, [ + "agent", + "delete", + "does-not-exist", + "--host", + `127.0.0.1:${daemon.port}`, + "--json", + ]); + assert.notStrictEqual(result.exitCode, 0, "delete should fail for an unknown ID"); + const { error } = JSON.parse(result.stderr); + assert.strictEqual(error.code, "AGENT_NOT_FOUND", result.stderr); + assert.match(error.details, /paseo ls/); + } finally { + await daemon.stop(); + } + console.log("✓ delete reports AGENT_NOT_FOUND for an unknown ID\n"); + } } finally { await rm(paseoHome, { recursive: true, force: true }); } diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 3e6813e148..d57333da05 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -5216,10 +5216,10 @@ export class Session { private async resolveAgentIdentifier( identifier: string, - ): Promise<{ ok: true; agentId: string } | { ok: false; error: string }> { + ): Promise<{ ok: true; agentId: string } | { ok: false; notFound: boolean; error: string }> { const trimmed = identifier.trim(); if (!trimmed) { - return { ok: false, error: "Agent identifier cannot be empty" }; + return { ok: false, notFound: false, error: "Agent identifier cannot be empty" }; } const stored = await this.agentStorage.list(); @@ -5243,6 +5243,7 @@ export class Session { if (prefixMatches.length > 1) { return { ok: false, + notFound: false, error: `Agent identifier "${trimmed}" is ambiguous (${prefixMatches .slice(0, 5) .map((id) => id.slice(0, 8)) @@ -5257,6 +5258,7 @@ export class Session { if (titleMatches.length > 1) { return { ok: false, + notFound: false, error: `Agent title "${trimmed}" is ambiguous (${titleMatches .slice(0, 5) .map((r) => r.id.slice(0, 8)) @@ -5264,7 +5266,7 @@ export class Session { }; } - return { ok: false, error: `Agent not found: ${trimmed}` }; + return { ok: false, notFound: true, error: `Agent not found: ${trimmed}` }; } private async getAgentPayloadById(agentId: string): Promise { @@ -7574,11 +7576,17 @@ export class Session { } private async handleFetchAgent(agentIdOrIdentifier: string, requestId: string): Promise { + // An unknown agent is a null agent, not an error. Errors are for empty or ambiguous identifiers. const resolved = await this.resolveAgentIdentifier(agentIdOrIdentifier); if (!resolved.ok) { this.emit({ type: "fetch_agent_response", - payload: { requestId, agent: null, project: null, error: resolved.error }, + payload: { + requestId, + agent: null, + project: null, + error: resolved.notFound ? null : resolved.error, + }, }); return; } @@ -7587,12 +7595,7 @@ export class Session { if (!agent) { this.emit({ type: "fetch_agent_response", - payload: { - requestId, - agent: null, - project: null, - error: `Agent not found: ${resolved.agentId}`, - }, + payload: { requestId, agent: null, project: null, error: null }, }); return; } diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts index 1be52bc997..001966ab39 100644 --- a/packages/server/src/server/session.workspaces.test.ts +++ b/packages/server/src/server/session.workspaces.test.ts @@ -3466,7 +3466,7 @@ test("fetch_agent_request still resolves archived historical agents", async () = session.resolveAgentIdentifier = async (identifier: string) => identifier === "Archived History Agent" ? { ok: true, agentId: agent.id } - : { ok: false, error: `Agent not found: ${identifier}` }; + : { ok: false, notFound: true, error: `Agent not found: ${identifier}` }; session.getAgentPayloadById = async (agentId: string) => (agentId === agent.id ? agent : null); session.buildProjectPlacementForWorkspaceId = async () => ({ projectKey: "proj-history-detail",