diff options
Diffstat (limited to 'internal/api/stories_test.go')
| -rw-r--r-- | internal/api/stories_test.go | 351 |
1 files changed, 0 insertions, 351 deletions
diff --git a/internal/api/stories_test.go b/internal/api/stories_test.go deleted file mode 100644 index f43ad86..0000000 --- a/internal/api/stories_test.go +++ /dev/null @@ -1,351 +0,0 @@ -package api - -import ( - "bytes" - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/thepeterstone/claudomator/internal/deployment" - "github.com/thepeterstone/claudomator/internal/task" -) - -func TestCreateStory_API(t *testing.T) { - srv, _ := testServer(t) - - body := `{"name":"My Story","project_id":"proj-1"}` - req := httptest.NewRequest("POST", "/api/stories", bytes.NewBufferString(body)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - srv.mux.ServeHTTP(w, req) - - if w.Code != http.StatusCreated { - t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) - } - var st task.Story - if err := json.NewDecoder(w.Body).Decode(&st); err != nil { - t.Fatalf("decode response: %v", err) - } - if st.Name != "My Story" { - t.Errorf("Name: want 'My Story', got %q", st.Name) - } - if st.ID == "" { - t.Error("ID should be auto-generated") - } - if st.Status != task.StoryPending { - t.Errorf("Status: want PENDING, got %q", st.Status) - } -} - -func TestGetStory_API(t *testing.T) { - srv, _ := testServer(t) - - // Create a story first. - body := `{"name":"Get Me"}` - req := httptest.NewRequest("POST", "/api/stories", bytes.NewBufferString(body)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - srv.mux.ServeHTTP(w, req) - if w.Code != http.StatusCreated { - t.Fatalf("create story: expected 201, got %d", w.Code) - } - var created task.Story - json.NewDecoder(w.Body).Decode(&created) - - // Fetch it. - req2 := httptest.NewRequest("GET", "/api/stories/"+created.ID, nil) - w2 := httptest.NewRecorder() - srv.mux.ServeHTTP(w2, req2) - - if w2.Code != http.StatusOK { - t.Fatalf("get story: expected 200, got %d: %s", w2.Code, w2.Body.String()) - } - var got task.Story - if err := json.NewDecoder(w2.Body).Decode(&got); err != nil { - t.Fatalf("decode: %v", err) - } - if got.ID != created.ID { - t.Errorf("ID: want %q, got %q", created.ID, got.ID) - } - if got.Name != "Get Me" { - t.Errorf("Name: want 'Get Me', got %q", got.Name) - } -} - -func TestAddTaskToStory_AutoWiresDependsOn(t *testing.T) { - srv, _ := testServer(t) - - // Create a story. - storyBody := `{"name":"Story For Tasks"}` - req := httptest.NewRequest("POST", "/api/stories", bytes.NewBufferString(storyBody)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - srv.mux.ServeHTTP(w, req) - if w.Code != http.StatusCreated { - t.Fatalf("create story: %d %s", w.Code, w.Body.String()) - } - var story task.Story - json.NewDecoder(w.Body).Decode(&story) - - addTask := func(name string) *task.Task { - body := `{"name":"` + name + `","agent":{"type":"claude","instructions":"do it"}}` - r := httptest.NewRequest("POST", "/api/stories/"+story.ID+"/tasks", bytes.NewBufferString(body)) - r.Header.Set("Content-Type", "application/json") - wr := httptest.NewRecorder() - srv.mux.ServeHTTP(wr, r) - if wr.Code != http.StatusCreated { - t.Fatalf("add task %s: expected 201, got %d: %s", name, wr.Code, wr.Body.String()) - } - var tk task.Task - json.NewDecoder(wr.Body).Decode(&tk) - return &tk - } - - task1 := addTask("Task 1") - task2 := addTask("Task 2") - task3 := addTask("Task 3") - - // task1 has no dependencies. - if len(task1.DependsOn) != 0 { - t.Errorf("task1.DependsOn: want [], got %v", task1.DependsOn) - } - // task2 depends on task1. - if len(task2.DependsOn) != 1 || task2.DependsOn[0] != task1.ID { - t.Errorf("task2.DependsOn: want [%s], got %v", task1.ID, task2.DependsOn) - } - // task3 depends on task2. - if len(task3.DependsOn) != 1 || task3.DependsOn[0] != task2.ID { - t.Errorf("task3.DependsOn: want [%s], got %v", task2.ID, task3.DependsOn) - } -} - -func TestBuildStoryElaboratePrompt(t *testing.T) { - prompt := buildStoryElaboratePrompt() - checks := []struct { - label string - want string - }{ - {"schema: name field", `"name"`}, - {"schema: branch_name field", `"branch_name"`}, - {"schema: tasks field", `"tasks"`}, - {"schema: validation field", `"validation"`}, - {"rule: git push", "git push origin"}, - {"rule: sequential subtasks", "sequentially"}, - {"rule: specific file paths", "file paths"}, - } - for _, c := range checks { - if !strings.Contains(prompt, c.want) { - t.Errorf("%s: prompt should contain %q", c.label, c.want) - } - } -} - -func TestHandleStoryApprove_WiresDepends(t *testing.T) { - srv, _ := testServer(t) - - body := `{ - "name": "My Story", - "branch_name": "story/my-story", - "tasks": [ - {"name": "Task 1", "instructions": "do task 1", "subtasks": []}, - {"name": "Task 2", "instructions": "do task 2", "subtasks": []}, - {"name": "Task 3", "instructions": "do task 3", "subtasks": []} - ], - "validation": {"type": "build", "steps": ["go build ./..."], "success_criteria": "compiles"} - }` - req := httptest.NewRequest("POST", "/api/stories/approve", bytes.NewBufferString(body)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - srv.mux.ServeHTTP(w, req) - - if w.Code != http.StatusCreated { - t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) - } - - var resp struct { - Story task.Story `json:"story"` - TaskIDs []string `json:"task_ids"` - } - if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { - t.Fatalf("decode response: %v", err) - } - if len(resp.TaskIDs) != 3 { - t.Fatalf("expected 3 task IDs, got %d", len(resp.TaskIDs)) - } - if resp.Story.Name != "My Story" { - t.Errorf("story name: want 'My Story', got %q", resp.Story.Name) - } - - // Verify depends_on chain via the store. - store := srv.store - task1, err := store.GetTask(resp.TaskIDs[0]) - if err != nil { - t.Fatalf("GetTask[0]: %v", err) - } - task2, err := store.GetTask(resp.TaskIDs[1]) - if err != nil { - t.Fatalf("GetTask[1]: %v", err) - } - task3, err := store.GetTask(resp.TaskIDs[2]) - if err != nil { - t.Fatalf("GetTask[2]: %v", err) - } - - if len(task1.DependsOn) != 0 { - t.Errorf("task1.DependsOn: want [], got %v", task1.DependsOn) - } - if len(task2.DependsOn) != 1 || task2.DependsOn[0] != task1.ID { - t.Errorf("task2.DependsOn: want [%s], got %v", task1.ID, task2.DependsOn) - } - if len(task3.DependsOn) != 1 || task3.DependsOn[0] != task2.ID { - t.Errorf("task3.DependsOn: want [%s], got %v", task2.ID, task3.DependsOn) - } -} - -func TestHandleStoryApprove_SetsRepositoryURL(t *testing.T) { - srv, store := testServer(t) - - proj := &task.Project{ - ID: "proj-repo", - Name: "claudomator", - RemoteURL: "/site/git.terst.org/repos/claudomator.git", - // LocalPath intentionally empty: branch creation is a non-fatal side effect, - // omitting it keeps the test fast and free of real git operations. - } - if err := store.CreateProject(proj); err != nil { - t.Fatalf("CreateProject: %v", err) - } - - body := `{ - "name": "Repo URL Story", - "branch_name": "story/repo-url", - "project_id": "proj-repo", - "tasks": [ - {"name": "Task A", "instructions": "do A", "subtasks": []}, - {"name": "Task B", "instructions": "do B", "subtasks": [ - {"name": "Sub B1", "instructions": "do B1"} - ]} - ], - "validation": {"type": "build", "steps": ["go build ./..."], "success_criteria": "ok"} - }` - req := httptest.NewRequest("POST", "/api/stories/approve", bytes.NewBufferString(body)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - srv.mux.ServeHTTP(w, req) - - if w.Code != http.StatusCreated { - t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) - } - - var resp struct { - TaskIDs []string `json:"task_ids"` - } - if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { - t.Fatalf("decode: %v", err) - } - - for _, id := range resp.TaskIDs { - tk, err := store.GetTask(id) - if err != nil { - t.Fatalf("GetTask %s: %v", id, err) - } - if tk.RepositoryURL != proj.RemoteURL { - t.Errorf("task %s RepositoryURL: want %q, got %q", tk.ID, proj.RemoteURL, tk.RepositoryURL) - } - } -} - -func TestApproveStory_AcceptanceCriteriaStored(t *testing.T) { - srv, store := testServer(t) - - proj := &task.Project{ - ID: "ac-proj", Name: "test", RemoteURL: "https://github.com/x/y", - Type: "web", DeployScript: "", - } - store.CreateProject(proj) - - body := `{ - "name": "AC Story", - "branch_name": "story/ac-test", - "project_id": "ac-proj", - "tasks": [ - { - "name": "Add feature", - "instructions": "implement the thing", - "acceptance_criteria": "run go test ./... and verify all pass", - "subtasks": [] - } - ], - "validation": {"type": "test", "steps": [], "success_criteria": "tests pass"} - }` - req := httptest.NewRequest("POST", "/api/stories/approve", strings.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - srv.mux.ServeHTTP(w, req) - - if w.Code != http.StatusCreated { - t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) - } - - var resp struct { - TaskIDs []string `json:"task_ids"` - } - json.NewDecoder(w.Body).Decode(&resp) - if len(resp.TaskIDs) == 0 { - t.Fatal("expected task_ids in response") - } - - tk, err := store.GetTask(resp.TaskIDs[0]) - if err != nil { - t.Fatalf("GetTask: %v", err) - } - if tk.AcceptanceCriteria != "run go test ./... and verify all pass" { - t.Errorf("expected acceptance criteria stored on task, got %q", tk.AcceptanceCriteria) - } -} - -func TestHandleStoryDeploymentStatus(t *testing.T) { - srv, store := testServer(t) - - // Create a story. - now := time.Now().UTC() - story := &task.Story{ - ID: "deploy-story-1", - Name: "Deploy Status Story", - Status: task.StoryInProgress, - CreatedAt: now, - UpdatedAt: now, - } - if err := store.CreateStory(story); err != nil { - t.Fatalf("CreateStory: %v", err) - } - - // Request deployment status — no tasks yet. - req := httptest.NewRequest("GET", "/api/stories/deploy-story-1/deployment-status", nil) - w := httptest.NewRecorder() - srv.mux.ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) - } - - var status deployment.Status - if err := json.NewDecoder(w.Body).Decode(&status); err != nil { - t.Fatalf("decode: %v", err) - } - // No tasks → no commits → IncludesFix = false (nothing to check). - if status.IncludesFix { - t.Error("expected IncludesFix=false when no commits") - } - - // 404 for unknown story. - req2 := httptest.NewRequest("GET", "/api/stories/nonexistent/deployment-status", nil) - w2 := httptest.NewRecorder() - srv.mux.ServeHTTP(w2, req2) - if w2.Code != http.StatusNotFound { - t.Errorf("expected 404 for unknown story, got %d", w2.Code) - } -} |
