package sqlite import ( "context" "sync" "testing" "github.com/steveyegge/beads/internal/types" ) func TestChildCountersTableExists(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Verify table exists by querying it var count int err := store.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='child_counters'`).Scan(&count) if err != nil { t.Fatalf("failed to check for child_counters table: %v", err) } if count != 1 { t.Errorf("child_counters table not found, got count %d", count) } } func TestGetNextChildNumber(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() parentID := "bd-af78e9a2" // Create parent issue first (required by foreign key) parent := &types.Issue{ ID: parentID, Title: "Parent epic", Description: "Test parent", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeEpic, } if err := store.CreateIssue(ctx, parent, "test-user"); err != nil { t.Fatalf("failed to create parent issue: %v", err) } // First child should be 1 child1, err := store.getNextChildNumber(ctx, parentID) if err != nil { t.Fatalf("getNextChildNumber failed: %v", err) } if child1 != 1 { t.Errorf("expected first child to be 1, got %d", child1) } // Second child should be 2 child2, err := store.getNextChildNumber(ctx, parentID) if err != nil { t.Fatalf("getNextChildNumber failed: %v", err) } if child2 != 2 { t.Errorf("expected second child to be 2, got %d", child2) } // Third child should be 3 child3, err := store.getNextChildNumber(ctx, parentID) if err != nil { t.Fatalf("getNextChildNumber failed: %v", err) } if child3 != 3 { t.Errorf("expected third child to be 3, got %d", child3) } } func TestGetNextChildNumber_DifferentParents(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() parent1 := "bd-af78e9a2" parent2 := "bd-bf89e0b3" // Use non-hierarchical ID to avoid counter interaction // Create parent issues first for _, id := range []string{parent1, parent2} { parent := &types.Issue{ ID: id, Title: "Parent " + id, Description: "Test parent", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeEpic, } if err := store.CreateIssue(ctx, parent, "test-user"); err != nil { t.Fatalf("failed to create parent issue %s: %v", id, err) } } // Each parent should have independent counters child1_1, err := store.getNextChildNumber(ctx, parent1) if err != nil { t.Fatalf("getNextChildNumber failed: %v", err) } if child1_1 != 1 { t.Errorf("expected parent1 child to be 1, got %d", child1_1) } child2_1, err := store.getNextChildNumber(ctx, parent2) if err != nil { t.Fatalf("getNextChildNumber failed: %v", err) } if child2_1 != 1 { t.Errorf("expected parent2 child to be 1, got %d", child2_1) } child1_2, err := store.getNextChildNumber(ctx, parent1) if err != nil { t.Fatalf("getNextChildNumber failed: %v", err) } if child1_2 != 2 { t.Errorf("expected parent1 second child to be 2, got %d", child1_2) } } func TestGetNextChildNumber_Concurrent(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() parentID := "bd-af78e9a2" numWorkers := 10 // Create parent issue first parent := &types.Issue{ ID: parentID, Title: "Parent epic", Description: "Test parent", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeEpic, } if err := store.CreateIssue(ctx, parent, "test-user"); err != nil { t.Fatalf("failed to create parent issue: %v", err) } // Track all generated child numbers childNumbers := make([]int, numWorkers) var wg sync.WaitGroup // Spawn concurrent workers for i := 0; i < numWorkers; i++ { wg.Add(1) go func(idx int) { defer wg.Done() child, err := store.getNextChildNumber(ctx, parentID) if err != nil { t.Errorf("concurrent getNextChildNumber failed: %v", err) return } childNumbers[idx] = child }(i) } wg.Wait() // Verify all numbers are unique and in range [1, numWorkers] seen := make(map[int]bool) for _, num := range childNumbers { if num < 1 || num > numWorkers { t.Errorf("child number %d out of expected range [1, %d]", num, numWorkers) } if seen[num] { t.Errorf("duplicate child number: %d", num) } seen[num] = true } // Verify we got all numbers from 1 to numWorkers if len(seen) != numWorkers { t.Errorf("expected %d unique child numbers, got %d", numWorkers, len(seen)) } } func TestGetNextChildNumber_NestedHierarchy(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create parent issues for nested hierarchy // Note: When creating bd-af78e9a2.1, the counter for bd-af78e9a2 is set to 1 (GH#728 fix) // When creating bd-af78e9a2.1.2, the counter for bd-af78e9a2.1 is set to 2 (GH#728 fix) parents := []string{"bd-af78e9a2", "bd-af78e9a2.1", "bd-af78e9a2.1.2"} for _, id := range parents { parent := &types.Issue{ ID: id, Title: "Parent " + id, Description: "Test parent", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeEpic, } if err := store.CreateIssue(ctx, parent, "test-user"); err != nil { t.Fatalf("failed to create parent issue %s: %v", id, err) } } // With GH#728 fix, counters are updated when explicit hierarchical IDs are created: // - Creating bd-af78e9a2.1 sets counter for bd-af78e9a2 to 1 // - Creating bd-af78e9a2.1.2 sets counter for bd-af78e9a2.1 to 2 // So getNextChildNumber returns the NEXT number after the existing children tests := []struct { parent string expected []int }{ {"bd-af78e9a2", []int{2, 3}}, // counter was 1 after creating .1 {"bd-af78e9a2.1", []int{3, 4}}, // counter was 2 after creating .1.2 {"bd-af78e9a2.1.2", []int{1, 2}}, // no children created, starts at 1 } for _, tt := range tests { for _, want := range tt.expected { got, err := store.getNextChildNumber(ctx, tt.parent) if err != nil { t.Fatalf("getNextChildNumber(%s) failed: %v", tt.parent, err) } if got != want { t.Errorf("parent %s: expected child %d, got %d", tt.parent, want, got) } } } }