mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 12:12:47 +00:00
* 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>
292 lines
10 KiB
TypeScript
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 ===");
|