import os from "node:os"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createSubagentSpawnTestConfig, 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(), registerSubagentRunMock: vi.fn(), emitSessionLifecycleEventMock: vi.fn(), hookRunner: { hasHooks: vi.fn(), runSubagentSpawning: vi.fn(), }, })); describe("spawnSubagentDirect thread binding delivery", () => { type SpawnModule = Awaited>; type SessionBindingService = NonNullable< Parameters[0]["getSessionBindingService"] >; type DeliveryTargetResolver = NonNullable< Parameters[0]["resolveConversationDeliveryTarget"] >; let spawnSubagentDirect: SpawnModule["spawnSubagentDirect"]; let currentConfig: Record; let currentSessionBindingService: ReturnType; let currentDeliveryTargetResolver: DeliveryTargetResolver; beforeAll(async () => { ({ spawnSubagentDirect } = await loadSubagentSpawnModuleForTest({ callGatewayMock: hoisted.callGatewayMock, getRuntimeConfig: () => currentConfig, updateSessionStoreMock: hoisted.updateSessionStoreMock, registerSubagentRunMock: hoisted.registerSubagentRunMock, emitSessionLifecycleEventMock: hoisted.emitSessionLifecycleEventMock, hookRunner: hoisted.hookRunner, resolveSubagentSpawnModelSelection: () => "openai-codex/gpt-5.4", resolveSandboxRuntimeStatus: () => ({ sandboxed: false }), getSessionBindingService: () => currentSessionBindingService, resolveConversationDeliveryTarget: (params) => currentDeliveryTargetResolver(params), })); }); beforeEach(() => { currentConfig = createSubagentSpawnTestConfig(os.tmpdir(), { agents: { defaults: { workspace: os.tmpdir(), }, list: [{ id: "main", workspace: "/tmp/workspace-main" }], }, }); currentSessionBindingService = { listBySession: () => [] }; currentDeliveryTargetResolver = (params) => ({ to: params.conversationId ? `channel:${String(params.conversationId)}` : undefined, }); hoisted.callGatewayMock.mockReset(); hoisted.updateSessionStoreMock.mockReset(); hoisted.registerSubagentRunMock.mockReset(); hoisted.emitSessionLifecycleEventMock.mockReset(); hoisted.hookRunner.hasHooks.mockReset(); hoisted.hookRunner.runSubagentSpawning.mockReset(); installAcceptedSubagentGatewayMock(hoisted.callGatewayMock); installSessionStoreCaptureMock(hoisted.updateSessionStoreMock); }); it("passes the target agent's bound account to thread binding hooks", async () => { const boundRoom = "!room:example.org"; let hookRequester: | { channel?: string; accountId?: string; to?: string; threadId?: string | number } | undefined; hoisted.hookRunner.hasHooks.mockImplementation( (hookName?: string) => hookName === "subagent_spawning", ); hoisted.hookRunner.runSubagentSpawning.mockImplementation(async (event: unknown) => { hookRequester = ( event as { requester?: { channel?: string; accountId?: string; to?: string; threadId?: string | number; }; } ).requester; return { status: "ok", threadBindingReady: true, deliveryOrigin: { channel: "matrix", to: `room:${boundRoom}`, threadId: "$thread-root", }, }; }); currentConfig = createSubagentSpawnTestConfig(os.tmpdir(), { agents: { defaults: { workspace: os.tmpdir(), subagents: { allowAgents: ["bot-alpha"], }, }, list: [ { id: "main", workspace: "/tmp/workspace-main" }, { id: "bot-alpha", workspace: "/tmp/workspace-bot-alpha" }, ], }, bindings: [ { type: "route", agentId: "bot-alpha", match: { channel: "matrix", peer: { kind: "channel", id: boundRoom, }, accountId: "bot-alpha", }, }, ], }); const result = await spawnSubagentDirect( { task: "reply with a marker", agentId: "bot-alpha", thread: true, mode: "session", }, { agentSessionKey: "agent:main:main", agentChannel: "matrix", agentAccountId: "bot-beta", agentTo: `room:${boundRoom}`, }, ); expect(result.status).toBe("accepted"); expect(hookRequester).toMatchObject({ channel: "matrix", accountId: "bot-alpha", to: `room:${boundRoom}`, }); const agentCall = hoisted.callGatewayMock.mock.calls.find( ([call]) => (call as { method?: string }).method === "agent", )?.[0] as { params?: Record } | undefined; expect(agentCall?.params).toMatchObject({ channel: "matrix", accountId: "bot-alpha", to: `room:${boundRoom}`, threadId: "$thread-root", deliver: true, }); expect(hoisted.registerSubagentRunMock).toHaveBeenCalledWith( expect.objectContaining({ requesterOrigin: { channel: "matrix", accountId: "bot-beta", to: `room:${boundRoom}`, }, expectsCompletionMessage: false, spawnMode: "session", }), ); }); it("keeps completion announcements when only a generic binding is available", async () => { hoisted.hookRunner.hasHooks.mockImplementation( (hookName?: string) => hookName === "subagent_spawning", ); hoisted.hookRunner.runSubagentSpawning.mockResolvedValue({ status: "ok", threadBindingReady: true, }); currentSessionBindingService = { listBySession: () => [ { status: "active", conversation: { channel: "collabchat", accountId: "work", conversationId: "collab_dm_1", }, }, ], }; currentDeliveryTargetResolver = () => ({ to: "channel:collab_dm_1", }); const result = await spawnSubagentDirect( { task: "reply with a marker", thread: true, mode: "session", }, { agentSessionKey: "agent:main:main", agentChannel: "matrix", agentAccountId: "sut", agentTo: "room:!parent:example", }, ); expect(result.status).toBe("accepted"); const agentCall = hoisted.callGatewayMock.mock.calls.find( ([call]) => (call as { method?: string }).method === "agent", )?.[0] as { params?: Record } | undefined; expect(agentCall?.params).toMatchObject({ channel: "matrix", accountId: "sut", to: "room:!parent:example", deliver: false, }); expect(hoisted.registerSubagentRunMock).toHaveBeenCalledWith( expect.objectContaining({ expectsCompletionMessage: true, requesterOrigin: { channel: "matrix", accountId: "sut", to: "room:!parent:example", }, }), ); }); });