diff --git a/src/agents/auth-profiles.store-cache.test.ts b/src/agents/auth-profiles.store-cache.test.ts index 7608e03c971..cfc36162832 100644 --- a/src/agents/auth-profiles.store-cache.test.ts +++ b/src/agents/auth-profiles.store-cache.test.ts @@ -22,6 +22,52 @@ async function loadFreshAuthProfilesModuleForTest() { await import("./auth-profiles.js")); } +function withAgentDirEnv(prefix: string, run: (agentDir: string) => void | Promise) { + const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + const previousAgentDir = process.env.OPENCLAW_AGENT_DIR; + const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR; + try { + process.env.OPENCLAW_AGENT_DIR = agentDir; + process.env.PI_CODING_AGENT_DIR = agentDir; + return run(agentDir); + } finally { + if (previousAgentDir === undefined) { + delete process.env.OPENCLAW_AGENT_DIR; + } else { + process.env.OPENCLAW_AGENT_DIR = previousAgentDir; + } + if (previousPiAgentDir === undefined) { + delete process.env.PI_CODING_AGENT_DIR; + } else { + process.env.PI_CODING_AGENT_DIR = previousPiAgentDir; + } + fs.rmSync(agentDir, { recursive: true, force: true }); + } +} + +function writeAuthStore(agentDir: string, key: string) { + const authPath = path.join(agentDir, "auth-profiles.json"); + fs.writeFileSync( + authPath, + `${JSON.stringify( + { + version: AUTH_STORE_VERSION, + profiles: { + "openai:default": { + type: "api_key", + provider: "openai", + key, + }, + }, + }, + null, + 2, + )}\n`, + "utf8", + ); + return authPath; +} + describe("auth profile store cache", () => { beforeEach(async () => { await loadFreshAuthProfilesModuleForTest(); @@ -33,98 +79,24 @@ describe("auth profile store cache", () => { vi.clearAllMocks(); }); - it("reuses the synced auth store while auth-profiles.json is unchanged", () => { - const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-store-cache-")); - const previousAgentDir = process.env.OPENCLAW_AGENT_DIR; - const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR; - try { - process.env.OPENCLAW_AGENT_DIR = agentDir; - process.env.PI_CODING_AGENT_DIR = agentDir; - fs.writeFileSync( - path.join(agentDir, "auth-profiles.json"), - `${JSON.stringify( - { - version: AUTH_STORE_VERSION, - profiles: { - "openai:default": { - type: "api_key", - provider: "openai", - key: "sk-test", - }, - }, - }, - null, - 2, - )}\n`, - "utf8", - ); + it("reuses the synced auth store while auth-profiles.json is unchanged", async () => { + await withAgentDirEnv("openclaw-auth-store-cache-", (agentDir) => { + writeAuthStore(agentDir, "sk-test"); ensureAuthProfileStore(agentDir); ensureAuthProfileStore(agentDir); expect(mocks.syncExternalCliCredentials).toHaveBeenCalledTimes(1); - } finally { - if (previousAgentDir === undefined) { - delete process.env.OPENCLAW_AGENT_DIR; - } else { - process.env.OPENCLAW_AGENT_DIR = previousAgentDir; - } - if (previousPiAgentDir === undefined) { - delete process.env.PI_CODING_AGENT_DIR; - } else { - process.env.PI_CODING_AGENT_DIR = previousPiAgentDir; - } - fs.rmSync(agentDir, { recursive: true, force: true }); - } + }); }); it("refreshes the cached auth store after auth-profiles.json changes", async () => { - const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-store-refresh-")); - const previousAgentDir = process.env.OPENCLAW_AGENT_DIR; - const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR; - try { - process.env.OPENCLAW_AGENT_DIR = agentDir; - process.env.PI_CODING_AGENT_DIR = agentDir; - const authPath = path.join(agentDir, "auth-profiles.json"); - fs.writeFileSync( - authPath, - `${JSON.stringify( - { - version: AUTH_STORE_VERSION, - profiles: { - "openai:default": { - type: "api_key", - provider: "openai", - key: "sk-test-1", - }, - }, - }, - null, - 2, - )}\n`, - "utf8", - ); + await withAgentDirEnv("openclaw-auth-store-refresh-", async (agentDir) => { + const authPath = writeAuthStore(agentDir, "sk-test-1"); ensureAuthProfileStore(agentDir); - fs.writeFileSync( - authPath, - `${JSON.stringify( - { - version: AUTH_STORE_VERSION, - profiles: { - "openai:default": { - type: "api_key", - provider: "openai", - key: "sk-test-2", - }, - }, - }, - null, - 2, - )}\n`, - "utf8", - ); + writeAuthStore(agentDir, "sk-test-2"); const bumpedMtime = new Date(Date.now() + 2_000); fs.utimesSync(authPath, bumpedMtime, bumpedMtime); @@ -134,19 +106,7 @@ describe("auth profile store cache", () => { expect(reloaded.profiles["openai:default"]).toMatchObject({ key: "sk-test-2", }); - } finally { - if (previousAgentDir === undefined) { - delete process.env.OPENCLAW_AGENT_DIR; - } else { - process.env.OPENCLAW_AGENT_DIR = previousAgentDir; - } - if (previousPiAgentDir === undefined) { - delete process.env.PI_CODING_AGENT_DIR; - } else { - process.env.PI_CODING_AGENT_DIR = previousPiAgentDir; - } - fs.rmSync(agentDir, { recursive: true, force: true }); - } + }); }); it("re-syncs external CLI credentials after the cache ttl when auth-profiles.json is absent", () => { diff --git a/src/agents/subagent-spawn.model-session.test.ts b/src/agents/subagent-spawn.model-session.test.ts index fb750eb3dc4..2518bcedc8d 100644 --- a/src/agents/subagent-spawn.model-session.test.ts +++ b/src/agents/subagent-spawn.model-session.test.ts @@ -2,6 +2,8 @@ import os from "node:os"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createSubagentSpawnTestConfig, + expectPersistedRuntimeModel, + installSessionStoreCaptureMock, loadSubagentSpawnModuleForTest, setupAcceptedSubagentGatewayMock, } from "./subagent-spawn.test-helpers.js"; @@ -56,18 +58,12 @@ describe("spawnSubagentDirect runtime model persistence", () => { return {}; }); let persistedStore: Record> | undefined; - updateSessionStoreMock.mockImplementation( - async ( - _storePath: string, - mutator: (store: Record>) => unknown, - ) => { - operations.push("store:update"); - const store: Record> = {}; - await mutator(store); + installSessionStoreCaptureMock(updateSessionStoreMock, { + operations, + onStore: (store) => { persistedStore = store; - return store; }, - ); + }); const result = await spawnSubagentDirect( { @@ -85,10 +81,10 @@ describe("spawnSubagentDirect runtime model persistence", () => { modelApplied: true, }); expect(updateSessionStoreMock).toHaveBeenCalledTimes(1); - const [persistedKey, persistedEntry] = Object.entries(persistedStore ?? {})[0] ?? []; - expect(persistedKey).toMatch(/^agent:main:subagent:/); - expect(persistedEntry).toMatchObject({ - modelProvider: "openai-codex", + expectPersistedRuntimeModel({ + persistedStore, + sessionKey: /^agent:main:subagent:/, + provider: "openai-codex", model: "gpt-5.4", }); expect(pruneLegacyStoreKeysMock).toHaveBeenCalledTimes(1); diff --git a/src/agents/subagent-spawn.test-helpers.ts b/src/agents/subagent-spawn.test-helpers.ts index c6c039a94ad..59772f2b380 100644 --- a/src/agents/subagent-spawn.test-helpers.ts +++ b/src/agents/subagent-spawn.test-helpers.ts @@ -1,12 +1,21 @@ import os from "node:os"; -import { vi } from "vitest"; +import { expect, vi } from "vitest"; type MockFn = (...args: unknown[]) => unknown; type MockImplementationTarget = { mockImplementation: (implementation: (opts: { method?: string }) => Promise) => unknown; }; +type SessionStore = Record>; +type SessionStoreMutator = (store: SessionStore) => unknown; +type HookRunner = { + hasHooks: (name?: string) => boolean; + runSubagentSpawning?: (...args: unknown[]) => Promise; +}; -export function createSubagentSpawnTestConfig(workspaceDir = os.tmpdir()) { +export function createSubagentSpawnTestConfig( + workspaceDir = os.tmpdir(), + overrides?: Record, +) { return { session: { mainKey: "main", @@ -27,6 +36,7 @@ export function createSubagentSpawnTestConfig(workspaceDir = os.tmpdir()) { workspace: workspaceDir, }, }, + ...overrides, }; } @@ -57,11 +67,58 @@ export function createDefaultSessionHelperMocks() { }; } +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; }) { @@ -106,14 +163,17 @@ export async function loadSubagentSpawnModuleForTest(params: { }); } - vi.doMock("./subagent-registry.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - countActiveRunsForSession: () => 0, - registerSubagentRun: () => {}, - }; - }); + 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(); @@ -127,7 +187,9 @@ export async function loadSubagentSpawnModuleForTest(params: { const actual = await importOriginal(); return { ...actual, - resolveAgentWorkspaceDir: () => params.workspaceDir ?? os.tmpdir(), + resolveAgentConfig: params.resolveAgentConfig ?? actual.resolveAgentConfig, + resolveAgentWorkspaceDir: + params.resolveAgentWorkspaceDir ?? (() => params.workspaceDir ?? os.tmpdir()), }; }); @@ -135,19 +197,55 @@ export async function loadSubagentSpawnModuleForTest(params: { 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: () => ({ hasHooks: () => false }), + getGlobalHookRunner: () => params.hookRunner ?? { hasHooks: () => false }, })); - vi.doMock("../utils/delivery-context.js", () => ({ - normalizeDeliveryContext: identityDeliveryContext, - })); + vi.doMock("../utils/delivery-context.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + normalizeDeliveryContext: identityDeliveryContext, + }; + }); - vi.doMock("./tools/sessions-helpers.js", () => createDefaultSessionHelperMocks()); + vi.doMock("./tools/sessions-helpers.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + ...createDefaultSessionHelperMocks(), + }; + }); - const { resetSubagentRegistryForTests } = await import("./subagent-registry.js"); + 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, + resetSubagentRegistryForTests: subagentRegistry.resetSubagentRegistryForTests, }; } diff --git a/src/agents/subagent-spawn.test.ts b/src/agents/subagent-spawn.test.ts index 2f9a2874dcd..eee0d3c3591 100644 --- a/src/agents/subagent-spawn.test.ts +++ b/src/agents/subagent-spawn.test.ts @@ -1,8 +1,10 @@ import os from "node:os"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { - createDefaultSessionHelperMocks, - identityDeliveryContext, + createSubagentSpawnTestConfig, + expectPersistedRuntimeModel, + installSessionStoreCaptureMock, + loadSubagentSpawnModuleForTest, } from "./subagent-spawn.test-helpers.js"; import { installAcceptedSubagentGatewayMock } from "./test-helpers/subagent-gateway.js"; @@ -15,89 +17,11 @@ const hoisted = vi.hoisted(() => ({ configOverride: {} as Record, })); -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("../config/sessions.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - updateSessionStore: (...args: unknown[]) => hoisted.updateSessionStoreMock(...args), - }; -}); - -vi.mock("../gateway/session-utils.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveGatewaySessionStoreTarget: (params: { key: string }) => ({ - agentId: "main", - storePath: "/tmp/subagent-spawn-session-store.json", - canonicalKey: params.key, - storeKeys: [params.key], - }), - pruneLegacyStoreKeys: (...args: unknown[]) => hoisted.pruneLegacyStoreKeysMock(...args), - }; -}); - -vi.mock("./subagent-registry.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - countActiveRunsForSession: () => 0, - registerSubagentRun: (args: unknown) => hoisted.registerSubagentRunMock(args), - }; -}); - -vi.mock("../sessions/session-lifecycle-events.js", () => ({ - emitSessionLifecycleEvent: (args: unknown) => hoisted.emitSessionLifecycleEventMock(args), -})); - -vi.mock("./subagent-announce.js", () => ({ - buildSubagentSystemPrompt: () => "system-prompt", -})); - -vi.mock("./subagent-depth.js", () => ({ - getSubagentDepthFromSessionStore: () => 0, -})); - -vi.mock("./model-selection.js", () => ({ - resolveSubagentSpawnModelSelection: () => "openai-codex/gpt-5.4", -})); - -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: identityDeliveryContext, -})); - -vi.mock("./tools/sessions-helpers.js", () => createDefaultSessionHelperMocks()); - -vi.mock("./agent-scope.js", () => ({ - resolveAgentConfig: () => undefined, -})); +let resetSubagentRegistryForTests: typeof import("./subagent-registry.js").resetSubagentRegistryForTests; +let spawnSubagentDirect: typeof import("./subagent-spawn.js").spawnSubagentDirect; function createConfigOverride(overrides?: Record) { - return { - session: { - mainKey: "main", - scope: "per-sender", - }, + return createSubagentSpawnTestConfig(os.tmpdir(), { agents: { defaults: { workspace: os.tmpdir(), @@ -110,12 +34,24 @@ function createConfigOverride(overrides?: Record) { ], }, ...overrides, - }; + }); } describe("spawnSubagentDirect seam flow", () => { - beforeEach(() => { - vi.resetModules(); + beforeEach(async () => { + ({ resetSubagentRegistryForTests, spawnSubagentDirect } = await loadSubagentSpawnModuleForTest({ + callGatewayMock: hoisted.callGatewayMock, + loadConfig: () => hoisted.configOverride, + updateSessionStoreMock: hoisted.updateSessionStoreMock, + pruneLegacyStoreKeysMock: hoisted.pruneLegacyStoreKeysMock, + registerSubagentRunMock: hoisted.registerSubagentRunMock, + emitSessionLifecycleEventMock: hoisted.emitSessionLifecycleEventMock, + resolveAgentConfig: () => undefined, + resolveSubagentSpawnModelSelection: () => "openai-codex/gpt-5.4", + resolveSandboxRuntimeStatus: () => ({ sandboxed: false }), + sessionStorePath: "/tmp/subagent-spawn-session-store.json", + })); + resetSubagentRegistryForTests(); hoisted.callGatewayMock.mockReset(); hoisted.updateSessionStoreMock.mockReset(); hoisted.pruneLegacyStoreKeysMock.mockReset(); @@ -137,7 +73,6 @@ describe("spawnSubagentDirect seam flow", () => { }); it("accepts a spawned run across session patching, runtime-model persistence, registry registration, and lifecycle emission", async () => { - const { spawnSubagentDirect } = await import("./subagent-spawn.js"); const operations: string[] = []; let persistedStore: Record> | undefined; @@ -151,18 +86,12 @@ describe("spawnSubagentDirect seam flow", () => { } return {}; }); - hoisted.updateSessionStoreMock.mockImplementation( - async ( - _storePath: string, - mutator: (store: Record>) => unknown, - ) => { - operations.push("store:update"); - const store: Record> = {}; - await mutator(store); + installSessionStoreCaptureMock(hoisted.updateSessionStoreMock, { + operations, + onStore: (store) => { persistedStore = store; - return store; }, - ); + }); const result = await spawnSubagentDirect( { @@ -216,10 +145,10 @@ describe("spawnSubagentDirect seam flow", () => { label: undefined, }); - const [persistedKey, persistedEntry] = Object.entries(persistedStore ?? {})[0] ?? []; - expect(persistedKey).toBe(childSessionKey); - expect(persistedEntry).toMatchObject({ - modelProvider: "openai-codex", + expectPersistedRuntimeModel({ + persistedStore, + sessionKey: childSessionKey, + provider: "openai-codex", model: "gpt-5.4", }); expect(operations.indexOf("gateway:sessions.patch")).toBeGreaterThan(-1); diff --git a/src/agents/subagent-spawn.workspace.test.ts b/src/agents/subagent-spawn.workspace.test.ts index 7eb356a29a7..074937cec7c 100644 --- a/src/agents/subagent-spawn.workspace.test.ts +++ b/src/agents/subagent-spawn.workspace.test.ts @@ -1,9 +1,9 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { - createDefaultSessionHelperMocks, - identityDeliveryContext, + createSubagentSpawnTestConfig, + loadSubagentSpawnModuleForTest, + setupAcceptedSubagentGatewayMock, } from "./subagent-spawn.test-helpers.js"; -import { installAcceptedSubagentGatewayMock } from "./test-helpers/subagent-gateway.js"; type TestAgentConfig = { id?: string; @@ -30,18 +30,7 @@ const hoisted = vi.hoisted(() => ({ })); let spawnSubagentDirect: typeof import("./subagent-spawn.js").spawnSubagentDirect; - -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, - }; -}); +let resetSubagentRegistryForTests: typeof import("./subagent-registry.js").resetSubagentRegistryForTests; vi.mock("@mariozechner/pi-ai/oauth", async () => { const actual = await vi.importActual( @@ -54,51 +43,8 @@ vi.mock("@mariozechner/pi-ai/oauth", async () => { }; }); -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: () => hoisted.hookRunner, -})); - -vi.mock("../utils/delivery-context.js", () => ({ - normalizeDeliveryContext: identityDeliveryContext, -})); - -vi.mock("./tools/sessions-helpers.js", () => createDefaultSessionHelperMocks()); - -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", - }, + return createSubagentSpawnTestConfig("/tmp/workspace-main", { agents: { list: [ { @@ -108,60 +54,15 @@ function createConfigOverride(overrides?: Record) { ], }, ...overrides, - }; + }); } -function setupGatewayMock() { - installAcceptedSubagentGatewayMock(hoisted.callGatewayMock); +function resolveTestAgentConfig(cfg: Record, agentId: string) { + return (cfg as TestConfig).agents?.list?.find((entry) => entry.id === agentId); } -async function loadFreshSubagentSpawnWorkspaceModuleForTest() { - vi.resetModules(); - vi.doMock("../gateway/call.js", () => ({ - callGateway: (opts: unknown) => hoisted.callGatewayMock(opts), - })); - vi.doMock("../config/config.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadConfig: () => hoisted.configOverride, - }; - }); - vi.doMock("./subagent-registry.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - countActiveRunsForSession: () => 0, - registerSubagentRun: (args: unknown) => hoisted.registerSubagentRunMock(args), - }; - }); - vi.doMock("./subagent-announce.js", () => ({ - buildSubagentSystemPrompt: () => "system-prompt", - })); - vi.doMock("./subagent-depth.js", () => ({ - getSubagentDepthFromSessionStore: () => 0, - })); - vi.doMock("./model-selection.js", () => ({ - resolveSubagentSpawnModelSelection: () => undefined, - })); - vi.doMock("./sandbox/runtime-status.js", () => ({ - resolveSandboxRuntimeStatus: () => ({ sandboxed: false }), - })); - vi.doMock("../plugins/hook-runner-global.js", () => ({ - getGlobalHookRunner: () => hoisted.hookRunner, - })); - vi.doMock("../utils/delivery-context.js", () => ({ - normalizeDeliveryContext: identityDeliveryContext, - })); - vi.doMock("./tools/sessions-helpers.js", () => createDefaultSessionHelperMocks()); - vi.doMock("./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}`, - })); - ({ spawnSubagentDirect } = await import("./subagent-spawn.js")); +function resolveTestAgentWorkspace(cfg: Record, agentId: string) { + return resolveTestAgentConfig(cfg, agentId)?.workspace ?? `/tmp/workspace-${agentId}`; } function getRegisteredRun() { @@ -193,14 +94,22 @@ async function expectAcceptedWorkspace(params: { agentId: string; expectedWorksp describe("spawnSubagentDirect workspace inheritance", () => { beforeEach(async () => { - await loadFreshSubagentSpawnWorkspaceModuleForTest(); + ({ resetSubagentRegistryForTests, spawnSubagentDirect } = await loadSubagentSpawnModuleForTest({ + callGatewayMock: hoisted.callGatewayMock, + loadConfig: () => hoisted.configOverride, + registerSubagentRunMock: hoisted.registerSubagentRunMock, + hookRunner: hoisted.hookRunner, + resolveAgentConfig: resolveTestAgentConfig, + resolveAgentWorkspaceDir: resolveTestAgentWorkspace, + })); + resetSubagentRegistryForTests(); hoisted.callGatewayMock.mockClear(); hoisted.registerSubagentRunMock.mockClear(); hoisted.hookRunner.hasHooks.mockReset(); hoisted.hookRunner.hasHooks.mockImplementation(() => false); hoisted.hookRunner.runSubagentSpawning.mockReset(); hoisted.configOverride = createConfigOverride(); - setupGatewayMock(); + setupAcceptedSubagentGatewayMock(hoisted.callGatewayMock); }); it("uses the target agent workspace for cross-agent spawns", async () => { diff --git a/src/agents/tool-policy-pipeline.test.ts b/src/agents/tool-policy-pipeline.test.ts index 94d8d8da1b7..57e07bcf58a 100644 --- a/src/agents/tool-policy-pipeline.test.ts +++ b/src/agents/tool-policy-pipeline.test.ts @@ -6,6 +6,31 @@ import { type DummyTool = { name: string }; +function runAllowlistWarningStep(params: { + allow: string[]; + label: string; + suppressUnavailableCoreToolWarning?: boolean; +}) { + const warnings: string[] = []; + const tools = [{ name: "exec" }] as unknown as DummyTool[]; + applyToolPolicyPipeline({ + // oxlint-disable-next-line typescript/no-explicit-any + tools: tools as any, + // oxlint-disable-next-line typescript/no-explicit-any + toolMeta: () => undefined, + warn: (msg) => warnings.push(msg), + steps: [ + { + policy: { allow: params.allow }, + label: params.label, + stripPluginOnlyAllowlist: true, + suppressUnavailableCoreToolWarning: params.suppressUnavailableCoreToolWarning, + }, + ], + }); + return warnings; +} + describe("tool-policy-pipeline", () => { beforeEach(() => { resetToolPolicyWarningCacheForTest(); @@ -53,43 +78,19 @@ describe("tool-policy-pipeline", () => { }); test("suppresses built-in profile warnings for unavailable gated core tools", () => { - const warnings: string[] = []; - const tools = [{ name: "exec" }] as unknown as DummyTool[]; - applyToolPolicyPipeline({ - // oxlint-disable-next-line typescript/no-explicit-any - tools: tools as any, - // oxlint-disable-next-line typescript/no-explicit-any - toolMeta: () => undefined, - warn: (msg) => warnings.push(msg), - steps: [ - { - policy: { allow: ["apply_patch"] }, - label: "tools.profile (coding)", - stripPluginOnlyAllowlist: true, - suppressUnavailableCoreToolWarning: true, - }, - ], + const warnings = runAllowlistWarningStep({ + allow: ["apply_patch"], + label: "tools.profile (coding)", + suppressUnavailableCoreToolWarning: true, }); expect(warnings).toEqual([]); }); test("still warns for profile steps when explicit alsoAllow entries are present", () => { - const warnings: string[] = []; - const tools = [{ name: "exec" }] as unknown as DummyTool[]; - applyToolPolicyPipeline({ - // oxlint-disable-next-line typescript/no-explicit-any - tools: tools as any, - // oxlint-disable-next-line typescript/no-explicit-any - toolMeta: () => undefined, - warn: (msg) => warnings.push(msg), - steps: [ - { - policy: { allow: ["apply_patch"] }, - label: "tools.profile (coding)", - stripPluginOnlyAllowlist: true, - suppressUnavailableCoreToolWarning: false, - }, - ], + const warnings = runAllowlistWarningStep({ + allow: ["apply_patch"], + label: "tools.profile (coding)", + suppressUnavailableCoreToolWarning: false, }); expect(warnings.length).toBe(1); expect(warnings[0]).toContain("unknown entries (apply_patch)"); @@ -99,21 +100,9 @@ describe("tool-policy-pipeline", () => { }); test("still warns for explicit allowlists that mention unavailable gated core tools", () => { - const warnings: string[] = []; - const tools = [{ name: "exec" }] as unknown as DummyTool[]; - applyToolPolicyPipeline({ - // oxlint-disable-next-line typescript/no-explicit-any - tools: tools as any, - // oxlint-disable-next-line typescript/no-explicit-any - toolMeta: () => undefined, - warn: (msg) => warnings.push(msg), - steps: [ - { - policy: { allow: ["apply_patch"] }, - label: "tools.allow", - stripPluginOnlyAllowlist: true, - }, - ], + const warnings = runAllowlistWarningStep({ + allow: ["apply_patch"], + label: "tools.allow", }); expect(warnings.length).toBe(1); expect(warnings[0]).toContain("unknown entries (apply_patch)");