import { beforeEach, describe, expect, it, vi } from "vitest"; import { spawnSubagentDirect } from "./subagent-spawn.js"; import { installAcceptedSubagentGatewayMock } from "./test-helpers/subagent-gateway.js"; type TestAgentConfig = { id?: string; workspace?: string; subagents?: { allowAgents?: string[]; }; }; type TestConfig = { agents?: { list?: TestAgentConfig[]; }; }; const hoisted = vi.hoisted(() => ({ callGatewayMock: vi.fn(), configOverride: {} as Record, registerSubagentRunMock: vi.fn(), })); vi.mock("../gateway/call.js", () => ({ callGateway: (opts: unknown) => hoisted.callGatewayMock(opts), })); vi.mock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, loadConfig: () => hoisted.configOverride, }; }); vi.mock("@mariozechner/pi-ai/oauth", () => ({ getOAuthApiKey: () => "", getOAuthProviders: () => [], })); vi.mock("./subagent-registry.js", () => ({ countActiveRunsForSession: () => 0, registerSubagentRun: (args: unknown) => hoisted.registerSubagentRunMock(args), })); vi.mock("./subagent-announce.js", () => ({ buildSubagentSystemPrompt: () => "system-prompt", })); vi.mock("./subagent-depth.js", () => ({ getSubagentDepthFromSessionStore: () => 0, })); vi.mock("./model-selection.js", () => ({ resolveSubagentSpawnModelSelection: () => undefined, })); vi.mock("./sandbox/runtime-status.js", () => ({ resolveSandboxRuntimeStatus: () => ({ sandboxed: false }), })); vi.mock("../plugins/hook-runner-global.js", () => ({ getGlobalHookRunner: () => ({ hasHooks: () => false }), })); vi.mock("../utils/delivery-context.js", () => ({ normalizeDeliveryContext: (value: unknown) => value, })); vi.mock("./tools/sessions-helpers.js", () => ({ resolveMainSessionAlias: () => ({ mainKey: "main", alias: "main" }), resolveInternalSessionKey: ({ key }: { key?: string }) => key ?? "agent:main:main", resolveDisplaySessionKey: ({ key }: { key?: string }) => key ?? "agent:main:main", })); vi.mock("./agent-scope.js", () => ({ resolveAgentConfig: (cfg: TestConfig, agentId: string) => cfg.agents?.list?.find((entry) => entry.id === agentId), resolveAgentWorkspaceDir: (cfg: TestConfig, agentId: string) => cfg.agents?.list?.find((entry) => entry.id === agentId)?.workspace ?? `/tmp/workspace-${agentId}`, })); function createConfigOverride(overrides?: Record) { return { session: { mainKey: "main", scope: "per-sender", }, agents: { list: [ { id: "main", workspace: "/tmp/workspace-main", }, ], }, ...overrides, }; } function setupGatewayMock() { installAcceptedSubagentGatewayMock(hoisted.callGatewayMock); } function getRegisteredRun() { return hoisted.registerSubagentRunMock.mock.calls.at(0)?.[0] as | Record | undefined; } async function expectAcceptedWorkspace(params: { agentId: string; expectedWorkspaceDir: string }) { const result = await spawnSubagentDirect( { task: "inspect workspace", agentId: params.agentId, }, { agentSessionKey: "agent:main:main", agentChannel: "telegram", agentAccountId: "123", agentTo: "456", workspaceDir: "/tmp/requester-workspace", }, ); expect(result.status).toBe("accepted"); expect(getRegisteredRun()).toMatchObject({ workspaceDir: params.expectedWorkspaceDir, }); } describe("spawnSubagentDirect workspace inheritance", () => { beforeEach(() => { hoisted.callGatewayMock.mockClear(); hoisted.registerSubagentRunMock.mockClear(); hoisted.configOverride = createConfigOverride(); setupGatewayMock(); }); it("uses the target agent workspace for cross-agent spawns", async () => { hoisted.configOverride = createConfigOverride({ agents: { list: [ { id: "main", workspace: "/tmp/workspace-main", subagents: { allowAgents: ["ops"], }, }, { id: "ops", workspace: "/tmp/workspace-ops", }, ], }, }); await expectAcceptedWorkspace({ agentId: "ops", expectedWorkspaceDir: "/tmp/workspace-ops", }); }); it("preserves the inherited workspace for same-agent spawns", async () => { await expectAcceptedWorkspace({ agentId: "main", expectedWorkspaceDir: "/tmp/requester-workspace", }); }); });