From 74cc740398cf2d90804ab19db728c844c2e056b7 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Tue, 3 Mar 2026 21:15:50 +0000 Subject: 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 --- internal/api/templates_test.go | 183 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 internal/api/templates_test.go (limited to 'internal/api/templates_test.go') diff --git a/internal/api/templates_test.go b/internal/api/templates_test.go new file mode 100644 index 0000000..bbcfc87 --- /dev/null +++ b/internal/api/templates_test.go @@ -0,0 +1,183 @@ +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/thepeterstone/claudomator/internal/storage" +) + +func TestListTemplates_Empty(t *testing.T) { + srv, _ := testServer(t) + + req := httptest.NewRequest("GET", "/api/templates", nil) + 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 templates []storage.Template + json.NewDecoder(w.Body).Decode(&templates) + if len(templates) != 0 { + t.Errorf("want 0 templates, got %d", len(templates)) + } +} + +func TestCreateTemplate_Success(t *testing.T) { + srv, _ := testServer(t) + + payload := `{ + "name": "Go: Run Tests", + "description": "Run the full test suite with race detector", + "claude": { + "model": "sonnet", + "instructions": "Run go test -race ./...", + "max_budget_usd": 0.50, + "allowed_tools": ["Bash"] + }, + "timeout": "10m", + "priority": "normal", + "tags": ["go", "testing"] + }` + req := httptest.NewRequest("POST", "/api/templates", bytes.NewBufferString(payload)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusCreated { + t.Fatalf("status: want 201, got %d; body: %s", w.Code, w.Body.String()) + } + var created storage.Template + json.NewDecoder(w.Body).Decode(&created) + if created.Name != "Go: Run Tests" { + t.Errorf("name: want 'Go: Run Tests', got %q", created.Name) + } + if created.ID == "" { + t.Error("expected auto-generated ID") + } +} + +func TestGetTemplate_AfterCreate(t *testing.T) { + srv, _ := testServer(t) + + payload := `{"name": "Fetch Me", "claude": {"instructions": "do thing", "model": "haiku"}}` + req := httptest.NewRequest("POST", "/api/templates", bytes.NewBufferString(payload)) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + if w.Code != http.StatusCreated { + t.Fatalf("create: want 201, got %d", w.Code) + } + var created storage.Template + json.NewDecoder(w.Body).Decode(&created) + + req2 := httptest.NewRequest("GET", fmt.Sprintf("/api/templates/%s", created.ID), nil) + w2 := httptest.NewRecorder() + srv.Handler().ServeHTTP(w2, req2) + + if w2.Code != http.StatusOK { + t.Fatalf("get: want 200, got %d; body: %s", w2.Code, w2.Body.String()) + } + var fetched storage.Template + json.NewDecoder(w2.Body).Decode(&fetched) + if fetched.ID != created.ID { + t.Errorf("id: want %q, got %q", created.ID, fetched.ID) + } + if fetched.Name != "Fetch Me" { + t.Errorf("name: want 'Fetch Me', got %q", fetched.Name) + } +} + +func TestGetTemplate_NotFound(t *testing.T) { + srv, _ := testServer(t) + + req := httptest.NewRequest("GET", "/api/templates/nonexistent", nil) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("status: want 404, got %d", w.Code) + } +} + +func TestUpdateTemplate(t *testing.T) { + srv, _ := testServer(t) + + payload := `{"name": "Original Name", "claude": {"instructions": "original"}}` + req := httptest.NewRequest("POST", "/api/templates", bytes.NewBufferString(payload)) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + var created storage.Template + json.NewDecoder(w.Body).Decode(&created) + + update := `{"name": "Updated Name", "claude": {"instructions": "updated"}}` + req2 := httptest.NewRequest("PUT", fmt.Sprintf("/api/templates/%s", created.ID), bytes.NewBufferString(update)) + w2 := httptest.NewRecorder() + srv.Handler().ServeHTTP(w2, req2) + + if w2.Code != http.StatusOK { + t.Fatalf("update: want 200, got %d; body: %s", w2.Code, w2.Body.String()) + } + var updated storage.Template + json.NewDecoder(w2.Body).Decode(&updated) + if updated.Name != "Updated Name" { + t.Errorf("name: want 'Updated Name', got %q", updated.Name) + } +} + +func TestUpdateTemplate_NotFound(t *testing.T) { + srv, _ := testServer(t) + + update := `{"name": "Ghost", "claude": {"instructions": "x"}}` + req := httptest.NewRequest("PUT", "/api/templates/nonexistent", bytes.NewBufferString(update)) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("status: want 404, got %d", w.Code) + } +} + +func TestDeleteTemplate(t *testing.T) { + srv, _ := testServer(t) + + payload := `{"name": "To Delete", "claude": {"instructions": "bye"}}` + req := httptest.NewRequest("POST", "/api/templates", bytes.NewBufferString(payload)) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + var created storage.Template + json.NewDecoder(w.Body).Decode(&created) + + req2 := httptest.NewRequest("DELETE", fmt.Sprintf("/api/templates/%s", created.ID), nil) + w2 := httptest.NewRecorder() + srv.Handler().ServeHTTP(w2, req2) + + if w2.Code != http.StatusNoContent { + t.Fatalf("delete: want 204, got %d; body: %s", w2.Code, w2.Body.String()) + } + + // Subsequent GET returns 404. + req3 := httptest.NewRequest("GET", fmt.Sprintf("/api/templates/%s", created.ID), nil) + w3 := httptest.NewRecorder() + srv.Handler().ServeHTTP(w3, req3) + + if w3.Code != http.StatusNotFound { + t.Fatalf("get after delete: want 404, got %d", w3.Code) + } +} + +func TestDeleteTemplate_NotFound(t *testing.T) { + srv, _ := testServer(t) + + req := httptest.NewRequest("DELETE", "/api/templates/nonexistent", nil) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("status: want 404, got %d", w.Code) + } +} -- cgit v1.2.3