From a92fbf7d40aa8a2ebf05c700207fc8ce1b792df4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 26 Mar 2026 16:13:55 +0000 Subject: [PATCH] test: dedupe remaining agent test seams --- src/agents/cli-runner.test.ts | 141 +++++++------- src/agents/model-auth.test.ts | 176 ++++++------------ .../extra-params.xai-tool-payload.test.ts | 83 +++------ src/agents/subagent-orphan-recovery.test.ts | 43 ++--- 4 files changed, 171 insertions(+), 272 deletions(-) diff --git a/src/agents/cli-runner.test.ts b/src/agents/cli-runner.test.ts index 34bff093156..24663d007c8 100644 --- a/src/agents/cli-runner.test.ts +++ b/src/agents/cli-runner.test.ts @@ -93,6 +93,12 @@ type MockRunExit = { noOutputTimedOut: boolean; }; +type TestCliBackendConfig = { + command: string; + env?: Record; + clearEnv?: string[]; +}; + function createManagedRun(exit: MockRunExit, pid = 1234) { return { runId: "run-supervisor", @@ -104,6 +110,47 @@ function createManagedRun(exit: MockRunExit, pid = 1234) { }; } +function mockSuccessfulCliRun() { + supervisorSpawnMock.mockResolvedValueOnce( + createManagedRun({ + reason: "exit", + exitCode: 0, + exitSignal: null, + durationMs: 50, + stdout: "ok", + stderr: "", + timedOut: false, + noOutputTimedOut: false, + }), + ); +} + +async function runCliAgentWithBackendConfig(params: { + backend: TestCliBackendConfig; + runId: string; +}) { + await runCliAgent({ + sessionId: "s1", + sessionFile: "/tmp/session.jsonl", + workspaceDir: "/tmp", + config: { + agents: { + defaults: { + cliBackends: { + "codex-cli": params.backend, + }, + }, + }, + } satisfies OpenClawConfig, + prompt: "hi", + provider: "codex-cli", + model: "gpt-5.2-codex", + timeoutMs: 1_000, + runId: params.runId, + cliSessionId: "thread-123", + }); +} + describe("runCliAgent with process supervisor", () => { afterEach(() => { vi.unstubAllEnvs(); @@ -168,47 +215,19 @@ describe("runCliAgent with process supervisor", () => { vi.stubEnv("PATH", "/usr/bin:/bin"); vi.stubEnv("HOME", "/tmp/trusted-home"); - supervisorSpawnMock.mockResolvedValueOnce( - createManagedRun({ - reason: "exit", - exitCode: 0, - exitSignal: null, - durationMs: 50, - stdout: "ok", - stderr: "", - timedOut: false, - noOutputTimedOut: false, - }), - ); - - await runCliAgent({ - sessionId: "s1", - sessionFile: "/tmp/session.jsonl", - workspaceDir: "/tmp", - config: { - agents: { - defaults: { - cliBackends: { - "codex-cli": { - command: "codex", - env: { - NODE_OPTIONS: "--require ./malicious.js", - LD_PRELOAD: "/tmp/pwn.so", - PATH: "/tmp/evil", - HOME: "/tmp/evil-home", - SAFE_KEY: "ok", - }, - }, - }, - }, + mockSuccessfulCliRun(); + await runCliAgentWithBackendConfig({ + backend: { + command: "codex", + env: { + NODE_OPTIONS: "--require ./malicious.js", + LD_PRELOAD: "/tmp/pwn.so", + PATH: "/tmp/evil", + HOME: "/tmp/evil-home", + SAFE_KEY: "ok", }, - } satisfies OpenClawConfig, - prompt: "hi", - provider: "codex-cli", - model: "gpt-5.2-codex", - timeoutMs: 1_000, + }, runId: "run-env-sanitized", - cliSessionId: "thread-123", }); const input = supervisorSpawnMock.mock.calls[0]?.[0] as { @@ -225,44 +244,16 @@ describe("runCliAgent with process supervisor", () => { vi.stubEnv("PATH", "/usr/bin:/bin"); vi.stubEnv("SAFE_CLEAR", "from-base"); - supervisorSpawnMock.mockResolvedValueOnce( - createManagedRun({ - reason: "exit", - exitCode: 0, - exitSignal: null, - durationMs: 50, - stdout: "ok", - stderr: "", - timedOut: false, - noOutputTimedOut: false, - }), - ); - - await runCliAgent({ - sessionId: "s1", - sessionFile: "/tmp/session.jsonl", - workspaceDir: "/tmp", - config: { - agents: { - defaults: { - cliBackends: { - "codex-cli": { - command: "codex", - env: { - SAFE_KEEP: "keep-me", - }, - clearEnv: ["SAFE_CLEAR"], - }, - }, - }, + mockSuccessfulCliRun(); + await runCliAgentWithBackendConfig({ + backend: { + command: "codex", + env: { + SAFE_KEEP: "keep-me", }, - } satisfies OpenClawConfig, - prompt: "hi", - provider: "codex-cli", - model: "gpt-5.2-codex", - timeoutMs: 1_000, + clearEnv: ["SAFE_CLEAR"], + }, runId: "run-clear-env", - cliSessionId: "thread-123", }); const input = supervisorSpawnMock.mock.calls[0]?.[0] as { diff --git a/src/agents/model-auth.test.ts b/src/agents/model-auth.test.ts index 3949a4655a5..7e800d140bd 100644 --- a/src/agents/model-auth.test.ts +++ b/src/agents/model-auth.test.ts @@ -1,5 +1,6 @@ import { streamSimpleOpenAICompletions, type Model } from "@mariozechner/pi-ai"; import { afterEach, describe, expect, it, vi } from "vitest"; +import type { ModelProviderConfig } from "../config/config.js"; import { withFetchPreconnect } from "../test-utils/fetch-mock.js"; import type { AuthProfileStore } from "./auth-profiles.js"; import { @@ -17,6 +18,46 @@ import { resolveUsableCustomProviderApiKey, } from "./model-auth.js"; +function createCustomProviderConfig( + baseUrl: string, + modelId = "llama3", + modelName = "Llama 3", +): ModelProviderConfig { + return { + baseUrl, + api: "openai-completions" as const, + models: [ + { + id: modelId, + name: modelName, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 8192, + maxTokens: 4096, + }, + ], + }; +} + +async function resolveCustomProviderAuth( + provider: string, + baseUrl: string, + modelId?: string, + modelName?: string, +) { + return resolveApiKeyForProvider({ + provider, + cfg: { + models: { + providers: { + [provider]: createCustomProviderConfig(baseUrl, modelId, modelName), + }, + }, + }, + }); +} + describe("resolveAwsSdkEnvVarName", () => { it("prefers bearer token over access keys and profile", () => { const env = { @@ -252,143 +293,38 @@ describe("resolveUsableCustomProviderApiKey", () => { describe("resolveApiKeyForProvider – synthetic local auth for custom providers", () => { it("synthesizes a local auth marker for custom providers with a local baseUrl and no apiKey", async () => { - const auth = await resolveApiKeyForProvider({ - provider: "custom-127-0-0-1-8080", - cfg: { - models: { - providers: { - "custom-127-0-0-1-8080": { - baseUrl: "http://127.0.0.1:8080/v1", - api: "openai-completions", - models: [ - { - id: "qwen-3.5", - name: "Qwen 3.5", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 8192, - maxTokens: 4096, - }, - ], - }, - }, - }, - }, - }); + const auth = await resolveCustomProviderAuth( + "custom-127-0-0-1-8080", + "http://127.0.0.1:8080/v1", + "qwen-3.5", + "Qwen 3.5", + ); expect(auth.apiKey).toBe(CUSTOM_LOCAL_AUTH_MARKER); expect(auth.source).toContain("synthetic local key"); }); it("synthesizes a local auth marker for localhost custom providers", async () => { - const auth = await resolveApiKeyForProvider({ - provider: "my-local", - cfg: { - models: { - providers: { - "my-local": { - baseUrl: "http://localhost:11434/v1", - api: "openai-completions", - models: [ - { - id: "llama3", - name: "Llama 3", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 8192, - maxTokens: 4096, - }, - ], - }, - }, - }, - }, - }); + const auth = await resolveCustomProviderAuth("my-local", "http://localhost:11434/v1"); expect(auth.apiKey).toBe(CUSTOM_LOCAL_AUTH_MARKER); }); it("synthesizes a local auth marker for IPv6 loopback (::1)", async () => { - const auth = await resolveApiKeyForProvider({ - provider: "my-ipv6", - cfg: { - models: { - providers: { - "my-ipv6": { - baseUrl: "http://[::1]:8080/v1", - api: "openai-completions", - models: [ - { - id: "llama3", - name: "Llama 3", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 8192, - maxTokens: 4096, - }, - ], - }, - }, - }, - }, - }); + const auth = await resolveCustomProviderAuth("my-ipv6", "http://[::1]:8080/v1"); expect(auth.apiKey).toBe(CUSTOM_LOCAL_AUTH_MARKER); }); it("synthesizes a local auth marker for 0.0.0.0", async () => { - const auth = await resolveApiKeyForProvider({ - provider: "my-wildcard", - cfg: { - models: { - providers: { - "my-wildcard": { - baseUrl: "http://0.0.0.0:11434/v1", - api: "openai-completions", - models: [ - { - id: "qwen", - name: "Qwen", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 8192, - maxTokens: 4096, - }, - ], - }, - }, - }, - }, - }); + const auth = await resolveCustomProviderAuth( + "my-wildcard", + "http://0.0.0.0:11434/v1", + "qwen", + "Qwen", + ); expect(auth.apiKey).toBe(CUSTOM_LOCAL_AUTH_MARKER); }); it("synthesizes a local auth marker for IPv4-mapped IPv6 (::ffff:127.0.0.1)", async () => { - const auth = await resolveApiKeyForProvider({ - provider: "my-mapped", - cfg: { - models: { - providers: { - "my-mapped": { - baseUrl: "http://[::ffff:127.0.0.1]:8080/v1", - api: "openai-completions", - models: [ - { - id: "llama3", - name: "Llama 3", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 8192, - maxTokens: 4096, - }, - ], - }, - }, - }, - }, - }); + const auth = await resolveCustomProviderAuth("my-mapped", "http://[::ffff:127.0.0.1]:8080/v1"); expect(auth.apiKey).toBe(CUSTOM_LOCAL_AUTH_MARKER); }); diff --git a/src/agents/pi-embedded-runner/extra-params.xai-tool-payload.test.ts b/src/agents/pi-embedded-runner/extra-params.xai-tool-payload.test.ts index e282ca9f882..f832e83fd2e 100644 --- a/src/agents/pi-embedded-runner/extra-params.xai-tool-payload.test.ts +++ b/src/agents/pi-embedded-runner/extra-params.xai-tool-payload.test.ts @@ -7,65 +7,44 @@ vi.mock("@mariozechner/pi-ai", async (importOriginal) => createPiAiStreamSimpleMock(() => importOriginal()), ); +function runToolPayloadCase(provider: "openai" | "xai", modelId: string) { + return runExtraParamsCase({ + applyProvider: provider, + applyModelId: modelId, + model: { + api: "openai-completions", + provider, + id: modelId, + } as Model<"openai-completions">, + payload: { + model: modelId, + messages: [], + tools: [ + { + type: "function", + function: { + name: "write", + description: "write a file", + parameters: { type: "object", properties: {} }, + strict: true, + }, + }, + ], + }, + }).payload as { + tools?: Array<{ function?: Record }>; + }; +} + describe("extra-params: xAI tool payload compatibility", () => { it("strips function.strict for xai providers", () => { - const payload = runExtraParamsCase({ - applyProvider: "xai", - applyModelId: "grok-4-1-fast-reasoning", - model: { - api: "openai-completions", - provider: "xai", - id: "grok-4-1-fast-reasoning", - } as Model<"openai-completions">, - payload: { - model: "grok-4-1-fast-reasoning", - messages: [], - tools: [ - { - type: "function", - function: { - name: "write", - description: "write a file", - parameters: { type: "object", properties: {} }, - strict: true, - }, - }, - ], - }, - }).payload as { - tools?: Array<{ function?: Record }>; - }; + const payload = runToolPayloadCase("xai", "grok-4-1-fast-reasoning"); expect(payload.tools?.[0]?.function).not.toHaveProperty("strict"); }); it("keeps function.strict for non-xai providers", () => { - const payload = runExtraParamsCase({ - applyProvider: "openai", - applyModelId: "gpt-5.4", - model: { - api: "openai-completions", - provider: "openai", - id: "gpt-5.4", - } as Model<"openai-completions">, - payload: { - model: "gpt-5.4", - messages: [], - tools: [ - { - type: "function", - function: { - name: "write", - description: "write a file", - parameters: { type: "object", properties: {} }, - strict: true, - }, - }, - ], - }, - }).payload as { - tools?: Array<{ function?: Record }>; - }; + const payload = runToolPayloadCase("openai", "gpt-5.4"); expect(payload.tools?.[0]?.function?.strict).toBe(true); }); diff --git a/src/agents/subagent-orphan-recovery.test.ts b/src/agents/subagent-orphan-recovery.test.ts index bde5769981d..5a112817177 100644 --- a/src/agents/subagent-orphan-recovery.test.ts +++ b/src/agents/subagent-orphan-recovery.test.ts @@ -46,6 +46,22 @@ function createTestRunRecord(overrides: Partial = {}): Subage }; } +function createActiveRuns(...runs: SubagentRunRecord[]) { + return new Map(runs.map((run) => [run.runId, run] satisfies [string, SubagentRunRecord])); +} + +async function expectSkippedRecovery(store: ReturnType) { + vi.mocked(sessions.loadSessionStore).mockReturnValue(store); + + const result = await recoverOrphanedSubagentSessions({ + getActiveRuns: () => createActiveRuns(createTestRunRecord()), + }); + + expect(result.recovered).toBe(0); + expect(result.skipped).toBe(1); + expect(gateway.callGateway).not.toHaveBeenCalled(); +} + describe("subagent-orphan-recovery", () => { beforeEach(() => { vi.clearAllMocks(); @@ -97,24 +113,13 @@ describe("subagent-orphan-recovery", () => { }); it("skips sessions that are not aborted", async () => { - vi.mocked(sessions.loadSessionStore).mockReturnValue({ + await expectSkippedRecovery({ "agent:main:subagent:test-session-1": { sessionId: "session-abc", updatedAt: Date.now(), abortedLastRun: false, }, }); - - const activeRuns = new Map(); - activeRuns.set("run-1", createTestRunRecord()); - - const result = await recoverOrphanedSubagentSessions({ - getActiveRuns: () => activeRuns, - }); - - expect(result.recovered).toBe(0); - expect(result.skipped).toBe(1); - expect(gateway.callGateway).not.toHaveBeenCalled(); }); it("skips runs that have already ended", async () => { @@ -225,19 +230,7 @@ describe("subagent-orphan-recovery", () => { }); it("skips sessions with missing session entry in store", async () => { - // Store has no matching entry - vi.mocked(sessions.loadSessionStore).mockReturnValue({}); - - const activeRuns = new Map(); - activeRuns.set("run-1", createTestRunRecord()); - - const result = await recoverOrphanedSubagentSessions({ - getActiveRuns: () => activeRuns, - }); - - expect(result.recovered).toBe(0); - expect(result.skipped).toBe(1); - expect(gateway.callGateway).not.toHaveBeenCalled(); + await expectSkippedRecovery({}); }); it("clears abortedLastRun flag after successful resume", async () => {