package executor import ( "context" "io" "log/slog" "strings" "testing" "github.com/thepeterstone/claudomator/internal/storage" "github.com/thepeterstone/claudomator/internal/task" ) func TestClaudeRunner_BuildArgs_BasicTask(t *testing.T) { r := &ClaudeRunner{} tk := &task.Task{ Claude: task.ClaudeConfig{ Instructions: "fix the bug", Model: "sonnet", SkipPlanning: true, }, } args := r.buildArgs(tk) expected := []string{"-p", "fix the bug", "--output-format", "stream-json", "--verbose", "--model", "sonnet"} if len(args) != len(expected) { t.Fatalf("args length: want %d, got %d: %v", len(expected), len(args), args) } for i, want := range expected { if args[i] != want { t.Errorf("arg[%d]: want %q, got %q", i, want, args[i]) } } } func TestClaudeRunner_BuildArgs_FullConfig(t *testing.T) { r := &ClaudeRunner{} tk := &task.Task{ Claude: task.ClaudeConfig{ Instructions: "implement feature", Model: "opus", MaxBudgetUSD: 5.0, PermissionMode: "bypassPermissions", SystemPromptAppend: "Follow TDD", AllowedTools: []string{"Bash", "Edit"}, DisallowedTools: []string{"Write"}, ContextFiles: []string{"/src"}, AdditionalArgs: []string{"--verbose"}, SkipPlanning: true, }, } args := r.buildArgs(tk) // Check key args are present. argMap := make(map[string]bool) for _, a := range args { argMap[a] = true } requiredArgs := []string{ "-p", "implement feature", "--output-format", "stream-json", "--model", "opus", "--max-budget-usd", "5.00", "--permission-mode", "bypassPermissions", "--append-system-prompt", "Follow TDD", "--allowedTools", "Bash", "Edit", "--disallowedTools", "Write", "--add-dir", "/src", "--verbose", } for _, req := range requiredArgs { if !argMap[req] { t.Errorf("missing arg %q in %v", req, args) } } } func TestClaudeRunner_BuildArgs_AlwaysIncludesVerbose(t *testing.T) { r := &ClaudeRunner{} tk := &task.Task{ Claude: task.ClaudeConfig{ Instructions: "do something", SkipPlanning: true, }, } args := r.buildArgs(tk) found := false for _, a := range args { if a == "--verbose" { found = true break } } if !found { t.Errorf("--verbose missing from args: %v", args) } } func TestClaudeRunner_BuildArgs_PreamblePrepended(t *testing.T) { r := &ClaudeRunner{} tk := &task.Task{ Claude: task.ClaudeConfig{ Instructions: "fix the bug", SkipPlanning: false, }, } args := r.buildArgs(tk) // The -p value should start with the preamble and end with the original instructions. if len(args) < 2 || args[0] != "-p" { t.Fatalf("expected -p as first arg, got: %v", args) } if !strings.HasPrefix(args[1], planningPreamble) { t.Errorf("instructions should start with planning preamble") } if !strings.HasSuffix(args[1], "fix the bug") { t.Errorf("instructions should end with original instructions") } } func TestClaudeRunner_BuildArgs_PreambleAddsBash(t *testing.T) { r := &ClaudeRunner{} tk := &task.Task{ Claude: task.ClaudeConfig{ Instructions: "do work", AllowedTools: []string{"Read"}, SkipPlanning: false, }, } args := r.buildArgs(tk) // Bash should be appended to allowed tools. foundBash := false for i, a := range args { if a == "--allowedTools" && i+1 < len(args) && args[i+1] == "Bash" { foundBash = true } } if !foundBash { t.Errorf("Bash should be added to --allowedTools when preamble is active: %v", args) } } func TestClaudeRunner_BuildArgs_PreambleBashNotDuplicated(t *testing.T) { r := &ClaudeRunner{} tk := &task.Task{ Claude: task.ClaudeConfig{ Instructions: "do work", AllowedTools: []string{"Bash", "Read"}, SkipPlanning: false, }, } args := r.buildArgs(tk) // Count Bash occurrences in --allowedTools values. bashCount := 0 for i, a := range args { if a == "--allowedTools" && i+1 < len(args) && args[i+1] == "Bash" { bashCount++ } } if bashCount != 1 { t.Errorf("Bash should appear exactly once in --allowedTools, got %d: %v", bashCount, args) } } func TestClaudeRunner_Run_InaccessibleWorkingDir_ReturnsError(t *testing.T) { r := &ClaudeRunner{ BinaryPath: "true", // would succeed if it ran Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), LogDir: t.TempDir(), } tk := &task.Task{ Claude: task.ClaudeConfig{ WorkingDir: "/nonexistent/path/does/not/exist", SkipPlanning: true, }, } exec := &storage.Execution{ID: "test-exec"} err := r.Run(context.Background(), tk, exec) if err == nil { t.Fatal("expected error for inaccessible working_dir, got nil") } if !strings.Contains(err.Error(), "working_dir") { t.Errorf("expected 'working_dir' in error, got: %v", err) } } func TestClaudeRunner_BinaryPath_Default(t *testing.T) { r := &ClaudeRunner{} if r.binaryPath() != "claude" { t.Errorf("want 'claude', got %q", r.binaryPath()) } } func TestClaudeRunner_BinaryPath_Custom(t *testing.T) { r := &ClaudeRunner{BinaryPath: "/usr/local/bin/claude"} if r.binaryPath() != "/usr/local/bin/claude" { t.Errorf("want custom path, got %q", r.binaryPath()) } }