import os from "node:os"; import { expect, vi } from "vitest"; import type { SubagentLifecycleHookRunner } from "../plugins/hooks.js"; type MockFn = (...args: unknown[]) => unknown; type MockImplementationTarget = { mockImplementation: (implementation: (opts: { method?: string }) => Promise) => unknown; }; type SessionStore = Record>; type SessionStoreMutator = (store: SessionStore) => unknown; type HookRunner = Pick; export function createSubagentSpawnTestConfig( workspaceDir = os.tmpdir(), overrides?: Record, ) { return { session: { mainKey: "main", scope: "per-sender", }, tools: { sessions_spawn: { attachments: { enabled: true, maxFiles: 50, maxFileBytes: 1 * 1024 * 1024, maxTotalBytes: 5 * 1024 * 1024, }, }, }, agents: { defaults: { workspace: workspaceDir, }, }, ...overrides, }; } export function setupAcceptedSubagentGatewayMock(callGatewayMock: MockImplementationTarget) { callGatewayMock.mockImplementation(async (opts: { method?: string }) => { if (opts.method === "sessions.patch") { return { ok: true }; } if (opts.method === "sessions.delete") { return { ok: true }; } if (opts.method === "agent") { return { runId: "run-1", status: "accepted", acceptedAt: 1000 }; } return {}; }); } export function identityDeliveryContext(value: unknown) { return value; } export function createDefaultSessionHelperMocks() { return { resolveMainSessionAlias: () => ({ mainKey: "main", alias: "main" }), resolveInternalSessionKey: ({ key }: { key?: string }) => key ?? "agent:main:main", resolveDisplaySessionKey: ({ key }: { key?: string }) => key ?? "agent:main:main", }; } export function installSessionStoreCaptureMock( updateSessionStoreMock: { mockImplementation: ( implementation: (storePath: string, mutator: SessionStoreMutator) => Promise, ) => unknown; }, params?: { operations?: string[]; onStore?: (store: SessionStore) => void; }, ) { updateSessionStoreMock.mockImplementation( async (_storePath: string, mutator: SessionStoreMutator) => { params?.operations?.push("store:update"); const store: SessionStore = {}; await mutator(store); params?.onStore?.(store); return store; }, ); } export function expectPersistedRuntimeModel(params: { persistedStore: SessionStore | undefined; sessionKey: string | RegExp; provider: string; model: string; }) { const [persistedKey, persistedEntry] = Object.entries(params.persistedStore ?? {})[0] ?? []; if (typeof params.sessionKey === "string") { expect(persistedKey).toBe(params.sessionKey); } else { expect(persistedKey).toMatch(params.sessionKey); } expect(persistedEntry).toMatchObject({ modelProvider: params.provider, model: params.model, }); } export async function loadSubagentSpawnModuleForTest(params: { callGatewayMock: MockFn; loadConfig?: () => Record; updateSessionStoreMock?: MockFn; pruneLegacyStoreKeysMock?: MockFn; registerSubagentRunMock?: MockFn; emitSessionLifecycleEventMock?: MockFn; hookRunner?: HookRunner; resolveAgentConfig?: (cfg: Record, agentId: string) => unknown; resolveAgentWorkspaceDir?: (cfg: Record, agentId: string) => string; resolveSubagentSpawnModelSelection?: () => string | undefined; resolveSandboxRuntimeStatus?: () => { sandboxed: boolean }; workspaceDir?: string; sessionStorePath?: string; }) { vi.resetModules(); vi.doMock("../gateway/call.js", () => ({ callGateway: (opts: unknown) => params.callGatewayMock(opts), })); vi.doMock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, loadConfig: () => params.loadConfig?.() ?? createSubagentSpawnTestConfig(params.workspaceDir ?? os.tmpdir()), }; }); if (params.updateSessionStoreMock) { vi.doMock("../config/sessions.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, updateSessionStore: (...args: unknown[]) => params.updateSessionStoreMock?.(...args), }; }); } if (params.pruneLegacyStoreKeysMock) { vi.doMock("../gateway/session-utils.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, resolveGatewaySessionStoreTarget: (targetParams: { key: string }) => ({ agentId: "main", storePath: params.sessionStorePath ?? "/tmp/subagent-spawn-model-session.json", canonicalKey: targetParams.key, storeKeys: [targetParams.key], }), pruneLegacyStoreKeys: (...args: unknown[]) => params.pruneLegacyStoreKeysMock?.(...args), }; }); } if (params.emitSessionLifecycleEventMock) { vi.doMock("../sessions/session-lifecycle-events.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, emitSessionLifecycleEvent: (...args: unknown[]) => params.emitSessionLifecycleEventMock?.(...args), }; }); } vi.doMock("./subagent-announce.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, buildSubagentSystemPrompt: () => "system-prompt", }; }); vi.doMock("./agent-scope.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, resolveAgentConfig: params.resolveAgentConfig ?? actual.resolveAgentConfig, resolveAgentWorkspaceDir: params.resolveAgentWorkspaceDir ?? (() => params.workspaceDir ?? os.tmpdir()), }; }); vi.doMock("./subagent-depth.js", () => ({ getSubagentDepthFromSessionStore: () => 0, })); vi.doMock("./model-selection.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, resolveSubagentSpawnModelSelection: params.resolveSubagentSpawnModelSelection ?? actual.resolveSubagentSpawnModelSelection, }; }); vi.doMock("./sandbox/runtime-status.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, resolveSandboxRuntimeStatus: params.resolveSandboxRuntimeStatus ?? actual.resolveSandboxRuntimeStatus, }; }); vi.doMock("../plugins/hook-runner-global.js", () => ({ getGlobalHookRunner: () => params.hookRunner ?? { hasHooks: () => false }, })); vi.doMock("../utils/delivery-context.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, normalizeDeliveryContext: identityDeliveryContext, }; }); vi.doMock("./tools/sessions-helpers.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, ...createDefaultSessionHelperMocks(), }; }); const subagentRegistry = await import("./subagent-registry.js"); if (params.registerSubagentRunMock) { vi.spyOn(subagentRegistry, "registerSubagentRun").mockImplementation( (...args: Parameters) => params.registerSubagentRunMock?.(...args) as ReturnType< typeof subagentRegistry.registerSubagentRun >, ); } return { ...(await import("./subagent-spawn.js")), resetSubagentRegistryForTests: subagentRegistry.resetSubagentRegistryForTests, }; }