ClaudeDo never passed --effort, so every session inherited whatever effortLevel the user's Claude Code config happened to carry. Settings -> General now holds one row per model alias (haiku medium/20, sonnet high/30, opus high/40, fable high/25) supplying the global effort and turn defaults; list- and task-level max-turns overrides still win, and the agent editor's inherited badge follows the model. --effort is applied to autonomous runs and to every ConPTY spec (task session, planning start/resume, ad-hoc, list handler). The model itself is deliberately not forced on interactive sessions. The single global 'Max turns' field is replaced by the table, and 'fable' joins ModelRegistry.Aliases. The migration also adds the is_manual columns used by the next commit.
248 lines
8.5 KiB
C#
248 lines
8.5 KiB
C#
using ClaudeDo.Worker.Runner;
|
|
|
|
namespace ClaudeDo.Worker.Tests.Runner;
|
|
|
|
public sealed class ClaudeArgsBuilderTests
|
|
{
|
|
private readonly ClaudeArgsBuilder _builder = new();
|
|
|
|
[Fact]
|
|
public void Default_Config_Produces_Base_Args()
|
|
{
|
|
var args = _builder.Build(new ClaudeRunConfig(null, null, null, null));
|
|
Assert.Contains("-p", args);
|
|
Assert.Contains("--output-format", args);
|
|
Assert.Contains("stream-json", args);
|
|
Assert.Contains("--verbose", args);
|
|
Assert.Contains("--permission-mode", args);
|
|
Assert.Contains("auto", args);
|
|
Assert.DoesNotContain("--dangerously-skip-permissions", args);
|
|
Assert.Contains("--json-schema", args);
|
|
Assert.DoesNotContain("--model", args);
|
|
Assert.DoesNotContain("--append-system-prompt", args);
|
|
Assert.DoesNotContain("--agents", args);
|
|
Assert.DoesNotContain("--resume", args);
|
|
Assert.DoesNotContain("--effort", args);
|
|
}
|
|
|
|
[Fact]
|
|
public void Effort_Adds_Effort_Flag()
|
|
{
|
|
var args = _builder.Build(new ClaudeRunConfig(null, null, null, null, Effort: "xhigh"));
|
|
Assert.Contains("--effort", args);
|
|
Assert.Equal("xhigh", args[args.ToList().IndexOf("--effort") + 1]);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
public void Blank_Effort_Omits_The_Flag(string? effort)
|
|
{
|
|
var args = _builder.Build(new ClaudeRunConfig(null, null, null, null, Effort: effort));
|
|
Assert.DoesNotContain("--effort", args);
|
|
}
|
|
|
|
[Fact]
|
|
public void Model_Adds_Model_Flag()
|
|
{
|
|
var args = _builder.Build(new ClaudeRunConfig("sonnet-4-6", null, null, null));
|
|
Assert.Contains("--model", args);
|
|
Assert.Contains("sonnet-4-6", args);
|
|
}
|
|
|
|
[Fact]
|
|
public void SystemPrompt_Adds_Append_System_Prompt_Flag()
|
|
{
|
|
var args = _builder.Build(new ClaudeRunConfig(null, "Be concise.", null, null));
|
|
Assert.Contains("--append-system-prompt", args);
|
|
Assert.Contains("Be concise.", args);
|
|
}
|
|
|
|
[Fact]
|
|
public void AgentPath_Adds_Agents_Flag_As_Json()
|
|
{
|
|
var args = _builder.Build(new ClaudeRunConfig(null, null, "/path/to/agent.md", null));
|
|
Assert.Contains("--agents", args);
|
|
Assert.Contains(args, a => a.Contains("/path/to/agent.md"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ResumeSessionId_Adds_Resume_Flag()
|
|
{
|
|
var args = _builder.Build(new ClaudeRunConfig(null, null, null, "sess-abc-123"));
|
|
Assert.Contains("--resume", args);
|
|
Assert.Contains("sess-abc-123", args);
|
|
}
|
|
|
|
[Fact]
|
|
public void All_Options_Set_Includes_All_Flags()
|
|
{
|
|
var args = _builder.Build(new ClaudeRunConfig("opus-4-6", "Be thorough.", "/agents/dev.md", "sess-xyz"));
|
|
Assert.Contains("--model", args);
|
|
Assert.Contains("opus-4-6", args);
|
|
Assert.Contains("--append-system-prompt", args);
|
|
Assert.Contains("--agents", args);
|
|
Assert.Contains("--resume", args);
|
|
Assert.Contains("sess-xyz", args);
|
|
Assert.Contains("--json-schema", args);
|
|
}
|
|
|
|
[Fact]
|
|
public void SystemPrompt_With_Quotes_Is_Passed_Verbatim()
|
|
{
|
|
var prompt = """Don't say "hello".""";
|
|
var args = _builder.Build(new ClaudeRunConfig(null, prompt, null, null));
|
|
Assert.Contains("--append-system-prompt", args);
|
|
Assert.Contains(prompt, args);
|
|
}
|
|
|
|
[Fact]
|
|
public void SystemPrompt_With_Newline_Is_Passed_As_Single_Element()
|
|
{
|
|
var prompt = "line1\nline2";
|
|
var args = _builder.Build(new ClaudeRunConfig(
|
|
Model: null,
|
|
SystemPrompt: prompt,
|
|
AgentPath: null,
|
|
ResumeSessionId: null));
|
|
|
|
var list = args.ToList();
|
|
var idx = list.IndexOf("--append-system-prompt");
|
|
Assert.True(idx >= 0);
|
|
Assert.Equal(prompt, list[idx + 1]);
|
|
}
|
|
|
|
[Fact]
|
|
public void SystemPrompt_With_Tab_Is_Passed_As_Single_Element()
|
|
{
|
|
var prompt = "col1\tcol2";
|
|
var args = _builder.Build(new ClaudeRunConfig(
|
|
Model: null,
|
|
SystemPrompt: prompt,
|
|
AgentPath: null,
|
|
ResumeSessionId: null));
|
|
|
|
Assert.Contains(prompt, args);
|
|
}
|
|
|
|
[Fact]
|
|
public void MaxTurns_Adds_Flag_When_Positive()
|
|
{
|
|
var args = _builder.Build(new ClaudeRunConfig(null, null, null, null, MaxTurns: 25));
|
|
Assert.Contains("--max-turns", args);
|
|
Assert.Contains("25", args);
|
|
}
|
|
|
|
[Fact]
|
|
public void MaxTurns_Omitted_When_Null_Or_ZeroOrLess()
|
|
{
|
|
var a = _builder.Build(new ClaudeRunConfig(null, null, null, null, MaxTurns: null));
|
|
var b = _builder.Build(new ClaudeRunConfig(null, null, null, null, MaxTurns: 0));
|
|
Assert.DoesNotContain("--max-turns", a);
|
|
Assert.DoesNotContain("--max-turns", b);
|
|
}
|
|
|
|
[Fact]
|
|
public void PermissionMode_bypass_Maps_To_Auto()
|
|
{
|
|
var args = _builder.Build(new ClaudeRunConfig(null, null, null, null, PermissionMode: "bypassPermissions"));
|
|
Assert.Contains("--permission-mode", args);
|
|
Assert.Contains("auto", args);
|
|
Assert.DoesNotContain("--dangerously-skip-permissions", args);
|
|
}
|
|
|
|
[Fact]
|
|
public void PermissionMode_acceptEdits_Emits_PermissionMode_Flag()
|
|
{
|
|
var args = _builder.Build(new ClaudeRunConfig(null, null, null, null, PermissionMode: "acceptEdits"));
|
|
Assert.Contains("--permission-mode", args);
|
|
Assert.Contains("acceptEdits", args);
|
|
Assert.DoesNotContain("--dangerously-skip-permissions", args);
|
|
}
|
|
|
|
[Fact]
|
|
public void PermissionMode_Null_Defaults_To_Auto()
|
|
{
|
|
var args = _builder.Build(new ClaudeRunConfig(null, null, null, null));
|
|
Assert.Contains("--permission-mode", args);
|
|
Assert.Contains("auto", args);
|
|
Assert.DoesNotContain("--dangerously-skip-permissions", args);
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_emits_mcpConfig_and_allowedTools_when_set()
|
|
{
|
|
var args = new ClaudeArgsBuilder().Build(new ClaudeRunConfig(
|
|
Model: null, SystemPrompt: null, AgentPath: null, ResumeSessionId: null,
|
|
McpConfigPath: "C:\\tmp\\t_mcp.json",
|
|
AllowedTools: "mcp__claudedo_run__SuggestImprovement"));
|
|
Assert.Contains("--mcp-config", args);
|
|
Assert.Contains(args, a => a.Contains("t_mcp.json"));
|
|
Assert.Contains("--allowedTools", args);
|
|
Assert.Contains("mcp__claudedo_run__SuggestImprovement", args);
|
|
}
|
|
|
|
[Fact]
|
|
public void SystemPrompt_ContainingDangerousFlag_IsPassedAsLiteral_NotAsFlag()
|
|
{
|
|
// If the system prompt contains "--dangerously-skip-permissions", it must arrive
|
|
// as the VALUE of --append-system-prompt, not as a standalone CLI flag.
|
|
const string dangerousPrompt = "--dangerously-skip-permissions";
|
|
var args = _builder.Build(new ClaudeRunConfig(null, dangerousPrompt, null, null));
|
|
|
|
var list = args.ToList();
|
|
var flagIdx = list.IndexOf("--append-system-prompt");
|
|
Assert.True(flagIdx >= 0, "--append-system-prompt flag must be present");
|
|
// The dangerous string sits immediately after the flag as its value.
|
|
Assert.Equal(dangerousPrompt, list[flagIdx + 1]);
|
|
// It does NOT appear as a separate element (i.e., not treated as its own flag).
|
|
Assert.Equal(1, list.Count(a => a == dangerousPrompt));
|
|
}
|
|
}
|
|
|
|
public sealed class MergeInstructionsTests
|
|
{
|
|
[Fact]
|
|
public void All_Empty_Returns_Empty()
|
|
{
|
|
var s = ClaudeDo.Worker.Runner.TaskRunner.MergeInstructions(null, null, null);
|
|
Assert.Equal("", s);
|
|
}
|
|
|
|
[Fact]
|
|
public void Only_Global_Returns_Global()
|
|
{
|
|
var s = ClaudeDo.Worker.Runner.TaskRunner.MergeInstructions("global rule", null, null);
|
|
Assert.Equal("global rule", s);
|
|
}
|
|
|
|
[Fact]
|
|
public void Only_Task_Returns_Task()
|
|
{
|
|
var s = ClaudeDo.Worker.Runner.TaskRunner.MergeInstructions(null, null, "task rule");
|
|
Assert.Equal("task rule", s);
|
|
}
|
|
|
|
[Fact]
|
|
public void Global_And_Task_Are_Prepended_With_Separator()
|
|
{
|
|
var s = ClaudeDo.Worker.Runner.TaskRunner.MergeInstructions("global rule", null, "task rule");
|
|
Assert.Equal("global rule\n\ntask rule", s);
|
|
}
|
|
|
|
[Fact]
|
|
public void All_Three_Are_Joined_In_Order()
|
|
{
|
|
var s = ClaudeDo.Worker.Runner.TaskRunner.MergeInstructions("G", "L", "T");
|
|
Assert.Equal("G\n\nL\n\nT", s);
|
|
}
|
|
|
|
[Fact]
|
|
public void Whitespace_Only_Parts_Are_Skipped()
|
|
{
|
|
var s = ClaudeDo.Worker.Runner.TaskRunner.MergeInstructions(" ", "L", "\t\n");
|
|
Assert.Equal("L", s);
|
|
}
|
|
}
|