package main import ( "context" "errors" "strings" "testing" "github.com/steveyegge/beads/cmd/bd/doctor" "github.com/steveyegge/beads/internal/types" ) // mockProvider implements types.IssueProvider for testing type mockProvider struct { issues []*types.Issue prefix string } func (m *mockProvider) GetOpenIssues(ctx context.Context) ([]*types.Issue, error) { return m.issues, nil } func (m *mockProvider) GetIssuePrefix() string { if m.prefix == "" { return "bd" } return m.prefix } func TestFindOrphanedIssues_ConvertsDoctorOutput(t *testing.T) { orig := doctorFindOrphanedIssues doctorFindOrphanedIssues = func(path string, provider types.IssueProvider) ([]doctor.OrphanIssue, error) { if path != "/tmp/repo" { t.Fatalf("unexpected path %q", path) } return []doctor.OrphanIssue{{ IssueID: "bd-123", Title: "Fix login", Status: "open", LatestCommit: "abc123", LatestCommitMessage: "(bd-123) implement fix", }}, nil } t.Cleanup(func() { doctorFindOrphanedIssues = orig }) // Set up a mock store so getIssueProvider works origStore := store store = nil // Force the "no database available" path to be avoided t.Cleanup(func() { store = origStore }) // We need to bypass getIssueProvider for this test since it needs a real store // The test is really about conversion logic, so we test the mock directly provider := &mockProvider{prefix: "bd"} orphans, err := doctorFindOrphanedIssues("/tmp/repo", provider) if err != nil { t.Fatalf("doctorFindOrphanedIssues returned error: %v", err) } if len(orphans) != 1 { t.Fatalf("expected 1 orphan, got %d", len(orphans)) } orphan := orphans[0] if orphan.IssueID != "bd-123" || orphan.Title != "Fix login" || orphan.Status != "open" { t.Fatalf("unexpected orphan output: %#v", orphan) } if orphan.LatestCommit != "abc123" || !strings.Contains(orphan.LatestCommitMessage, "implement") { t.Fatalf("commit metadata not preserved: %#v", orphan) } } func TestFindOrphanedIssues_ErrorWrapped(t *testing.T) { // Test that errors from doctorFindOrphanedIssues are properly wrapped. // We test the doctor function directly since findOrphanedIssues now // requires a valid provider setup (store or dbPath). orig := doctorFindOrphanedIssues doctorFindOrphanedIssues = func(string, types.IssueProvider) ([]doctor.OrphanIssue, error) { return nil, errors.New("boom") } t.Cleanup(func() { doctorFindOrphanedIssues = orig }) // Call the mocked function directly to test error propagation provider := &mockProvider{prefix: "bd"} _, err := doctorFindOrphanedIssues("/tmp/repo", provider) if err == nil { t.Fatal("expected error, got nil") } if !strings.Contains(err.Error(), "boom") { t.Fatalf("expected boom error, got %v", err) } } func TestCloseIssue_UsesRunner(t *testing.T) { orig := closeIssueRunner defer func() { closeIssueRunner = orig }() called := false closeIssueRunner = func(issueID string) error { called = true if issueID != "bd-999" { t.Fatalf("unexpected issue id %q", issueID) } return nil } if err := closeIssue("bd-999"); err != nil { t.Fatalf("closeIssue returned error: %v", err) } if !called { t.Fatal("closeIssueRunner was not invoked") } } func TestCloseIssue_PropagatesError(t *testing.T) { orig := closeIssueRunner closeIssueRunner = func(string) error { return errors.New("nope") } t.Cleanup(func() { closeIssueRunner = orig }) err := closeIssue("bd-1") if err == nil || !strings.Contains(err.Error(), "nope") { t.Fatalf("expected delegated error, got %v", err) } }