Files
paseo/packages/cli/src/commands/agent/ls.test.ts
T
Mohamed Boudra a880c9619d feat: background agents for hidden helpers (#8683)
* feat(server): add background agents

An agent can be created background in any workspace. It stays a durable,
addressable agent, but default discovery, workspace status and attention
leave it out. Effective background is agent.background ||
workspace.background.

- Protocol: `create_agent_request.background` now marks the agent itself
  and stays the workspace intent for a workspace the request creates.
  Snapshots carry `background`. The daemon advertises
  `features.backgroundAgents`. Clients advertise the `background_agents`
  capability.
- Daemon: the flag persists in the agent record, so restart and resume
  keep it. AgentDirectory owns visibility. Inclusive reads and
  subscriptions from sources without `background_agents` see only agents
  in background workspaces, because older apps would otherwise open a tab
  for each one.
- Inheritance: omitted on agent-scoped creation, children and new
  workspaces take the caller agent's background.
- MCP: create_agent accepts `background`.
- SDK: placing a background agent in an existing workspace, or under a
  caller, fails fast with "Update the host to use background agents." on
  older hosts.
- CLI: `ls -a` and `ls --background` add a BACKGROUND column.

* feat(app): hide background agents from tabs, track and status

* docs(plugins): point hidden helpers at background agents

* fix: keep background agents hidden across cache reload and connected hosts

* fix: keep remote creation working on older hosts and inherit foreign parents' background
2026-11-04 07:19:21 +07:00

157 lines
5.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { AgentSnapshotPayload } from "@getpaseo/protocol/messages";
import { render } from "../../output/index.js";
import { buildAgentLsFetchOptions, listAgents } from "./ls.js";
const daemonTarget = { kind: "endpoint" as const, host: "example.test:12345" };
function createPagingClient() {
const daemonAgents: AgentSnapshotPayload[] = [3, 2, 1].map((n) => ({
id: `${n}${n}${n}${n}${n}${n}${n}${n}-0000-4000-8000-000000000000`,
provider: "codex",
title: `agent ${n}`,
status: "closed",
archivedAt: null,
cwd: "/tmp/project",
createdAt: `2026-10-0${n}T00:00:00.000Z`,
updatedAt: `2026-10-0${n}T00:00:00.000Z`,
labels: {},
model: null,
lastUserMessageAt: null,
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: null,
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: false,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
}));
// The fake daemon pages two agents at a time, newest first by the requested key
// (updated_at by default), and agent 1 is updated right after the first page is served.
const fetchAgents = async (options?: {
sort?: { key: string }[];
page?: { cursor?: string };
}) => {
const key: "createdAt" | "updatedAt" =
options?.sort?.[0]?.key === "created_at" ? "createdAt" : "updatedAt";
const cursor = options?.page?.cursor;
const ordered = daemonAgents
.filter((agent) => cursor === undefined || agent[key] < cursor)
.sort((left, right) => right[key].localeCompare(left[key]));
const page = ordered.slice(0, 2);
const hasMore = ordered.length > 2;
if (cursor === undefined) {
daemonAgents[2]!.updatedAt = "2026-10-09T00:00:00.000Z";
}
return {
entries: page.map((agent) => ({ agent })),
pageInfo: { nextCursor: hasMore ? page[page.length - 1]![key] : null },
};
};
return { fetchAgents, close: async () => {} };
}
describe("buildAgentLsFetchOptions", () => {
it("fetches active agents by default", () => {
expect(buildAgentLsFetchOptions({})).toEqual({
scope: "active",
});
});
it("keeps label and thinking filters within the active scope", () => {
expect(
buildAgentLsFetchOptions({
label: ["surface=workspace"],
thinking: " medium ",
}),
).toEqual({
scope: "active",
filter: {
labels: { surface: "workspace" },
thinkingOptionId: "medium",
},
});
});
it("asks the daemon for internal agents only with --internal", () => {
expect(buildAgentLsFetchOptions({ background: true, global: true })).toEqual({
filter: { includeBackground: true },
});
});
it("fetches global non-archived agents for -g", () => {
expect(buildAgentLsFetchOptions({ global: true })).toEqual({});
});
it("includes archived and background agents with -a", () => {
expect(buildAgentLsFetchOptions({ all: true })).toEqual({
filter: {
includeArchived: true,
includeBackground: true,
},
});
});
it("fetches all global agents for -a -g", () => {
expect(buildAgentLsFetchOptions({ all: true, global: true })).toEqual({
filter: {
includeArchived: true,
includeBackground: true,
},
});
});
it("applies filters to global queries", () => {
expect(
buildAgentLsFetchOptions({
global: true,
label: ["surface=workspace"],
thinking: " medium ",
}),
).toEqual({
filter: {
labels: { surface: "workspace" },
thinkingOptionId: "medium",
},
});
});
});
describe("runLsCommand", () => {
it("lists agents on every page, including one updated during the listing", async () => {
const result = await listAgents({ daemonTarget, all: true, global: true }, async () =>
createPagingClient(),
);
expect(result.data.map((item) => item.name)).toEqual(["agent 3", "agent 2", "agent 1"]);
});
it("marks background agents in a BACKGROUND column only for inclusive listings", async () => {
const client = createPagingClient();
const fetchAgents: typeof client.fetchAgents = async (options) => {
const page = await client.fetchAgents(options);
return {
...page,
entries: page.entries.map(({ agent }) => ({
agent: agent.title === "agent 2" ? { ...agent, background: true } : agent,
})),
};
};
const connect = async () => ({ ...client, fetchAgents });
const inclusive = await listAgents({ daemonTarget, all: true, global: true }, connect);
const rows = render(inclusive, { noColor: true }).split("\n");
expect(rows[0]).toMatch(/BACKGROUND\s*$/);
expect(rows.slice(1).map((row) => row.trim().split(/\s+/).at(-1))).toEqual(["-", "yes", "-"]);
const ordinary = await listAgents({ daemonTarget, global: true }, connect);
expect(render(ordinary, { noColor: true })).not.toContain("BACKGROUND");
});
});