diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-03-03 21:15:50 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-03-03 21:15:50 +0000 |
| commit | 74cc740398cf2d90804ab19db728c844c2e056b7 (patch) | |
| tree | e8532d1da9273e1613beb7b762b16134da0de286 /internal/api/elaborate_test.go | |
| parent | f527972f4d8311a09e639ede6c4da4ca669cfd5e (diff) | |
Add elaborate, logs-stream, templates, and subtask-list endpoints
- POST /api/tasks/elaborate: calls claude to draft a task config from
a natural-language prompt
- GET /api/executions/{id}/logs/stream: SSE tail of stdout.log
- CRUD /api/templates: create/list/get/update/delete reusable task configs
- GET /api/tasks/{id}/subtasks: list child tasks
- Server.NewServer accepts claudeBinPath for elaborate; injectable
elaborateCmdPath and logStore for test isolation
- Valid-transition guard added to POST /api/tasks/{id}/run
- CLI passes claude binary path through to the server
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/api/elaborate_test.go')
| -rw-r--r-- | internal/api/elaborate_test.go | 171 |
1 files changed, 171 insertions, 0 deletions
diff --git a/internal/api/elaborate_test.go b/internal/api/elaborate_test.go new file mode 100644 index 0000000..ff158a8 --- /dev/null +++ b/internal/api/elaborate_test.go @@ -0,0 +1,171 @@ +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "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 TestElaborateTask_Success(t *testing.T) { + srv, _ := testServer(t) + + // Build fake Claude output: {"result": "<task-json>"} + task := elaboratedTask{ + Name: "Run Go tests with race detector", + Description: "Runs the Go test suite with -race flag and checks coverage.", + Claude: elaboratedClaude{ + 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.Claude.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.", + Claude: elaboratedClaude{ + 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") + } +} |
