summaryrefslogtreecommitdiff
path: root/internal/api/elaborate_test.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-03 21:15:50 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-03 21:15:50 +0000
commit74cc740398cf2d90804ab19db728c844c2e056b7 (patch)
treee8532d1da9273e1613beb7b762b16134da0de286 /internal/api/elaborate_test.go
parentf527972f4d8311a09e639ede6c4da4ca669cfd5e (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.go171
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")
+ }
+}