mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
test: dedupe remaining agent test seams
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user