Files
beads/internal/storage/sqlite/adaptive_e2e_test.go
Steve Yegge 57253f93a3 Context propagation with graceful cancellation (bd-rtp, bd-yb8, bd-2o2)
Complete implementation of signal-aware context propagation for graceful
cancellation across all commands and storage operations.

Key changes:

1. Signal-aware contexts (bd-rtp):
   - Added rootCtx/rootCancel in main.go using signal.NotifyContext()
   - Set up in PersistentPreRun, cancelled in PersistentPostRun
   - Daemon uses same pattern in runDaemonLoop()
   - Handles SIGINT/SIGTERM for graceful shutdown

2. Context propagation (bd-yb8):
   - All commands now use rootCtx instead of context.Background()
   - sqlite.New() receives context for cancellable operations
   - Database operations respect context cancellation
   - Storage layer propagates context through all queries

3. Cancellation tests (bd-2o2):
   - Added import_cancellation_test.go with comprehensive tests
   - Added export cancellation test in export_test.go
   - Tests verify database integrity after cancellation
   - All cancellation tests passing

Fixes applied during review:
   - Fixed rootCtx lifecycle (removed premature defer from PersistentPreRun)
   - Fixed test context contamination (reset rootCtx in test cleanup)
   - Fixed export tests missing context setup

Impact:
   - Pressing Ctrl+C during import/export now cancels gracefully
   - No database corruption or hanging transactions
   - Clean shutdown of all operations

Tested:
   - go build ./cmd/bd ✓
   - go test ./cmd/bd -run TestImportCancellation ✓
   - go test ./cmd/bd -run TestExportCommand ✓
   - Manual Ctrl+C testing verified

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 21:57:23 -05:00

164 lines
4.6 KiB
Go

package sqlite
import (
"context"
"strings"
"testing"
"github.com/steveyegge/beads/internal/types"
)
func TestAdaptiveIDLength_E2E(t *testing.T) {
if testing.Short() {
t.Skip("skipping slow E2E test in short mode")
}
// Create in-memory database
ctx := context.Background()
db, err := New(ctx, ":memory:")
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer db.Close()
// Initialize with prefix
if err := db.SetConfig(ctx, "issue_prefix", "test"); err != nil {
t.Fatalf("Failed to set prefix: %v", err)
}
// Helper to create issue and verify ID length
createAndCheckLength := func(title string, expectedHashLen int) string {
issue := &types.Issue{
Title: title,
Description: "Test",
Status: "open",
Priority: 1,
IssueType: "task",
}
if err := db.CreateIssue(ctx, issue, "test@example.com"); err != nil {
t.Fatalf("Failed to create issue: %v", err)
}
// Check ID format: test-xxxx
if !strings.HasPrefix(issue.ID, "test-") {
t.Errorf("ID should start with test-, got %s", issue.ID)
}
hashPart := strings.TrimPrefix(issue.ID, "test-")
if len(hashPart) != expectedHashLen {
t.Errorf("Issue %s: hash length = %d, want %d", title, len(hashPart), expectedHashLen)
}
return issue.ID
}
// Test 1: First few issues should use 3-char IDs (base36 allows shorter IDs)
t.Run("first_50_issues_use_3_chars", func(t *testing.T) {
for i := 0; i < 50; i++ {
title := formatTitle("Issue %d", i)
createAndCheckLength(title, 3)
}
})
// Test 2: Issues 50-200 should transition to 4 chars
// (3 chars good up to ~160 issues with 25% threshold)
t.Run("issues_50_to_200_use_3_or_4_chars", func(t *testing.T) {
for i := 50; i < 200; i++ {
title := formatTitle("Issue %d", i)
issue := &types.Issue{
Title: title,
Description: "Test",
Status: "open",
Priority: 1,
IssueType: "task",
}
if err := db.CreateIssue(ctx, issue, "test@example.com"); err != nil {
t.Fatalf("Failed to create issue: %v", err)
}
// Most should be 3 chars initially, transitioning to 4 after ~160
hashPart := strings.TrimPrefix(issue.ID, "test-")
if len(hashPart) < 3 || len(hashPart) > 4 {
t.Errorf("Issue %d has hash length %d, expected 3-4", i, len(hashPart))
}
}
})
// Test 3: At 500-1000 issues, should scale to 4-5 chars
// (4 chars good up to ~980 issues with 25% threshold)
t.Run("verify_adaptive_scaling_works", func(t *testing.T) {
// Just verify that we can create more issues and the algorithm doesn't break
// The actual length will be determined by the adaptive algorithm
for i := 200; i < 250; i++ {
title := formatTitle("Issue %d", i)
issue := &types.Issue{
Title: title,
Description: "Test",
Status: "open",
Priority: 1,
IssueType: "task",
}
if err := db.CreateIssue(ctx, issue, "test@example.com"); err != nil {
t.Fatalf("Failed to create issue: %v", err)
}
// Should use 4-5 chars depending on database size
hashPart := strings.TrimPrefix(issue.ID, "test-")
if len(hashPart) < 3 || len(hashPart) > 5 {
t.Errorf("Issue %d has hash length %d, expected 3-5", i, len(hashPart))
}
}
})
}
func formatTitle(format string, i int) string {
// Use sprintf to format title
return strings.Replace(format, "%d", strings.Repeat("x", i%10), 1) + string(rune('a'+i%26))
}
func TestAdaptiveIDLength_CustomConfig(t *testing.T) {
// Create in-memory database
ctx := context.Background()
db, err := New(ctx, ":memory:")
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer db.Close()
// Initialize with custom config
if err := db.SetConfig(ctx, "issue_prefix", "test"); err != nil {
t.Fatalf("Failed to set prefix: %v", err)
}
// Set stricter collision threshold (1%) and min length of 5
if err := db.SetConfig(ctx, "max_collision_prob", "0.01"); err != nil {
t.Fatalf("Failed to set max_collision_prob: %v", err)
}
if err := db.SetConfig(ctx, "min_hash_length", "5"); err != nil {
t.Fatalf("Failed to set min_hash_length: %v", err)
}
// With min_hash_length=5, all IDs should be at least 5 chars
for i := 0; i < 20; i++ {
issue := &types.Issue{
Title: formatTitle("Issue %d", i),
Description: "Test",
Status: "open",
Priority: 1,
IssueType: "task",
}
if err := db.CreateIssue(ctx, issue, "test@example.com"); err != nil {
t.Fatalf("Failed to create issue: %v", err)
}
hashPart := strings.TrimPrefix(issue.ID, "test-")
// With min_hash_length=5, should use at least 5 chars
if len(hashPart) < 5 {
t.Errorf("Issue %d with min_hash_length=5: hash length = %d, want >= 5", i, len(hashPart))
}
}
}