- Hash ID generation now returns full 64-char SHA256 - Progressive collision handling: 6→7→8 chars on INSERT failure - Added child_counters table for hierarchical IDs - Updated all docs to reflect 6-char design - Collision math: 97% of 1K issues stay at 6 chars Next: Implement progressive retry logic in CreateIssue (bd-168) Amp-Thread-ID: https://ampcode.com/threads/T-9931c1b7-c989-47a1-8e6a-a04469bd937d Co-authored-by: Amp <amp@ampcode.com>
234 lines
5.7 KiB
Go
234 lines
5.7 KiB
Go
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-af78e9a2.1"
|
|
|
|
// 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
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Create nested hierarchy counters
|
|
// bd-af78e9a2 → .1, .2
|
|
// bd-af78e9a2.1 → .1.1, .1.2
|
|
// bd-af78e9a2.1.2 → .1.2.1, .1.2.2
|
|
|
|
tests := []struct {
|
|
parent string
|
|
expected []int
|
|
}{
|
|
{"bd-af78e9a2", []int{1, 2}},
|
|
{"bd-af78e9a2.1", []int{1, 2}},
|
|
{"bd-af78e9a2.1.2", []int{1, 2}},
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|