/// import { afterEach, beforeEach, describe, expect, test } from "bun:test" import { existsSync, mkdtempSync, rmSync } from "node:fs" import { tmpdir } from "node:os" import { join } from "node:path" import type { BackgroundManager } from "../../features/background-agent" import { createAgentTeamsTools } from "./tools" import { getTeamDir, getTeamInboxPath, getTeamTaskDir } from "./paths" interface LaunchCall { description: string prompt: string agent: string parentSessionID: string parentMessageID: string parentAgent?: string model?: { providerID: string modelID: string } } interface ResumeCall { sessionId: string prompt: string parentSessionID: string parentMessageID: string parentAgent?: string } interface CancelCall { taskId: string options?: unknown } interface MockManagerHandles { manager: BackgroundManager launchCalls: LaunchCall[] resumeCalls: ResumeCall[] cancelCalls: CancelCall[] } interface TestToolContext { sessionID: string messageID: string agent: string abort: AbortSignal } function createMockManager(): MockManagerHandles { const launchCalls: LaunchCall[] = [] const resumeCalls: ResumeCall[] = [] const cancelCalls: CancelCall[] = [] const launchedTasks = new Map() let launchCount = 0 const manager = { launch: async (args: LaunchCall) => { launchCount += 1 launchCalls.push(args) const task = { id: `bg-${launchCount}`, sessionID: `ses-worker-${launchCount}` } launchedTasks.set(task.id, task) return task }, getTask: (taskId: string) => launchedTasks.get(taskId), resume: async (args: ResumeCall) => { resumeCalls.push(args) return { id: `resume-${resumeCalls.length}` } }, cancelTask: async (taskId: string, options?: unknown) => { cancelCalls.push({ taskId, options }) return true }, } as unknown as BackgroundManager return { manager, launchCalls, resumeCalls, cancelCalls } } function createFailingLaunchManager(): { manager: BackgroundManager; cancelCalls: CancelCall[] } { const cancelCalls: CancelCall[] = [] const manager = { launch: async () => ({ id: "bg-fail" }), getTask: () => ({ id: "bg-fail", parentSessionID: "ses-main", parentMessageID: "msg-main", description: "failed launch", prompt: "prompt", agent: "sisyphus-junior", status: "error", error: "launch failed", }), resume: async () => ({ id: "resume-unused" }), cancelTask: async (taskId: string, options?: unknown) => { cancelCalls.push({ taskId, options }) return true }, } as unknown as BackgroundManager return { manager, cancelCalls } } function createContext(): TestToolContext { return { sessionID: "ses-main", messageID: "msg-main", agent: "sisyphus", abort: new AbortController().signal, } } async function executeJsonTool( tools: ReturnType, toolName: keyof ReturnType, args: Record, context: TestToolContext, ): Promise { const output = await tools[toolName].execute(args, context) return JSON.parse(output) } describe("agent-teams tools functional", () => { let originalCwd: string let tempProjectDir: string beforeEach(() => { originalCwd = process.cwd() tempProjectDir = mkdtempSync(join(tmpdir(), "agent-teams-tools-")) process.chdir(tempProjectDir) }) afterEach(() => { process.chdir(originalCwd) rmSync(tempProjectDir, { recursive: true, force: true }) }) test("team_create/read_config/delete work with project-local storage", async () => { //#given const { manager } = createMockManager() const tools = createAgentTeamsTools(manager) const context = createContext() //#when const created = await executeJsonTool( tools, "team_create", { team_name: "core", description: "Core team" }, context, ) as { team_name: string; team_file_path: string; lead_agent_id: string } //#then expect(created.team_name).toBe("core") expect(created.lead_agent_id).toBe("team-lead@core") expect(created.team_file_path).toBe(join(tempProjectDir, ".sisyphus", "agent-teams", "teams", "core", "config.json")) expect(existsSync(created.team_file_path)).toBe(true) expect(existsSync(getTeamInboxPath("core", "team-lead"))).toBe(true) //#when const config = await executeJsonTool(tools, "read_config", { team_name: "core" }, context) as { name: string members: Array<{ name: string }> } //#then expect(config.name).toBe("core") expect(config.members.map((member) => member.name)).toEqual(["team-lead"]) //#when const deleted = await executeJsonTool(tools, "team_delete", { team_name: "core" }, context) as { success: boolean } //#then expect(deleted.success).toBe(true) expect(existsSync(getTeamDir("core"))).toBe(false) expect(existsSync(getTeamTaskDir("core"))).toBe(false) }) test("task tools create/update/get/list and emit assignment inbox message", async () => { //#given const { manager } = createMockManager() const tools = createAgentTeamsTools(manager) const context = createContext() await executeJsonTool(tools, "team_create", { team_name: "core" }, context) await executeJsonTool( tools, "spawn_teammate", { team_name: "core", name: "worker_1", prompt: "Handle release prep", }, context, ) //#when const createdTask = await executeJsonTool( tools, "team_task_create", { team_name: "core", subject: "Draft release notes", description: "Prepare release notes for next publish.", }, context, ) as { id: string; status: string } //#then expect(createdTask.id).toMatch(/^T-[a-f0-9-]+$/) expect(createdTask.status).toBe("pending") //#when const updatedTask = await executeJsonTool( tools, "team_task_update", { team_name: "core", task_id: createdTask.id, owner: "worker_1", status: "in_progress", }, context, ) as { owner?: string; status: string } //#then expect(updatedTask.owner).toBe("worker_1") expect(updatedTask.status).toBe("in_progress") //#when const fetchedTask = await executeJsonTool( tools, "team_task_get", { team_name: "core", task_id: createdTask.id }, context, ) as { id: string; owner?: string } const listedTasks = await executeJsonTool(tools, "team_task_list", { team_name: "core" }, context) as Array<{ id: string }> const inbox = await executeJsonTool( tools, "read_inbox", { team_name: "core", agent_name: "worker_1", unread_only: true, mark_as_read: false, }, context, ) as Array<{ summary?: string; text: string }> //#then expect(fetchedTask.id).toBe(createdTask.id) expect(fetchedTask.owner).toBe("worker_1") expect(listedTasks.some((task) => task.id === createdTask.id)).toBe(true) expect(inbox.some((message) => message.summary === "task_assignment")).toBe(true) const assignment = inbox.find((message) => message.summary === "task_assignment") expect(assignment).toBeDefined() const payload = JSON.parse(assignment!.text) as { type: string; taskId: string } expect(payload.type).toBe("task_assignment") expect(payload.taskId).toBe(createdTask.id) //#when const clearedOwnerTask = await executeJsonTool( tools, "team_task_update", { team_name: "core", task_id: createdTask.id, owner: "", }, context, ) as { owner?: string } //#then expect(clearedOwnerTask.owner).toBeUndefined() }) test("rejects invalid task id input for task_get", async () => { //#given const { manager } = createMockManager() const tools = createAgentTeamsTools(manager) const context = createContext() await executeJsonTool(tools, "team_create", { team_name: "core" }, context) //#when const result = await executeJsonTool( tools, "team_task_get", { team_name: "core", task_id: "../../etc/passwd" }, context, ) as { error?: string } //#then expect(result.error).toBe("task_id_invalid") }) test("requires owner to be a team member when setting task owner", async () => { //#given const { manager } = createMockManager() const tools = createAgentTeamsTools(manager) const context = createContext() await executeJsonTool(tools, "team_create", { team_name: "core" }, context) const createdTask = await executeJsonTool( tools, "team_task_create", { team_name: "core", subject: "Investigate bug", description: "Investigate and report root cause", }, context, ) as { id: string } //#when const result = await executeJsonTool( tools, "team_task_update", { team_name: "core", task_id: createdTask.id, owner: "ghost_user", }, context, ) as { error?: string } //#then expect(result.error).toBe("owner_not_in_team") }) test("allows assigning team-lead as task owner", async () => { //#given const { manager } = createMockManager() const tools = createAgentTeamsTools(manager) const context = createContext() await executeJsonTool(tools, "team_create", { team_name: "core" }, context) const createdTask = await executeJsonTool( tools, "team_task_create", { team_name: "core", subject: "Prepare checklist", description: "Prepare release checklist", }, context, ) as { id: string } //#when const updated = await executeJsonTool( tools, "team_task_update", { team_name: "core", task_id: createdTask.id, owner: "team-lead", }, context, ) as { owner?: string } //#then expect(updated.owner).toBe("team-lead") }) test("spawn_teammate + send_message + force_kill_teammate execute end-to-end", async () => { //#given const { manager, launchCalls, resumeCalls, cancelCalls } = createMockManager() const tools = createAgentTeamsTools(manager) const context = createContext() await executeJsonTool(tools, "team_create", { team_name: "core" }, context) //#when const spawned = await executeJsonTool( tools, "spawn_teammate", { team_name: "core", name: "worker_1", prompt: "Handle release prep", }, context, ) as { name: string; session_id: string; task_id: string } //#then expect(spawned.name).toBe("worker_1") expect(spawned.session_id).toBe("ses-worker-1") expect(spawned.task_id).toBe("bg-1") expect(launchCalls).toHaveLength(1) expect(launchCalls[0]).toMatchObject({ description: "[team:core] worker_1", agent: "sisyphus-junior", parentSessionID: "ses-main", parentMessageID: "msg-main", parentAgent: "sisyphus", }) //#when const sent = await executeJsonTool( tools, "send_message", { team_name: "core", type: "message", recipient: "worker_1", summary: "sync", content: "Please update status.", }, context, ) as { success: boolean } //#then expect(sent.success).toBe(true) expect(resumeCalls).toHaveLength(1) expect(resumeCalls[0].sessionId).toBe("ses-worker-1") //#when const invalidSender = await executeJsonTool( tools, "send_message", { team_name: "core", type: "message", sender: "ghost_user", recipient: "worker_1", summary: "sync", content: "Please update status.", }, context, ) as { error?: string } //#then expect(invalidSender.error).toBe("invalid_sender") //#given const createdTask = await executeJsonTool( tools, "team_task_create", { team_name: "core", subject: "Follow-up", description: "Collect teammate update", }, context, ) as { id: string } await executeJsonTool( tools, "team_task_update", { team_name: "core", task_id: createdTask.id, owner: "worker_1", status: "in_progress", }, context, ) //#when const killed = await executeJsonTool( tools, "force_kill_teammate", { team_name: "core", agent_name: "worker_1", }, context, ) as { success: boolean } //#then expect(killed.success).toBe(true) expect(cancelCalls).toHaveLength(1) expect(cancelCalls[0].taskId).toBe("bg-1") expect(cancelCalls[0].options).toEqual( expect.objectContaining({ source: "team_force_kill", abortSession: true, skipNotification: true, }), ) //#when const configAfterKill = await executeJsonTool(tools, "read_config", { team_name: "core" }, context) as { members: Array<{ name: string }> } const taskAfterKill = await executeJsonTool( tools, "team_task_get", { team_name: "core", task_id: createdTask.id, }, context, ) as { owner?: string; status: string } //#then expect(configAfterKill.members.some((member) => member.name === "worker_1")).toBe(false) expect(taskAfterKill.owner).toBeUndefined() expect(taskAfterKill.status).toBe("pending") }) test("rolls back teammate and cancels background task when launch fails", async () => { //#given const { manager, cancelCalls } = createFailingLaunchManager() const tools = createAgentTeamsTools(manager) const context = createContext() await executeJsonTool(tools, "team_create", { team_name: "core" }, context) //#when const spawnResult = await executeJsonTool( tools, "spawn_teammate", { team_name: "core", name: "worker_1", prompt: "Handle release prep", }, context, ) as { error?: string } //#then expect(spawnResult.error).toBe("teammate_launch_failed:launch failed") //#when const config = await executeJsonTool(tools, "read_config", { team_name: "core" }, context) as { members: Array<{ name: string }> } //#then expect(config.members.map((member) => member.name)).toEqual(["team-lead"]) expect(cancelCalls).toHaveLength(1) expect(cancelCalls[0].taskId).toBe("bg-fail") expect(cancelCalls[0].options).toEqual( expect.objectContaining({ source: "team_launch_failed", abortSession: true, skipNotification: true, }), ) }) test("returns explicit error on invalid model override format", async () => { //#given const { manager, launchCalls } = createMockManager() const tools = createAgentTeamsTools(manager) const context = createContext() await executeJsonTool(tools, "team_create", { team_name: "core" }, context) //#when const spawnResult = await executeJsonTool( tools, "spawn_teammate", { team_name: "core", name: "worker_1", prompt: "Handle release prep", model: "invalid-format", }, context, ) as { error?: string } //#then expect(spawnResult.error).toBe("invalid_model_override_format") expect(launchCalls).toHaveLength(0) //#when const config = await executeJsonTool(tools, "read_config", { team_name: "core" }, context) as { members: Array<{ name: string }> } //#then expect(config.members.map((member) => member.name)).toEqual(["team-lead"]) }) test("read_inbox returns team_not_found for unknown team", async () => { //#given const { manager } = createMockManager() const tools = createAgentTeamsTools(manager) const context = createContext() //#when const result = await executeJsonTool( tools, "read_inbox", { team_name: "missing_team", agent_name: "team-lead", }, context, ) as { error?: string } //#then expect(result.error).toBe("team_not_found") }) })