mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
test: trim slow agent fallback coverage
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -509,6 +509,11 @@ function resolveImageFallbackDefaultProvider(cfg: OpenClawConfig | undefined): s
|
||||
return DEFAULT_PROVIDER;
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
resolveFallbackCandidates,
|
||||
resolveImageFallbackCandidates,
|
||||
} as const;
|
||||
|
||||
function resolveFallbackCandidates(params: {
|
||||
cfg: OpenClawConfig | undefined;
|
||||
provider: string;
|
||||
|
||||
@@ -25,6 +25,10 @@ import {
|
||||
resolveModelRefFromString,
|
||||
} from "./model-selection.js";
|
||||
|
||||
vi.mock("./provider-model-normalization.runtime.js", () => ({
|
||||
normalizeProviderModelIdWithRuntime: () => undefined,
|
||||
}));
|
||||
|
||||
const EXPLICIT_ALLOWLIST_CONFIG = {
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -195,11 +199,14 @@ describe("model-selection", () => {
|
||||
expected: { provider: string; model: string },
|
||||
) => {
|
||||
for (const raw of variants) {
|
||||
expect(parseModelRef(raw, defaultProvider), raw).toEqual(expected);
|
||||
expect(
|
||||
parseModelRef(raw, defaultProvider, { allowPluginNormalization: false }),
|
||||
raw,
|
||||
).toEqual(expected);
|
||||
}
|
||||
};
|
||||
|
||||
it.each([
|
||||
const parseModelRefCases = [
|
||||
{
|
||||
name: "parses explicit provider/model refs",
|
||||
variants: ["anthropic/claude-3-5-sonnet"],
|
||||
@@ -335,19 +342,30 @@ describe("model-selection", () => {
|
||||
defaultProvider: "google-vertex",
|
||||
expected: { provider: "google-vertex", model: "gemini-3.1-flash-lite-preview" },
|
||||
},
|
||||
])("$name", ({ variants, defaultProvider, expected }) => {
|
||||
expectParsedModelVariants(variants, defaultProvider, expected);
|
||||
];
|
||||
|
||||
it("parses and normalizes provider/model refs", () => {
|
||||
for (const { variants, defaultProvider, expected } of parseModelRefCases) {
|
||||
expectParsedModelVariants(variants, defaultProvider, expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("round-trips normalized refs through modelKey", () => {
|
||||
const parsed = parseModelRef(" opus-4.6 ", "anthropic");
|
||||
const parsed = parseModelRef(" opus-4.6 ", "anthropic", {
|
||||
allowPluginNormalization: false,
|
||||
});
|
||||
expect(parsed).toEqual({ provider: "anthropic", model: "claude-opus-4-6" });
|
||||
expect(modelKey(parsed?.provider ?? "", parsed?.model ?? "")).toBe(
|
||||
"anthropic/claude-opus-4-6",
|
||||
);
|
||||
});
|
||||
it.each(["", " ", "/", "anthropic/", "/model"])("returns null for invalid ref %j", (raw) => {
|
||||
expect(parseModelRef(raw, "anthropic")).toBeNull();
|
||||
it("returns null for invalid refs", () => {
|
||||
for (const raw of ["", " ", "/", "anthropic/", "/model"]) {
|
||||
expect(
|
||||
parseModelRef(raw, "anthropic", { allowPluginNormalization: false }),
|
||||
raw,
|
||||
).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -13,11 +13,13 @@ vi.mock("./auth-profiles/source-check.js", () => ({
|
||||
}));
|
||||
|
||||
describe("Outcome/fallback runtime contract - Pi fallback classifier", () => {
|
||||
it.each([
|
||||
const fallbackClassificationCases = [
|
||||
["empty", "empty_result"],
|
||||
["reasoning-only", "reasoning_only_result"],
|
||||
["planning-only", "planning_only_result"],
|
||||
] as const)(
|
||||
] as const;
|
||||
|
||||
it.each(fallbackClassificationCases)(
|
||||
"maps harness classification %s to a format fallback code",
|
||||
(classification, code) => {
|
||||
expect(
|
||||
@@ -38,54 +40,47 @@ describe("Outcome/fallback runtime contract - Pi fallback classifier", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["empty", "empty_result"],
|
||||
["reasoning-only", "reasoning_only_result"],
|
||||
["planning-only", "planning_only_result"],
|
||||
] as const)(
|
||||
"advances to the configured fallback after a classified GPT-5 %s terminal result",
|
||||
async (classification, code) => {
|
||||
const primary = createContractRunResult({
|
||||
meta: {
|
||||
durationMs: 1,
|
||||
agentHarnessResultClassification: classification,
|
||||
},
|
||||
});
|
||||
const fallback = createContractRunResult({
|
||||
payloads: [{ text: "fallback ok" }],
|
||||
meta: { durationMs: 1, finalAssistantVisibleText: "fallback ok" },
|
||||
});
|
||||
const run = vi.fn().mockResolvedValueOnce(primary).mockResolvedValueOnce(fallback);
|
||||
it("advances to the configured fallback after a classified GPT-5 terminal result", async () => {
|
||||
const primary = createContractRunResult({
|
||||
meta: {
|
||||
durationMs: 1,
|
||||
agentHarnessResultClassification: "empty",
|
||||
},
|
||||
});
|
||||
const fallback = createContractRunResult({
|
||||
payloads: [{ text: "fallback ok" }],
|
||||
meta: { durationMs: 1, finalAssistantVisibleText: "fallback ok" },
|
||||
});
|
||||
const run = vi.fn().mockResolvedValueOnce(primary).mockResolvedValueOnce(fallback);
|
||||
|
||||
const result = await runWithModelFallback({
|
||||
cfg: createContractFallbackConfig() as unknown as OpenClawConfig,
|
||||
provider: OUTCOME_FALLBACK_RUNTIME_CONTRACT.primaryProvider,
|
||||
model: OUTCOME_FALLBACK_RUNTIME_CONTRACT.primaryModel,
|
||||
run,
|
||||
classifyResult: ({ provider, model, result }) =>
|
||||
classifyEmbeddedPiRunResultForModelFallback({
|
||||
provider,
|
||||
model,
|
||||
result,
|
||||
}),
|
||||
});
|
||||
const result = await runWithModelFallback({
|
||||
cfg: createContractFallbackConfig() as unknown as OpenClawConfig,
|
||||
provider: OUTCOME_FALLBACK_RUNTIME_CONTRACT.primaryProvider,
|
||||
model: OUTCOME_FALLBACK_RUNTIME_CONTRACT.primaryModel,
|
||||
run,
|
||||
classifyResult: ({ provider, model, result }) =>
|
||||
classifyEmbeddedPiRunResultForModelFallback({
|
||||
provider,
|
||||
model,
|
||||
result,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.result).toBe(fallback);
|
||||
expect(run).toHaveBeenCalledTimes(2);
|
||||
expect(run.mock.calls[1]).toEqual([
|
||||
OUTCOME_FALLBACK_RUNTIME_CONTRACT.fallbackProvider,
|
||||
OUTCOME_FALLBACK_RUNTIME_CONTRACT.fallbackModel,
|
||||
]);
|
||||
expect(result.attempts[0]).toMatchObject({
|
||||
provider: OUTCOME_FALLBACK_RUNTIME_CONTRACT.primaryProvider,
|
||||
model: OUTCOME_FALLBACK_RUNTIME_CONTRACT.primaryModel,
|
||||
reason: "format",
|
||||
code,
|
||||
});
|
||||
},
|
||||
);
|
||||
expect(result.result).toBe(fallback);
|
||||
expect(run).toHaveBeenCalledTimes(2);
|
||||
expect(run.mock.calls[1]).toEqual([
|
||||
OUTCOME_FALLBACK_RUNTIME_CONTRACT.fallbackProvider,
|
||||
OUTCOME_FALLBACK_RUNTIME_CONTRACT.fallbackModel,
|
||||
]);
|
||||
expect(result.attempts[0]).toMatchObject({
|
||||
provider: OUTCOME_FALLBACK_RUNTIME_CONTRACT.primaryProvider,
|
||||
model: OUTCOME_FALLBACK_RUNTIME_CONTRACT.primaryModel,
|
||||
reason: "format",
|
||||
code: "empty_result",
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
const nonFallbackCases = [
|
||||
{
|
||||
name: "intentional NO_REPLY",
|
||||
result: createContractRunResult({
|
||||
@@ -153,17 +148,24 @@ describe("Outcome/fallback runtime contract - Pi fallback classifier", () => {
|
||||
}),
|
||||
hasBlockReplyPipelineOutput: true,
|
||||
},
|
||||
])("does not fallback for $name", async (contractCase) => {
|
||||
expect(
|
||||
classifyEmbeddedPiRunResultForModelFallback({
|
||||
provider: OUTCOME_FALLBACK_RUNTIME_CONTRACT.primaryProvider,
|
||||
model: OUTCOME_FALLBACK_RUNTIME_CONTRACT.primaryModel,
|
||||
result: contractCase.result,
|
||||
hasDirectlySentBlockReply: contractCase.hasDirectlySentBlockReply,
|
||||
hasBlockReplyPipelineOutput: contractCase.hasBlockReplyPipelineOutput,
|
||||
}),
|
||||
).toBeNull();
|
||||
];
|
||||
|
||||
it("does not classify terminal results with visible output or side effects as fallbacks", () => {
|
||||
for (const contractCase of nonFallbackCases) {
|
||||
expect(
|
||||
classifyEmbeddedPiRunResultForModelFallback({
|
||||
provider: OUTCOME_FALLBACK_RUNTIME_CONTRACT.primaryProvider,
|
||||
model: OUTCOME_FALLBACK_RUNTIME_CONTRACT.primaryModel,
|
||||
result: contractCase.result,
|
||||
hasDirectlySentBlockReply: contractCase.hasDirectlySentBlockReply,
|
||||
hasBlockReplyPipelineOutput: contractCase.hasBlockReplyPipelineOutput,
|
||||
}),
|
||||
).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps running on the primary when terminal output is not classified as fallback", async () => {
|
||||
const contractCase = nonFallbackCases[0];
|
||||
const run = vi.fn().mockResolvedValue(contractCase.result);
|
||||
const result = await runWithModelFallback({
|
||||
cfg: createContractFallbackConfig() as unknown as OpenClawConfig,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
filterToolsByPolicy,
|
||||
@@ -15,6 +15,15 @@ import {
|
||||
import { createStubTool } from "./test-helpers/pi-tool-stubs.js";
|
||||
import { providerAliasCases } from "./test-helpers/provider-alias-cases.js";
|
||||
|
||||
vi.mock("../channels/plugins/session-conversation.js", () => ({
|
||||
resolveSessionConversation: ({ rawId }: { rawId: string }) => ({
|
||||
id: rawId,
|
||||
threadId: undefined,
|
||||
baseConversationId: rawId,
|
||||
parentConversationCandidates: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("pi-tools.policy", () => {
|
||||
it("treats * in allow as allow-all", () => {
|
||||
const tools = [createStubTool("read"), createStubTool("exec")];
|
||||
|
||||
@@ -103,6 +103,19 @@ const mocks = vi.hoisted(() => ({
|
||||
),
|
||||
}));
|
||||
|
||||
const openClawToolsFactoryMocks = vi.hoisted(() => {
|
||||
const tool = (name: string) => ({
|
||||
name,
|
||||
displaySummary: `${name} test stub`,
|
||||
description: `${name} test stub`,
|
||||
parameters: { type: "object", properties: {} },
|
||||
execute: vi.fn(async () => ({ type: "json", data: { ok: true } })),
|
||||
});
|
||||
return {
|
||||
tool,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../infra/outbound/message-action-runner.js", async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import("../../infra/outbound/message-action-runner.js")
|
||||
@@ -130,6 +143,76 @@ vi.mock("../../cli/command-secret-targets.js", () => ({
|
||||
getScopedChannelsCommandSecretTargets: mocks.getScopedChannelsCommandSecretTargets,
|
||||
}));
|
||||
|
||||
vi.mock("./agents-list-tool.js", () => ({
|
||||
createAgentsListTool: () => openClawToolsFactoryMocks.tool("agents"),
|
||||
}));
|
||||
vi.mock("./canvas-tool.js", () => ({
|
||||
createCanvasTool: () => openClawToolsFactoryMocks.tool("canvas"),
|
||||
}));
|
||||
vi.mock("./cron-tool.js", () => ({
|
||||
createCronTool: () => openClawToolsFactoryMocks.tool("cron"),
|
||||
}));
|
||||
vi.mock("./gateway-tool.js", () => ({
|
||||
createGatewayTool: () => openClawToolsFactoryMocks.tool("gateway"),
|
||||
}));
|
||||
vi.mock("./heartbeat-response-tool.js", () => ({
|
||||
createHeartbeatResponseTool: () => openClawToolsFactoryMocks.tool("heartbeat_response"),
|
||||
}));
|
||||
vi.mock("./image-generate-tool.js", () => ({
|
||||
createImageGenerateTool: () => null,
|
||||
}));
|
||||
vi.mock("./image-tool.js", () => ({
|
||||
createImageTool: () => null,
|
||||
}));
|
||||
vi.mock("./manifest-capability-availability.js", () => ({
|
||||
hasSnapshotCapabilityAvailability: () => false,
|
||||
hasSnapshotProviderEnvAvailability: () => false,
|
||||
loadCapabilityMetadataSnapshot: () => ({ index: {}, plugins: [] }),
|
||||
}));
|
||||
vi.mock("./music-generate-tool.js", () => ({
|
||||
createMusicGenerateTool: () => null,
|
||||
}));
|
||||
vi.mock("./nodes-tool.js", () => ({
|
||||
createNodesTool: () => openClawToolsFactoryMocks.tool("nodes"),
|
||||
}));
|
||||
vi.mock("./pdf-tool.js", () => ({
|
||||
createPdfTool: () => null,
|
||||
}));
|
||||
vi.mock("./session-status-tool.js", () => ({
|
||||
createSessionStatusTool: () => openClawToolsFactoryMocks.tool("session_status"),
|
||||
}));
|
||||
vi.mock("./sessions-history-tool.js", () => ({
|
||||
createSessionsHistoryTool: () => openClawToolsFactoryMocks.tool("sessions_history"),
|
||||
}));
|
||||
vi.mock("./sessions-list-tool.js", () => ({
|
||||
createSessionsListTool: () => openClawToolsFactoryMocks.tool("sessions_list"),
|
||||
}));
|
||||
vi.mock("./sessions-send-tool.js", () => ({
|
||||
createSessionsSendTool: () => openClawToolsFactoryMocks.tool("sessions_send"),
|
||||
}));
|
||||
vi.mock("./sessions-spawn-tool.js", () => ({
|
||||
createSessionsSpawnTool: () => openClawToolsFactoryMocks.tool("sessions_spawn"),
|
||||
}));
|
||||
vi.mock("./sessions-yield-tool.js", () => ({
|
||||
createSessionsYieldTool: () => openClawToolsFactoryMocks.tool("sessions_yield"),
|
||||
}));
|
||||
vi.mock("./subagents-tool.js", () => ({
|
||||
createSubagentsTool: () => openClawToolsFactoryMocks.tool("subagents"),
|
||||
}));
|
||||
vi.mock("./tts-tool.js", () => ({
|
||||
createTtsTool: () => openClawToolsFactoryMocks.tool("tts"),
|
||||
}));
|
||||
vi.mock("./update-plan-tool.js", () => ({
|
||||
createUpdatePlanTool: () => openClawToolsFactoryMocks.tool("update_plan"),
|
||||
}));
|
||||
vi.mock("./video-generate-tool.js", () => ({
|
||||
createVideoGenerateTool: () => null,
|
||||
}));
|
||||
vi.mock("./web-tools.js", () => ({
|
||||
createWebFetchTool: () => openClawToolsFactoryMocks.tool("web_fetch"),
|
||||
createWebSearchTool: () => openClawToolsFactoryMocks.tool("web_search"),
|
||||
}));
|
||||
|
||||
function mockSendResult(overrides: { channel?: string; to?: string } = {}) {
|
||||
mocks.runMessageAction.mockClear();
|
||||
mocks.runMessageAction.mockResolvedValue({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { withTempDir } from "../test-helpers/temp-dir.js";
|
||||
import {
|
||||
type ConfigDocBaselineEntry,
|
||||
@@ -9,6 +9,14 @@ import {
|
||||
writeConfigDocBaselineArtifacts,
|
||||
} from "./doc-baseline.js";
|
||||
|
||||
vi.mock("./doc-baseline.runtime.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./doc-baseline.runtime.js")>();
|
||||
return {
|
||||
...actual,
|
||||
collectBundledChannelConfigs: () => undefined,
|
||||
};
|
||||
});
|
||||
|
||||
describe("config doc baseline integration", () => {
|
||||
let sharedRenderedPromise: Promise<
|
||||
Awaited<ReturnType<typeof renderConfigDocBaselineArtifacts>>
|
||||
|
||||
Reference in New Issue
Block a user