fix(orphans): honor --db flag for cross-repo orphan detection (#1200)

* fix(orphans): honor --db flag for cross-repo orphan detection

Problem:
- `bd orphans --db /path` ignored the --db flag entirely
- FindOrphanedIssues() hardcoded local .beads/ directory

Solution:
- Introduce IssueProvider interface for abstract issue lookup
- Add StorageProvider adapter wrapping Storage instances
- Update FindOrphanedIssues to accept provider instead of path
- Wire orphans command to create provider from --db flag

Closes: steveyegge/beads#1196

* test(orphans): add cross-repo and provider tests for --db flag fix

- Add TestFindOrphanedIssues_WithMockProvider (table-driven, UT-01 through UT-09)
- Add TestFindOrphanedIssues_CrossRepo (validates --db flag honored)
- Add TestFindOrphanedIssues_LocalProvider (backward compat RT-01)
- Add TestFindOrphanedIssues_ProviderError (error handling UT-07)
- Add TestFindOrphanedIssues_IntegrationCrossRepo (IT-02 full)
- Add TestLocalProvider_* unit tests

Coverage for IssueProvider interface and cross-repo orphan detection.

* docs: add bd orphans command to CLI reference

Document the orphan detection command including the cross-repo
workflow enabled by the --db flag fix in this PR.
This commit is contained in:
Peter Chanthamynavong
2026-01-21 19:52:31 -08:00
committed by GitHub
parent a0dac11e42
commit c11fa799be
7 changed files with 852 additions and 61 deletions
+35 -2
View File
@@ -9,9 +9,13 @@ import (
"github.com/spf13/cobra"
"github.com/steveyegge/beads/cmd/bd/doctor"
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
)
// doctorFindOrphanedIssues is the function used to find orphaned issues.
// It accepts a git path and an IssueProvider for flexibility (cross-repo, mock testing).
var doctorFindOrphanedIssues = doctor.FindOrphanedIssues
var closeIssueRunner = func(issueID string) error {
@@ -103,9 +107,38 @@ type orphanIssueOutput struct {
LatestCommitMessage string `json:"latest_commit_message,omitempty"`
}
// findOrphanedIssues wraps the shared doctor package function and converts to output format
// getIssueProvider returns an IssueProvider based on the current configuration.
// If --db flag is set, it creates a provider from that database path.
// Otherwise, it uses the global store (already opened in PersistentPreRun).
func getIssueProvider() (types.IssueProvider, func(), error) {
// If --db flag is set and we have a dbPath, create a provider from that path
if dbPath != "" {
provider, err := doctor.NewLocalProvider(dbPath)
if err != nil {
return nil, nil, fmt.Errorf("failed to open database at %s: %w", dbPath, err)
}
return provider, func() { provider.Close() }, nil
}
// Use the global store (already opened by PersistentPreRun)
if store != nil {
provider := storage.NewStorageProvider(store)
return provider, func() {}, nil // No cleanup needed for global store
}
return nil, nil, fmt.Errorf("no database available")
}
// findOrphanedIssues wraps the shared doctor package function and converts to output format.
// It respects the --db flag for cross-repo orphan detection.
func findOrphanedIssues(path string) ([]orphanIssueOutput, error) {
orphans, err := doctorFindOrphanedIssues(path)
provider, cleanup, err := getIssueProvider()
if err != nil {
return nil, fmt.Errorf("unable to find orphaned issues: %w", err)
}
defer cleanup()
orphans, err := doctorFindOrphanedIssues(path, provider)
if err != nil {
return nil, fmt.Errorf("unable to find orphaned issues: %w", err)
}