* Extract client SDK package * Polish SDK client identity defaults * Build client before dependent CI jobs * Restore daemon client server export * Extract protocol and client SDK packages * Fix provider override schema validation * Fix app test daemon client imports * Simplify workspace build targets * Fix CLI test server build bootstrap * Run SDK package tests in CI * Fix rebase package split drift * Restore lockfile registry metadata * Update SDK config test for prompt default * Move terminal stream router test to client package * Fix rebase drift for protocol imports * Fix SDK agent capability fixture * Restore legacy server client exports * Fix server export compatibility test * Advertise custom mode icon client capability * Remove server daemon-client exports * Format rebased mode control import * Fix rebase drift for protocol imports Files added by upstream PRs (#893, #1147, #1154) referenced the pre-split shared/ paths that this branch moves into @getpaseo/protocol. Redirect those imports to the protocol package so typecheck stays green after the rebase.
315 lines
8.2 KiB
TypeScript
315 lines
8.2 KiB
TypeScript
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { selectSubagentsForParent } from "./select";
|
|
import { useSessionStore, type Agent } from "@/stores/session-store";
|
|
|
|
const SERVER_ID = "server-1";
|
|
const AGENT_TIMESTAMP = new Date("2026-03-08T10:00:00.000Z");
|
|
const EMPTY_PENDING_ARCHIVE_IDS = new Set<string>();
|
|
|
|
const AGENT_DEFAULTS: Agent = {
|
|
serverId: SERVER_ID,
|
|
id: "agent",
|
|
provider: "codex",
|
|
status: "idle",
|
|
createdAt: AGENT_TIMESTAMP,
|
|
updatedAt: AGENT_TIMESTAMP,
|
|
lastUserMessageAt: null,
|
|
lastActivityAt: AGENT_TIMESTAMP,
|
|
capabilities: {
|
|
supportsStreaming: true,
|
|
supportsSessionPersistence: true,
|
|
supportsDynamicModes: true,
|
|
supportsMcpServers: true,
|
|
supportsReasoningStream: true,
|
|
supportsToolInvocations: true,
|
|
},
|
|
currentModeId: null,
|
|
availableModes: [],
|
|
pendingPermissions: [],
|
|
persistence: null,
|
|
runtimeInfo: undefined,
|
|
lastUsage: undefined,
|
|
lastError: null,
|
|
title: "Agent",
|
|
cwd: "/tmp/project",
|
|
model: null,
|
|
features: undefined,
|
|
thinkingOptionId: undefined,
|
|
requiresAttention: false,
|
|
attentionReason: null,
|
|
attentionTimestamp: null,
|
|
archivedAt: null,
|
|
parentAgentId: null,
|
|
labels: {},
|
|
projectPlacement: null,
|
|
};
|
|
|
|
function makeAgent(input: Partial<Agent> & Pick<Agent, "id">): Agent {
|
|
return { ...AGENT_DEFAULTS, ...input };
|
|
}
|
|
|
|
function setAgents(agents: Agent[]): void {
|
|
useSessionStore.getState().initializeSession(SERVER_ID, null as unknown as DaemonClient);
|
|
useSessionStore
|
|
.getState()
|
|
.setAgents(SERVER_ID, new Map(agents.map((agent) => [agent.id, agent])));
|
|
}
|
|
|
|
afterEach(() => {
|
|
useSessionStore.getState().clearSession(SERVER_ID);
|
|
});
|
|
|
|
describe("selectSubagentsForParent", () => {
|
|
it("returns only non-archived children for the requested parent", () => {
|
|
setAgents([
|
|
makeAgent({ id: "parent-a" }),
|
|
makeAgent({ id: "child-a", parentAgentId: "parent-a" }),
|
|
makeAgent({
|
|
id: "archived-child",
|
|
parentAgentId: "parent-a",
|
|
archivedAt: new Date("2026-03-08T12:00:00.000Z"),
|
|
}),
|
|
]);
|
|
|
|
const rows = selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent-a",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
);
|
|
|
|
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
|
|
});
|
|
|
|
it("excludes siblings, unrelated agents, and grandchildren", () => {
|
|
setAgents([
|
|
makeAgent({ id: "parent-a" }),
|
|
makeAgent({ id: "parent-b" }),
|
|
makeAgent({ id: "child-a", parentAgentId: "parent-a" }),
|
|
makeAgent({ id: "sibling-b", parentAgentId: "parent-b" }),
|
|
makeAgent({ id: "grandchild-a", parentAgentId: "child-a" }),
|
|
makeAgent({ id: "unrelated" }),
|
|
]);
|
|
|
|
const rows = selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent-a",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
);
|
|
|
|
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
|
|
});
|
|
|
|
it("shows only direct children for each parent", () => {
|
|
setAgents([
|
|
makeAgent({ id: "parent" }),
|
|
makeAgent({ id: "child", parentAgentId: "parent" }),
|
|
makeAgent({ id: "grandchild", parentAgentId: "child" }),
|
|
]);
|
|
|
|
const parentRows = selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
);
|
|
const childRows = selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "child",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
);
|
|
|
|
expect(parentRows.map((row) => row.id)).toEqual(["child"]);
|
|
expect(childRows.map((row) => row.id)).toEqual(["grandchild"]);
|
|
});
|
|
|
|
it("sorts by createdAt ascending", () => {
|
|
setAgents([
|
|
makeAgent({ id: "parent" }),
|
|
makeAgent({
|
|
id: "third",
|
|
parentAgentId: "parent",
|
|
createdAt: new Date("2026-03-08T10:03:00.000Z"),
|
|
}),
|
|
makeAgent({
|
|
id: "first",
|
|
parentAgentId: "parent",
|
|
createdAt: new Date("2026-03-08T10:01:00.000Z"),
|
|
}),
|
|
makeAgent({
|
|
id: "second",
|
|
parentAgentId: "parent",
|
|
createdAt: new Date("2026-03-08T10:02:00.000Z"),
|
|
}),
|
|
]);
|
|
|
|
const rows = selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
);
|
|
|
|
expect(rows.map((row) => row.id)).toEqual(["first", "second", "third"]);
|
|
});
|
|
|
|
it("maps only row-rendered fields and does not expose onOpen", () => {
|
|
const createdAt = new Date("2026-03-08T10:01:00.000Z");
|
|
setAgents([
|
|
makeAgent({ id: "parent" }),
|
|
makeAgent({
|
|
id: "child",
|
|
parentAgentId: "parent",
|
|
provider: "claude",
|
|
title: "Review child",
|
|
status: "running",
|
|
requiresAttention: true,
|
|
createdAt,
|
|
model: "should-not-leak",
|
|
cwd: "/private/project",
|
|
}),
|
|
]);
|
|
|
|
const rows = selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
);
|
|
|
|
expect(rows).toEqual([
|
|
{
|
|
id: "child",
|
|
provider: "claude",
|
|
title: "Review child",
|
|
status: "running",
|
|
requiresAttention: true,
|
|
createdAt,
|
|
},
|
|
]);
|
|
expect(Object.keys(rows[0] ?? {}).sort()).toEqual([
|
|
"createdAt",
|
|
"id",
|
|
"provider",
|
|
"requiresAttention",
|
|
"status",
|
|
"title",
|
|
]);
|
|
expect(rows[0]).not.toHaveProperty("onOpen");
|
|
expect(rows[0]).not.toHaveProperty("model");
|
|
expect(rows[0]).not.toHaveProperty("cwd");
|
|
});
|
|
|
|
it("moves a child when parentAgentId changes", () => {
|
|
const child = makeAgent({ id: "child", parentAgentId: "parent-a" });
|
|
setAgents([makeAgent({ id: "parent-a" }), makeAgent({ id: "parent-b" }), child]);
|
|
|
|
expect(
|
|
selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent-a",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
).map((row) => row.id),
|
|
).toEqual(["child"]);
|
|
expect(
|
|
selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent-b",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
).map((row) => row.id),
|
|
).toEqual([]);
|
|
|
|
setAgents([
|
|
makeAgent({ id: "parent-a" }),
|
|
makeAgent({ id: "parent-b" }),
|
|
{ ...child, parentAgentId: "parent-b" },
|
|
]);
|
|
|
|
expect(
|
|
selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent-a",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
).map((row) => row.id),
|
|
).toEqual([]);
|
|
expect(
|
|
selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent-b",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
).map((row) => row.id),
|
|
).toEqual(["child"]);
|
|
});
|
|
|
|
it("excludes children whose archive is pending", () => {
|
|
setAgents([
|
|
makeAgent({ id: "parent" }),
|
|
makeAgent({ id: "child-a", parentAgentId: "parent" }),
|
|
makeAgent({ id: "child-b", parentAgentId: "parent" }),
|
|
]);
|
|
|
|
const rows = selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent",
|
|
},
|
|
new Set(["child-b"]),
|
|
);
|
|
|
|
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
|
|
});
|
|
|
|
it("returns the shared empty array when pending archive hides the last child", () => {
|
|
setAgents([makeAgent({ id: "parent" }), makeAgent({ id: "child", parentAgentId: "parent" })]);
|
|
|
|
const rows = selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent",
|
|
},
|
|
new Set(["child"]),
|
|
);
|
|
|
|
expect(rows).toEqual([]);
|
|
expect(rows).toBe(
|
|
selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "missing-parent",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
),
|
|
);
|
|
});
|
|
});
|