import { beforeEach, describe, expect, it, vi } from "vitest"; const callGatewayMock = vi.fn(); vi.mock("../gateway/call.js", () => ({ callGateway: (opts: unknown) => callGatewayMock(opts), })); let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = { session: { mainKey: "main", scope: "per-sender", }, }; vi.mock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, loadConfig: () => configOverride, resolveGatewayPort: () => 18789, }; }); import { createClawdbotTools } from "./clawdbot-tools.js"; import { resetSubagentRegistryForTests } from "./subagent-registry.js"; describe("clawdbot-tools: subagents", () => { beforeEach(() => { configOverride = { session: { mainKey: "main", scope: "per-sender", }, }; }); it("sessions_spawn applies a model to the child session", async () => { resetSubagentRegistryForTests(); callGatewayMock.mockReset(); const calls: Array<{ method?: string; params?: unknown }> = []; let agentCallCount = 0; callGatewayMock.mockImplementation(async (opts: unknown) => { const request = opts as { method?: string; params?: unknown }; calls.push(request); if (request.method === "sessions.patch") { return { ok: true }; } if (request.method === "agent") { agentCallCount += 1; const runId = `run-${agentCallCount}`; return { runId, status: "accepted", acceptedAt: 3000 + agentCallCount, }; } if (request.method === "agent.wait") { return { status: "timeout" }; } if (request.method === "sessions.delete") { return { ok: true }; } return {}; }); const tool = createClawdbotTools({ agentSessionKey: "discord:group:req", agentSurface: "discord", }).find((candidate) => candidate.name === "sessions_spawn"); if (!tool) throw new Error("missing sessions_spawn tool"); const result = await tool.execute("call3", { task: "do thing", runTimeoutSeconds: 1, model: "claude-haiku-4-5", cleanup: "keep", }); expect(result.details).toMatchObject({ status: "accepted", modelApplied: true, }); const patchIndex = calls.findIndex((call) => call.method === "sessions.patch"); const agentIndex = calls.findIndex((call) => call.method === "agent"); expect(patchIndex).toBeGreaterThan(-1); expect(agentIndex).toBeGreaterThan(-1); expect(patchIndex).toBeLessThan(agentIndex); const patchCall = calls[patchIndex]; expect(patchCall?.params).toMatchObject({ key: expect.stringContaining("subagent:"), model: "claude-haiku-4-5", }); }); it("sessions_spawn applies default subagent model from defaults config", async () => { resetSubagentRegistryForTests(); callGatewayMock.mockReset(); configOverride = { session: { mainKey: "main", scope: "per-sender" }, agents: { defaults: { subagents: { model: "minimax/MiniMax-M2.1" } } }, }; const calls: Array<{ method?: string; params?: unknown }> = []; callGatewayMock.mockImplementation(async (opts: unknown) => { const request = opts as { method?: string; params?: unknown }; calls.push(request); if (request.method === "sessions.patch") { return { ok: true }; } if (request.method === "agent") { return { runId: "run-default-model", status: "accepted" }; } return {}; }); const tool = createClawdbotTools({ agentSessionKey: "agent:main:main", agentChannel: "discord", }).find((candidate) => candidate.name === "sessions_spawn"); if (!tool) throw new Error("missing sessions_spawn tool"); const result = await tool.execute("call-default-model", { task: "do thing", }); expect(result.details).toMatchObject({ status: "accepted", modelApplied: true, }); const patchCall = calls.find((call) => call.method === "sessions.patch"); expect(patchCall?.params).toMatchObject({ model: "minimax/MiniMax-M2.1", }); }); });