/// import { afterEach, describe, expect, mock, test } from "bun:test" import { randomUUID } from "node:crypto" import { mkdir, rm, stat } from "node:fs/promises" import { tmpdir } from "node:os" import path from "node:path" import { TeamModeConfigSchema } from "../../config/schema/team-mode" import type { TeamModeConfig } from "../../config/schema/team-mode" import type { ExecutorContext } from "../../tools/delegate-task/executor-types" import type { LiveDeliveryClient } from "./tools/messaging" import { BackgroundManager } from "../background-agent/manager" import type { BackgroundTask, LaunchInput } from "../background-agent/types" import { SessionCategoryRegistry } from "../../shared/session-category-registry" import { clearAllSessionPromptParams, getSessionPromptParams, } from "../../shared/session-prompt-params-state" import { getRuntimeStateDir, resolveBaseDir } from "./team-registry/paths" import type { TeamSpec } from "./types" const resolveMemberMock = mock(async (member: TeamSpec["members"][number]) => ({ agentToUse: `${member.name}-agent`, model: { providerID: "openai", modelID: "gpt-5.4-mini", variant: "medium", reasoningEffort: "high", temperature: 0.1, top_p: 0.9, maxTokens: 2048, thinking: { type: "enabled", budgetTokens: 1024 }, }, fallbackChain: undefined, systemContent: `system:${member.name}`, })) mock.module("./team-runtime/resolve-member", () => ({ resolveMember: resolveMemberMock })) mock.module("./team-layout-tmux/layout", () => ({ canVisualize: () => false, createTeamLayout: mock(async () => undefined), removeTeamLayout: mock(async () => undefined), })) const { sendMessage } = await import("./team-mailbox/send") const { createTeamRun } = await import("./team-runtime/create") const { deleteTeam } = await import("./team-runtime/shutdown") const { aggregateStatus } = await import("./team-runtime/status") const { createTask, claimTask, listTasks, updateTaskStatus } = await import("./team-tasklist") const { resumeAllTeams } = await import("./team-state-store/resume") const { loadRuntimeState, saveRuntimeState } = await import("./team-state-store/store") const temporaryDirectories: string[] = [] type MockClient = ExecutorContext["client"] & { session: { get: ReturnType } } function createConfig(baseDir: string, overrides: Partial = {}): TeamModeConfig { return TeamModeConfigSchema.parse({ enabled: true, base_dir: baseDir, max_wall_clock_minutes: 1, ...overrides }) } function createSpec(name: string, leadAgentId: string, members: TeamSpec["members"]): TeamSpec { return { version: 1, name, createdAt: Date.now(), leadAgentId, members } } function createClient(aliveSessionIds: ReadonlySet): MockClient { return { session: { get: mock(async ({ path: { id } }: { path: { id: string } }) => aliveSessionIds.has(id) ? { data: { id } } : { error: Object.assign(new Error("session not found"), { status: 404 }) }), }, } as MockClient } function createManager(launchImpl?: (input: LaunchInput) => Promise) { const manager = Object.create(BackgroundManager.prototype) as BackgroundManager let launchCount = 0 manager.launch = mock((input: LaunchInput) => launchImpl?.(input) ?? Promise.resolve({ id: `task-${++launchCount}`, sessionID: `ses_mock_${randomUUID()}`, status: "running", } as BackgroundTask)) manager.getTask = mock(() => undefined) manager.cancelTask = mock(async () => true) manager.getTasksByParentSession = mock(() => []) return manager } function createContext(directory: string, manager: BackgroundManager, aliveSessionIds: ReadonlySet): ExecutorContext { return { client: createClient(aliveSessionIds), manager, directory } } async function createBaseDir(): Promise { const directory = path.join(tmpdir(), `team-mode-int-${randomUUID()}`) temporaryDirectories.push(directory) await mkdir(directory, { recursive: true }) return directory } async function exists(targetPath: string): Promise { try { await stat(targetPath) return true } catch { return false } } afterEach(async () => { resolveMemberMock.mockClear() SessionCategoryRegistry.clear() clearAllSessionPromptParams() await Promise.all(temporaryDirectories.splice(0).map(async (directory) => rm(directory, { recursive: true, force: true }))) }) describe("team-mode integration", () => { test("C-10.1 creates a single-member echo team, delivers mail, surfaces unread status, and deletes runtime", async () => { // given const baseDir = await createBaseDir() const config = createConfig(baseDir) const manager = createManager() const runtime = await createTeamRun(createSpec("echo-team", "echo", [{ kind: "subagent_type", name: "echo", subagent_type: "atlas", backendType: "in-process", isActive: true }]), "ses_lead", createContext(baseDir, manager, new Set(["ses_lead"])), config, manager) // when const delivered = await sendMessage({ version: 1, messageId: randomUUID(), from: "echo", to: "echo", kind: "message", body: "hello", timestamp: Date.now() }, runtime.teamRunId, config, { isLead: true, activeMembers: ["echo"] }) const status = await aggregateStatus(runtime.teamRunId, config) await deleteTeam(runtime.teamRunId, config, undefined, manager) // then expect(runtime.status).toBe("active") expect(runtime.members).toHaveLength(1) expect(runtime.members[0]?.sessionId).toMatch(/^ses_mock_/) expect(delivered.deliveredTo).toEqual(["echo"]) expect(status.members[0]?.unreadMessages).toBe(1) expect(await exists(getRuntimeStateDir(resolveBaseDir(config), runtime.teamRunId))).toBe(false) }) test("C-10.2 runs a 2-member pipeline where worker claims and completes a lead-created task", async () => { // given const baseDir = await createBaseDir() const config = createConfig(baseDir) const manager = createManager() const runtime = await createTeamRun(createSpec("pipeline-team", "lead", [ { kind: "subagent_type", name: "lead", subagent_type: "sisyphus", backendType: "in-process", isActive: true }, { kind: "subagent_type", name: "worker", subagent_type: "atlas", backendType: "in-process", isActive: true }, ]), "ses_lead", createContext(baseDir, manager, new Set(["ses_lead"])), config, manager) const createdTask = await createTask(runtime.teamRunId, { subject: "X", description: "Ship X", blocks: [], blockedBy: [], status: "pending" }, config) // when const claimedTask = await claimTask(runtime.teamRunId, createdTask.id, "worker", config) await updateTaskStatus(runtime.teamRunId, createdTask.id, "in_progress", "worker", config) await updateTaskStatus(runtime.teamRunId, createdTask.id, "completed", "worker", config) const completedTasks = await listTasks(runtime.teamRunId, config, { status: "completed" }) // then expect(claimedTask.status).toBe("claimed") expect(claimedTask.owner).toBe("worker") expect(completedTasks).toHaveLength(1) expect(completedTasks[0]?.subject).toBe("X") }) test("C-10.3 resumes alive teams, orphans dead leads, fails stuck creating teams, and cleans deleting runs", async () => { // given const baseDir = await createBaseDir() const aliveSessionIds = new Set(["ses_alive"]) const config = createConfig(baseDir) const manager = createManager() const context = createContext(baseDir, manager, aliveSessionIds) const aliveRuntime = await createTeamRun(createSpec("alive-team", "lead", [{ kind: "subagent_type", name: "lead", subagent_type: "sisyphus", backendType: "in-process", isActive: true }]), "ses_alive", context, config, manager) const deadRuntime = await createTeamRun(createSpec("dead-team", "lead", [{ kind: "subagent_type", name: "lead", subagent_type: "atlas", backendType: "in-process", isActive: true }]), "ses_dead", context, config, manager) const stuckRuntime = await createTeamRun(createSpec("stuck-team", "lead", [{ kind: "subagent_type", name: "lead", subagent_type: "atlas", backendType: "in-process", isActive: true }]), "ses_stuck", context, config, manager) const deletingRuntime = await createTeamRun(createSpec("deleting-team", "lead", [{ kind: "subagent_type", name: "lead", subagent_type: "atlas", backendType: "in-process", isActive: true }]), "ses_delete", context, config, manager) await saveRuntimeState({ ...(await loadRuntimeState(stuckRuntime.teamRunId, config)), status: "creating", createdAt: Date.now() - 40 * 60 * 1000 }, config) await saveRuntimeState({ ...(await loadRuntimeState(deletingRuntime.teamRunId, config)), status: "deleting" }, config) // when const report = await resumeAllTeams(context, config) // then expect(report).toEqual({ resumed: 1, marked_failed: 1, marked_orphaned: 1, cleaned: 1, errors: [] }) expect((await loadRuntimeState(aliveRuntime.teamRunId, config)).status).toBe("active") expect((await loadRuntimeState(deadRuntime.teamRunId, config)).status).toBe("orphaned") expect((await loadRuntimeState(stuckRuntime.teamRunId, config)).status).toBe("failed") expect(await exists(getRuntimeStateDir(resolveBaseDir(config), deletingRuntime.teamRunId))).toBe(false) }) test("C-10.5 end-to-end: createTeamRun persists category-aware routing and team_send_message reapplies it on promptAsync", async () => { // given - a 2-member team; resolveMemberMock returns agentToUse + model per member const baseDir = await createBaseDir() const config = createConfig(baseDir) const manager = createManager() type RecordedPrompt = { sessionId: string agent?: string model?: { providerID: string; modelID: string } variant?: string directory?: string } const recorded: RecordedPrompt[] = [] const promptAsyncSpy = mock(async (input: { path: { id: string } body: { parts: Array<{ type: string; text?: string }> agent?: string model?: { providerID: string; modelID: string } variant?: string } query?: { directory: string } }) => { recorded.push({ sessionId: input.path.id, agent: input.body.agent, model: input.body.model, variant: input.body.variant, directory: input.query?.directory, }) return undefined }) const recordingClient = { session: { get: mock(async ({ path: { id } }: { path: { id: string } }) => ({ data: { id } })), promptAsync: promptAsyncSpy, }, } as ExecutorContext["client"] & LiveDeliveryClient const ctx = { client: recordingClient, manager, directory: baseDir } const runtime = await createTeamRun(createSpec("msg-team", "lead", [ { kind: "subagent_type", name: "lead", subagent_type: "sisyphus", backendType: "in-process", isActive: true }, { kind: "category", name: "worker", category: "quick", prompt: "work the queue", backendType: "in-process", isActive: true }, ]), "ses_lead", ctx, config, manager) const leadMember = runtime.members.find((member) => member.name === "lead") const workerMember = runtime.members.find((member) => member.name === "worker") if (!leadMember?.sessionId || !workerMember?.sessionId) { throw new Error("expected both team members to hold sessionIds") } const { createTeamSendMessageTool } = await import("./tools/messaging") const tool = createTeamSendMessageTool(config, recordingClient) // when - the lead (via its spawned session) sends a live message to the worker const toolContext = { sessionID: leadMember.sessionId, messageID: randomUUID(), agent: "test-agent", directory: baseDir, worktree: baseDir, abort: new AbortController().signal, metadata: () => {}, ask: async () => undefined, } as Parameters["execute"]>[1] await tool.execute({ teamRunId: runtime.teamRunId, to: "worker", body: "integration-ping", }, toolContext) // then - runtime state carries the resolved identity end-to-end, and promptAsync receives it const persistedRuntime = await loadRuntimeState(runtime.teamRunId, config) const persistedWorker = persistedRuntime.members.find((member) => member.name === "worker") expect(persistedWorker?.subagent_type).toBe("worker-agent") expect(persistedWorker?.category).toBe("quick") expect(persistedWorker?.model).toEqual({ providerID: "openai", modelID: "gpt-5.4-mini", variant: "medium", reasoningEffort: "high", temperature: 0.1, top_p: 0.9, maxTokens: 2048, thinking: { type: "enabled", budgetTokens: 1024 }, }) expect(recorded).toHaveLength(1) expect(recorded[0]?.sessionId).toBe(workerMember.sessionId) expect(recorded[0]?.agent).toBe("worker-agent") expect(recorded[0]?.model).toEqual({ providerID: "openai", modelID: "gpt-5.4-mini" }) expect(recorded[0]?.variant).toBe("medium") expect(recorded[0]?.directory).toBe(baseDir) expect(SessionCategoryRegistry.get(workerMember.sessionId)).toBe("quick") expect(getSessionPromptParams(workerMember.sessionId)).toEqual({ temperature: 0.1, topP: 0.9, maxOutputTokens: 2048, options: { reasoningEffort: "high", thinking: { type: "enabled", budgetTokens: 1024 }, }, }) }) test("C-10.4 keeps member spawn concurrency within max_parallel_members", async () => { // given const baseDir = await createBaseDir() let inFlight = 0 let maxInFlight = 0 const manager = createManager(async () => { inFlight += 1 maxInFlight = Math.max(maxInFlight, inFlight) await new Promise((resolve) => setTimeout(resolve, 10)) inFlight -= 1 return { id: `task-${randomUUID()}`, sessionID: `ses_mock_${randomUUID()}`, status: "running" } as BackgroundTask }) // when await createTeamRun(createSpec("parallel-team", "lead", [ { kind: "subagent_type", name: "lead", subagent_type: "sisyphus", backendType: "in-process", isActive: true }, { kind: "subagent_type", name: "worker-a", subagent_type: "atlas", backendType: "in-process", isActive: true }, { kind: "subagent_type", name: "worker-b", subagent_type: "atlas", backendType: "in-process", isActive: true }, ]), "ses_lead", createContext(baseDir, manager, new Set(["ses_lead"])), createConfig(baseDir, { max_parallel_members: 2 }), manager) // then expect(maxInFlight).toBeLessThanOrEqual(2) }) })