From d0d7633a0467da94cb2a07356fafa74e0d95bfe2 Mon Sep 17 00:00:00 2001 From: Benjamin Goosman Date: Mon, 2 Mar 2026 13:00:56 -0500 Subject: [PATCH 1/6] fix: add cursor agent support --- internal/agent/agent.go | 1 + internal/agent/agent_test.go | 27 +++++++++++++++++++++++++++ internal/daemon/server_jobs_test.go | 19 +++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 41d2b5ea..1e3f6854 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -106,6 +106,7 @@ func SetAnthropicAPIKey(key string) { // aliases maps short names to full agent names var aliases = map[string]string{ "claude": "claude-code", + "agent": "cursor", } // resolveAlias returns the canonical agent name, resolving aliases diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index bfbcbde1..2c4cfbd1 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -31,6 +31,33 @@ func TestAgentRegistry(t *testing.T) { } } +func TestCanonicalNameAliases(t *testing.T) { + tests := []struct { + input string + want string + }{ + {input: "claude", want: "claude-code"}, + {input: "agent", want: "cursor"}, + {input: "cursor", want: "cursor"}, + } + + for _, tt := range tests { + if got := CanonicalName(tt.input); got != tt.want { + t.Errorf("CanonicalName(%q) = %q, want %q", tt.input, got, tt.want) + } + } +} + +func TestGetSupportsAgentAlias(t *testing.T) { + a, err := Get("agent") + if err != nil { + t.Fatalf("Get(agent) returned error: %v", err) + } + if a.Name() != "cursor" { + t.Fatalf("Get(agent) resolved to %q, want %q", a.Name(), "cursor") + } +} + func TestAvailableAgents(t *testing.T) { agents := Available() if len(agents) < len(expectedAgents) { diff --git a/internal/daemon/server_jobs_test.go b/internal/daemon/server_jobs_test.go index 92dc6e35..f239e6c7 100644 --- a/internal/daemon/server_jobs_test.go +++ b/internal/daemon/server_jobs_test.go @@ -908,6 +908,7 @@ func TestHandleEnqueueAgentAvailability(t *testing.T) { tests := []struct { name string requestAgent string + defaultAgent string mockBinaries []string // binary names to place in PATH expectedAgent string // expected agent stored in job expectedCode int // expected HTTP status code @@ -933,6 +934,21 @@ func TestHandleEnqueueAgentAvailability(t *testing.T) { expectedAgent: "claude-code", expectedCode: http.StatusCreated, }, + { + name: "explicit agent alias resolves to cursor", + requestAgent: "agent", + mockBinaries: []string{"agent"}, + expectedAgent: "cursor", + expectedCode: http.StatusCreated, + }, + { + name: "default agent alias resolves to cursor", + requestAgent: "", + defaultAgent: "agent", + mockBinaries: []string{"agent"}, + expectedAgent: "cursor", + expectedCode: http.StatusCreated, + }, { name: "explicit codex kept when available", requestAgent: "codex", @@ -959,6 +975,9 @@ func TestHandleEnqueueAgentAvailability(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Each subtest gets its own server/DB to avoid SHA dedup conflicts server, _, _ := newTestServer(t) + if tt.defaultAgent != "" { + server.configWatcher.Config().DefaultAgent = tt.defaultAgent + } // Isolate PATH: only mock binaries + git (no real agent CLIs) origPath := os.Getenv("PATH") From 29d47b0be672ddef87f7a33a801fb82764f2ead9 Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Thu, 5 Mar 2026 08:24:54 -0600 Subject: [PATCH 2/6] fix: reject unknown agent names and fix ACP alias collision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GetAvailable() now returns an error for unrecognized agent names (typos like "claud") instead of silently falling back. Known-but- unavailable agents (binary not installed) still fall back as before. Fix isConfiguredACPAgentName() to use exact comparison instead of alias-resolved comparison, preventing the "agent" → "cursor" alias from incorrectly matching ACP config when acp.name = "agent". Co-Authored-By: Claude Opus 4.6 --- internal/agent/acp.go | 13 ++++------ internal/agent/acp_test.go | 46 +++++++++++++++++++++++++++++++++++- internal/agent/agent.go | 15 ++++++++++++ internal/agent/agent_test.go | 28 ++++++++++++++++++++++ 4 files changed, 92 insertions(+), 10 deletions(-) diff --git a/internal/agent/acp.go b/internal/agent/acp.go index ace326d3..5903c52c 100644 --- a/internal/agent/acp.go +++ b/internal/agent/acp.go @@ -1485,13 +1485,7 @@ func isConfiguredACPAgentName(name string, cfg *config.Config) bool { return false } - // Compare both raw and alias-normalized forms so ACP names configured as - // aliases (for example "claude") still match canonical requests - // (for example "claude-code"). - if rawName == configuredName { - return true - } - return resolveAlias(rawName) == resolveAlias(configuredName) + return rawName == configuredName } func configuredACPAgent(cfg *config.Config) *ACPAgent { @@ -1509,9 +1503,10 @@ func configuredACPAgent(cfg *config.Config) *ACPAgent { // It treats cfg.ACP.Name as an alias for "acp" and applies cfg.ACP command/mode/model // at resolution time instead of package-init time. func GetAvailableWithConfig(preferred string, cfg *config.Config) (Agent, error) { - preferred = resolveAlias(strings.TrimSpace(preferred)) + rawPreferred := strings.TrimSpace(preferred) + preferred = resolveAlias(rawPreferred) - if isConfiguredACPAgentName(preferred, cfg) { + if isConfiguredACPAgentName(rawPreferred, cfg) { acpAgent := configuredACPAgent(cfg) if _, err := exec.LookPath(acpAgent.CommandName()); err == nil { return acpAgent, nil diff --git a/internal/agent/acp_test.go b/internal/agent/acp_test.go index 168989d7..81a50a1e 100644 --- a/internal/agent/acp_test.go +++ b/internal/agent/acp_test.go @@ -321,7 +321,7 @@ func TestGetAvailableWithConfigResolvedACPBranchFallsBackWhenConfiguredCommandMi }, } - resolved, err := GetAvailableWithConfig("nonexistent-agent", cfg) + resolved, err := GetAvailableWithConfig("custom-acp", cfg) if err != nil { t.Fatalf("GetAvailableWithConfig failed: %v", err) } @@ -1113,3 +1113,47 @@ func TestReadTextFileWindow(t *testing.T) { } }) } + +func TestACPAliasCollisionFixed(t *testing.T) { + // When acp.name = "agent", requesting "cursor" should resolve to the + // real cursor agent, not to ACP via the "agent" → "cursor" alias. + // The cursor agent's binary is called "agent" (not "cursor"). + fakeBin := t.TempDir() + agentBin := "agent" + if runtime.GOOS == "windows" { + agentBin += ".exe" + } + agentPath := filepath.Join(fakeBin, agentBin) + if err := os.WriteFile(agentPath, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { + t.Fatalf("failed to create fake agent binary: %v", err) + } + t.Setenv("PATH", fakeBin) + + cfg := &config.Config{ + ACP: &config.ACPAgentConfig{ + Name: "agent", + Command: "acp-agent", + }, + } + + resolved, err := GetAvailableWithConfig("cursor", cfg) + if err != nil { + t.Fatalf("GetAvailableWithConfig failed: %v", err) + } + + if resolved.Name() != "cursor" { + t.Fatalf("Expected cursor agent, got %q", resolved.Name()) + } +} + +func TestGetAvailableWithConfigUnknownAgentErrors(t *testing.T) { + cfg := &config.Config{} + + _, err := GetAvailableWithConfig("typo-agent", cfg) + if err == nil { + t.Fatal("Expected error for unknown agent name") + } + if !strings.Contains(err.Error(), "unknown agent") { + t.Fatalf("Expected 'unknown agent' error, got: %v", err) + } +} diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 1e3f6854..771e7516 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -5,6 +5,8 @@ import ( "fmt" "io" "os/exec" + "sort" + "strings" "sync" "sync/atomic" ) @@ -173,6 +175,19 @@ func GetAvailable(preferred string) (Agent, error) { // Resolve alias upfront for consistent comparisons preferred = resolveAlias(preferred) + // Reject unknown agent names (typos, config mistakes). + // Known-but-unavailable agents still fall back below. + if preferred != "" { + if _, ok := registry[preferred]; !ok { + known := Available() + sort.Strings(known) + return nil, fmt.Errorf( + "unknown agent %q (known: %s)", + preferred, strings.Join(known, ", "), + ) + } + } + // Try preferred agent first if preferred != "" && IsAvailable(preferred) { return Get(preferred) diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index 2c4cfbd1..7d7d7d4e 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -388,3 +388,31 @@ func TestAgentReviewPassesModelFlag(t *testing.T) { }) } } + +func TestGetAvailableRejectsUnknownAgent(t *testing.T) { + _, err := GetAvailable("typo-agent") + if err == nil { + t.Fatal("Expected error for unknown agent name") + } + if !strings.Contains(err.Error(), "unknown agent") { + t.Fatalf("Expected 'unknown agent' error, got: %v", err) + } +} + +func TestGetAvailableFallsBackForKnownUnavailable(t *testing.T) { + // "codex" is a registered agent but its binary is typically not + // installed in the test environment. GetAvailable should fall back + // to another available agent instead of returning an "unknown agent" + // error. + resolved, err := GetAvailable("codex") + if err != nil { + if strings.Contains(err.Error(), "unknown agent") { + t.Fatalf("Known agent 'codex' should not produce unknown agent error: %v", err) + } + // "no agents available" is acceptable in a minimal test env + return + } + if resolved == nil { + t.Fatal("Expected a non-nil agent") + } +} From 51a16d61c051e7d46da3c939be7e6c245876a25e Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Thu, 5 Mar 2026 09:21:48 -0600 Subject: [PATCH 3/6] fix: improve test determinism and add ACP canonical-match regression test Make TestGetAvailableFallsBackForKnownUnavailable deterministic by isolating the registry and PATH instead of relying on ambient state. Add TestACPNameDoesNotMatchCanonicalRequest to lock the contract that acp.name="claude" matches request "claude" but not "claude-code". Add clarifying comment to isConfiguredACPAgentName explaining exact match semantics. Co-Authored-By: Claude Opus 4.6 --- internal/agent/acp.go | 4 ++++ internal/agent/acp_test.go | 34 ++++++++++++++++++++++++++++++++++ internal/agent/agent_test.go | 31 ++++++++++++++++++++++--------- 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/internal/agent/acp.go b/internal/agent/acp.go index 5903c52c..bbe7e73c 100644 --- a/internal/agent/acp.go +++ b/internal/agent/acp.go @@ -1485,6 +1485,10 @@ func isConfiguredACPAgentName(name string, cfg *config.Config) bool { return false } + // Exact match only — no alias resolution. This prevents collisions + // where an alias like "agent" → "cursor" would incorrectly route + // cursor requests to ACP. Callers pass rawPreferred (pre-alias) so + // `acp.name = "claude"` matches request "claude" but not "claude-code". return rawName == configuredName } diff --git a/internal/agent/acp_test.go b/internal/agent/acp_test.go index 81a50a1e..31a23248 100644 --- a/internal/agent/acp_test.go +++ b/internal/agent/acp_test.go @@ -1157,3 +1157,37 @@ func TestGetAvailableWithConfigUnknownAgentErrors(t *testing.T) { t.Fatalf("Expected 'unknown agent' error, got: %v", err) } } + +func TestACPNameDoesNotMatchCanonicalRequest(t *testing.T) { + // acp.name = "claude" should match request "claude" but NOT "claude-code". + // Requesting the canonical name should go to the real agent, not ACP. + fakeBin := t.TempDir() + binName := "claude" + if runtime.GOOS == "windows" { + binName += ".exe" + } + claudePath := filepath.Join(fakeBin, binName) + if err := os.WriteFile(claudePath, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { + t.Fatalf("failed to create fake claude binary: %v", err) + } + t.Setenv("PATH", fakeBin) + + cfg := &config.Config{ + ACP: &config.ACPAgentConfig{ + Name: "claude", + Command: defaultACPCommand, + }, + } + + resolved, err := GetAvailableWithConfig("claude-code", cfg) + if err != nil { + t.Fatalf("GetAvailableWithConfig failed: %v", err) + } + + if resolved.Name() == "acp" { + t.Fatalf("Request for 'claude-code' should not route to ACP when acp.name='claude'") + } + if resolved.Name() != "claude-code" { + t.Fatalf("Expected claude-code agent, got %q", resolved.Name()) + } +} diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index 7d7d7d4e..b2748cd0 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -5,6 +5,7 @@ import ( "context" "errors" "os" + "path/filepath" "strings" "sync" "testing" @@ -400,19 +401,31 @@ func TestGetAvailableRejectsUnknownAgent(t *testing.T) { } func TestGetAvailableFallsBackForKnownUnavailable(t *testing.T) { - // "codex" is a registered agent but its binary is typically not - // installed in the test environment. GetAvailable should fall back - // to another available agent instead of returning an "unknown agent" - // error. + // Isolate registry: "codex" has a missing binary, "claude-code" + // has its binary on PATH. Request "codex" and verify fallback + // returns "claude-code" without an "unknown agent" error. + fakeBin := t.TempDir() + claudeBin := filepath.Join(fakeBin, "claude") + if err := os.WriteFile(claudeBin, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { + t.Fatalf("failed to create fake claude binary: %v", err) + } + t.Setenv("PATH", fakeBin) + + originalRegistry := registry + registry = map[string]Agent{ + "codex": NewCodexAgent("definitely-not-on-path"), + "claude-code": NewClaudeAgent(""), + } + t.Cleanup(func() { registry = originalRegistry }) + resolved, err := GetAvailable("codex") if err != nil { if strings.Contains(err.Error(), "unknown agent") { - t.Fatalf("Known agent 'codex' should not produce unknown agent error: %v", err) + t.Fatalf("Known agent should not produce unknown agent error: %v", err) } - // "no agents available" is acceptable in a minimal test env - return + t.Fatalf("Expected fallback, got error: %v", err) } - if resolved == nil { - t.Fatal("Expected a non-nil agent") + if resolved.Name() != "claude-code" { + t.Fatalf("Expected fallback to 'claude-code', got %q", resolved.Name()) } } From 59dc3068dae814b17f6bb9d2cce3ffe0a32096a1 Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Thu, 5 Mar 2026 09:52:39 -0600 Subject: [PATCH 4/6] fix: add Windows .exe extension to fake binary in fallback test Append .exe on Windows so exec.LookPath resolves the stub binary via PATHEXT, matching the pattern used in other agent tests. Co-Authored-By: Claude Opus 4.6 --- internal/agent/agent_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index b2748cd0..460bb7ad 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -6,6 +6,7 @@ import ( "errors" "os" "path/filepath" + "runtime" "strings" "sync" "testing" @@ -405,7 +406,11 @@ func TestGetAvailableFallsBackForKnownUnavailable(t *testing.T) { // has its binary on PATH. Request "codex" and verify fallback // returns "claude-code" without an "unknown agent" error. fakeBin := t.TempDir() - claudeBin := filepath.Join(fakeBin, "claude") + binName := "claude" + if runtime.GOOS == "windows" { + binName += ".exe" + } + claudeBin := filepath.Join(fakeBin, binName) if err := os.WriteFile(claudeBin, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { t.Fatalf("failed to create fake claude binary: %v", err) } From 9454aea3c463cccc6b966cf1db274d7e88c8b8ae Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Thu, 5 Mar 2026 09:57:24 -0600 Subject: [PATCH 5/6] fix: return 400 for unknown agent names, 503 for no agents available Add UnknownAgentError typed error so callers can distinguish typos (bad request) from genuinely unavailable agents (service unavailable). Server enqueue and fix endpoints now return 400 with "invalid agent" for unknown names instead of 503 with "no review agent available". Co-Authored-By: Claude Opus 4.6 --- internal/agent/agent.go | 22 ++++++++++++++++++---- internal/daemon/server.go | 16 +++++++++++++--- internal/daemon/server_jobs_test.go | 6 ++++++ 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 771e7516..55f6a26c 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -105,6 +105,20 @@ func SetAnthropicAPIKey(key string) { anthropicAPIKey.Store(key) } +// UnknownAgentError is returned when a requested agent name is not +// in the registry and is not a recognized alias. +type UnknownAgentError struct { + Name string + Known []string +} + +func (e *UnknownAgentError) Error() string { + return fmt.Errorf( + "unknown agent %q (known: %s)", + e.Name, strings.Join(e.Known, ", "), + ).Error() +} + // aliases maps short names to full agent names var aliases = map[string]string{ "claude": "claude-code", @@ -181,10 +195,10 @@ func GetAvailable(preferred string) (Agent, error) { if _, ok := registry[preferred]; !ok { known := Available() sort.Strings(known) - return nil, fmt.Errorf( - "unknown agent %q (known: %s)", - preferred, strings.Join(known, ", "), - ) + return nil, &UnknownAgentError{ + Name: preferred, + Known: known, + } } } diff --git a/internal/daemon/server.go b/internal/daemon/server.go index b872ea94..8211f85c 100644 --- a/internal/daemon/server.go +++ b/internal/daemon/server.go @@ -606,9 +606,14 @@ func (s *Server) handleEnqueue(w http.ResponseWriter, r *http.Request) { // Resolve to an installed agent: if the configured agent isn't available, // fall back through the chain (codex -> claude-code -> gemini -> ...). - // Fail fast with 503 if nothing is installed at all. + // Unknown agent names (typos) return 400; no agents at all returns 503. if resolved, err := agent.GetAvailableWithConfig(agentName, cfg); err != nil { - writeError(w, http.StatusServiceUnavailable, fmt.Sprintf("no review agent available: %v", err)) + var unknownErr *agent.UnknownAgentError + if errors.As(err, &unknownErr) { + writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid agent: %v", err)) + } else { + writeError(w, http.StatusServiceUnavailable, fmt.Sprintf("no review agent available: %v", err)) + } return } else { agentName = resolved.Name() @@ -1972,7 +1977,12 @@ func (s *Server) handleFixJob(w http.ResponseWriter, r *http.Request) { reasoning := "standard" agentName := config.ResolveAgentForWorkflow("", parentJob.RepoPath, cfg, "fix", reasoning) if resolved, err := agent.GetAvailableWithConfig(agentName, cfg); err != nil { - writeError(w, http.StatusServiceUnavailable, fmt.Sprintf("no agent available: %v", err)) + var unknownErr *agent.UnknownAgentError + if errors.As(err, &unknownErr) { + writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid agent: %v", err)) + } else { + writeError(w, http.StatusServiceUnavailable, fmt.Sprintf("no agent available: %v", err)) + } return } else { agentName = resolved.Name() diff --git a/internal/daemon/server_jobs_test.go b/internal/daemon/server_jobs_test.go index f239e6c7..34a704d8 100644 --- a/internal/daemon/server_jobs_test.go +++ b/internal/daemon/server_jobs_test.go @@ -969,6 +969,12 @@ func TestHandleEnqueueAgentAvailability(t *testing.T) { mockBinaries: nil, expectedCode: http.StatusServiceUnavailable, }, + { + name: "unknown agent returns 400", + requestAgent: "typo-agent", + mockBinaries: nil, + expectedCode: http.StatusBadRequest, + }, } for _, tt := range tests { From 7de28c44701a313d1ea59c0010e926169a78fb06 Mon Sep 17 00:00:00 2001 From: Wes McKinney Date: Thu, 5 Mar 2026 10:21:23 -0600 Subject: [PATCH 6/6] fix: add fix-endpoint agent availability tests, use fmt.Sprintf Add TestHandleFixJobAgentAvailability covering the 400/503 split in handleFixJob: unknown fix agent returns 400, no agents returns 503. Replace unnecessary fmt.Errorf().Error() with fmt.Sprintf in UnknownAgentError.Error() to avoid allocating a throwaway error. Co-Authored-By: Claude Opus 4.6 --- internal/agent/agent.go | 4 +- internal/daemon/server_ops_test.go | 93 ++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 55f6a26c..ab5992e1 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -113,10 +113,10 @@ type UnknownAgentError struct { } func (e *UnknownAgentError) Error() string { - return fmt.Errorf( + return fmt.Sprintf( "unknown agent %q (known: %s)", e.Name, strings.Join(e.Known, ", "), - ).Error() + ) } // aliases maps short names to full agent names diff --git a/internal/daemon/server_ops_test.go b/internal/daemon/server_ops_test.go index 59f73426..51899fcd 100644 --- a/internal/daemon/server_ops_test.go +++ b/internal/daemon/server_ops_test.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "testing" "time" @@ -906,3 +907,95 @@ func TestHandleFixJobStaleValidation(t *testing.T) { } }) } + +func TestHandleFixJobAgentAvailability(t *testing.T) { + // Create shared git repo + completed parent review job. + gitPath, err := exec.LookPath("git") + if err != nil { + t.Fatal("git not found in PATH") + } + gitOnlyDir := t.TempDir() + if runtime.GOOS == "windows" { + wrapper := fmt.Sprintf("@\"%s\" %%*\r\n", gitPath) + if err := os.WriteFile(filepath.Join(gitOnlyDir, "git.cmd"), []byte(wrapper), 0755); err != nil { + t.Fatal(err) + } + } else { + wrapper := fmt.Sprintf("#!/bin/sh\nexec '%s' \"$@\"\n", gitPath) + if err := os.WriteFile(filepath.Join(gitOnlyDir, "git"), []byte(wrapper), 0755); err != nil { + t.Fatal(err) + } + } + + tests := []struct { + name string + fixAgent string + mockBinaries []string + expectedCode int + }{ + { + name: "unknown fix agent returns 400", + fixAgent: "typo-agent", + mockBinaries: nil, + expectedCode: http.StatusBadRequest, + }, + { + name: "no agents available returns 503", + fixAgent: "codex", + mockBinaries: nil, + expectedCode: http.StatusServiceUnavailable, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server, db, tmpDir := newTestServer(t) + server.configWatcher.Config().FixAgent = tt.fixAgent + + repoDir := filepath.Join(tmpDir, "repo-fix-avail") + testutil.InitTestGitRepo(t, repoDir) + repo, _ := db.GetOrCreateRepo(repoDir) + commit, _ := db.GetOrCreateCommit( + repo.ID, "fix-avail-abc", "Author", "Subject", time.Now(), + ) + reviewJob, _ := db.EnqueueJob(storage.EnqueueOpts{ + RepoID: repo.ID, + CommitID: commit.ID, + GitRef: "fix-avail-abc", + Agent: "test", + }) + db.ClaimJob("w1") + db.CompleteJob(reviewJob.ID, "test", "prompt", "FAIL: issues found") + + // Isolate PATH + mockDir := t.TempDir() + mockScript := "#!/bin/sh\nexit 0\n" + for _, bin := range tt.mockBinaries { + name := bin + content := mockScript + if runtime.GOOS == "windows" { + name = bin + ".cmd" + content = "@exit /b 0\r\n" + } + if err := os.WriteFile(filepath.Join(mockDir, name), []byte(content), 0755); err != nil { + t.Fatal(err) + } + } + origPath := os.Getenv("PATH") + os.Setenv("PATH", mockDir+string(os.PathListSeparator)+gitOnlyDir) + t.Cleanup(func() { os.Setenv("PATH", origPath) }) + + req := testutil.MakeJSONRequest( + t, http.MethodPost, "/api/job/fix", + fixJobRequest{ParentJobID: reviewJob.ID}, + ) + w := httptest.NewRecorder() + server.handleFixJob(w, req) + + if w.Code != tt.expectedCode { + t.Fatalf("Expected status %d, got %d: %s", + tt.expectedCode, w.Code, w.Body.String()) + } + }) + } +}