mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 08:54:42 +00:00
fix(cli): delete archived agents with agent delete --all and --cwd (#6027)
* fix(cli): delete archived agents in bulk commands * test(cli): cover archived agents in bulk delete from the CLI suite The daemon-backed test imported server test utilities from a CLI test, which the CI contract in scripts/ci-workflow.test.mjs forbids. Cover --all and --cwd against archived agents in the runDeleteCommand unit test instead. * fix(cli): read every history page when bulk-deleting agents agent delete --all and --cwd read one page of fetch_agents, so agents past the first 200 were skipped, and fetch_agents leaves out saved agents whose provider is unavailable. List agents from the paged agent history, which the History view uses, so bulk delete covers both. * fix(cli): delete an agent by ID without listing history --------- Co-authored-by: 3ae3ae <ogyshjsm@naver.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const daemonTarget = { kind: "endpoint" as const, host: "example.test:12345" };
|
||||
|
||||
@@ -7,18 +7,45 @@ import { runDeleteCommand } from "./delete.js";
|
||||
const agent = {
|
||||
id: "11111111-1111-4111-8111-111111111111",
|
||||
status: "running",
|
||||
archivedAt: null,
|
||||
archivedAt: null as string | null,
|
||||
cwd: "/tmp/project",
|
||||
};
|
||||
const archivedAgent = {
|
||||
id: "22222222-2222-4222-8222-222222222222",
|
||||
status: "closed",
|
||||
archivedAt: "2026-10-01T00:00:00.000Z",
|
||||
cwd: "/tmp/project/nested",
|
||||
};
|
||||
const archivedOutsideAgent = {
|
||||
id: "33333333-3333-4333-8333-333333333333",
|
||||
status: "closed",
|
||||
archivedAt: "2026-10-01T00:00:00.000Z",
|
||||
cwd: "/tmp/elsewhere",
|
||||
};
|
||||
let daemonAgents: (typeof agent)[] = [agent];
|
||||
const toEntry = (entry: typeof agent) => ({ agent: entry });
|
||||
// The fake daemon pages history two agents at a time.
|
||||
const historyPage = (cursor: string | undefined) => {
|
||||
const start = Number(cursor ?? 0);
|
||||
const end = start + 2;
|
||||
return {
|
||||
entries: daemonAgents.slice(start, end).map(toEntry),
|
||||
pageInfo: { nextCursor: end < daemonAgents.length ? String(end) : null },
|
||||
};
|
||||
};
|
||||
const cancelAgent = vi.fn(async () => {
|
||||
throw new Error("active run cancellation was not acknowledged");
|
||||
});
|
||||
const fetchAgentHistory = vi.fn(async (options: { page: { cursor?: string } }) =>
|
||||
historyPage(options.page.cursor),
|
||||
);
|
||||
const deleteAgent = vi.fn(async () => undefined);
|
||||
const close = vi.fn(async () => undefined);
|
||||
|
||||
vi.mock("../../utils/client.js", () => ({
|
||||
connectToDaemon: vi.fn(async () => ({
|
||||
fetchAgents: vi.fn(async () => ({ entries: [{ agent }] })),
|
||||
fetchAgents: vi.fn(async () => historyPage(undefined)),
|
||||
fetchAgentHistory,
|
||||
fetchAgent: vi.fn(async () => ({ agent })),
|
||||
cancelAgent,
|
||||
deleteAgent,
|
||||
@@ -28,9 +55,16 @@ vi.mock("../../utils/client.js", () => ({
|
||||
}));
|
||||
|
||||
describe("runDeleteCommand", () => {
|
||||
beforeEach(() => {
|
||||
daemonAgents = [agent];
|
||||
deleteAgent.mockClear();
|
||||
fetchAgentHistory.mockClear();
|
||||
});
|
||||
|
||||
it("force-deletes a running agent when graceful cancellation is refused", async () => {
|
||||
const result = await runDeleteCommand(agent.id, { daemonTarget }, {} as never);
|
||||
|
||||
expect(fetchAgentHistory).not.toHaveBeenCalled();
|
||||
expect(cancelAgent).toHaveBeenCalledWith(agent.id);
|
||||
expect(deleteAgent).toHaveBeenCalledWith(agent.id);
|
||||
expect(result.data).toEqual({
|
||||
@@ -38,4 +72,30 @@ describe("runDeleteCommand", () => {
|
||||
agentIds: [agent.id],
|
||||
});
|
||||
});
|
||||
|
||||
it("deletes archived agents on every history page with --all", async () => {
|
||||
daemonAgents = [agent, archivedAgent, archivedOutsideAgent];
|
||||
|
||||
const result = await runDeleteCommand(undefined, { daemonTarget, all: true }, {} as never);
|
||||
|
||||
expect(result.data).toEqual({
|
||||
deletedCount: 3,
|
||||
agentIds: [agent.id, archivedAgent.id, archivedOutsideAgent.id],
|
||||
});
|
||||
});
|
||||
|
||||
it("deletes archived agents inside the directory with --cwd", async () => {
|
||||
daemonAgents = [agent, archivedAgent, archivedOutsideAgent];
|
||||
|
||||
const result = await runDeleteCommand(
|
||||
undefined,
|
||||
{ daemonTarget, cwd: "/tmp/project" },
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(result.data).toEqual({
|
||||
deletedCount: 2,
|
||||
agentIds: [agent.id, archivedAgent.id],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Command } from "commander";
|
||||
import { connectToDaemon } from "../../utils/client.js";
|
||||
import { isSameOrDescendantPath } from "../../utils/paths.js";
|
||||
import type { DaemonClient, FetchAgentHistoryEntry } from "@getpaseo/client/internal/daemon-client";
|
||||
|
||||
export function addDeleteOptions(cmd: Command): Command {
|
||||
return cmd
|
||||
@@ -33,6 +34,20 @@ export interface AgentDeleteOptions extends CommandOptions {
|
||||
|
||||
export type AgentDeleteResult = SingleResult<DeleteResult>;
|
||||
|
||||
// History includes archived agents and agents whose provider is unavailable.
|
||||
async function fetchHistoryAgents(client: DaemonClient) {
|
||||
const agents: FetchAgentHistoryEntry["agent"][] = [];
|
||||
let cursor: string | undefined;
|
||||
do {
|
||||
const payload = await client.fetchAgentHistory({
|
||||
page: { limit: 200, ...(cursor ? { cursor } : {}) },
|
||||
});
|
||||
agents.push(...payload.entries.map((entry) => entry.agent));
|
||||
cursor = payload.pageInfo.nextCursor ?? undefined;
|
||||
} while (cursor);
|
||||
return agents;
|
||||
}
|
||||
|
||||
export async function runDeleteCommand(
|
||||
id: string | undefined,
|
||||
options: AgentDeleteOptions,
|
||||
@@ -50,19 +65,16 @@ export async function runDeleteCommand(
|
||||
const client = await connectToDaemon({ target: options.daemonTarget });
|
||||
|
||||
try {
|
||||
const fetchPayload = await client.fetchAgents({ filter: { includeArchived: true } });
|
||||
let agents = fetchPayload.entries.map((entry) => entry.agent);
|
||||
const deletedIds: string[] = [];
|
||||
let agents: FetchAgentHistoryEntry["agent"][];
|
||||
|
||||
if (options.all) {
|
||||
agents = agents.filter((a) => !a.archivedAt);
|
||||
} else if (options.cwd) {
|
||||
agents = agents.filter((a) => {
|
||||
if (a.archivedAt) return false;
|
||||
return isSameOrDescendantPath(options.cwd!, a.cwd);
|
||||
});
|
||||
} else if (id) {
|
||||
const fetchResult = await client.fetchAgent({ agentId: id });
|
||||
if (options.all || options.cwd) {
|
||||
agents = await fetchHistoryAgents(client);
|
||||
if (!options.all) {
|
||||
agents = agents.filter((a) => isSameOrDescendantPath(options.cwd!, a.cwd));
|
||||
}
|
||||
} else {
|
||||
const fetchResult = await client.fetchAgent({ agentId: id! });
|
||||
if (!fetchResult) {
|
||||
const error: CommandError = {
|
||||
code: "AGENT_NOT_FOUND",
|
||||
|
||||
Reference in New Issue
Block a user