mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-01 08:13:35 +00:00
245 lines
6.5 KiB
TypeScript
245 lines
6.5 KiB
TypeScript
/**
|
|
* Tests agent harness runtime helpers and task dispatch behavior.
|
|
*/
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
attachModelProviderRequestTransport,
|
|
buildAgentHarnessUserInputAnswers,
|
|
classifyAgentHarnessTerminalOutcome,
|
|
deliverAgentHarnessUserInputPrompt,
|
|
formatAgentHarnessUserInputPrompt,
|
|
getModelProviderRequestTransport,
|
|
type AgentHarnessTerminalOutcomeClassification,
|
|
} from "./agent-harness-runtime.js";
|
|
|
|
const { loadResearchAutocapture } = vi.hoisted(() => ({
|
|
loadResearchAutocapture: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../skills/research/autocapture.js", () => {
|
|
loadResearchAutocapture();
|
|
return {
|
|
runSkillResearchAutoCapture: vi.fn(),
|
|
};
|
|
});
|
|
|
|
describe("classifyAgentHarnessTerminalOutcome", () => {
|
|
it("does not classify an in-flight turn", () => {
|
|
expect(
|
|
classifyAgentHarnessTerminalOutcome({
|
|
assistantTexts: [],
|
|
reasoningText: "",
|
|
planText: "",
|
|
promptError: null,
|
|
turnCompleted: false,
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("does not classify prompt errors as terminal empty-output outcomes", () => {
|
|
expect(
|
|
classifyAgentHarnessTerminalOutcome({
|
|
assistantTexts: [],
|
|
reasoningText: "",
|
|
planText: "",
|
|
promptError: new Error("turn failed"),
|
|
turnCompleted: true,
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("does not classify deliberate silent replies such as NO_REPLY", () => {
|
|
expect(
|
|
classifyAgentHarnessTerminalOutcome({
|
|
assistantTexts: ["NO_REPLY"],
|
|
reasoningText: "",
|
|
planText: "",
|
|
promptError: null,
|
|
turnCompleted: true,
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("treats empty-string prompt errors as terminal errors", () => {
|
|
expect(
|
|
classifyAgentHarnessTerminalOutcome({
|
|
assistantTexts: [],
|
|
reasoningText: "",
|
|
planText: "",
|
|
promptError: "",
|
|
turnCompleted: true,
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("treats whitespace-only assistant text as not visible", () => {
|
|
expect(
|
|
classifyAgentHarnessTerminalOutcome({
|
|
assistantTexts: [" ", "\n\t"],
|
|
reasoningText: "",
|
|
planText: "",
|
|
promptError: null,
|
|
turnCompleted: true,
|
|
}),
|
|
).toBe("empty");
|
|
});
|
|
|
|
it("classifies a completed turn with plan text only as planning-only", () => {
|
|
expect(
|
|
classifyAgentHarnessTerminalOutcome({
|
|
assistantTexts: [],
|
|
reasoningText: "",
|
|
planText: "1. inspect\n2. patch\n3. test",
|
|
promptError: null,
|
|
turnCompleted: true,
|
|
}),
|
|
).toBe("planning-only");
|
|
});
|
|
|
|
it("prefers planning-only when both plan and reasoning text are present", () => {
|
|
expect(
|
|
classifyAgentHarnessTerminalOutcome({
|
|
assistantTexts: [],
|
|
reasoningText: "I need to inspect the files.",
|
|
planText: "I will inspect, patch, and test.",
|
|
promptError: null,
|
|
turnCompleted: true,
|
|
}),
|
|
).toBe("planning-only");
|
|
});
|
|
|
|
it("classifies a completed turn with reasoning text only as reasoning-only", () => {
|
|
expect(
|
|
classifyAgentHarnessTerminalOutcome({
|
|
assistantTexts: [],
|
|
reasoningText: "The answer depends on the current repository state.",
|
|
planText: "",
|
|
promptError: null,
|
|
turnCompleted: true,
|
|
}),
|
|
).toBe("reasoning-only");
|
|
});
|
|
|
|
it("classifies a completed turn with no visible output as empty", () => {
|
|
expect(
|
|
classifyAgentHarnessTerminalOutcome({
|
|
assistantTexts: [],
|
|
reasoningText: " ",
|
|
planText: "\n",
|
|
promptError: null,
|
|
turnCompleted: true,
|
|
}),
|
|
).toBe("empty");
|
|
});
|
|
|
|
it("returns only terminal fallback classifications, not ok", () => {
|
|
const classification: AgentHarnessTerminalOutcomeClassification =
|
|
classifyAgentHarnessTerminalOutcome({
|
|
assistantTexts: [],
|
|
reasoningText: "",
|
|
planText: "",
|
|
promptError: null,
|
|
turnCompleted: true,
|
|
}) ?? "empty";
|
|
|
|
expect(classification).toBe("empty");
|
|
});
|
|
});
|
|
|
|
describe("agent harness runtime SDK facade", () => {
|
|
beforeEach(() => {
|
|
loadResearchAutocapture.mockClear();
|
|
});
|
|
|
|
it("does not load research autocapture when the SDK facade is imported", async () => {
|
|
await import("./agent-harness-runtime.js");
|
|
|
|
expect(loadResearchAutocapture).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("exposes attached model request transport metadata helpers", () => {
|
|
const model = attachModelProviderRequestTransport(
|
|
{ id: "gpt-test", provider: "custom-openai" },
|
|
{ auth: { mode: "header", headerName: "x-api-key", value: "secret" } },
|
|
);
|
|
|
|
expect(getModelProviderRequestTransport(model)).toEqual({
|
|
auth: { mode: "header", headerName: "x-api-key", value: "secret" },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("agent harness user input helpers", () => {
|
|
it("formats prompts and delivers through blocking replies first", async () => {
|
|
const onBlockReply = vi.fn();
|
|
|
|
await deliverAgentHarnessUserInputPrompt(
|
|
{ onBlockReply },
|
|
[
|
|
{
|
|
id: "mode",
|
|
header: "Mode",
|
|
question: "Pick a mode",
|
|
isOther: true,
|
|
options: [{ label: "Deep", description: "Use more context" }],
|
|
},
|
|
],
|
|
{ intro: "Runtime needs input:" },
|
|
);
|
|
|
|
expect(onBlockReply).toHaveBeenCalledWith({
|
|
text: [
|
|
"Runtime needs input:",
|
|
"",
|
|
"Mode",
|
|
"Pick a mode",
|
|
"1. Deep - Use more context",
|
|
"Other: reply with your own answer.",
|
|
].join("\n"),
|
|
});
|
|
});
|
|
|
|
it("normalizes keyed multi-question answers with option indexes", () => {
|
|
expect(
|
|
buildAgentHarnessUserInputAnswers(
|
|
[
|
|
{
|
|
id: "repo",
|
|
header: "Repository",
|
|
question: "Which repo?",
|
|
isOther: true,
|
|
},
|
|
{
|
|
id: "mode",
|
|
header: "Mode",
|
|
question: "Which mode?",
|
|
isOther: false,
|
|
options: [{ label: "Fast" }, { label: "Deep" }],
|
|
},
|
|
],
|
|
"repo: openclaw\nmode: 2",
|
|
),
|
|
).toEqual({
|
|
answers: {
|
|
mode: { answers: ["Deep"] },
|
|
repo: { answers: ["openclaw"] },
|
|
},
|
|
});
|
|
});
|
|
|
|
it("supports runtime-specific text formatting", () => {
|
|
expect(
|
|
formatAgentHarnessUserInputPrompt(
|
|
[
|
|
{
|
|
id: "answer",
|
|
header: "Header",
|
|
question: "a < b",
|
|
},
|
|
],
|
|
{ formatText: (text) => text.replaceAll("<", "<") },
|
|
),
|
|
).toContain("a < b");
|
|
});
|
|
});
|