import { beforeEach, describe, expect, it, vi } from "vitest"; const hoisted = vi.hoisted(() => { const spawnSubagentDirectMock = vi.fn(); const spawnAcpDirectMock = vi.fn(); return { spawnSubagentDirectMock, spawnAcpDirectMock, }; }); vi.mock("../subagent-spawn.js", () => ({ SUBAGENT_SPAWN_MODES: ["run", "session"], spawnSubagentDirect: (...args: unknown[]) => hoisted.spawnSubagentDirectMock(...args), })); vi.mock("../acp-spawn.js", () => ({ ACP_SPAWN_MODES: ["run", "session"], ACP_SPAWN_STREAM_TARGETS: ["parent"], spawnAcpDirect: (...args: unknown[]) => hoisted.spawnAcpDirectMock(...args), })); const { createSessionsSpawnTool } = await import("./sessions-spawn-tool.js"); describe("sessions_spawn tool", () => { beforeEach(() => { hoisted.spawnSubagentDirectMock.mockReset().mockResolvedValue({ status: "accepted", childSessionKey: "agent:main:subagent:1", runId: "run-subagent", }); hoisted.spawnAcpDirectMock.mockReset().mockResolvedValue({ status: "accepted", childSessionKey: "agent:codex:acp:1", runId: "run-acp", }); }); it("uses subagent runtime by default", async () => { const tool = createSessionsSpawnTool({ agentSessionKey: "agent:main:main", agentChannel: "discord", agentAccountId: "default", agentTo: "channel:123", agentThreadId: "456", }); const result = await tool.execute("call-1", { task: "build feature", agentId: "main", model: "anthropic/claude-sonnet-4-6", thinking: "medium", runTimeoutSeconds: 5, thread: true, mode: "session", cleanup: "keep", }); expect(result.details).toMatchObject({ status: "accepted", childSessionKey: "agent:main:subagent:1", runId: "run-subagent", }); expect(hoisted.spawnSubagentDirectMock).toHaveBeenCalledWith( expect.objectContaining({ task: "build feature", agentId: "main", model: "anthropic/claude-sonnet-4-6", thinking: "medium", runTimeoutSeconds: 5, thread: true, mode: "session", cleanup: "keep", }), expect.objectContaining({ agentSessionKey: "agent:main:main", }), ); expect(hoisted.spawnAcpDirectMock).not.toHaveBeenCalled(); }); it("routes to ACP runtime when runtime=acp", async () => { const tool = createSessionsSpawnTool({ agentSessionKey: "agent:main:main", agentChannel: "discord", agentAccountId: "default", agentTo: "channel:123", agentThreadId: "456", }); const result = await tool.execute("call-2", { runtime: "acp", task: "investigate the failing CI run", agentId: "codex", cwd: "/workspace", thread: true, mode: "session", streamTo: "parent", }); expect(result.details).toMatchObject({ status: "accepted", childSessionKey: "agent:codex:acp:1", runId: "run-acp", }); expect(hoisted.spawnAcpDirectMock).toHaveBeenCalledWith( expect.objectContaining({ task: "investigate the failing CI run", agentId: "codex", cwd: "/workspace", thread: true, mode: "session", streamTo: "parent", }), expect.objectContaining({ agentSessionKey: "agent:main:main", }), ); expect(hoisted.spawnSubagentDirectMock).not.toHaveBeenCalled(); }); it("forwards ACP sandbox options and requester sandbox context", async () => { const tool = createSessionsSpawnTool({ agentSessionKey: "agent:main:subagent:parent", sandboxed: true, }); await tool.execute("call-2b", { runtime: "acp", task: "investigate", agentId: "codex", sandbox: "require", }); expect(hoisted.spawnAcpDirectMock).toHaveBeenCalledWith( expect.objectContaining({ task: "investigate", sandbox: "require", }), expect.objectContaining({ agentSessionKey: "agent:main:subagent:parent", sandboxed: true, }), ); }); it("rejects attachments for ACP runtime", async () => { const tool = createSessionsSpawnTool({ agentSessionKey: "agent:main:main", agentChannel: "discord", agentAccountId: "default", agentTo: "channel:123", agentThreadId: "456", }); const result = await tool.execute("call-3", { runtime: "acp", task: "analyze file", attachments: [{ name: "a.txt", content: "hello", encoding: "utf8" }], }); expect(result.details).toMatchObject({ status: "error", }); const details = result.details as { error?: string }; expect(details.error).toContain("attachments are currently unsupported for runtime=acp"); expect(hoisted.spawnAcpDirectMock).not.toHaveBeenCalled(); expect(hoisted.spawnSubagentDirectMock).not.toHaveBeenCalled(); }); it('rejects streamTo when runtime is not "acp"', async () => { const tool = createSessionsSpawnTool({ agentSessionKey: "agent:main:main", }); const result = await tool.execute("call-3b", { runtime: "subagent", task: "analyze file", streamTo: "parent", }); expect(result.details).toMatchObject({ status: "error", }); const details = result.details as { error?: string }; expect(details.error).toContain("streamTo is only supported for runtime=acp"); expect(hoisted.spawnAcpDirectMock).not.toHaveBeenCalled(); expect(hoisted.spawnSubagentDirectMock).not.toHaveBeenCalled(); }); it("keeps attachment content schema unconstrained for llama.cpp grammar safety", () => { const tool = createSessionsSpawnTool(); const schema = tool.parameters as { properties?: { attachments?: { items?: { properties?: { content?: { type?: string; maxLength?: number; }; }; }; }; }; }; const contentSchema = schema.properties?.attachments?.items?.properties?.content; expect(contentSchema?.type).toBe("string"); expect(contentSchema?.maxLength).toBeUndefined(); }); });