package main import ( "context" "path/filepath" "strings" "testing" "github.com/steveyegge/beads/internal/config" "github.com/steveyegge/beads/internal/types" ) // setupSyncValidationTest creates a test store and properly initializes globals. // Returns the store and a cleanup function that should be deferred. func setupSyncValidationTest(t *testing.T) (*testing.T, func()) { t.Helper() tmpDir := t.TempDir() testDBPath := filepath.Join(tmpDir, ".beads", "issues.db") testStore := newTestStore(t, testDBPath) // Save original state origStore := store origStoreActive := storeActive origDBPath := dbPath // Set up test state store = testStore storeMutex.Lock() storeActive = true storeMutex.Unlock() dbPath = testDBPath cleanup := func() { storeMutex.Lock() store = origStore storeActive = origStoreActive storeMutex.Unlock() dbPath = origDBPath } return t, cleanup } // TestValidateOpenIssuesForSync_ModeNone verifies validation is skipped when // validation.on-sync is set to "none" (default). func TestValidateOpenIssuesForSync_ModeNone(t *testing.T) { _, cleanup := setupSyncValidationTest(t) defer cleanup() // Create a bug without required sections (should fail validation) ctx := context.Background() issue := &types.Issue{ ID: "test-001", Title: "Bug without sections", IssueType: types.TypeBug, Description: "No steps to reproduce or acceptance criteria", Status: types.StatusOpen, } if err := store.CreateIssue(ctx, issue, "test-user"); err != nil { t.Fatalf("CreateIssue: %v", err) } // Set validation mode to "none" if err := config.Initialize(); err != nil { t.Fatalf("config.Initialize: %v", err) } config.Set("validation.on-sync", "none") // Should return nil (skip validation) err := validateOpenIssuesForSync(ctx) if err != nil { t.Errorf("validateOpenIssuesForSync with mode=none returned error: %v", err) } } // TestValidateOpenIssuesForSync_ModeEmpty verifies validation is skipped when // validation.on-sync is empty (backwards compatibility). func TestValidateOpenIssuesForSync_ModeEmpty(t *testing.T) { _, cleanup := setupSyncValidationTest(t) defer cleanup() // Create a bug without required sections ctx := context.Background() issue := &types.Issue{ ID: "test-001", Title: "Bug without sections", IssueType: types.TypeBug, Description: "No sections", Status: types.StatusOpen, } if err := store.CreateIssue(ctx, issue, "test-user"); err != nil { t.Fatalf("CreateIssue: %v", err) } // Set validation mode to empty string if err := config.Initialize(); err != nil { t.Fatalf("config.Initialize: %v", err) } config.Set("validation.on-sync", "") // Should return nil (skip validation) err := validateOpenIssuesForSync(ctx) if err != nil { t.Errorf("validateOpenIssuesForSync with mode=empty returned error: %v", err) } } // TestValidateOpenIssuesForSync_ModeWarn verifies sync proceeds when // validation.on-sync is "warn" even with invalid issues. func TestValidateOpenIssuesForSync_ModeWarn(t *testing.T) { _, cleanup := setupSyncValidationTest(t) defer cleanup() // Create a bug without required sections ctx := context.Background() issue := &types.Issue{ ID: "test-001", Title: "Bug without sections", IssueType: types.TypeBug, Description: "No steps to reproduce or acceptance criteria", Status: types.StatusOpen, } if err := store.CreateIssue(ctx, issue, "test-user"); err != nil { t.Fatalf("CreateIssue: %v", err) } // Set validation mode to "warn" if err := config.Initialize(); err != nil { t.Fatalf("config.Initialize: %v", err) } config.Set("validation.on-sync", "warn") // Should return nil (warnings printed but sync proceeds) // The function prints to stderr but returns nil to allow sync to continue err := validateOpenIssuesForSync(ctx) if err != nil { t.Errorf("validateOpenIssuesForSync with mode=warn should return nil, got: %v", err) } } // TestValidateOpenIssuesForSync_ModeError verifies sync is blocked when // validation.on-sync is "error" and issues fail validation. func TestValidateOpenIssuesForSync_ModeError(t *testing.T) { _, cleanup := setupSyncValidationTest(t) defer cleanup() // Create a bug without required sections ctx := context.Background() issue := &types.Issue{ ID: "test-001", Title: "Bug without sections", IssueType: types.TypeBug, Description: "No sections at all", Status: types.StatusOpen, } if err := store.CreateIssue(ctx, issue, "test-user"); err != nil { t.Fatalf("CreateIssue: %v", err) } // Set validation mode to "error" if err := config.Initialize(); err != nil { t.Fatalf("config.Initialize: %v", err) } config.Set("validation.on-sync", "error") // Should return error (function also prints to stderr which we allow) err := validateOpenIssuesForSync(ctx) if err == nil { t.Error("validateOpenIssuesForSync with mode=error should return error for invalid issues") } if !strings.Contains(err.Error(), "template validation failed") { t.Errorf("expected 'template validation failed' in error, got: %v", err) } } // TestValidateOpenIssuesForSync_NoWarnings verifies no errors when all issues pass validation. func TestValidateOpenIssuesForSync_NoWarnings(t *testing.T) { _, cleanup := setupSyncValidationTest(t) defer cleanup() // Create a bug WITH all required sections ctx := context.Background() issue := &types.Issue{ ID: "test-001", Title: "Bug with sections", IssueType: types.TypeBug, Description: `## Steps to Reproduce 1. Do this 2. Do that ## Acceptance Criteria - It works`, Status: types.StatusOpen, } if err := store.CreateIssue(ctx, issue, "test-user"); err != nil { t.Fatalf("CreateIssue: %v", err) } // Set validation mode to "error" (strictest mode) if err := config.Initialize(); err != nil { t.Fatalf("config.Initialize: %v", err) } config.Set("validation.on-sync", "error") // Should return nil (no validation errors) err := validateOpenIssuesForSync(ctx) if err != nil { t.Errorf("validateOpenIssuesForSync should not return error for valid issues: %v", err) } } // TestValidateOpenIssuesForSync_SkipsClosedIssues verifies closed issues are not validated. func TestValidateOpenIssuesForSync_SkipsClosedIssues(t *testing.T) { _, cleanup := setupSyncValidationTest(t) defer cleanup() ctx := context.Background() // Create a closed bug without required sections (should be skipped) closedIssue := &types.Issue{ ID: "test-001", Title: "Closed bug without sections", IssueType: types.TypeBug, Description: "No sections", Status: types.StatusClosed, } if err := store.CreateIssue(ctx, closedIssue, "test-user"); err != nil { t.Fatalf("CreateIssue: %v", err) } // Set validation mode to "error" if err := config.Initialize(); err != nil { t.Fatalf("config.Initialize: %v", err) } config.Set("validation.on-sync", "error") // Should return nil (closed issues are not validated) err := validateOpenIssuesForSync(ctx) if err != nil { t.Errorf("validateOpenIssuesForSync should skip closed issues: %v", err) } } // TestValidateOpenIssuesForSync_ChoreHasNoRequirements verifies chore type // has no required sections and passes validation. func TestValidateOpenIssuesForSync_ChoreHasNoRequirements(t *testing.T) { _, cleanup := setupSyncValidationTest(t) defer cleanup() // Create a chore without any sections (should pass - no requirements) ctx := context.Background() issue := &types.Issue{ ID: "test-001", Title: "Chore issue", IssueType: types.TypeChore, Description: "Just a description, no sections needed", Status: types.StatusOpen, } if err := store.CreateIssue(ctx, issue, "test-user"); err != nil { t.Fatalf("CreateIssue: %v", err) } // Set validation mode to "error" if err := config.Initialize(); err != nil { t.Fatalf("config.Initialize: %v", err) } config.Set("validation.on-sync", "error") // Should return nil (chore has no requirements) err := validateOpenIssuesForSync(ctx) if err != nil { t.Errorf("validateOpenIssuesForSync should not error for chore issues: %v", err) } }