mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 04:50:44 +00:00
Summary: - Adds a plugin-owned CLI backend argument rewrite hook and wires Anthropic `claude-cli` to translate non-off `/think` levels into Claude Code `--effort`, with docs, changelog, API baseline, and tests. - Reproducibility: yes. Current main has a high-confidence source reproduction: choose `claude-cli`, set a non ... builds argv from backend args that contain no `--effort` even though `thinkLevel` exists on the run params. Automerge notes: - No ClawSweeper repair was needed after automerge opt-in. Validation: - ClawSweeper review passed for headbe17754009. - Required merge gates passed before the squash merge. Prepared head SHA:be17754009Review: https://github.com/openclaw/openclaw/pull/77410#issuecomment-4372812685 Co-authored-by: stainlu <stainlu@newtype-ai.org>
292 lines
10 KiB
TypeScript
292 lines
10 KiB
TypeScript
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";
|
|
|
|
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("resolveClaudeCliExecutionArgs", () => {
|
|
it("omits effort args when thinking is off", () => {
|
|
expect(
|
|
resolveClaudeCliExecutionArgs({
|
|
workspaceDir: "/tmp",
|
|
provider: "claude-cli",
|
|
modelId: "claude-sonnet-4-6",
|
|
thinkingLevel: "off",
|
|
useResume: false,
|
|
baseArgs: ["-p", "--output-format", "stream-json"],
|
|
}),
|
|
).toEqual(["-p", "--output-format", "stream-json"]);
|
|
});
|
|
|
|
it("maps OpenClaw thinking levels to Claude effort args", () => {
|
|
expect(
|
|
resolveClaudeCliExecutionArgs({
|
|
workspaceDir: "/tmp",
|
|
provider: "claude-cli",
|
|
modelId: "claude-opus-4-7",
|
|
thinkingLevel: "minimal",
|
|
useResume: false,
|
|
baseArgs: ["-p"],
|
|
}),
|
|
).toEqual(["-p", "--effort", "low"]);
|
|
expect(
|
|
resolveClaudeCliExecutionArgs({
|
|
workspaceDir: "/tmp",
|
|
provider: "claude-cli",
|
|
modelId: "claude-opus-4-7",
|
|
thinkingLevel: "adaptive",
|
|
useResume: false,
|
|
baseArgs: ["-p"],
|
|
}),
|
|
).toEqual(["-p", "--effort", "medium"]);
|
|
expect(
|
|
resolveClaudeCliExecutionArgs({
|
|
workspaceDir: "/tmp",
|
|
provider: "claude-cli",
|
|
modelId: "claude-opus-4-7",
|
|
thinkingLevel: "xhigh",
|
|
useResume: true,
|
|
baseArgs: ["-p", "--resume", "{sessionId}"],
|
|
}),
|
|
).toEqual(["-p", "--resume", "{sessionId}", "--effort", "xhigh"]);
|
|
});
|
|
|
|
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"]);
|
|
});
|
|
});
|
|
|
|
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("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");
|
|
expect(backend.config.resumeArgs).toContain("--setting-sources");
|
|
expect(backend.config.resumeArgs).toContain("user");
|
|
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");
|
|
});
|
|
});
|