summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api')
-rw-r--r--internal/api/server.go8
-rw-r--r--internal/api/server_test.go134
-rw-r--r--internal/api/task_view.go41
3 files changed, 181 insertions, 2 deletions
diff --git a/internal/api/server.go b/internal/api/server.go
index 8a20349..800ad3e 100644
--- a/internal/api/server.go
+++ b/internal/api/server.go
@@ -514,7 +514,11 @@ func (s *Server) handleListTasks(w http.ResponseWriter, r *http.Request) {
if tasks == nil {
tasks = []*task.Task{}
}
- writeJSON(w, http.StatusOK, tasks)
+ views := make([]*taskView, len(tasks))
+ for i, tk := range tasks {
+ views[i] = s.enrichTask(tk)
+ }
+ writeJSON(w, http.StatusOK, views)
}
func (s *Server) handleGetTask(w http.ResponseWriter, r *http.Request) {
@@ -524,7 +528,7 @@ func (s *Server) handleGetTask(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "task not found"})
return
}
- writeJSON(w, http.StatusOK, t)
+ writeJSON(w, http.StatusOK, s.enrichTask(t))
}
func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
diff --git a/internal/api/server_test.go b/internal/api/server_test.go
index 516e289..83f83f4 100644
--- a/internal/api/server_test.go
+++ b/internal/api/server_test.go
@@ -1781,3 +1781,137 @@ func TestDeploymentStatus_NotFound(t *testing.T) {
t.Fatalf("want 404, got %d", w.Code)
}
}
+
+// TestListTasks_ReadyTask_IncludesDeploymentStatus verifies that GET /api/tasks
+// returns a deployment_status field for READY tasks containing deployed_commit,
+// fix_commits, and includes_fix.
+func TestListTasks_ReadyTask_IncludesDeploymentStatus(t *testing.T) {
+ srv, store := testServer(t)
+
+ tk := createTaskWithState(t, store, "enrich-list-ready-1", task.StateReady)
+ exec := &storage.Execution{
+ ID: "enrich-list-exec-1",
+ TaskID: tk.ID,
+ StartTime: time.Now(),
+ EndTime: time.Now(),
+ Status: "COMPLETED",
+ Commits: []task.GitCommit{{Hash: "aabbcc", Message: "fix: list test"}},
+ }
+ if err := store.CreateExecution(exec); err != nil {
+ t.Fatal(err)
+ }
+
+ req := httptest.NewRequest("GET", "/api/tasks", nil)
+ w := httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("want 200, got %d; body: %s", w.Code, w.Body.String())
+ }
+
+ var tasks []map[string]interface{}
+ if err := json.NewDecoder(w.Body).Decode(&tasks); err != nil {
+ t.Fatalf("decode: %v", err)
+ }
+
+ var found map[string]interface{}
+ for _, tsk := range tasks {
+ if tsk["id"] == tk.ID {
+ found = tsk
+ break
+ }
+ }
+ if found == nil {
+ t.Fatalf("task %q not found in list response", tk.ID)
+ }
+
+ ds, ok := found["deployment_status"].(map[string]interface{})
+ if !ok {
+ t.Fatalf("READY task missing deployment_status field; got: %v", found["deployment_status"])
+ }
+ if _, ok := ds["deployed_commit"]; !ok {
+ t.Error("deployment_status missing deployed_commit")
+ }
+ if _, ok := ds["includes_fix"]; !ok {
+ t.Error("deployment_status missing includes_fix")
+ }
+}
+
+// TestGetTask_ReadyTask_IncludesDeploymentStatus verifies that GET /api/tasks/{id}
+// returns a deployment_status field for a READY task.
+func TestGetTask_ReadyTask_IncludesDeploymentStatus(t *testing.T) {
+ srv, store := testServer(t)
+
+ tk := createTaskWithState(t, store, "enrich-get-ready-1", task.StateReady)
+ exec := &storage.Execution{
+ ID: "enrich-get-exec-1",
+ TaskID: tk.ID,
+ StartTime: time.Now(),
+ EndTime: time.Now(),
+ Status: "COMPLETED",
+ Commits: []task.GitCommit{{Hash: "ddeeff", Message: "fix: get test"}},
+ }
+ if err := store.CreateExecution(exec); err != nil {
+ t.Fatal(err)
+ }
+
+ req := httptest.NewRequest("GET", "/api/tasks/"+tk.ID, nil)
+ w := httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("want 200, got %d", w.Code)
+ }
+
+ var resp map[string]interface{}
+ if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
+ t.Fatalf("decode: %v", err)
+ }
+
+ ds, ok := resp["deployment_status"].(map[string]interface{})
+ if !ok {
+ t.Fatalf("READY task GET response missing deployment_status; got: %v", resp["deployment_status"])
+ }
+ if _, ok := ds["deployed_commit"]; !ok {
+ t.Error("deployment_status missing deployed_commit")
+ }
+ if _, ok := ds["includes_fix"]; !ok {
+ t.Error("deployment_status missing includes_fix")
+ }
+}
+
+// TestListTasks_NonReadyTask_OmitsDeploymentStatus verifies that non-READY tasks
+// (e.g. PENDING) do not include a deployment_status field.
+func TestListTasks_NonReadyTask_OmitsDeploymentStatus(t *testing.T) {
+ srv, store := testServer(t)
+
+ tk := createTaskWithState(t, store, "enrich-list-pending-1", task.StatePending)
+
+ req := httptest.NewRequest("GET", "/api/tasks", nil)
+ w := httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("want 200, got %d", w.Code)
+ }
+
+ var tasks []map[string]interface{}
+ if err := json.NewDecoder(w.Body).Decode(&tasks); err != nil {
+ t.Fatalf("decode: %v", err)
+ }
+
+ var found map[string]interface{}
+ for _, tsk := range tasks {
+ if tsk["id"] == tk.ID {
+ found = tsk
+ break
+ }
+ }
+ if found == nil {
+ t.Fatalf("task %q not found in list", tk.ID)
+ }
+
+ if _, ok := found["deployment_status"]; ok {
+ t.Error("PENDING task should not include deployment_status field")
+ }
+}
diff --git a/internal/api/task_view.go b/internal/api/task_view.go
new file mode 100644
index 0000000..5791058
--- /dev/null
+++ b/internal/api/task_view.go
@@ -0,0 +1,41 @@
+package api
+
+import (
+ "database/sql"
+
+ "github.com/thepeterstone/claudomator/internal/deployment"
+ "github.com/thepeterstone/claudomator/internal/task"
+)
+
+// taskView wraps a task with computed fields that are derived from execution
+// history and deployment state. It is used as the JSON response type for task
+// list and get endpoints so that callers receive enriched data in one request.
+type taskView struct {
+ *task.Task
+ Changestats *task.Changestats `json:"changestats,omitempty"`
+ DeploymentStatus *deployment.Status `json:"deployment_status,omitempty"`
+}
+
+// enrichTask fetches the latest execution for the given task and attaches
+// changestats and deployment_status fields for READY tasks.
+// Non-READY tasks are returned without these extra fields.
+func (s *Server) enrichTask(tk *task.Task) *taskView {
+ view := &taskView{Task: tk}
+
+ if tk.State != task.StateReady {
+ return view
+ }
+
+ exec, err := s.store.GetLatestExecution(tk.ID)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ // No execution yet — still include deployment status (empty commits).
+ view.DeploymentStatus = deployment.Check(nil, tk.Agent.ProjectDir)
+ }
+ return view
+ }
+
+ view.Changestats = exec.Changestats
+ view.DeploymentStatus = deployment.Check(exec.Commits, tk.Agent.ProjectDir)
+ return view
+}