Files
paseo/packages/cli/tests/11-agent-archive.test.ts
T
18eabeedab Archive agents beyond the first directory page (#7535)
* fix(cli): resolve agent archive through the daemon, not a listing page

`paseo agent archive` resolved its argument against a single
`fetch_agents` response. That listing is capped at 200 entries per page
and the command never pages, so an agent past the cap could not be
archived: the CLI reported AGENT_NOT_FOUND before any request reached the
daemon, while `paseo agent ls --global` and `paseo agent inspect` still
returned it.

Resolve the argument with `fetch_agent` instead, the same authoritative
daemon lookup `agent inspect`, `agent stop` and `agent delete` use. The
already-archived and running checks read the returned record unchanged.

Closes #4909

* fix(cli): only report a missing agent as AGENT_NOT_FOUND in archive

The lookup rejection was mapped to AGENT_NOT_FOUND whatever it said, so
an ambiguous identifier, a timeout or a daemon handler failure all read
as a missing agent. The daemon reports an unknown identifier only as an
error string, so recognize that one rejection by its text and rethrow
anything else unchanged.

---------

Co-authored-by: L4XB <lukas.buck@e-mail.de>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: HTeo <hteo1337@gmail.com>
2026-10-21 05:52:11 +02:00

292 lines
10 KiB
TypeScript

#!/usr/bin/env npx tsx
/**
* Phase 11: Agent Archive Command Tests
*
* Tests the agent archive command - archiving (soft-delete) agents.
* Since daemon may not be running, we test both:
* - Help and argument parsing
* - Graceful error handling when daemon not running
* - All flags are accepted
*
* Tests:
* - agent archive --help shows options
* - agent archive requires ID argument
* - agent archive handles daemon not running
* - agent archive --force flag is accepted
*/
import assert from "node:assert";
import { getAvailablePort } from "./helpers/network.ts";
import { createTempDirs, runPaseoCli, startTestDaemon } from "./helpers/test-daemon.ts";
import { $ } from "zx";
import { mkdtemp, rm, writeFile } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
$.verbose = false;
const TARGET_AGENT_ID = "11111111-2222-4333-8444-555555555555";
/**
* A PASEO_HOME whose archive-inclusive agent listing overflows one page.
* `fetch_agents` answers at most 200 entries, so the older unarchived agent
* sits behind 201 newer archived ones and never appears on the first page.
*/
async function seedHomePastPageCap(
targetAgentId: string,
newerAgentCount = 201,
): Promise<{ paseoHome: string; workDir: string }> {
const dirs = await createTempDirs();
const olderAt = "2026-09-03T18:29:21.949Z";
const newerAt = "2026-09-10T12:00:00.000Z";
const writeRecord = (record: Record<string, unknown>) =>
writeFile(
join(dirs.paseoHome, "agents", `${record.id as string}.json`),
JSON.stringify(record, null, 2),
);
await writeRecord({
id: targetAgentId,
provider: "codex",
cwd: dirs.workDir,
createdAt: olderAt,
updatedAt: olderAt,
title: "finished agent",
labels: {},
lastStatus: "closed",
requiresAttention: true,
attentionReason: "finished",
});
await Promise.all(
Array.from({ length: newerAgentCount }, (_unused, index) =>
writeRecord({
id: `aaaaaaaa-0000-4000-8000-${String(index).padStart(12, "0")}`,
provider: "codex",
cwd: dirs.workDir,
createdAt: newerAt,
updatedAt: newerAt,
title: `archived ${index}`,
labels: {},
lastStatus: "closed",
archivedAt: newerAt,
}),
),
);
return dirs;
}
/** The CLI writes its JSON error to stderr, after any Node warnings. */
function parseJsonError(stderr: string): unknown {
const start = stderr.indexOf("{");
assert(start >= 0, `expected a JSON error, got: ${stderr}`);
return JSON.parse(stderr.slice(start));
}
console.log("=== Agent Archive Command Tests ===\n");
// Allocate an unused endpoint for connection-error and argument-validation checks.
const port = await getAvailablePort();
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-test-home-"));
try {
// Test 1: agent archive --help shows options
{
console.log("Test 1: agent archive --help shows options");
const result = await $`npx paseo agent archive --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "agent archive --help should exit 0");
assert(result.stdout.includes("--force"), "help should mention --force flag");
assert(result.stdout.includes("--host"), "help should mention --host option");
assert(result.stdout.includes("<id>"), "help should mention required id argument");
console.log("✓ agent archive --help shows options\n");
}
// Test 2: agent archive requires ID argument
{
console.log("Test 2: agent archive requires ID argument");
const result =
await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent archive`.nothrow();
assert.notStrictEqual(result.exitCode, 0, "should fail without id");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes("missing") ||
output.toLowerCase().includes("required") ||
output.toLowerCase().includes("argument");
assert(hasError, "error should mention missing argument");
console.log("✓ agent archive requires ID argument\n");
}
// Test 3: agent archive handles daemon not running
{
console.log("Test 3: agent archive handles daemon not running");
const result =
await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent archive abc123`.nothrow();
// Should fail because daemon not running
assert.notStrictEqual(result.exitCode, 0, "should fail when daemon not running");
const output = result.stdout + result.stderr;
const hasError =
output.toLowerCase().includes("daemon") ||
output.toLowerCase().includes("connect") ||
output.toLowerCase().includes("cannot");
assert(hasError, "error message should mention connection issue");
console.log("✓ agent archive handles daemon not running\n");
}
// Test 4: agent archive --force flag is accepted
{
console.log("Test 4: agent archive --force flag is accepted");
const result =
await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent archive abc123 --force`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --force flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ agent archive --force flag is accepted\n");
}
// Test 5: agent archive with ID and --host flag is accepted
{
console.log("Test 5: agent archive with ID and --host flag is accepted");
const result =
await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} agent archive abc123 --host localhost:${port}`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept --host flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ agent archive with ID and --host flag is accepted\n");
}
// Test 6: agent shows archive in subcommands
{
console.log("Test 6: agent --help shows archive subcommand");
const result = await $`npx paseo agent --help`.nothrow();
assert.strictEqual(result.exitCode, 0, "agent --help should exit 0");
assert(result.stdout.includes("archive"), "help should mention archive subcommand");
console.log("✓ agent --help shows archive subcommand\n");
}
// Test 7: -q (quiet) flag is accepted with agent archive
{
console.log("Test 7: -q (quiet) flag is accepted with agent archive");
const result =
await $`PASEO_HOME=${paseoHome} npx paseo --host localhost:${port} -q agent archive abc123`.nothrow();
const output = result.stdout + result.stderr;
assert(!output.includes("unknown option"), "should accept -q flag");
assert(!output.includes("error: option"), "should not have option parsing error");
console.log("✓ -q (quiet) flag is accepted with agent archive\n");
}
// Test 8: archives an agent that sits past the agent listing page cap
{
console.log("Test 8: archives an agent past the agent listing page cap");
const daemon = await startTestDaemon(await seedHomePastPageCap(TARGET_AGENT_ID));
const host = `127.0.0.1:${daemon.port}`;
try {
const listed = await runPaseoCli(daemon, [
"agent",
"ls",
"--global",
"--json",
"--host",
host,
]);
assert.strictEqual(listed.exitCode, 0, listed.stderr);
assert(
(JSON.parse(listed.stdout) as Array<{ id: string }>).some((a) => a.id === TARGET_AGENT_ID),
"the agent should be listed before it is archived",
);
const archived = await runPaseoCli(daemon, [
"agent",
"archive",
TARGET_AGENT_ID,
"--json",
"--host",
host,
]);
assert.strictEqual(archived.exitCode, 0, archived.stdout + archived.stderr);
const result = JSON.parse(archived.stdout) as { agentId: string; status: string };
assert.strictEqual(result.agentId, TARGET_AGENT_ID);
assert.strictEqual(result.status, "archived");
const remaining = await runPaseoCli(daemon, [
"agent",
"ls",
"--global",
"--json",
"--host",
host,
]);
assert.strictEqual(remaining.exitCode, 0, remaining.stderr);
assert(
!(JSON.parse(remaining.stdout) as Array<{ id: string }>).some(
(a) => a.id === TARGET_AGENT_ID,
),
"the archived agent should leave the active list",
);
const repeated = await runPaseoCli(daemon, [
"agent",
"archive",
TARGET_AGENT_ID,
"--json",
"--host",
host,
]);
assert.strictEqual(repeated.exitCode, 1);
assert.strictEqual(
(parseJsonError(repeated.stderr) as { error: { code: string } }).error.code,
"AGENT_ALREADY_ARCHIVED",
);
const missing = await runPaseoCli(daemon, [
"agent",
"archive",
"no-such-agent",
"--json",
"--host",
host,
]);
assert.strictEqual(missing.exitCode, 1);
assert.deepStrictEqual(parseJsonError(missing.stderr), {
error: {
code: "AGENT_NOT_FOUND",
message: "Agent not found: no-such-agent",
details: 'Use "paseo ls" to list available agents',
},
});
} finally {
await daemon.stop();
}
console.log("✓ archives an agent past the agent listing page cap\n");
}
// Keep archive's existing case-insensitive and unique partial-name inputs working.
for (const query of ["FINISHED AGENT", "nished age", "ABCDEFAB"]) {
console.log(`Test 9: archives by existing identifier form: ${query}`);
const targetId = "abcdefab-2222-4333-8444-555555555555";
const daemon = await startTestDaemon(await seedHomePastPageCap(targetId, 0));
try {
const archived = await runPaseoCli(daemon, [
"agent",
"archive",
query,
"--json",
"--host",
`127.0.0.1:${daemon.port}`,
]);
assert.strictEqual(archived.exitCode, 0, archived.stdout + archived.stderr);
const result = JSON.parse(archived.stdout) as { agentId: string; status: string };
assert.strictEqual(result.agentId, targetId);
assert.strictEqual(result.status, "archived");
} finally {
await daemon.stop();
}
}
} finally {
// Clean up temp directory
await rm(paseoHome, { recursive: true, force: true });
}
console.log("=== All agent archive tests passed ===");