import { afterEach, describe, expect, it, vi } from "vitest"; const noop = () => {}; vi.mock("../gateway/call.js", () => ({ callGateway: vi.fn(async () => ({ status: "ok", startedAt: 111, endedAt: 222, })), })); vi.mock("../infra/agent-events.js", () => ({ onAgentEvent: vi.fn(() => noop), })); vi.mock("../config/config.js", () => ({ loadConfig: vi.fn(() => ({ agents: { defaults: { subagents: { archiveAfterMinutes: 0 } } }, })), })); vi.mock("./subagent-announce.js", () => ({ runSubagentAnnounceFlow: vi.fn(async () => true), buildSubagentSystemPrompt: vi.fn(() => "test prompt"), })); vi.mock("./subagent-registry.store.js", () => ({ loadSubagentRegistryFromDisk: vi.fn(() => new Map()), saveSubagentRegistryToDisk: vi.fn(() => {}), })); describe("subagent registry nested agent tracking", () => { afterEach(async () => { const mod = await import("./subagent-registry.js"); mod.resetSubagentRegistryForTests({ persist: false }); }); it("listSubagentRunsForRequester returns children of the requesting session", async () => { const { registerSubagentRun, listSubagentRunsForRequester } = await import("./subagent-registry.js"); // Main agent spawns a depth-1 orchestrator registerSubagentRun({ runId: "run-orch", childSessionKey: "agent:main:subagent:orch-uuid", requesterSessionKey: "agent:main:main", requesterDisplayKey: "main", task: "orchestrate something", cleanup: "keep", label: "orchestrator", }); // Depth-1 orchestrator spawns a depth-2 leaf registerSubagentRun({ runId: "run-leaf", childSessionKey: "agent:main:subagent:orch-uuid:subagent:leaf-uuid", requesterSessionKey: "agent:main:subagent:orch-uuid", requesterDisplayKey: "subagent:orch-uuid", task: "do leaf work", cleanup: "keep", label: "leaf", }); // Main sees its direct child (the orchestrator) const mainRuns = listSubagentRunsForRequester("agent:main:main"); expect(mainRuns).toHaveLength(1); expect(mainRuns[0].runId).toBe("run-orch"); // Orchestrator sees its direct child (the leaf) const orchRuns = listSubagentRunsForRequester("agent:main:subagent:orch-uuid"); expect(orchRuns).toHaveLength(1); expect(orchRuns[0].runId).toBe("run-leaf"); // Leaf has no children const leafRuns = listSubagentRunsForRequester( "agent:main:subagent:orch-uuid:subagent:leaf-uuid", ); expect(leafRuns).toHaveLength(0); }); it("announce uses requesterSessionKey to route to the correct parent", async () => { const { registerSubagentRun } = await import("./subagent-registry.js"); // Register a sub-sub-agent whose parent is a sub-agent registerSubagentRun({ runId: "run-subsub", childSessionKey: "agent:main:subagent:orch:subagent:child", requesterSessionKey: "agent:main:subagent:orch", requesterDisplayKey: "subagent:orch", task: "nested task", cleanup: "keep", label: "nested-leaf", }); // When announce fires for the sub-sub-agent, it should target the sub-agent (depth-1), // NOT the main session. The registry entry's requesterSessionKey ensures this. // We verify the registry entry has the correct requesterSessionKey. const { listSubagentRunsForRequester } = await import("./subagent-registry.js"); const orchRuns = listSubagentRunsForRequester("agent:main:subagent:orch"); expect(orchRuns).toHaveLength(1); expect(orchRuns[0].requesterSessionKey).toBe("agent:main:subagent:orch"); expect(orchRuns[0].childSessionKey).toBe("agent:main:subagent:orch:subagent:child"); }); it("countActiveRunsForSession only counts active children of the specific session", async () => { const { registerSubagentRun, countActiveRunsForSession } = await import("./subagent-registry.js"); // Main spawns orchestrator (active) registerSubagentRun({ runId: "run-orch-active", childSessionKey: "agent:main:subagent:orch1", requesterSessionKey: "agent:main:main", requesterDisplayKey: "main", task: "orchestrate", cleanup: "keep", }); // Orchestrator spawns two leaves registerSubagentRun({ runId: "run-leaf-1", childSessionKey: "agent:main:subagent:orch1:subagent:leaf1", requesterSessionKey: "agent:main:subagent:orch1", requesterDisplayKey: "subagent:orch1", task: "leaf 1", cleanup: "keep", }); registerSubagentRun({ runId: "run-leaf-2", childSessionKey: "agent:main:subagent:orch1:subagent:leaf2", requesterSessionKey: "agent:main:subagent:orch1", requesterDisplayKey: "subagent:orch1", task: "leaf 2", cleanup: "keep", }); // Main has 1 active child expect(countActiveRunsForSession("agent:main:main")).toBe(1); // Orchestrator has 2 active children expect(countActiveRunsForSession("agent:main:subagent:orch1")).toBe(2); }); it("countActiveDescendantRuns traverses through ended parents", async () => { const { addSubagentRunForTests, countActiveDescendantRuns } = await import("./subagent-registry.js"); addSubagentRunForTests({ runId: "run-parent-ended", childSessionKey: "agent:main:subagent:orch-ended", requesterSessionKey: "agent:main:main", requesterDisplayKey: "main", task: "orchestrate", cleanup: "keep", createdAt: 1, startedAt: 1, endedAt: 2, cleanupHandled: false, }); addSubagentRunForTests({ runId: "run-leaf-active", childSessionKey: "agent:main:subagent:orch-ended:subagent:leaf", requesterSessionKey: "agent:main:subagent:orch-ended", requesterDisplayKey: "orch-ended", task: "leaf", cleanup: "keep", createdAt: 1, startedAt: 1, cleanupHandled: false, }); expect(countActiveDescendantRuns("agent:main:main")).toBe(1); expect(countActiveDescendantRuns("agent:main:subagent:orch-ended")).toBe(1); }); });