Files
beads/internal/storage/sqlite/prefix_validation_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

170 lines
4.2 KiB
Go

package sqlite
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/steveyegge/beads/internal/types"
)
func TestPrefixValidation(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "beads-prefix-test-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
dbPath := filepath.Join(tmpDir, "test.db")
ctx := context.Background()
store, err := New(ctx, dbPath)
if err != nil {
t.Fatalf("failed to create storage: %v", err)
}
defer store.Close()
ctx = context.Background()
// Set prefix to "test"
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
t.Fatalf("failed to set prefix: %v", err)
}
tests := []struct {
name string
issueID string
wantErr bool
}{
{
name: "valid prefix - matches",
issueID: "test-123",
wantErr: false,
},
{
name: "invalid prefix - wrong prefix",
issueID: "bd-456",
wantErr: true,
},
{
name: "invalid prefix - no dash",
issueID: "test123",
wantErr: true,
},
{
name: "invalid prefix - empty",
issueID: "",
wantErr: false, // Empty ID triggers auto-generation
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
issue := &types.Issue{
ID: tt.issueID,
Title: "Test issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
err := store.CreateIssue(ctx, issue, "test-user")
if (err != nil) != tt.wantErr {
t.Errorf("CreateIssue() error = %v, wantErr %v", err, tt.wantErr)
}
// If we expected success and the ID was empty, verify it was generated with correct prefix
if err == nil && tt.issueID == "" {
if issue.ID == "" {
t.Error("ID should be generated")
}
if issue.ID[:5] != "test-" {
t.Errorf("Generated ID should have prefix 'test-', got %s", issue.ID)
}
}
})
}
}
func TestPrefixValidationBatch(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "beads-prefix-batch-test-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
dbPath := filepath.Join(tmpDir, "test.db")
ctx := context.Background()
store, err := New(ctx, dbPath)
if err != nil {
t.Fatalf("failed to create storage: %v", err)
}
defer store.Close()
ctx = context.Background()
// Set prefix to "batch"
if err := store.SetConfig(ctx, "issue_prefix", "batch"); err != nil {
t.Fatalf("failed to set prefix: %v", err)
}
tests := []struct {
name string
issues []*types.Issue
wantErr bool
}{
{
name: "all valid prefixes",
issues: []*types.Issue{
{ID: "batch-1", Title: "Test 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
{ID: "batch-2", Title: "Test 2", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
},
wantErr: false,
},
{
name: "one invalid prefix in batch",
issues: []*types.Issue{
{ID: "batch-10", Title: "Test 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
{ID: "wrong-20", Title: "Test 2", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
},
wantErr: true,
},
{
name: "mixed auto-generated and explicit",
issues: []*types.Issue{
{ID: "batch-100", Title: "Explicit ID", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
{ID: "", Title: "Auto ID", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
},
wantErr: false,
},
{
name: "mixed with invalid prefix",
issues: []*types.Issue{
{ID: "", Title: "Auto ID", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
{ID: "invalid-500", Title: "Wrong prefix", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := store.CreateIssues(ctx, tt.issues, "test-user")
if (err != nil) != tt.wantErr {
t.Errorf("CreateIssues() error = %v, wantErr %v", err, tt.wantErr)
}
// For successful batches, verify all IDs have correct prefix
if err == nil {
for i, issue := range tt.issues {
if issue.ID[:6] != "batch-" {
t.Errorf("Issue %d: ID should have prefix 'batch-', got %s", i, issue.ID)
}
}
}
})
}
}