mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-18 02:21:37 +00:00
* fix(anthropic): delegate adaptive CLI effort Strip static --effort args for adaptive runs so Claude Code resolves effort from its environment, settings, and model default. Preserve configured effort for off or absent thinking and replace it only for concrete OpenClaw levels. Fixes #103245 Co-authored-by: Dan Korotin <korotin.daniil@gmail.com> * fix(anthropic): make effort dispatch exhaustive --------- Co-authored-by: Dan Korotin <korotin.daniil@gmail.com>
411 lines
14 KiB
TypeScript
411 lines
14 KiB
TypeScript
// Anthropic tests cover cli shared plugin behavior.
|
|
import { describe, expect, it } from "vitest";
|
|
import { buildAnthropicCliBackend } from "./cli-backend.js";
|
|
import {
|
|
CLAUDE_CLI_CLEAR_ENV,
|
|
normalizeClaudeBackendConfig,
|
|
normalizeClaudePermissionArgs,
|
|
normalizeClaudeSettingSourcesArgs,
|
|
resolveClaudePermissionMode,
|
|
resolveClaudeCliExecutionArgs,
|
|
} from "./cli-shared.js";
|
|
|
|
const CLAUDE_CLI_DISALLOWED_TOOLS =
|
|
"ScheduleWakeup,CronCreate,Bash(run_in_background:true),Monitor";
|
|
|
|
function expectDefaultDisallowedTools(args: readonly string[] | undefined) {
|
|
const disallowedIndex = args?.indexOf("--disallowedTools") ?? -1;
|
|
expect(disallowedIndex).toBeGreaterThanOrEqual(0);
|
|
expect(args?.[disallowedIndex + 1]).toBe(CLAUDE_CLI_DISALLOWED_TOOLS);
|
|
}
|
|
|
|
describe("normalizeClaudePermissionArgs", () => {
|
|
it("leaves args alone when they omit permission flags", () => {
|
|
expect(
|
|
normalizeClaudePermissionArgs(["-p", "--output-format", "stream-json", "--verbose"]),
|
|
).toEqual(["-p", "--output-format", "stream-json", "--verbose"]);
|
|
});
|
|
|
|
it("removes legacy skip-permissions without adding bypassPermissions", () => {
|
|
expect(
|
|
normalizeClaudePermissionArgs(["-p", "--dangerously-skip-permissions", "--verbose"]),
|
|
).toEqual(["-p", "--verbose"]);
|
|
});
|
|
|
|
it("keeps explicit permission-mode overrides", () => {
|
|
expect(normalizeClaudePermissionArgs(["-p", "--permission-mode", "acceptEdits"])).toEqual([
|
|
"-p",
|
|
"--permission-mode",
|
|
"acceptEdits",
|
|
]);
|
|
expect(normalizeClaudePermissionArgs(["-p", "--permission-mode=acceptEdits"])).toEqual([
|
|
"-p",
|
|
"--permission-mode=acceptEdits",
|
|
]);
|
|
});
|
|
|
|
it("drops malformed permission-mode flags in both split and equals forms", () => {
|
|
expect(
|
|
normalizeClaudePermissionArgs(["-p", "--permission-mode", "--output-format", "stream-json"]),
|
|
).toEqual(["-p", "--output-format", "stream-json"]);
|
|
expect(normalizeClaudePermissionArgs(["-p", "--permission-mode="])).toEqual(["-p"]);
|
|
expect(normalizeClaudePermissionArgs(["-p", "--permission-mode=--output-format"])).toEqual([
|
|
"-p",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("normalizeClaudeSettingSourcesArgs", () => {
|
|
it("injects user-only setting sources when args omit the flag", () => {
|
|
expect(
|
|
normalizeClaudeSettingSourcesArgs(["-p", "--output-format", "stream-json", "--verbose"]),
|
|
).toEqual(["-p", "--output-format", "stream-json", "--verbose", "--setting-sources", "user"]);
|
|
});
|
|
|
|
it("forces explicit project or local setting sources back to user-only", () => {
|
|
expect(normalizeClaudeSettingSourcesArgs(["-p", "--setting-sources", "project"])).toEqual([
|
|
"-p",
|
|
"--setting-sources",
|
|
"user",
|
|
]);
|
|
expect(normalizeClaudeSettingSourcesArgs(["-p", "--setting-sources=local,user"])).toEqual([
|
|
"-p",
|
|
"--setting-sources=user",
|
|
]);
|
|
});
|
|
|
|
it("treats a bare setting-sources flag as malformed and falls back to user-only", () => {
|
|
expect(
|
|
normalizeClaudeSettingSourcesArgs([
|
|
"-p",
|
|
"--setting-sources",
|
|
"--output-format",
|
|
"stream-json",
|
|
]),
|
|
).toEqual(["-p", "--output-format", "stream-json", "--setting-sources", "user"]);
|
|
});
|
|
});
|
|
|
|
describe("Claude CLI model aliases", () => {
|
|
it("keeps pinned Claude CLI model refs on exact selectors", () => {
|
|
const aliases = buildAnthropicCliBackend().config.modelAliases;
|
|
|
|
expect(aliases?.["opus"]).toBe("opus");
|
|
expect(aliases?.["opus-4.8"]).toBe("claude-opus-4-8");
|
|
expect(aliases?.["opus-4.7"]).toBe("claude-opus-4-7");
|
|
expect(aliases?.["opus-4.6"]).toBe("claude-opus-4-6");
|
|
expect(aliases?.["claude-opus-4-8"]).toBe("claude-opus-4-8");
|
|
expect(aliases?.["claude-opus-4-7"]).toBe("claude-opus-4-7");
|
|
expect(aliases?.["claude-opus-4-6"]).toBe("claude-opus-4-6");
|
|
});
|
|
});
|
|
|
|
describe("resolveClaudeCliExecutionArgs", () => {
|
|
it.each(["off", undefined] as const)(
|
|
"preserves configured effort args when thinking is %s",
|
|
(thinkingLevel) => {
|
|
const baseArgs = ["-p", "--effort", "xhigh", "--effort=low"];
|
|
|
|
expect(
|
|
resolveClaudeCliExecutionArgs({
|
|
workspaceDir: "/tmp",
|
|
provider: "claude-cli",
|
|
modelId: "claude-sonnet-4-6",
|
|
thinkingLevel,
|
|
useResume: false,
|
|
baseArgs,
|
|
}),
|
|
).toEqual(baseArgs);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
["minimal", "low"],
|
|
["low", "low"],
|
|
["medium", "medium"],
|
|
["high", "high"],
|
|
["xhigh", "xhigh"],
|
|
["max", "max"],
|
|
] as const)("maps %s thinking to --effort %s", (thinkingLevel, effort) => {
|
|
expect(
|
|
resolveClaudeCliExecutionArgs({
|
|
workspaceDir: "/tmp",
|
|
provider: "claude-cli",
|
|
modelId: "claude-opus-4-7",
|
|
thinkingLevel,
|
|
useResume: false,
|
|
baseArgs: ["-p"],
|
|
}),
|
|
).toEqual(["-p", "--effort", effort]);
|
|
});
|
|
|
|
it("strips configured effort args when thinking is adaptive", () => {
|
|
expect(
|
|
resolveClaudeCliExecutionArgs({
|
|
workspaceDir: "/tmp",
|
|
provider: "claude-cli",
|
|
modelId: "claude-opus-4-8",
|
|
thinkingLevel: "adaptive",
|
|
useResume: true,
|
|
baseArgs: [
|
|
"-p",
|
|
"--effort",
|
|
"xhigh",
|
|
"--output-format",
|
|
"stream-json",
|
|
"--effort=low",
|
|
"--verbose",
|
|
"--resume",
|
|
"{sessionId}",
|
|
],
|
|
}),
|
|
).toEqual(["-p", "--output-format", "stream-json", "--verbose", "--resume", "{sessionId}"]);
|
|
});
|
|
|
|
it("replaces static effort args when a session thinking level is active", () => {
|
|
expect(
|
|
resolveClaudeCliExecutionArgs({
|
|
workspaceDir: "/tmp",
|
|
provider: "claude-cli",
|
|
modelId: "claude-opus-4-7",
|
|
thinkingLevel: "max",
|
|
useResume: false,
|
|
baseArgs: ["-p", "--effort", "low", "--effort=high"],
|
|
}),
|
|
).toEqual(["-p", "--effort", "max"]);
|
|
});
|
|
|
|
it("forces isolated no-tool one-shot args for side-question execution", () => {
|
|
expect(
|
|
resolveClaudeCliExecutionArgs({
|
|
workspaceDir: "/tmp",
|
|
provider: "claude-cli",
|
|
modelId: "claude-opus-4-7",
|
|
thinkingLevel: "max",
|
|
useResume: true,
|
|
executionMode: "side-question",
|
|
baseArgs: [
|
|
"-p",
|
|
"--output-format",
|
|
"stream-json",
|
|
"--allowedTools=mcp__openclaw__*",
|
|
"--allowedTools",
|
|
"Read",
|
|
"Grep",
|
|
"--permission-mode",
|
|
"bypassPermissions",
|
|
"--session-id=abc",
|
|
"--resume",
|
|
"old-session",
|
|
"--resume-session-at",
|
|
"old-message",
|
|
"--resume-session-at=old-message-equals",
|
|
"--mcp-config",
|
|
"/tmp/side-question-mcp.json",
|
|
"--bare",
|
|
"--safe-mode",
|
|
"--strict-mcp-config",
|
|
"--no-session-persistence",
|
|
"--max-turns",
|
|
"4",
|
|
"--effort",
|
|
"high",
|
|
],
|
|
}),
|
|
).toEqual([
|
|
"-p",
|
|
"--output-format",
|
|
"stream-json",
|
|
"--safe-mode",
|
|
"--tools",
|
|
"",
|
|
"--disallowedTools",
|
|
"mcp__*",
|
|
"--strict-mcp-config",
|
|
"--no-session-persistence",
|
|
"--max-turns",
|
|
"1",
|
|
"--permission-mode",
|
|
"default",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("normalizeClaudeBackendConfig", () => {
|
|
it("normalizes both args and resumeArgs for custom overrides", () => {
|
|
const normalized = normalizeClaudeBackendConfig({
|
|
command: "claude",
|
|
args: ["-p", "--output-format", "stream-json", "--verbose"],
|
|
resumeArgs: ["-p", "--output-format", "stream-json", "--verbose", "--resume", "{sessionId}"],
|
|
});
|
|
|
|
expect(normalized.args).toEqual([
|
|
"-p",
|
|
"--output-format",
|
|
"stream-json",
|
|
"--verbose",
|
|
"--setting-sources",
|
|
"user",
|
|
"--permission-mode",
|
|
"bypassPermissions",
|
|
]);
|
|
expect(normalized.resumeArgs).toEqual([
|
|
"-p",
|
|
"--output-format",
|
|
"stream-json",
|
|
"--verbose",
|
|
"--resume",
|
|
"{sessionId}",
|
|
"--setting-sources",
|
|
"user",
|
|
"--permission-mode",
|
|
"bypassPermissions",
|
|
]);
|
|
expect(normalized.output).toBe("jsonl");
|
|
expect(normalized.liveSession).toBe("claude-stdio");
|
|
expect(normalized.input).toBe("stdin");
|
|
});
|
|
|
|
it("derives Claude bypass from OpenClaw YOLO policy and disables it for safer policy", () => {
|
|
expect(resolveClaudePermissionMode({ backendId: "claude-cli" })).toEqual({
|
|
mode: "bypassPermissions",
|
|
overrideExisting: false,
|
|
});
|
|
expect(
|
|
resolveClaudePermissionMode({
|
|
backendId: "claude-cli",
|
|
config: { tools: { exec: { security: "allowlist", ask: "on-miss" } } },
|
|
}),
|
|
).toEqual({ overrideExisting: false });
|
|
});
|
|
|
|
it("derives Claude bypass from per-agent OpenClaw exec policy", () => {
|
|
expect(
|
|
resolveClaudePermissionMode({
|
|
backendId: "claude-cli",
|
|
agentId: "safe-agent",
|
|
config: {
|
|
tools: { exec: { security: "full", ask: "off" } },
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "safe-agent",
|
|
tools: { exec: { security: "allowlist", ask: "on-miss" } },
|
|
},
|
|
],
|
|
},
|
|
},
|
|
}),
|
|
).toEqual({ overrideExisting: false });
|
|
expect(
|
|
resolveClaudePermissionMode({
|
|
backendId: "claude-cli",
|
|
agentId: "yolo-agent",
|
|
config: {
|
|
tools: { exec: { security: "allowlist", ask: "on-miss" } },
|
|
agents: {
|
|
list: [
|
|
{
|
|
id: "yolo-agent",
|
|
tools: { exec: { security: "full", ask: "off" } },
|
|
},
|
|
],
|
|
},
|
|
},
|
|
}),
|
|
).toEqual({
|
|
mode: "bypassPermissions",
|
|
overrideExisting: false,
|
|
});
|
|
});
|
|
|
|
it("does not infer live stdio when explicit transport overrides are incompatible", () => {
|
|
const normalized = normalizeClaudeBackendConfig({
|
|
command: "claude",
|
|
output: "json",
|
|
input: "arg",
|
|
});
|
|
|
|
expect(normalized.output).toBe("json");
|
|
expect(normalized.liveSession).toBeUndefined();
|
|
expect(normalized.input).toBe("arg");
|
|
});
|
|
|
|
it("is wired through the anthropic cli backend normalize hook", () => {
|
|
const backend = buildAnthropicCliBackend();
|
|
const normalizeConfig = backend.normalizeConfig;
|
|
|
|
expect(normalizeConfig).toBeTypeOf("function");
|
|
|
|
const normalized = normalizeConfig?.({
|
|
...backend.config,
|
|
args: ["-p", "--output-format", "stream-json", "--verbose"],
|
|
resumeArgs: ["-p", "--output-format", "stream-json", "--verbose", "--resume", "{sessionId}"],
|
|
});
|
|
|
|
expect(normalized?.args).toContain("--setting-sources");
|
|
expect(normalized?.args).toContain("user");
|
|
expect(normalized?.args).toContain("--permission-mode");
|
|
expect(normalized?.args).toContain("bypassPermissions");
|
|
expect(normalized?.resumeArgs).toContain("--setting-sources");
|
|
expect(normalized?.resumeArgs).toContain("user");
|
|
expect(normalized?.resumeArgs).toContain("--permission-mode");
|
|
expect(normalized?.resumeArgs).toContain("bypassPermissions");
|
|
expect(normalized?.liveSession).toBe("claude-stdio");
|
|
expect(backend.resolveExecutionArgs).toBe(resolveClaudeCliExecutionArgs);
|
|
});
|
|
|
|
it("opts bundled Claude CLI into bounded raw transcript reseed without disabling native resume", () => {
|
|
const backend = buildAnthropicCliBackend();
|
|
|
|
expect(backend.config.reseedFromRawTranscriptWhenUncompacted).toBe(true);
|
|
expect(backend.config.sessionMode).toBe("always");
|
|
expect(backend.config.resumeArgs).toContain("--resume");
|
|
expect(backend.config.resumeArgs).toContain("{sessionId}");
|
|
});
|
|
|
|
it("passes system prompt on every turn (issue #80374 — systemPromptWhen must be 'always')", () => {
|
|
// Before fix this was hardcoded to "first", which silently dropped updated
|
|
// OpenClaw system prompt context on resumed / compacted claude-cli sessions.
|
|
const backend = buildAnthropicCliBackend();
|
|
expect(backend.config.systemPromptWhen).toBe("always");
|
|
});
|
|
|
|
it("leaves claude cli subscription-managed, restricts setting sources, and clears inherited env overrides", () => {
|
|
const backend = buildAnthropicCliBackend();
|
|
|
|
expect(backend.config.env).toBeUndefined();
|
|
expect(backend.config.liveSession).toBe("claude-stdio");
|
|
expect(backend.config.output).toBe("jsonl");
|
|
expect(backend.config.input).toBe("stdin");
|
|
expect(backend.config.args).toContain("--setting-sources");
|
|
expect(backend.config.args).toContain("user");
|
|
expectDefaultDisallowedTools(backend.config.args);
|
|
expect(backend.config.resumeArgs).toContain("--setting-sources");
|
|
expect(backend.config.resumeArgs).toContain("user");
|
|
expectDefaultDisallowedTools(backend.config.resumeArgs);
|
|
expect(backend.config.clearEnv).toEqual([...CLAUDE_CLI_CLEAR_ENV]);
|
|
expect(backend.config.clearEnv).toContain("ANTHROPIC_API_TOKEN");
|
|
expect(backend.config.clearEnv).toContain("ANTHROPIC_BASE_URL");
|
|
expect(backend.config.clearEnv).toContain("ANTHROPIC_CUSTOM_HEADERS");
|
|
expect(backend.config.clearEnv).toContain("ANTHROPIC_OAUTH_TOKEN");
|
|
expect(backend.config.clearEnv).toContain("CLAUDE_CONFIG_DIR");
|
|
expect(backend.config.clearEnv).toContain("CLAUDE_CODE_USE_BEDROCK");
|
|
expect(backend.config.clearEnv).toContain("CLAUDE_CODE_OAUTH_TOKEN");
|
|
expect(backend.config.clearEnv).toContain("CLAUDE_CODE_PLUGIN_CACHE_DIR");
|
|
expect(backend.config.clearEnv).toContain("CLAUDE_CODE_PLUGIN_SEED_DIR");
|
|
expect(backend.config.clearEnv).toContain("CLAUDE_CODE_REMOTE");
|
|
expect(backend.config.clearEnv).toContain("CLAUDE_CODE_USE_COWORK_PLUGINS");
|
|
expect(backend.config.clearEnv).toContain("OTEL_METRICS_EXPORTER");
|
|
expect(backend.config.clearEnv).toContain("OTEL_EXPORTER_OTLP_PROTOCOL");
|
|
expect(backend.config.clearEnv).toContain("OTEL_SDK_DISABLED");
|
|
});
|
|
|
|
it("disables native background Bash and Monitor tools in args and resumeArgs", () => {
|
|
const backend = buildAnthropicCliBackend();
|
|
|
|
expectDefaultDisallowedTools(backend.config.args);
|
|
expectDefaultDisallowedTools(backend.config.resumeArgs);
|
|
});
|
|
});
|