package api import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" ) // createFakeClaude writes a shell script to a temp dir that prints output and exits with the // given code. Returns the script path. Used to mock the claude binary in elaborate tests. func createFakeClaude(t *testing.T, output string, exitCode int) string { t.Helper() dir := t.TempDir() outputFile := filepath.Join(dir, "output.json") if err := os.WriteFile(outputFile, []byte(output), 0600); err != nil { t.Fatal(err) } script := filepath.Join(dir, "claude") content := fmt.Sprintf("#!/bin/sh\ncat %q\nexit %d\n", outputFile, exitCode) if err := os.WriteFile(script, []byte(content), 0755); err != nil { t.Fatal(err) } return script } func TestElaboratePrompt_ContainsWorkDir(t *testing.T) { prompt := buildElaboratePrompt("/some/custom/path") if !strings.Contains(prompt, "/some/custom/path") { t.Error("prompt should contain the provided workDir") } if strings.Contains(prompt, "/root/workspace/claudomator") { t.Error("prompt should not hardcode /root/workspace/claudomator") } } func TestElaboratePrompt_EmptyWorkDir(t *testing.T) { prompt := buildElaboratePrompt("") if strings.Contains(prompt, "/root") { t.Error("prompt should not reference /root when workDir is empty") } } func TestElaborateTask_Success(t *testing.T) { srv, _ := testServer(t) // Build fake Claude output: {"result": ""} task := elaboratedTask{ Name: "Run Go tests with race detector", Description: "Runs the Go test suite with -race flag and checks coverage.", Agent: elaboratedAgent{ Type: "claude", Model: "sonnet", Instructions: "Run go test -race ./... and report results.", WorkingDir: "", MaxBudgetUSD: 0.5, AllowedTools: []string{"Bash"}, }, Timeout: "15m", Priority: "normal", Tags: []string{"testing", "ci"}, } taskJSON, err := json.Marshal(task) if err != nil { t.Fatal(err) } wrapper := map[string]string{"result": string(taskJSON)} wrapperJSON, err := json.Marshal(wrapper) if err != nil { t.Fatal(err) } srv.elaborateCmdPath = createFakeClaude(t, string(wrapperJSON), 0) body := `{"prompt":"run the Go test suite with race detector and fail if coverage < 80%"}` req := httptest.NewRequest("POST", "/api/tasks/elaborate", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status: want 200, got %d; body: %s", w.Code, w.Body.String()) } var result elaboratedTask if err := json.NewDecoder(w.Body).Decode(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } if result.Name == "" { t.Error("expected non-empty name") } if result.Agent.Instructions == "" { t.Error("expected non-empty instructions") } } func TestElaborateTask_EmptyPrompt(t *testing.T) { srv, _ := testServer(t) body := `{"prompt":""}` req := httptest.NewRequest("POST", "/api/tasks/elaborate", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("status: want 400, got %d; body: %s", w.Code, w.Body.String()) } var resp map[string]string json.NewDecoder(w.Body).Decode(&resp) if resp["error"] == "" { t.Error("expected error message in response") } } func TestElaborateTask_MarkdownFencedJSON(t *testing.T) { srv, _ := testServer(t) // Build a valid task JSON but wrap it in markdown fences as haiku sometimes does. task := elaboratedTask{ Name: "Test task", Description: "Does something.", Agent: elaboratedAgent{ Type: "claude", Model: "sonnet", Instructions: "Do the thing.", MaxBudgetUSD: 0.5, AllowedTools: []string{"Bash"}, }, Timeout: "15m", Priority: "normal", Tags: []string{"test"}, } taskJSON, _ := json.Marshal(task) fenced := "```json\n" + string(taskJSON) + "\n```" wrapper := map[string]string{"result": fenced} wrapperJSON, _ := json.Marshal(wrapper) srv.elaborateCmdPath = createFakeClaude(t, string(wrapperJSON), 0) body := `{"prompt":"do something"}` req := httptest.NewRequest("POST", "/api/tasks/elaborate", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status: want 200, got %d; body: %s", w.Code, w.Body.String()) } var result elaboratedTask if err := json.NewDecoder(w.Body).Decode(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } if result.Name != task.Name { t.Errorf("name: want %q, got %q", task.Name, result.Name) } } func TestElaborateTask_InvalidJSONFromClaude(t *testing.T) { srv, _ := testServer(t) // Fake Claude returns something that is not valid JSON. srv.elaborateCmdPath = createFakeClaude(t, "not valid json at all", 0) body := `{"prompt":"do something"}` req := httptest.NewRequest("POST", "/api/tasks/elaborate", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusBadGateway { t.Fatalf("status: want 502, got %d; body: %s", w.Code, w.Body.String()) } var resp map[string]string json.NewDecoder(w.Body).Decode(&resp) if resp["error"] == "" { t.Error("expected error message in response") } }