diff options
Diffstat (limited to 'internal/storage')
| -rw-r--r-- | internal/storage/db.go | 55 | ||||
| -rw-r--r-- | internal/storage/db_test.go | 58 |
2 files changed, 113 insertions, 0 deletions
diff --git a/internal/storage/db.go b/internal/storage/db.go index 1aac754..b3df696 100644 --- a/internal/storage/db.go +++ b/internal/storage/db.go @@ -409,6 +409,61 @@ func (s *DB) DeleteTask(id string) error { return nil } +// RecentExecution is returned by ListRecentExecutions (JOIN with tasks for name). +type RecentExecution struct { + ID string `json:"id"` + TaskID string `json:"task_id"` + TaskName string `json:"task_name"` + State string `json:"state"` + StartedAt time.Time `json:"started_at"` + FinishedAt *time.Time `json:"finished_at,omitempty"` + DurationMS *int64 `json:"duration_ms,omitempty"` + ExitCode int `json:"exit_code"` + CostUSD float64 `json:"cost_usd"` + StdoutPath string `json:"stdout_path"` +} + +// ListRecentExecutions returns executions since the given time, joined with task names. +// If taskID is non-empty, only executions for that task are returned. +func (s *DB) ListRecentExecutions(since time.Time, limit int, taskID string) ([]*RecentExecution, error) { + query := `SELECT e.id, e.task_id, t.name, e.status, e.start_time, e.end_time, e.exit_code, e.cost_usd, e.stdout_path + FROM executions e + JOIN tasks t ON e.task_id = t.id + WHERE e.start_time >= ?` + args := []interface{}{since.UTC()} + + if taskID != "" { + query += " AND e.task_id = ?" + args = append(args, taskID) + } + query += " ORDER BY e.start_time DESC LIMIT ?" + args = append(args, limit) + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var results []*RecentExecution + for rows.Next() { + var re RecentExecution + var endTime time.Time + var stdoutPath sql.NullString + if err := rows.Scan(&re.ID, &re.TaskID, &re.TaskName, &re.State, &re.StartedAt, &endTime, &re.ExitCode, &re.CostUSD, &stdoutPath); err != nil { + return nil, err + } + re.StdoutPath = stdoutPath.String + if !endTime.IsZero() { + re.FinishedAt = &endTime + ms := endTime.Sub(re.StartedAt).Milliseconds() + re.DurationMS = &ms + } + results = append(results, &re) + } + return results, rows.Err() +} + // UpdateTaskQuestion stores the pending question JSON on a task. // Pass empty string to clear the question after it has been answered. func (s *DB) UpdateTaskQuestion(taskID, questionJSON string) error { diff --git a/internal/storage/db_test.go b/internal/storage/db_test.go index 395574c..fcdc529 100644 --- a/internal/storage/db_test.go +++ b/internal/storage/db_test.go @@ -473,6 +473,64 @@ func TestStorage_UpdateTaskQuestion(t *testing.T) { } } +func TestDeleteTask_RemovesTaskAndExecutions(t *testing.T) { + db := testDB(t) + now := time.Now().UTC() + db.CreateTask(makeTestTask("del-task", now)) + db.CreateExecution(&Execution{ID: "del-exec-1", TaskID: "del-task", StartTime: now, Status: "COMPLETED"}) + db.CreateExecution(&Execution{ID: "del-exec-2", TaskID: "del-task", StartTime: now.Add(time.Minute), Status: "COMPLETED"}) + + if err := db.DeleteTask("del-task"); err != nil { + t.Fatalf("DeleteTask: %v", err) + } + + _, err := db.GetTask("del-task") + if err == nil { + t.Error("expected error getting deleted task, got nil") + } + + execs, err := db.ListExecutions("del-task") + if err != nil { + t.Fatalf("ListExecutions: %v", err) + } + if len(execs) != 0 { + t.Errorf("want 0 executions after delete, got %d", len(execs)) + } +} + +func TestDeleteTask_CascadesSubtasks(t *testing.T) { + db := testDB(t) + now := time.Now().UTC() + + parent := makeTestTask("parent-del", now) + child := makeTestTask("child-del", now) + child.ParentTaskID = "parent-del" + + db.CreateTask(parent) + db.CreateTask(child) + + if err := db.DeleteTask("parent-del"); err != nil { + t.Fatalf("DeleteTask: %v", err) + } + + _, err := db.GetTask("parent-del") + if err == nil { + t.Error("parent should be deleted") + } + _, err = db.GetTask("child-del") + if err == nil { + t.Error("child should be deleted when parent is deleted") + } +} + +func TestDeleteTask_NotFound(t *testing.T) { + db := testDB(t) + err := db.DeleteTask("nonexistent") + if err == nil { + t.Fatal("expected error for nonexistent task, got nil") + } +} + func TestStorage_GetLatestExecution(t *testing.T) { db := testDB(t) now := time.Now().UTC() |
