From fca5d57fefd09ecd054757e5a36404c88c118f07 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Tue, 21 Apr 2026 15:55:40 +0200 Subject: [PATCH] feat(worker): extend ClaudeArgsBuilder with MaxTurns and PermissionMode --- .../Runner/ClaudeArgsBuilder.cs | 14 +++- .../Runner/ClaudeArgsBuilderTests.cs | 84 +++++++++++++++++++ 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/src/ClaudeDo.Worker/Runner/ClaudeArgsBuilder.cs b/src/ClaudeDo.Worker/Runner/ClaudeArgsBuilder.cs index 26cc9e1..15a649f 100644 --- a/src/ClaudeDo.Worker/Runner/ClaudeArgsBuilder.cs +++ b/src/ClaudeDo.Worker/Runner/ClaudeArgsBuilder.cs @@ -6,7 +6,9 @@ public sealed record ClaudeRunConfig( string? Model, string? SystemPrompt, string? AgentPath, - string? ResumeSessionId + string? ResumeSessionId, + int? MaxTurns = null, + string? PermissionMode = null ); public sealed class ClaudeArgsBuilder @@ -30,12 +32,20 @@ public sealed class ClaudeArgsBuilder "-p", "--output-format stream-json", "--verbose", - "--dangerously-skip-permissions", }; + var permissionMode = string.IsNullOrWhiteSpace(config.PermissionMode) ? "bypassPermissions" : config.PermissionMode; + if (permissionMode.Equals("bypassPermissions", StringComparison.OrdinalIgnoreCase)) + args.Add("--dangerously-skip-permissions"); + else + args.Add($"--permission-mode {permissionMode}"); + if (config.Model is not null) args.Add($"--model {config.Model}"); + if (config.MaxTurns is int turns && turns > 0) + args.Add($"--max-turns {turns}"); + if (config.SystemPrompt is not null) args.Add($"--append-system-prompt {Escape(config.SystemPrompt)}"); diff --git a/tests/ClaudeDo.Worker.Tests/Runner/ClaudeArgsBuilderTests.cs b/tests/ClaudeDo.Worker.Tests/Runner/ClaudeArgsBuilderTests.cs index 2d38a5a..a0e585d 100644 --- a/tests/ClaudeDo.Worker.Tests/Runner/ClaudeArgsBuilderTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Runner/ClaudeArgsBuilderTests.cs @@ -92,4 +92,88 @@ public sealed class ClaudeArgsBuilderTests Assert.Contains("\"col1", 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 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_Keeps_DangerousFlag() + { + var args = _builder.Build(new ClaudeRunConfig(null, null, null, null, PermissionMode: "bypassPermissions")); + Assert.Contains("--dangerously-skip-permissions", args); + Assert.DoesNotContain("--permission-mode", 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 acceptEdits", args); + Assert.DoesNotContain("--dangerously-skip-permissions", args); + } + + [Fact] + public void PermissionMode_Null_Defaults_To_BypassPermissions() + { + var args = _builder.Build(new ClaudeRunConfig(null, null, null, null)); + Assert.Contains("--dangerously-skip-permissions", args); + } +} + +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); + } }