From 4ed60d950dfc4f2004a6ed7cd1568962ddee156f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 5 Apr 2026 23:02:21 +0100 Subject: [PATCH] test: isolate agent runtime seams --- src/agents/model-auth-env.ts | 5 +- ...orward-compat.errors-and-overrides.test.ts | 2 +- src/agents/sandbox/browser.create.test.ts | 7 +- src/agents/subagent-spawn.test-helpers.ts | 28 +++--- src/agents/subagent-spawn.test.ts | 8 +- src/agents/subagent-spawn.workspace.test.ts | 8 +- src/agents/tools/image-generate-tool.test.ts | 87 +++++++++++++++++++ 7 files changed, 124 insertions(+), 21 deletions(-) diff --git a/src/agents/model-auth-env.ts b/src/agents/model-auth-env.ts index 632e091ad87..01c039a0071 100644 --- a/src/agents/model-auth-env.ts +++ b/src/agents/model-auth-env.ts @@ -16,6 +16,7 @@ export function resolveEnvApiKey( env: NodeJS.ProcessEnv = process.env, ): EnvApiKeyResult | null { const normalized = normalizeProviderIdForAuth(provider); + const candidateMap = resolveProviderEnvApiKeyCandidates(); const applied = new Set(getShellEnvAppliedKeys()); const pick = (envVar: string): EnvApiKeyResult | null => { const value = normalizeOptionalSecretInput(env[envVar]); @@ -26,8 +27,8 @@ export function resolveEnvApiKey( return { apiKey: value, source }; }; - const candidates = resolveProviderEnvApiKeyCandidates()[normalized]; - if (candidates) { + const candidates = Object.hasOwn(candidateMap, normalized) ? candidateMap[normalized] : undefined; + if (Array.isArray(candidates)) { for (const envVar of candidates) { const resolved = pick(envVar); if (resolved) { diff --git a/src/agents/pi-embedded-runner/model.forward-compat.errors-and-overrides.test.ts b/src/agents/pi-embedded-runner/model.forward-compat.errors-and-overrides.test.ts index 70bae435936..915162b2b20 100644 --- a/src/agents/pi-embedded-runner/model.forward-compat.errors-and-overrides.test.ts +++ b/src/agents/pi-embedded-runner/model.forward-compat.errors-and-overrides.test.ts @@ -104,7 +104,7 @@ describe("resolveModel forward-compat errors and overrides", () => { expectResolvedForwardCompatFallbackResult({ result: resolveModelForTest("google-antigravity", "claude-opus-4-6-thinking", "/tmp/agent"), expectedModel: { - api: "google-gemini-cli", + api: "google-generative-ai", baseUrl: "https://cloudcode-pa.googleapis.com", id: "claude-opus-4-6-thinking", provider: "google-antigravity", diff --git a/src/agents/sandbox/browser.create.test.ts b/src/agents/sandbox/browser.create.test.ts index 6f2639ff09f..5792729042c 100644 --- a/src/agents/sandbox/browser.create.test.ts +++ b/src/agents/sandbox/browser.create.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { collectDockerFlagValues, findDockerArgsCall } from "./test-args.js"; import type { SandboxConfig } from "./types.js"; import { SANDBOX_MOUNT_FORMAT_VERSION } from "./workspace-mounts.js"; @@ -103,8 +103,11 @@ function buildConfig(enableNoVnc: boolean): SandboxConfig { } describe("ensureSandboxBrowser create args", () => { - beforeEach(async () => { + beforeAll(async () => { await loadFreshBrowserModulesForTest(); + }); + + beforeEach(() => { BROWSER_BRIDGES.clear(); resetNoVncObserverTokensForTests(); dockerMocks.dockerContainerState.mockClear(); diff --git a/src/agents/subagent-spawn.test-helpers.ts b/src/agents/subagent-spawn.test-helpers.ts index bf91d681110..cc2a9a2528e 100644 --- a/src/agents/subagent-spawn.test-helpers.ts +++ b/src/agents/subagent-spawn.test-helpers.ts @@ -119,8 +119,13 @@ export async function loadSubagentSpawnModuleForTest(params: { resolveSandboxRuntimeStatus?: () => { sandboxed: boolean }; workspaceDir?: string; sessionStorePath?: string; + resetModules?: boolean; }) { - vi.resetModules(); + if (params.resetModules ?? true) { + vi.resetModules(); + } + + const resetSubagentRegistryForTests = vi.fn(); vi.doMock("./subagent-spawn.runtime.js", () => ({ callGateway: (opts: unknown) => params.callGatewayMock(opts), @@ -178,17 +183,16 @@ export async function loadSubagentSpawnModuleForTest(params: { getSubagentDepthFromSessionStore: () => 0, })); - 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 - >, - ); - } + vi.doMock("./subagent-registry.js", () => ({ + countActiveRunsForSession: () => 0, + registerSubagentRun: + params.registerSubagentRunMock ?? vi.fn((_record: Record) => undefined), + resetSubagentRegistryForTests, + })); + + const subagentSpawnModule = await import("./subagent-spawn.js"); return { - ...(await import("./subagent-spawn.js")), - resetSubagentRegistryForTests: subagentRegistry.resetSubagentRegistryForTests, + ...subagentSpawnModule, + resetSubagentRegistryForTests, }; } diff --git a/src/agents/subagent-spawn.test.ts b/src/agents/subagent-spawn.test.ts index 48c14747722..651cba94b2c 100644 --- a/src/agents/subagent-spawn.test.ts +++ b/src/agents/subagent-spawn.test.ts @@ -1,5 +1,5 @@ import os from "node:os"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createSubagentSpawnTestConfig, expectPersistedRuntimeModel, @@ -38,7 +38,7 @@ function createConfigOverride(overrides?: Record) { } describe("spawnSubagentDirect seam flow", () => { - beforeEach(async () => { + beforeAll(async () => { ({ resetSubagentRegistryForTests, spawnSubagentDirect } = await loadSubagentSpawnModuleForTest({ callGatewayMock: hoisted.callGatewayMock, loadConfig: () => hoisted.configOverride, @@ -50,7 +50,11 @@ describe("spawnSubagentDirect seam flow", () => { 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(); diff --git a/src/agents/subagent-spawn.workspace.test.ts b/src/agents/subagent-spawn.workspace.test.ts index 074937cec7c..470c1bebbcd 100644 --- a/src/agents/subagent-spawn.workspace.test.ts +++ b/src/agents/subagent-spawn.workspace.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createSubagentSpawnTestConfig, loadSubagentSpawnModuleForTest, @@ -93,7 +93,7 @@ async function expectAcceptedWorkspace(params: { agentId: string; expectedWorksp } describe("spawnSubagentDirect workspace inheritance", () => { - beforeEach(async () => { + beforeAll(async () => { ({ resetSubagentRegistryForTests, spawnSubagentDirect } = await loadSubagentSpawnModuleForTest({ callGatewayMock: hoisted.callGatewayMock, loadConfig: () => hoisted.configOverride, @@ -101,7 +101,11 @@ describe("spawnSubagentDirect workspace inheritance", () => { hookRunner: hoisted.hookRunner, resolveAgentConfig: resolveTestAgentConfig, resolveAgentWorkspaceDir: resolveTestAgentWorkspace, + resetModules: false, })); + }); + + beforeEach(() => { resetSubagentRegistryForTests(); hoisted.callGatewayMock.mockClear(); hoisted.registerSubagentRunMock.mockClear(); diff --git a/src/agents/tools/image-generate-tool.test.ts b/src/agents/tools/image-generate-tool.test.ts index 8054429d017..ae41aef654b 100644 --- a/src/agents/tools/image-generate-tool.test.ts +++ b/src/agents/tools/image-generate-tool.test.ts @@ -69,6 +69,13 @@ function requireImageGenerateTool(tool: ReturnType { }); it("generates images and returns details.media paths", async () => { + vi.spyOn(imageGenerationRuntime, "listRuntimeImageGenerationProviders").mockReturnValue([ + { + id: "openai", + defaultModel: "gpt-image-1", + models: ["gpt-image-1"], + capabilities: { + generate: { + maxCount: 4, + supportsSize: true, + supportsAspectRatio: true, + }, + edit: { + enabled: false, + maxInputImages: 0, + }, + geometry: { + sizes: ["1024x1024", "1024x1536", "1536x1024"], + aspectRatios: ["1:1", "16:9"], + }, + }, + generateImage: vi.fn(async () => { + throw new Error("not used"); + }), + }, + ]); const generateImage = vi.spyOn(imageGenerationRuntime, "generateImage").mockResolvedValue({ provider: "openai", model: "gpt-image-1", @@ -370,6 +403,33 @@ describe("createImageGenerateTool", () => { }); it("includes MEDIA paths in content text so follow-up replies use the real saved file", async () => { + vi.spyOn(imageGenerationRuntime, "listRuntimeImageGenerationProviders").mockReturnValue([ + { + id: "google", + defaultModel: "gemini-3.1-flash-image-preview", + models: ["gemini-3.1-flash-image-preview"], + capabilities: { + generate: { + maxCount: 4, + supportsAspectRatio: true, + supportsResolution: true, + }, + edit: { + enabled: true, + maxInputImages: 5, + supportsAspectRatio: true, + supportsResolution: true, + }, + geometry: { + resolutions: ["1K", "2K", "4K"], + aspectRatios: ["1:1", "16:9"], + }, + }, + generateImage: vi.fn(async () => { + throw new Error("not used"); + }), + }, + ]); vi.spyOn(imageGenerationRuntime, "generateImage").mockResolvedValue({ provider: "google", model: "gemini-3.1-flash-image-preview", @@ -419,6 +479,33 @@ describe("createImageGenerateTool", () => { }); it("rejects counts outside the supported range", async () => { + vi.spyOn(imageGenerationRuntime, "listRuntimeImageGenerationProviders").mockReturnValue([ + { + id: "google", + defaultModel: "gemini-3.1-flash-image-preview", + models: ["gemini-3.1-flash-image-preview"], + capabilities: { + generate: { + maxCount: 4, + supportsAspectRatio: true, + supportsResolution: true, + }, + edit: { + enabled: true, + maxInputImages: 5, + supportsAspectRatio: true, + supportsResolution: true, + }, + geometry: { + resolutions: ["1K", "2K", "4K"], + aspectRatios: ["1:1", "16:9"], + }, + }, + generateImage: vi.fn(async () => { + throw new Error("not used"); + }), + }, + ]); const tool = createImageGenerateTool({ config: { agents: {