test: isolate agent runtime seams

This commit is contained in:
Peter Steinberger
2026-04-05 23:02:21 +01:00
parent 05f9dd7a01
commit 4ed60d950d
7 changed files with 124 additions and 21 deletions

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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();

View File

@@ -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<typeof subagentRegistry.registerSubagentRun>) =>
params.registerSubagentRunMock?.(...args) as ReturnType<
typeof subagentRegistry.registerSubagentRun
>,
);
}
vi.doMock("./subagent-registry.js", () => ({
countActiveRunsForSession: () => 0,
registerSubagentRun:
params.registerSubagentRunMock ?? vi.fn((_record: Record<string, unknown>) => undefined),
resetSubagentRegistryForTests,
}));
const subagentSpawnModule = await import("./subagent-spawn.js");
return {
...(await import("./subagent-spawn.js")),
resetSubagentRegistryForTests: subagentRegistry.resetSubagentRegistryForTests,
...subagentSpawnModule,
resetSubagentRegistryForTests,
};
}

View File

@@ -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<string, unknown>) {
}
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();

View File

@@ -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();

View File

@@ -69,6 +69,13 @@ function requireImageGenerateTool(tool: ReturnType<typeof createImageGenerateToo
return tool;
}
function ensureDefaultImageGenerationProvidersStubbed() {
if (vi.isMockFunction(imageGenerationRuntime.listRuntimeImageGenerationProviders)) {
return;
}
stubImageGenerationProviders();
}
function createToolWithPrimaryImageModel(
primary: string,
extra?: {
@@ -76,6 +83,7 @@ function createToolWithPrimaryImageModel(
workspaceDir?: string;
},
) {
ensureDefaultImageGenerationProvidersStubbed();
return requireImageGenerateTool(
createImageGenerateTool({
config: {
@@ -252,6 +260,31 @@ describe("createImageGenerateTool", () => {
});
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: {