test: dedupe remaining agent test seams

This commit is contained in:
Peter Steinberger
2026-03-26 16:13:55 +00:00
parent 880b2fb7fd
commit a92fbf7d40
4 changed files with 171 additions and 272 deletions

View File

@@ -93,6 +93,12 @@ type MockRunExit = {
noOutputTimedOut: boolean;
};
type TestCliBackendConfig = {
command: string;
env?: Record<string, string>;
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 {

View File

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

View File

@@ -7,65 +7,44 @@ vi.mock("@mariozechner/pi-ai", async (importOriginal) =>
createPiAiStreamSimpleMock(() => importOriginal<typeof import("@mariozechner/pi-ai")>()),
);
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<string, unknown> }>;
};
}
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<string, unknown> }>;
};
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<string, unknown> }>;
};
const payload = runToolPayloadCase("openai", "gpt-5.4");
expect(payload.tools?.[0]?.function?.strict).toBe(true);
});

View File

@@ -46,6 +46,22 @@ function createTestRunRecord(overrides: Partial<SubagentRunRecord> = {}): Subage
};
}
function createActiveRuns(...runs: SubagentRunRecord[]) {
return new Map(runs.map((run) => [run.runId, run] satisfies [string, SubagentRunRecord]));
}
async function expectSkippedRecovery(store: ReturnType<typeof sessions.loadSessionStore>) {
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<string, SubagentRunRecord>();
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<string, SubagentRunRecord>();
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 () => {