Files
paseo/packages/app/src/subagents/detach-subagent.test.ts
T
Mohamed BoudraandGitHub 7af92120fe Detach subagents without archiving them (#1612)
* Add detach action for subagents

* Translate detached subagent connection error

* Address subagent detach review feedback

* Fix subagent integration test import

* Focus detached subagents as tabs
2026-06-19 17:19:05 +08:00

154 lines
4.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { ConfirmDialogInput } from "@/utils/confirm-dialog";
import {
requestDetachSubagent,
resolveDetachSubagentDialog,
type DetachSubagentDeps,
type ResolveDetachSubagentDialogInput,
} from "./detach-subagent";
interface RecordedDetach {
serverId: string;
agentId: string;
}
interface RecordedOpen {
serverId: string;
agentId: string;
}
interface FakeDetachSubagentEnv {
deps: DetachSubagentDeps;
recordedDetaches: RecordedDetach[];
recordedOpens: RecordedOpen[];
recordedConfirmInputs: ConfirmDialogInput[];
recordedErrors: unknown[];
}
function createFakeEnv(
options: {
confirmResult?: boolean;
initialSubagents?: Array<{ id: string; snapshot: ResolveDetachSubagentDialogInput }>;
} = {},
): FakeDetachSubagentEnv {
const subagents = new Map<string, ResolveDetachSubagentDialogInput | undefined>();
for (const entry of options.initialSubagents ?? []) {
subagents.set(entry.id, entry.snapshot);
}
const recordedDetaches: RecordedDetach[] = [];
const recordedOpens: RecordedOpen[] = [];
const recordedConfirmInputs: ConfirmDialogInput[] = [];
const recordedErrors: unknown[] = [];
return {
recordedDetaches,
recordedOpens,
recordedConfirmInputs,
recordedErrors,
deps: {
getSubagent: (id) => subagents.get(id),
confirm: async (dialog) => {
recordedConfirmInputs.push(dialog);
return options.confirmResult ?? false;
},
detachAgent: async (input) => {
recordedDetaches.push(input);
},
openDetachedAgent: (input) => {
recordedOpens.push(input);
},
reportError: (error) => {
recordedErrors.push(error);
},
},
};
}
describe("resolveDetachSubagentDialog", () => {
it("uses non-destructive copy for named subagents", () => {
expect(resolveDetachSubagentDialog({ title: "Review branch" })).toEqual({
title: "Detach subagent?",
message: "Review branch will leave this track and continue as a standalone agent.",
confirmLabel: "Detach",
cancelLabel: "Cancel",
destructive: false,
});
});
it("falls back to this subagent when the title is not displayable", () => {
expect(resolveDetachSubagentDialog({ title: "New Agent" })).toEqual({
title: "Detach subagent?",
message: "This subagent will leave this track and continue as a standalone agent.",
confirmLabel: "Detach",
cancelLabel: "Cancel",
destructive: false,
});
});
});
describe("requestDetachSubagent", () => {
it("detaches the subagent with the server id when the user confirms", async () => {
const env = createFakeEnv({
confirmResult: true,
initialSubagents: [{ id: "child-agent", snapshot: { title: "Review branch" } }],
});
await requestDetachSubagent({ serverId: "server-1", subagentId: "child-agent" }, env.deps);
expect(env.recordedDetaches).toEqual([{ serverId: "server-1", agentId: "child-agent" }]);
});
it("opens the detached subagent after detach succeeds", async () => {
const env = createFakeEnv({
confirmResult: true,
initialSubagents: [{ id: "child-agent", snapshot: { title: "Review branch" } }],
});
await requestDetachSubagent({ serverId: "server-1", subagentId: "child-agent" }, env.deps);
expect(env.recordedOpens).toEqual([{ serverId: "server-1", agentId: "child-agent" }]);
});
it("does not detach the subagent when the user cancels", async () => {
const env = createFakeEnv({
confirmResult: false,
initialSubagents: [{ id: "child-agent", snapshot: { title: "Review branch" } }],
});
await requestDetachSubagent({ serverId: "server-1", subagentId: "child-agent" }, env.deps);
expect(env.recordedDetaches).toEqual([]);
expect(env.recordedOpens).toEqual([]);
});
it("asks for confirmation using the resolved dialog for the subagent", async () => {
const env = createFakeEnv({
confirmResult: false,
initialSubagents: [{ id: "child-agent", snapshot: { title: "Review branch" } }],
});
await requestDetachSubagent({ serverId: "server-1", subagentId: "child-agent" }, env.deps);
expect(env.recordedConfirmInputs).toEqual([
resolveDetachSubagentDialog({ title: "Review branch" }),
]);
});
it("reports detach errors after the user confirms", async () => {
const env = createFakeEnv({
confirmResult: true,
initialSubagents: [{ id: "child-agent", snapshot: { title: "Review branch" } }],
});
const error = new Error("daemon offline");
env.deps.detachAgent = async () => {
throw error;
};
await expect(
requestDetachSubagent({ serverId: "server-1", subagentId: "child-agent" }, env.deps),
).resolves.toBeUndefined();
expect(env.recordedErrors).toEqual([error]);
expect(env.recordedOpens).toEqual([]);
});
});