Adds a new `sessions_list` tool to the Subagent tool group, allowing agents to query the status of their spawned sub-tasks. Supports both list mode (all runs) and detail mode (specific runId). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
169 lines
5.7 KiB
TypeScript
169 lines
5.7 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import type { SubagentRunRecord } from "../subagent/types.js";
|
|
|
|
// Mock the registry module before importing the tool
|
|
vi.mock("../subagent/registry.js", () => ({
|
|
listSubagentRuns: vi.fn(),
|
|
getSubagentRun: vi.fn(),
|
|
}));
|
|
|
|
import { createSessionsListTool } from "./sessions-list.js";
|
|
import { listSubagentRuns, getSubagentRun } from "../subagent/registry.js";
|
|
|
|
const mockListSubagentRuns = vi.mocked(listSubagentRuns);
|
|
const mockGetSubagentRun = vi.mocked(getSubagentRun);
|
|
|
|
function makeRecord(overrides: Partial<SubagentRunRecord> = {}): SubagentRunRecord {
|
|
return {
|
|
runId: "run-001",
|
|
childSessionId: "child-001",
|
|
requesterSessionId: "parent-001",
|
|
task: "Test task",
|
|
cleanup: "delete",
|
|
createdAt: 1700000000000,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("sessions_list tool", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("returns empty message when no runs exist", async () => {
|
|
mockListSubagentRuns.mockReturnValue([]);
|
|
const tool = createSessionsListTool({ sessionId: "parent-001" });
|
|
const result = await tool.execute("call-1", {});
|
|
|
|
expect(result.content[0]).toEqual({
|
|
type: "text",
|
|
text: "No subagent runs for this session.",
|
|
});
|
|
expect(result.details).toEqual({ runs: [] });
|
|
});
|
|
|
|
it("lists multiple runs with correct status mapping", async () => {
|
|
const now = Date.now();
|
|
const runs: SubagentRunRecord[] = [
|
|
makeRecord({
|
|
runId: "run-aaa",
|
|
label: "Code Review",
|
|
startedAt: now - 45000,
|
|
}),
|
|
makeRecord({
|
|
runId: "run-bbb",
|
|
label: "Test Analysis",
|
|
startedAt: now - 60000,
|
|
endedAt: now - 30000,
|
|
outcome: { status: "ok" },
|
|
}),
|
|
makeRecord({
|
|
runId: "run-ccc",
|
|
label: "Lint Check",
|
|
startedAt: now - 60000,
|
|
endedAt: now,
|
|
outcome: { status: "error", error: "timeout" },
|
|
}),
|
|
];
|
|
mockListSubagentRuns.mockReturnValue(runs);
|
|
|
|
const tool = createSessionsListTool({ sessionId: "parent-001" });
|
|
const result = await tool.execute("call-1", {});
|
|
|
|
const text = result.content[0]!;
|
|
expect(text.type).toBe("text");
|
|
expect((text as { text: string }).text).toContain("3 total");
|
|
expect((text as { text: string }).text).toContain("[running]");
|
|
expect((text as { text: string }).text).toContain("[ok]");
|
|
expect((text as { text: string }).text).toContain("[error]");
|
|
expect((text as { text: string }).text).toContain("Code Review");
|
|
expect((text as { text: string }).text).toContain("Test Analysis");
|
|
expect((text as { text: string }).text).toContain("Lint Check");
|
|
|
|
expect(result.details!.runs).toHaveLength(3);
|
|
expect(result.details!.runs[0]!.status).toBe("running");
|
|
expect(result.details!.runs[1]!.status).toBe("ok");
|
|
expect(result.details!.runs[2]!.status).toBe("error");
|
|
});
|
|
|
|
it("returns detail for a specific runId", async () => {
|
|
const now = Date.now();
|
|
const record = makeRecord({
|
|
runId: "run-detail",
|
|
label: "Deep Analysis",
|
|
task: "Analyze the authentication module thoroughly",
|
|
startedAt: now - 90000,
|
|
endedAt: now - 10000,
|
|
outcome: { status: "ok" },
|
|
findings: "Found 2 potential issues in token validation.",
|
|
findingsCaptured: true,
|
|
});
|
|
mockGetSubagentRun.mockReturnValue(record);
|
|
|
|
const tool = createSessionsListTool({ sessionId: "parent-001" });
|
|
const result = await tool.execute("call-1", { runId: "run-detail" });
|
|
|
|
const text = (result.content[0] as { text: string }).text;
|
|
expect(text).toContain("Run: run-detail");
|
|
expect(text).toContain("Label: Deep Analysis");
|
|
expect(text).toContain("Status: ok");
|
|
expect(text).toContain("Found 2 potential issues");
|
|
expect(text).toContain("Duration:");
|
|
|
|
expect(result.details!.runs).toHaveLength(1);
|
|
expect(result.details!.runs[0]!.runId).toBe("run-detail");
|
|
});
|
|
|
|
it("returns not found for unknown runId", async () => {
|
|
mockGetSubagentRun.mockReturnValue(undefined);
|
|
|
|
const tool = createSessionsListTool({ sessionId: "parent-001" });
|
|
const result = await tool.execute("call-1", { runId: "nonexistent" });
|
|
|
|
const text = (result.content[0] as { text: string }).text;
|
|
expect(text).toContain("Run not found");
|
|
expect(result.details).toEqual({ runs: [] });
|
|
});
|
|
|
|
it("rejects runId belonging to a different requester", async () => {
|
|
const record = makeRecord({
|
|
runId: "run-other",
|
|
requesterSessionId: "other-parent",
|
|
});
|
|
mockGetSubagentRun.mockReturnValue(record);
|
|
|
|
const tool = createSessionsListTool({ sessionId: "parent-001" });
|
|
const result = await tool.execute("call-1", { runId: "run-other" });
|
|
|
|
const text = (result.content[0] as { text: string }).text;
|
|
expect(text).toContain("Run not found");
|
|
expect(result.details).toEqual({ runs: [] });
|
|
});
|
|
|
|
it("handles missing sessionId gracefully", async () => {
|
|
const tool = createSessionsListTool({});
|
|
const result = await tool.execute("call-1", {});
|
|
|
|
const text = (result.content[0] as { text: string }).text;
|
|
expect(text).toContain("No session ID available");
|
|
expect(result.details).toEqual({ runs: [] });
|
|
});
|
|
|
|
it("shows findings status for running task", async () => {
|
|
const now = Date.now();
|
|
const record = makeRecord({
|
|
runId: "run-running",
|
|
label: "Still Running",
|
|
startedAt: now - 30000,
|
|
// no endedAt
|
|
});
|
|
mockGetSubagentRun.mockReturnValue(record);
|
|
|
|
const tool = createSessionsListTool({ sessionId: "parent-001" });
|
|
const result = await tool.execute("call-1", { runId: "run-running" });
|
|
|
|
const text = (result.content[0] as { text: string }).text;
|
|
expect(text).toContain("Status: running");
|
|
expect(text).toContain("Findings: (still running)");
|
|
});
|
|
});
|