import os from "node:os"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createSubagentSpawnTestConfig, expectPersistedRuntimeModel, installSessionStoreCaptureMock, loadSubagentSpawnModuleForTest, } from "./subagent-spawn.test-helpers.js"; import { installAcceptedSubagentGatewayMock } from "./test-helpers/subagent-gateway.js"; const hoisted = vi.hoisted(() => ({ callGatewayMock: vi.fn(), updateSessionStoreMock: vi.fn(), pruneLegacyStoreKeysMock: vi.fn(), registerSubagentRunMock: vi.fn(), emitSessionLifecycleEventMock: vi.fn(), resolveAgentConfigMock: vi.fn(), configOverride: {} as Record, })); let resetSubagentRegistryForTests: typeof import("./subagent-registry.js").resetSubagentRegistryForTests; let spawnSubagentDirect: typeof import("./subagent-spawn.js").spawnSubagentDirect; function createConfigOverride(overrides?: Record) { return createSubagentSpawnTestConfig(os.tmpdir(), { agents: { defaults: { workspace: os.tmpdir(), }, list: [ { id: "main", workspace: "/tmp/workspace-main", }, ], }, ...overrides, }); } describe("spawnSubagentDirect seam flow", () => { beforeAll(async () => { ({ resetSubagentRegistryForTests, spawnSubagentDirect } = await loadSubagentSpawnModuleForTest({ callGatewayMock: hoisted.callGatewayMock, getRuntimeConfig: () => hoisted.configOverride, updateSessionStoreMock: hoisted.updateSessionStoreMock, pruneLegacyStoreKeysMock: hoisted.pruneLegacyStoreKeysMock, registerSubagentRunMock: hoisted.registerSubagentRunMock, emitSessionLifecycleEventMock: hoisted.emitSessionLifecycleEventMock, resolveAgentConfig: hoisted.resolveAgentConfigMock, resolveSubagentSpawnModelSelection: () => "openai-codex/gpt-5.4", resolveSandboxRuntimeStatus: () => ({ sandboxed: false }), sessionStorePath: "/tmp/subagent-spawn-session-store.json", resetModules: false, })); }); beforeEach(() => { resetSubagentRegistryForTests(); hoisted.callGatewayMock.mockReset(); hoisted.updateSessionStoreMock.mockReset(); hoisted.pruneLegacyStoreKeysMock.mockReset(); hoisted.registerSubagentRunMock.mockReset(); hoisted.emitSessionLifecycleEventMock.mockReset(); hoisted.resolveAgentConfigMock.mockReset(); hoisted.resolveAgentConfigMock.mockImplementation( (cfg: { agents?: { list?: Array<{ id?: string }> } }, agentId: string) => cfg.agents?.list?.find((agent) => agent.id === agentId), ); hoisted.configOverride = createConfigOverride(); installAcceptedSubagentGatewayMock(hoisted.callGatewayMock); hoisted.updateSessionStoreMock.mockImplementation( async ( _storePath: string, mutator: (store: Record>) => unknown, ) => { const store: Record> = {}; await mutator(store); return store; }, ); }); it("rejects explicit same-agent targets when allowAgents excludes the requester", async () => { hoisted.configOverride = createConfigOverride({ agents: { defaults: { workspace: os.tmpdir(), }, list: [ { id: "task-manager", workspace: "/tmp/workspace-task-manager", subagents: { allowAgents: ["planner"], }, }, { id: "planner", workspace: "/tmp/workspace-planner", }, ], }, }); const result = await spawnSubagentDirect( { task: "spawn myself explicitly", agentId: "task-manager", }, { agentSessionKey: "agent:task-manager:main", }, ); expect(result).toMatchObject({ status: "forbidden", error: "agentId is not allowed for sessions_spawn (allowed: planner)", }); expect(hoisted.callGatewayMock).not.toHaveBeenCalledWith( expect.objectContaining({ method: "agent" }), ); }); it("allows omitted agentId to default to requester even when allowAgents excludes requester", async () => { hoisted.configOverride = createConfigOverride({ agents: { defaults: { workspace: os.tmpdir(), }, list: [ { id: "task-manager", workspace: "/tmp/workspace-task-manager", subagents: { allowAgents: ["planner"], }, }, { id: "planner", workspace: "/tmp/workspace-planner", }, ], }, }); const result = await spawnSubagentDirect( { task: "spawn default target", }, { agentSessionKey: "agent:task-manager:main", }, ); expect(result).toMatchObject({ status: "accepted", childSessionKey: expect.stringMatching(/^agent:task-manager:subagent:/), }); }); it("accepts a spawned run across session patching, runtime-model persistence, registry registration, and lifecycle emission", async () => { const operations: string[] = []; let persistedStore: Record> | undefined; hoisted.callGatewayMock.mockImplementation(async (request: { method?: string }) => { operations.push(`gateway:${request.method ?? "unknown"}`); if (request.method === "agent") { return { runId: "run-1" }; } if (request.method?.startsWith("sessions.")) { return { ok: true }; } return {}; }); installSessionStoreCaptureMock(hoisted.updateSessionStoreMock, { operations, onStore: (store) => { persistedStore = store; }, }); const result = await spawnSubagentDirect( { task: "inspect the spawn seam", model: "openai-codex/gpt-5.4", }, { agentSessionKey: "agent:main:main", agentChannel: "discord", agentAccountId: "acct-1", agentTo: "user-1", agentThreadId: 42, workspaceDir: "/tmp/requester-workspace", }, ); expect(result).toMatchObject({ status: "accepted", runId: "run-1", mode: "run", modelApplied: true, }); expect(result.childSessionKey).toMatch(/^agent:main:subagent:/); const childSessionKey = result.childSessionKey as string; expect(hoisted.pruneLegacyStoreKeysMock).toHaveBeenCalledTimes(3); expect(hoisted.updateSessionStoreMock).toHaveBeenCalledTimes(3); expect(hoisted.registerSubagentRunMock).toHaveBeenCalledWith( expect.objectContaining({ runId: "run-1", childSessionKey, requesterSessionKey: "agent:main:main", requesterDisplayKey: "agent:main:main", requesterOrigin: { channel: "discord", accountId: "acct-1", to: "user-1", threadId: 42, }, task: "inspect the spawn seam", cleanup: "keep", model: "openai-codex/gpt-5.4", workspaceDir: "/tmp/requester-workspace", expectsCompletionMessage: true, spawnMode: "run", }), ); expect(hoisted.emitSessionLifecycleEventMock).toHaveBeenCalledWith({ sessionKey: childSessionKey, reason: "create", parentSessionKey: "agent:main:main", label: undefined, }); expectPersistedRuntimeModel({ persistedStore, sessionKey: childSessionKey, provider: "openai-codex", model: "gpt-5.4", overrideSource: "user", }); expect(operations.indexOf("store:update")).toBeGreaterThan(-1); expect(operations.indexOf("gateway:agent")).toBeGreaterThan( operations.lastIndexOf("store:update"), ); expect(hoisted.callGatewayMock).toHaveBeenCalledWith( expect.objectContaining({ method: "agent", params: expect.objectContaining({ sessionKey: childSessionKey, cleanupBundleMcpOnRunEnd: true, }), }), ); }); it("omits requesterOrigin threadId when no requester thread is provided", async () => { hoisted.callGatewayMock.mockImplementation(async (request: { method?: string }) => { if (request.method === "agent") { return { runId: "run-1" }; } if (request.method?.startsWith("sessions.")) { return { ok: true }; } return {}; }); installSessionStoreCaptureMock(hoisted.updateSessionStoreMock); const result = await spawnSubagentDirect( { task: "inspect unthreaded spawn", model: "openai-codex/gpt-5.4", }, { agentSessionKey: "agent:main:main", agentChannel: "discord", agentAccountId: "acct-1", agentTo: "user-1", }, ); expect(result.status).toBe("accepted"); const registerInput = hoisted.registerSubagentRunMock.mock.calls[0]?.[0]; expect(registerInput?.requesterOrigin).toMatchObject({ channel: "discord", accountId: "acct-1", to: "user-1", }); expect(registerInput?.requesterOrigin).not.toHaveProperty("threadId"); }); it("pins admin-only methods to operator.admin and preserves least-privilege for others (#59428)", async () => { const capturedCalls: Array<{ method?: string; scopes?: string[] }> = []; hoisted.callGatewayMock.mockImplementation( async (request: { method?: string; scopes?: string[] }) => { capturedCalls.push({ method: request.method, scopes: request.scopes }); if (request.method === "agent") { return { runId: "run-1" }; } if (request.method?.startsWith("sessions.")) { return { ok: true }; } return {}; }, ); installSessionStoreCaptureMock(hoisted.updateSessionStoreMock); const result = await spawnSubagentDirect( { task: "verify per-method scope routing", model: "openai-codex/gpt-5.4", }, { agentSessionKey: "agent:main:main", agentChannel: "discord", agentAccountId: "acct-1", agentTo: "user-1", workspaceDir: "/tmp/requester-workspace", }, ); expect(result.status).toBe("accepted"); expect(capturedCalls.length).toBeGreaterThan(0); for (const call of capturedCalls) { if (call.method === "sessions.patch" || call.method === "sessions.delete") { // Admin-only methods must be pinned to operator.admin. expect(call.scopes).toEqual(["operator.admin"]); } else { // Non-admin methods (e.g. "agent") must NOT be forced to admin scope // so the gateway preserves least-privilege and senderIsOwner stays false. expect(call.scopes).toBeUndefined(); } } }); it("forwards normalized thinking to the agent run", async () => { const calls: Array<{ method?: string; params?: unknown }> = []; hoisted.callGatewayMock.mockImplementation( async (request: { method?: string; params?: unknown }) => { calls.push(request); if (request.method === "agent") { return { runId: "run-thinking", status: "accepted", acceptedAt: 1000 }; } if (request.method?.startsWith("sessions.")) { return { ok: true }; } return {}; }, ); installSessionStoreCaptureMock(hoisted.updateSessionStoreMock); const result = await spawnSubagentDirect( { task: "verify thinking forwarding", thinking: "high", }, { agentSessionKey: "agent:main:main", agentChannel: "discord", }, ); expect(result).toMatchObject({ status: "accepted", }); const agentCall = calls.find((call) => call.method === "agent"); expect(agentCall?.params).toMatchObject({ thinking: "high", }); }); it("does not duplicate long subagent task text in the initial user message (#72019)", async () => { const calls: Array<{ method?: string; params?: unknown }> = []; hoisted.callGatewayMock.mockImplementation( async (request: { method?: string; params?: unknown }) => { calls.push(request); if (request.method === "agent") { return { runId: "run-no-dup", status: "accepted", acceptedAt: 1000 }; } if (request.method?.startsWith("sessions.")) { return { ok: true }; } return {}; }, ); installSessionStoreCaptureMock(hoisted.updateSessionStoreMock); const task = "UNIQUE_LONG_SUBAGENT_TASK_TOKEN\n keep indentation"; const result = await spawnSubagentDirect( { task, }, { agentSessionKey: "agent:main:main", agentChannel: "discord", }, ); expect(result.status).toBe("accepted"); const agentCall = calls.find((call) => call.method === "agent"); const params = agentCall?.params as { message?: string; extraSystemPrompt?: string }; expect(params.message).not.toContain("UNIQUE_LONG_SUBAGENT_TASK_TOKEN"); expect(params.message).not.toContain("[Subagent Task]:"); expect(params.message).toContain("**Your Role**"); expect(params.extraSystemPrompt).toBe("system-prompt"); }); it("returns an error when the initial child session patch is rejected", async () => { hoisted.callGatewayMock.mockImplementation( async (request: { method?: string; params?: unknown }) => { if (request.method === "agent") { return { runId: "run-1", status: "accepted", acceptedAt: 1000 }; } if (request.method === "sessions.delete") { return { ok: true }; } return {}; }, ); hoisted.updateSessionStoreMock.mockRejectedValueOnce(new Error("invalid model: bad-model")); const result = await spawnSubagentDirect( { task: "verify patch rejection", model: "bad-model", }, { agentSessionKey: "agent:main:main", agentChannel: "discord", }, ); expect(result).toMatchObject({ status: "error", childSessionKey: expect.stringMatching(/^agent:main:subagent:/), }); expect(result.error ?? "").toContain("invalid model"); expect( hoisted.callGatewayMock.mock.calls.some( (call) => (call[0] as { method?: string }).method === "agent", ), ).toBe(false); }); });