summaryrefslogtreecommitdiff
path: root/internal/storage/db_test.go
diff options
context:
space:
mode:
authorClaudomator Agent <agent@claudomator>2026-03-09 07:33:02 +0000
committerClaudomator Agent <agent@claudomator>2026-03-09 07:33:13 +0000
commit8b6c97e0ca25f62d8e8039199f1b1383b44380b2 (patch)
treee46b766ce66be1bb9ee30bdf4521fe76c60d7dba /internal/storage/db_test.go
parent361040939eb428f990c97ab0ab983e5360761b27 (diff)
storage: fix DeleteTask atomicity and use recursive CTE
Replace BFS loop with a single recursive CTE to collect all descendant task IDs in one query, and wrap all DELETE statements in a transaction so a partial failure cannot leave orphaned executions. Add TestDeleteTask_DeepSubtaskCascadeAtomic: creates a 3-level task hierarchy with executions at each level, deletes the root, and verifies all tasks and executions are removed with an explicit orphan-row check. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/storage/db_test.go')
-rw-r--r--internal/storage/db_test.go64
1 files changed, 64 insertions, 0 deletions
diff --git a/internal/storage/db_test.go b/internal/storage/db_test.go
index 8b10817..5f786ac 100644
--- a/internal/storage/db_test.go
+++ b/internal/storage/db_test.go
@@ -564,6 +564,70 @@ func TestDeleteTask_NotFound(t *testing.T) {
}
}
+func TestDeleteTask_DeepSubtaskCascadeAtomic(t *testing.T) {
+ db := testDB(t)
+ now := time.Now().UTC()
+
+ // 3-level hierarchy: root -> child -> grandchild
+ root := makeTestTask("deep-root", now)
+ child := makeTestTask("deep-child", now)
+ child.ParentTaskID = "deep-root"
+ grandchild := makeTestTask("deep-grandchild", now)
+ grandchild.ParentTaskID = "deep-child"
+
+ for _, tk := range []*task.Task{root, child, grandchild} {
+ if err := db.CreateTask(tk); err != nil {
+ t.Fatalf("creating task %q: %v", tk.ID, err)
+ }
+ }
+
+ // Add one execution per level.
+ for i, tid := range []string{"deep-root", "deep-child", "deep-grandchild"} {
+ e := &Execution{
+ ID: fmt.Sprintf("deep-exec-%d", i),
+ TaskID: tid,
+ StartTime: now,
+ Status: "COMPLETED",
+ }
+ if err := db.CreateExecution(e); err != nil {
+ t.Fatalf("creating execution for %q: %v", tid, err)
+ }
+ }
+
+ if err := db.DeleteTask("deep-root"); err != nil {
+ t.Fatalf("DeleteTask: %v", err)
+ }
+
+ // All three tasks must be gone.
+ for _, tid := range []string{"deep-root", "deep-child", "deep-grandchild"} {
+ _, err := db.GetTask(tid)
+ if err == nil {
+ t.Errorf("task %q should have been deleted", tid)
+ }
+ }
+
+ // No executions should remain for any deleted task (no orphans).
+ rows, err := db.db.Query(`
+ SELECT e.id FROM executions e
+ LEFT JOIN tasks t ON e.task_id = t.id
+ WHERE t.id IS NULL`)
+ if err != nil {
+ t.Fatalf("orphan check query: %v", err)
+ }
+ defer rows.Close()
+ var orphans []string
+ for rows.Next() {
+ var eid string
+ if err := rows.Scan(&eid); err != nil {
+ t.Fatal(err)
+ }
+ orphans = append(orphans, eid)
+ }
+ if len(orphans) != 0 {
+ t.Errorf("orphaned execution rows after DeleteTask: %v", orphans)
+ }
+}
+
func TestStorage_GetLatestExecution(t *testing.T) {
db := testDB(t)
now := time.Now().UTC()