Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
1375 lines
43 KiB
TypeScript
1375 lines
43 KiB
TypeScript
// Qa Lab tests cover runtime tool fixture plugin behavior.
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { runRuntimeToolFixture } from "./runtime-tool-fixture.js";
|
|
import type { QaSuiteRuntimeEnv } from "./suite-runtime-types.js";
|
|
|
|
const tempRoots: string[] = [];
|
|
|
|
async function makeEnv(overrides: Partial<QaSuiteRuntimeEnv> = {}): Promise<QaSuiteRuntimeEnv> {
|
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "runtime-tool-fixture-"));
|
|
tempRoots.push(workspaceDir);
|
|
return {
|
|
repoRoot: workspaceDir,
|
|
providerMode: "mock-openai",
|
|
primaryModel: "openai/gpt-5.5",
|
|
alternateModel: "openai/gpt-5.5",
|
|
mock: null,
|
|
cfg: {},
|
|
transport: {} as QaSuiteRuntimeEnv["transport"],
|
|
gateway: {
|
|
baseUrl: "http://127.0.0.1:1",
|
|
tempRoot: workspaceDir,
|
|
workspaceDir,
|
|
runtimeEnv: {},
|
|
call: vi.fn(),
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
async function writeQaSessionTranscript(
|
|
env: QaSuiteRuntimeEnv,
|
|
sessionKey: string,
|
|
messages: Array<Record<string, unknown>>,
|
|
) {
|
|
const sessionsDir = path.join(env.gateway.tempRoot, "state", "agents", "qa", "sessions");
|
|
await fs.mkdir(sessionsDir, { recursive: true });
|
|
const sessionId = sessionKey.replace(/[^a-z0-9]+/giu, "-");
|
|
const storePath = path.join(sessionsDir, "sessions.json");
|
|
let store: Record<string, unknown> = {};
|
|
try {
|
|
store = JSON.parse(await fs.readFile(storePath, "utf8")) as Record<string, unknown>;
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
throw error;
|
|
}
|
|
}
|
|
store[sessionKey] = { sessionId, sessionFile: `${sessionId}.jsonl` };
|
|
await fs.writeFile(storePath, JSON.stringify(store), "utf8");
|
|
await fs.writeFile(
|
|
path.join(sessionsDir, `${sessionId}.jsonl`),
|
|
messages.map((message) => JSON.stringify({ message })).join("\n"),
|
|
"utf8",
|
|
);
|
|
}
|
|
|
|
async function writeLiveRuntimeToolEvidence(env: QaSuiteRuntimeEnv, toolName = "read") {
|
|
await writeQaSessionTranscript(env, `agent:qa:runtime-tool:${toolName}:happy`, [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "tool_use",
|
|
id: `call-${toolName}-happy`,
|
|
name: toolName,
|
|
input: { path: "README.md" },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
role: "tool",
|
|
toolName,
|
|
tool_call_id: `call-${toolName}-happy`,
|
|
content: "README contents",
|
|
},
|
|
]);
|
|
await writeQaSessionTranscript(env, `agent:qa:runtime-tool:${toolName}:failure`, [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "tool_use",
|
|
id: `call-${toolName}-failure`,
|
|
name: toolName,
|
|
input: { path: "/missing" },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
role: "tool",
|
|
toolName,
|
|
tool_call_id: `call-${toolName}-failure`,
|
|
isError: true,
|
|
content: "outside allowed scope",
|
|
},
|
|
]);
|
|
}
|
|
|
|
async function runMockRuntimeToolFixtureWithOutputs(params: {
|
|
toolName: string;
|
|
happyArgs: Record<string, unknown>;
|
|
failureArgs: Record<string, unknown>;
|
|
happyOutput: string;
|
|
failureOutput: string;
|
|
}) {
|
|
const env = await makeEnv({
|
|
mock: { baseUrl: "http://127.0.0.1:9999" },
|
|
});
|
|
const promptSnippet = `target=${params.toolName}`;
|
|
const failurePromptSnippet = `failure target=${params.toolName}`;
|
|
const happyCallId = `call-${params.toolName}-happy`;
|
|
const failureCallId = `call-${params.toolName}-failure`;
|
|
const fetchJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([
|
|
{
|
|
allInputText: promptSnippet,
|
|
plannedToolCallId: happyCallId,
|
|
plannedToolName: params.toolName,
|
|
plannedToolArgs: params.happyArgs,
|
|
},
|
|
{
|
|
allInputText: promptSnippet,
|
|
toolOutputCallId: happyCallId,
|
|
toolOutput: params.happyOutput,
|
|
},
|
|
{
|
|
allInputText: failurePromptSnippet,
|
|
plannedToolCallId: failureCallId,
|
|
plannedToolName: params.toolName,
|
|
plannedToolArgs: params.failureArgs,
|
|
},
|
|
{
|
|
allInputText: failurePromptSnippet,
|
|
toolOutputCallId: failureCallId,
|
|
toolOutput: params.failureOutput,
|
|
},
|
|
]);
|
|
|
|
return runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: params.toolName,
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
},
|
|
promptSnippet,
|
|
failurePromptSnippet,
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set([params.toolName])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson,
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
);
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
tempRoots.splice(0).map((tempRoot) => fs.rm(tempRoot, { recursive: true, force: true })),
|
|
);
|
|
});
|
|
|
|
describe("runtime tool fixture", () => {
|
|
it("checks effective tools on the same session used for the happy prompt", async () => {
|
|
const env = await makeEnv();
|
|
await writeLiveRuntimeToolEvidence(env);
|
|
const createdKeys: string[] = [];
|
|
const promptKeys: string[] = [];
|
|
const readEffectiveTools = vi.fn(async (_env, sessionKey: string) => {
|
|
expect(sessionKey).toBe("agent:qa:runtime-tool:read:happy");
|
|
return new Set(["read"]);
|
|
});
|
|
|
|
await runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "read",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
},
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => {
|
|
createdKeys.push(key);
|
|
return key;
|
|
}),
|
|
readEffectiveTools,
|
|
runAgentPrompt: vi.fn(async (_env, params) => {
|
|
promptKeys.push(params.sessionKey);
|
|
return {};
|
|
}),
|
|
fetchJson: vi.fn(),
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
);
|
|
|
|
expect(createdKeys).toEqual([
|
|
"agent:qa:runtime-tool:read:happy",
|
|
"agent:qa:runtime-tool:read:failure",
|
|
]);
|
|
expect(promptKeys).toEqual([
|
|
"agent:qa:runtime-tool:read:happy",
|
|
"agent:qa:runtime-tool:read:failure",
|
|
]);
|
|
});
|
|
|
|
it("requires live runtime tool fixtures to produce transcript tool output", async () => {
|
|
const env = await makeEnv();
|
|
await writeQaSessionTranscript(env, "agent:qa:runtime-tool:read:happy", [
|
|
{ role: "assistant", content: "I checked README.md and it looks good." },
|
|
]);
|
|
await writeQaSessionTranscript(env, "agent:qa:runtime-tool:read:failure", [
|
|
{ role: "assistant", content: "The denied-input path looks good." },
|
|
]);
|
|
|
|
await expect(
|
|
runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "read",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
},
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["read"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson: vi.fn(),
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
),
|
|
).rejects.toThrow("expected live happy-path tool call for read");
|
|
});
|
|
|
|
it("accepts live runtime tool fixtures only after transcript tool output", async () => {
|
|
const env = await makeEnv();
|
|
await writeLiveRuntimeToolEvidence(env);
|
|
|
|
const details = await runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "read",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
},
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["read"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson: vi.fn(),
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
);
|
|
|
|
expect(details).toContain("read live provider happy planned args");
|
|
expect(details).toContain("read live provider failure planned args");
|
|
});
|
|
|
|
it("allows async live runtime tool fixtures to prove the happy path with the planned call", async () => {
|
|
const env = await makeEnv();
|
|
await writeQaSessionTranscript(env, "agent:qa:runtime-tool:image_generate:happy", [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "tool_use",
|
|
id: "call-image-happy",
|
|
name: "image_generate",
|
|
input: { prompt: "QA lighthouse runtime parity fixture" },
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
await writeQaSessionTranscript(env, "agent:qa:runtime-tool:image_generate:failure", [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "tool_use",
|
|
id: "call-image-failure",
|
|
name: "image_generate",
|
|
input: { __qaFailureMode: "denied-input" },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
role: "tool",
|
|
toolName: "image_generate",
|
|
tool_call_id: "call-image-failure",
|
|
isError: true,
|
|
content: "denied-input",
|
|
},
|
|
]);
|
|
|
|
const details = await runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "image_generate",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
},
|
|
happyPathOutputRequired: false,
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["image_generate"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson: vi.fn(),
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
);
|
|
|
|
expect(details).toContain(
|
|
"image_generate live provider happy direct output not required for this async fixture",
|
|
);
|
|
expect(details).toContain("image_generate live provider failure planned args");
|
|
});
|
|
|
|
it("still requires async live runtime tool fixtures to call the happy-path tool", async () => {
|
|
const env = await makeEnv();
|
|
await writeQaSessionTranscript(env, "agent:qa:runtime-tool:image_generate:happy", [
|
|
{ role: "assistant", content: "I can start image generation later." },
|
|
]);
|
|
await writeQaSessionTranscript(env, "agent:qa:runtime-tool:image_generate:failure", [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "tool_use",
|
|
id: "call-image-failure",
|
|
name: "image_generate",
|
|
input: { __qaFailureMode: "denied-input" },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
role: "tool",
|
|
toolName: "image_generate",
|
|
tool_call_id: "call-image-failure",
|
|
isError: true,
|
|
content: "denied-input",
|
|
},
|
|
]);
|
|
|
|
await expect(
|
|
runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "image_generate",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
},
|
|
happyPathOutputRequired: false,
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["image_generate"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson: vi.fn(),
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
),
|
|
).rejects.toThrow("expected live happy-path tool call for image_generate");
|
|
});
|
|
|
|
it("requires live failure fixtures to produce failure-shaped tool output", async () => {
|
|
const env = await makeEnv();
|
|
await writeQaSessionTranscript(env, "agent:qa:runtime-tool:read:happy", [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "tool_use",
|
|
id: "call-read-happy",
|
|
name: "read",
|
|
input: { path: "README.md" },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
role: "tool",
|
|
toolName: "read",
|
|
tool_call_id: "call-read-happy",
|
|
content: "README contents",
|
|
},
|
|
]);
|
|
await writeQaSessionTranscript(env, "agent:qa:runtime-tool:read:failure", [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "tool_use",
|
|
id: "call-read-failure",
|
|
name: "read",
|
|
input: { path: "/missing" },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
role: "tool",
|
|
toolName: "read",
|
|
tool_call_id: "call-read-failure",
|
|
content: "README contents",
|
|
},
|
|
]);
|
|
|
|
await expect(
|
|
runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "read",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
},
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["read"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson: vi.fn(),
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
),
|
|
).rejects.toThrow("expected live failure-path tool failure output for read");
|
|
});
|
|
|
|
it("rejects failure-shaped live happy-path tool output", async () => {
|
|
const env = await makeEnv();
|
|
await writeQaSessionTranscript(env, "agent:qa:runtime-tool:read:happy", [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "tool_use",
|
|
id: "call-read-happy",
|
|
name: "read",
|
|
input: { path: "README.md" },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
role: "tool",
|
|
toolName: "read",
|
|
tool_call_id: "call-read-happy",
|
|
isError: true,
|
|
content: "ENOENT: no such file or directory",
|
|
},
|
|
]);
|
|
await writeQaSessionTranscript(env, "agent:qa:runtime-tool:read:failure", [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "tool_use",
|
|
id: "call-read-failure",
|
|
name: "read",
|
|
input: { path: "/missing" },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
role: "tool",
|
|
toolName: "read",
|
|
tool_call_id: "call-read-failure",
|
|
isError: true,
|
|
content: "ENOENT: no such file or directory",
|
|
},
|
|
]);
|
|
|
|
await expect(
|
|
runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "read",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
},
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["read"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson: vi.fn(),
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
),
|
|
).rejects.toThrow("expected live happy-path successful tool output for read");
|
|
});
|
|
|
|
it("does not fail Codex-native fixtures solely because OpenClaw dynamic exposure is absent", async () => {
|
|
const env = await makeEnv({
|
|
mock: { baseUrl: "http://127.0.0.1:9999" },
|
|
gateway: {
|
|
baseUrl: "http://127.0.0.1:1",
|
|
tempRoot: "",
|
|
workspaceDir: "",
|
|
runtimeEnv: { OPENCLAW_QA_FORCE_RUNTIME: "codex" },
|
|
call: vi.fn(),
|
|
},
|
|
});
|
|
env.gateway.tempRoot = env.repoRoot;
|
|
env.gateway.workspaceDir = env.repoRoot;
|
|
|
|
const fetchJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([
|
|
{
|
|
allInputText: "target=read",
|
|
plannedToolName: "read",
|
|
plannedToolArgs: { path: "README.md" },
|
|
},
|
|
]);
|
|
|
|
const details = await runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "read",
|
|
toolCoverage: {
|
|
bucket: "codex-native-workspace",
|
|
expectedLayer: "codex-native-workspace",
|
|
reason: "Codex owns read natively.",
|
|
},
|
|
promptSnippet: "target=read",
|
|
failurePromptSnippet: "failure target=read",
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set<string>()),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson,
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
);
|
|
|
|
expect(details).toContain("codex-native-workspace read");
|
|
expect(details).toContain("OpenClaw dynamic exposure is intentionally omitted");
|
|
expect(details).toContain("mock provider happy planned args (diagnostic only)");
|
|
});
|
|
|
|
it("reports Codex-native async planned-only happy fixtures without dereferencing missing output", async () => {
|
|
const env = await makeEnv({
|
|
mock: { baseUrl: "http://127.0.0.1:9999" },
|
|
gateway: {
|
|
baseUrl: "http://127.0.0.1:1",
|
|
tempRoot: "",
|
|
workspaceDir: "",
|
|
runtimeEnv: { OPENCLAW_QA_FORCE_RUNTIME: "codex" },
|
|
call: vi.fn(),
|
|
},
|
|
});
|
|
env.gateway.tempRoot = env.repoRoot;
|
|
env.gateway.workspaceDir = env.repoRoot;
|
|
|
|
const fetchJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([
|
|
{
|
|
allInputText: "target=image_generate",
|
|
plannedToolCallId: "call-image-happy",
|
|
plannedToolName: "image_generate",
|
|
plannedToolArgs: { prompt: "QA lighthouse runtime parity fixture" },
|
|
},
|
|
{
|
|
allInputText: "failure target=image_generate",
|
|
plannedToolCallId: "call-image-failure",
|
|
plannedToolName: "image_generate",
|
|
plannedToolArgs: { __qaFailureMode: "denied-input" },
|
|
},
|
|
{
|
|
allInputText: "failure target=image_generate",
|
|
toolOutputCallId: "call-image-failure",
|
|
toolOutput: "Error: denied-input",
|
|
},
|
|
]);
|
|
|
|
const details = await runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "image_generate",
|
|
toolCoverage: {
|
|
bucket: "codex-native-workspace",
|
|
expectedLayer: "codex-native-workspace",
|
|
reason: "Codex owns image generation natively in this fixture.",
|
|
},
|
|
promptSnippet: "target=image_generate",
|
|
failurePromptSnippet: "failure target=image_generate",
|
|
happyPathOutputRequired: false,
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set<string>()),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson,
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
);
|
|
|
|
expect(details).toContain("codex-native-workspace image_generate");
|
|
expect(details).toContain('"prompt":"QA lighthouse runtime parity fixture"');
|
|
expect(details).toContain('"__qaFailureMode":"denied-input"');
|
|
});
|
|
|
|
it("requires mock runtime tool fixtures to produce tool output", async () => {
|
|
const env = await makeEnv({
|
|
mock: { baseUrl: "http://127.0.0.1:9999" },
|
|
});
|
|
const fetchJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([
|
|
{
|
|
allInputText: "target=read",
|
|
plannedToolName: "read",
|
|
plannedToolArgs: { path: "README.md" },
|
|
},
|
|
{
|
|
allInputText: "failure target=read",
|
|
plannedToolName: "read",
|
|
plannedToolArgs: { path: "/missing" },
|
|
},
|
|
{
|
|
allInputText: "failure target=read",
|
|
toolOutput: "ENOENT: no such file or directory",
|
|
},
|
|
]);
|
|
|
|
await expect(
|
|
runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "read",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
},
|
|
promptSnippet: "target=read",
|
|
failurePromptSnippet: "failure target=read",
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["read"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson,
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
),
|
|
).rejects.toThrow("expected mock happy-path tool output for read");
|
|
});
|
|
|
|
it("allows async mock runtime tool fixtures to prove the happy path with the planned call", async () => {
|
|
const env = await makeEnv({
|
|
mock: { baseUrl: "http://127.0.0.1:9999" },
|
|
});
|
|
const fetchJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([
|
|
{
|
|
allInputText: "target=image_generate",
|
|
plannedToolCallId: "call-image-happy",
|
|
plannedToolName: "image_generate",
|
|
plannedToolArgs: { prompt: "QA lighthouse runtime parity fixture" },
|
|
},
|
|
{
|
|
allInputText: "failure target=image_generate",
|
|
plannedToolCallId: "call-image-failure",
|
|
plannedToolName: "image_generate",
|
|
plannedToolArgs: { __qaFailureMode: "denied-input" },
|
|
},
|
|
{
|
|
allInputText: "failure target=image_generate",
|
|
toolOutputCallId: "call-image-failure",
|
|
toolOutput: "Error: denied-input",
|
|
},
|
|
]);
|
|
|
|
const details = await runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "image_generate",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
},
|
|
promptSnippet: "target=image_generate",
|
|
failurePromptSnippet: "failure target=image_generate",
|
|
happyPathOutputRequired: false,
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["image_generate"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson,
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
);
|
|
|
|
expect(details).toContain(
|
|
"image_generate mock provider happy direct output not required for this async fixture",
|
|
);
|
|
expect(details).toContain('"prompt":"QA lighthouse runtime parity fixture"');
|
|
expect(details).toContain('"__qaFailureMode":"denied-input"');
|
|
});
|
|
|
|
it("accepts mock runtime tool fixtures only after planned calls return output", async () => {
|
|
const env = await makeEnv({
|
|
mock: { baseUrl: "http://127.0.0.1:9999" },
|
|
});
|
|
const fetchJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([
|
|
{
|
|
allInputText: "target=read",
|
|
plannedToolCallId: "call-read-happy",
|
|
plannedToolName: "read",
|
|
plannedToolArgs: { path: "README.md" },
|
|
},
|
|
{
|
|
allInputText: "target=read",
|
|
toolOutputCallId: "call-read-happy",
|
|
toolOutput: "README contents",
|
|
},
|
|
{
|
|
allInputText: "failure target=read",
|
|
plannedToolCallId: "call-read-failure",
|
|
plannedToolName: "read",
|
|
plannedToolArgs: { path: "/missing" },
|
|
},
|
|
{
|
|
allInputText: "failure target=read",
|
|
toolOutputCallId: "call-read-failure",
|
|
toolOutput: "ENOENT: no such file or directory",
|
|
},
|
|
]);
|
|
|
|
const details = await runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "read",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
},
|
|
promptSnippet: "target=read",
|
|
failurePromptSnippet: "failure target=read",
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["read"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson,
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
);
|
|
|
|
expect(details).toContain("read mock provider happy planned args");
|
|
expect(details).toContain("read mock provider failure planned args");
|
|
});
|
|
|
|
it("accepts non-required mock fixtures when both paths are planned without direct output", async () => {
|
|
const env = await makeEnv({
|
|
mock: { baseUrl: "http://127.0.0.1:9999" },
|
|
});
|
|
const fetchJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([
|
|
{
|
|
allInputText: "target=image_generate",
|
|
plannedToolCallId: "call-image-happy",
|
|
plannedToolName: "image_generate",
|
|
plannedToolArgs: { prompt: "QA lighthouse", filename: "runtime-tool-fixture" },
|
|
},
|
|
{
|
|
allInputText: "failure target=image_generate",
|
|
plannedToolCallId: "call-image-failure",
|
|
plannedToolName: "image_generate",
|
|
plannedToolArgs: { __qaFailureMode: "denied-input" },
|
|
},
|
|
]);
|
|
|
|
const details = await runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "image_generate",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
required: false,
|
|
action: "optional runtime parity gate with async image completion coverage",
|
|
},
|
|
promptSnippet: "target=image_generate",
|
|
failurePromptSnippet: "failure target=image_generate",
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["image_generate"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson,
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
);
|
|
|
|
expect(details).toContain("image_generate mock provider report-only");
|
|
expect(details).toContain("image_generate mock provider happy planned args");
|
|
expect(details).toContain("image_generate mock provider failure planned args");
|
|
});
|
|
|
|
it("still rejects failed happy output for non-required mock fixtures", async () => {
|
|
const env = await makeEnv({
|
|
mock: { baseUrl: "http://127.0.0.1:9999" },
|
|
});
|
|
const fetchJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([
|
|
{
|
|
allInputText: "target=image_generate",
|
|
plannedToolCallId: "call-image-happy",
|
|
plannedToolName: "image_generate",
|
|
plannedToolArgs: { prompt: "QA lighthouse" },
|
|
},
|
|
{
|
|
allInputText: "target=image_generate",
|
|
toolOutputCallId: "call-image-happy",
|
|
toolOutput: "Failed: provider rejected image request",
|
|
},
|
|
{
|
|
allInputText: "failure target=image_generate",
|
|
plannedToolCallId: "call-image-failure",
|
|
plannedToolName: "image_generate",
|
|
plannedToolArgs: { __qaFailureMode: "denied-input" },
|
|
},
|
|
]);
|
|
|
|
await expect(
|
|
runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "image_generate",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
required: false,
|
|
action: "optional runtime parity gate with async image completion coverage",
|
|
},
|
|
promptSnippet: "target=image_generate",
|
|
failurePromptSnippet: "failure target=image_generate",
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["image_generate"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson,
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
),
|
|
).rejects.toThrow("expected mock happy-path successful tool output for image_generate");
|
|
});
|
|
|
|
it("still rejects successful failure output for non-required mock fixtures", async () => {
|
|
const env = await makeEnv({
|
|
mock: { baseUrl: "http://127.0.0.1:9999" },
|
|
});
|
|
const fetchJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([
|
|
{
|
|
allInputText: "target=image_generate",
|
|
plannedToolCallId: "call-image-happy",
|
|
plannedToolName: "image_generate",
|
|
plannedToolArgs: { prompt: "QA lighthouse" },
|
|
},
|
|
{
|
|
allInputText: "failure target=image_generate",
|
|
plannedToolCallId: "call-image-failure",
|
|
plannedToolName: "image_generate",
|
|
plannedToolArgs: { __qaFailureMode: "denied-input" },
|
|
},
|
|
{
|
|
allInputText: "failure target=image_generate",
|
|
toolOutputCallId: "call-image-failure",
|
|
toolOutput: "Task queued for async image delivery",
|
|
},
|
|
]);
|
|
|
|
await expect(
|
|
runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "image_generate",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
required: false,
|
|
action: "optional runtime parity gate with async image completion coverage",
|
|
},
|
|
promptSnippet: "target=image_generate",
|
|
failurePromptSnippet: "failure target=image_generate",
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["image_generate"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson,
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
),
|
|
).rejects.toThrow("expected mock failure-path tool failure output for image_generate");
|
|
});
|
|
|
|
it("rejects malformed report-only failure plans for non-required mock fixtures", async () => {
|
|
const env = await makeEnv({
|
|
mock: { baseUrl: "http://127.0.0.1:9999" },
|
|
});
|
|
const fetchJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([
|
|
{
|
|
allInputText: "target=image_generate",
|
|
plannedToolCallId: "call-image-happy",
|
|
plannedToolName: "image_generate",
|
|
plannedToolArgs: { prompt: "QA lighthouse" },
|
|
},
|
|
{
|
|
allInputText: "failure target=image_generate",
|
|
plannedToolCallId: "call-image-failure",
|
|
plannedToolName: "image_generate",
|
|
plannedToolArgs: { prompt: "not a denied-input failure" },
|
|
},
|
|
]);
|
|
|
|
await expect(
|
|
runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "image_generate",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
required: false,
|
|
action: "optional runtime parity gate with async image completion coverage",
|
|
},
|
|
promptSnippet: "target=image_generate",
|
|
failurePromptSnippet: "failure target=image_generate",
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["image_generate"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson,
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
),
|
|
).rejects.toThrow("expected mock failure-path denied-input args for image_generate");
|
|
});
|
|
|
|
it("rejects malformed report-only happy plans for non-required mock fixtures", async () => {
|
|
const env = await makeEnv({
|
|
mock: { baseUrl: "http://127.0.0.1:9999" },
|
|
});
|
|
const fetchJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([
|
|
{
|
|
allInputText: "target=image_generate",
|
|
plannedToolCallId: "call-image-happy",
|
|
plannedToolName: "image_generate",
|
|
plannedToolArgs: {},
|
|
},
|
|
{
|
|
allInputText: "failure target=image_generate",
|
|
plannedToolCallId: "call-image-failure",
|
|
plannedToolName: "image_generate",
|
|
plannedToolArgs: { __qaFailureMode: "denied-input" },
|
|
},
|
|
]);
|
|
|
|
await expect(
|
|
runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "image_generate",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
required: false,
|
|
action: "optional runtime parity gate with async image completion coverage",
|
|
},
|
|
promptSnippet: "target=image_generate",
|
|
failurePromptSnippet: "failure target=image_generate",
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["image_generate"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson,
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
),
|
|
).rejects.toThrow("expected mock happy-path prompt args for image_generate");
|
|
});
|
|
|
|
it("rejects failure-shaped mock happy-path tool output", async () => {
|
|
const env = await makeEnv({
|
|
mock: { baseUrl: "http://127.0.0.1:9999" },
|
|
});
|
|
const fetchJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([
|
|
{
|
|
allInputText: "target=read",
|
|
plannedToolCallId: "call-read-happy",
|
|
plannedToolName: "read",
|
|
plannedToolArgs: { path: "README.md" },
|
|
},
|
|
{
|
|
allInputText: "target=read",
|
|
toolOutputCallId: "call-read-happy",
|
|
toolOutput: "ENOENT: no such file or directory",
|
|
},
|
|
{
|
|
allInputText: "failure target=read",
|
|
plannedToolCallId: "call-read-failure",
|
|
plannedToolName: "read",
|
|
plannedToolArgs: { path: "/missing" },
|
|
},
|
|
{
|
|
allInputText: "failure target=read",
|
|
toolOutputCallId: "call-read-failure",
|
|
toolOutput: "ENOENT: no such file or directory",
|
|
},
|
|
]);
|
|
|
|
await expect(
|
|
runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "read",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
},
|
|
promptSnippet: "target=read",
|
|
failurePromptSnippet: "failure target=read",
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["read"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson,
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
),
|
|
).rejects.toThrow("expected mock happy-path successful tool output for read");
|
|
});
|
|
|
|
it("requires mock failure fixtures to produce failure-shaped tool output", async () => {
|
|
const env = await makeEnv({
|
|
mock: { baseUrl: "http://127.0.0.1:9999" },
|
|
});
|
|
const fetchJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([
|
|
{
|
|
allInputText: "target=read",
|
|
plannedToolCallId: "call-read-happy",
|
|
plannedToolName: "read",
|
|
plannedToolArgs: { path: "README.md" },
|
|
},
|
|
{
|
|
allInputText: "target=read",
|
|
toolOutputCallId: "call-read-happy",
|
|
toolOutput: "README contents",
|
|
},
|
|
{
|
|
allInputText: "failure target=read",
|
|
plannedToolCallId: "call-read-failure",
|
|
plannedToolName: "read",
|
|
plannedToolArgs: { path: "/missing" },
|
|
},
|
|
{
|
|
allInputText: "failure target=read",
|
|
toolOutputCallId: "call-read-failure",
|
|
toolOutput: "README contents",
|
|
},
|
|
]);
|
|
|
|
await expect(
|
|
runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "read",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
},
|
|
promptSnippet: "target=read",
|
|
failurePromptSnippet: "failure target=read",
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["read"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson,
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
),
|
|
).rejects.toThrow("expected mock failure-path tool failure output for read");
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "required-field",
|
|
toolName: "sessions_spawn",
|
|
happyArgs: { task: "reply ok" },
|
|
happyOutput: "accepted",
|
|
failureOutput: "task required",
|
|
},
|
|
{
|
|
name: "unavailable-provider",
|
|
toolName: "web_search",
|
|
happyArgs: { query: "OpenClaw runtime parity fixed query" },
|
|
happyOutput: "result",
|
|
failureOutput: "web_search is disabled or no provider is available.",
|
|
},
|
|
])("accepts $name messages as mock failure fixture output", async (fixture) => {
|
|
const details = await runMockRuntimeToolFixtureWithOutputs({
|
|
...fixture,
|
|
failureArgs: { __qaFailureMode: "denied-input" },
|
|
});
|
|
|
|
expect(details).toContain(`${fixture.toolName} mock provider failure planned args`);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "neutral required-text",
|
|
toolName: "sessions_spawn",
|
|
happyArgs: { task: "reply ok" },
|
|
happyOutput: "accepted",
|
|
failureOutput: "no action required",
|
|
expectedError: "expected mock failure-path tool failure output for sessions_spawn",
|
|
},
|
|
{
|
|
name: "unavailable-provider happy output",
|
|
toolName: "web_search",
|
|
happyArgs: { query: "OpenClaw runtime parity fixed query" },
|
|
happyOutput: "web_search is disabled or no provider is available.",
|
|
failureOutput: "web_search is disabled or no provider is available.",
|
|
expectedError: "expected mock happy-path successful tool output for web_search",
|
|
},
|
|
])("rejects $name as mock fixture output", async (fixture) => {
|
|
await expect(
|
|
runMockRuntimeToolFixtureWithOutputs({
|
|
toolName: fixture.toolName,
|
|
happyArgs: fixture.happyArgs,
|
|
failureArgs: { __qaFailureMode: "denied-input" },
|
|
happyOutput: fixture.happyOutput,
|
|
failureOutput: fixture.failureOutput,
|
|
}),
|
|
).rejects.toThrow(fixture.expectedError);
|
|
});
|
|
|
|
it("allows successful happy-path tool output to mention errors", async () => {
|
|
const env = await makeEnv({
|
|
mock: { baseUrl: "http://127.0.0.1:9999" },
|
|
});
|
|
const fetchJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([
|
|
{
|
|
allInputText: "target=read",
|
|
plannedToolCallId: "call-read-happy",
|
|
plannedToolName: "read",
|
|
plannedToolArgs: { path: "README.md" },
|
|
},
|
|
{
|
|
allInputText: "target=read",
|
|
toolOutputCallId: "call-read-happy",
|
|
toolOutput: "README documents error handling and missing-file behavior.",
|
|
},
|
|
{
|
|
allInputText: "failure target=read",
|
|
plannedToolCallId: "call-read-failure",
|
|
plannedToolName: "read",
|
|
plannedToolArgs: { path: "/missing" },
|
|
},
|
|
{
|
|
allInputText: "failure target=read",
|
|
toolOutputCallId: "call-read-failure",
|
|
toolOutput: "ENOENT: no such file or directory",
|
|
},
|
|
]);
|
|
|
|
const details = await runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "read",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
},
|
|
promptSnippet: "target=read",
|
|
failurePromptSnippet: "failure target=read",
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["read"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson,
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
);
|
|
|
|
expect(details).toContain("read mock provider happy planned args");
|
|
});
|
|
|
|
it("rejects unrelated tool output after a planned mock runtime tool call", async () => {
|
|
const env = await makeEnv({
|
|
mock: { baseUrl: "http://127.0.0.1:9999" },
|
|
});
|
|
const fetchJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([
|
|
{
|
|
allInputText: "target=read",
|
|
plannedToolCallId: "call-read-happy",
|
|
plannedToolName: "read",
|
|
plannedToolArgs: { path: "README.md" },
|
|
},
|
|
{
|
|
allInputText: "target=read",
|
|
toolOutputCallId: "call-write-happy",
|
|
toolOutput: "README contents from some other tool",
|
|
},
|
|
{
|
|
allInputText: "failure target=read",
|
|
plannedToolCallId: "call-read-failure",
|
|
plannedToolName: "read",
|
|
plannedToolArgs: { path: "/missing" },
|
|
},
|
|
{
|
|
allInputText: "failure target=read",
|
|
toolOutputCallId: "call-read-failure",
|
|
toolOutput: "ENOENT: no such file or directory",
|
|
},
|
|
]);
|
|
|
|
await expect(
|
|
runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "read",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
},
|
|
promptSnippet: "target=read",
|
|
failurePromptSnippet: "failure target=read",
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["read"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson,
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
),
|
|
).rejects.toThrow("expected mock happy-path tool output for read");
|
|
});
|
|
|
|
it("rejects mismatched planned and output call ids on the same mock request", async () => {
|
|
const env = await makeEnv({
|
|
mock: { baseUrl: "http://127.0.0.1:9999" },
|
|
});
|
|
const fetchJson = vi
|
|
.fn()
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([
|
|
{
|
|
allInputText: "target=read",
|
|
plannedToolCallId: "call-read-happy",
|
|
plannedToolName: "read",
|
|
plannedToolArgs: { path: "README.md" },
|
|
toolOutputCallId: "call-write-previous",
|
|
toolOutput: "previous write output",
|
|
},
|
|
{
|
|
allInputText: "failure target=read",
|
|
plannedToolCallId: "call-read-failure",
|
|
plannedToolName: "read",
|
|
plannedToolArgs: { path: "/missing" },
|
|
},
|
|
{
|
|
allInputText: "failure target=read",
|
|
toolOutputCallId: "call-read-failure",
|
|
toolOutput: "ENOENT: no such file or directory",
|
|
},
|
|
]);
|
|
|
|
await expect(
|
|
runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "read",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
},
|
|
promptSnippet: "target=read",
|
|
failurePromptSnippet: "failure target=read",
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set(["read"])),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson,
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
),
|
|
).rejects.toThrow("expected mock happy-path tool output for read");
|
|
});
|
|
|
|
it("still fails required OpenClaw dynamic fixtures when the tool is absent", async () => {
|
|
const env = await makeEnv();
|
|
|
|
await expect(
|
|
runRuntimeToolFixture(
|
|
env,
|
|
{
|
|
toolName: "web_search",
|
|
toolCoverage: {
|
|
bucket: "openclaw-dynamic-integration",
|
|
expectedLayer: "openclaw-dynamic",
|
|
},
|
|
},
|
|
{
|
|
createSession: vi.fn(async (_env, _label, key) => key!),
|
|
readEffectiveTools: vi.fn(async () => new Set<string>()),
|
|
runAgentPrompt: vi.fn(async () => ({})),
|
|
fetchJson: vi.fn(),
|
|
ensureImageGenerationConfigured: vi.fn(),
|
|
},
|
|
),
|
|
).rejects.toThrow("web_search not present in effective tools");
|
|
});
|
|
});
|