feat: add beads CLI wrapper package (gt-u1j.13)
Implements the internal/beads package for shelling out to the bd CLI. Interface: - New(workDir) - constructor - List(opts) - query issues with filters - Ready() - get issues not blocked - Blocked() - get blocked issues - Show(id) - get issue details - Create(opts) - create new issue - Update(id, opts) - update issue - Close(ids...) - close issues - AddDependency/RemoveDependency - manage deps - Sync/SyncFromMain/SyncStatus - sync operations Error handling: - ErrNotInstalled - bd binary not found - ErrNotARepo - not a beads repository - ErrSyncConflict - sync conflict detected - ErrNotFound - issue not found 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
185
internal/beads/beads_test.go
Normal file
185
internal/beads/beads_test.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package beads
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestNew verifies the constructor.
|
||||
func TestNew(t *testing.T) {
|
||||
b := New("/some/path")
|
||||
if b == nil {
|
||||
t.Fatal("New returned nil")
|
||||
}
|
||||
if b.workDir != "/some/path" {
|
||||
t.Errorf("workDir = %q, want /some/path", b.workDir)
|
||||
}
|
||||
}
|
||||
|
||||
// TestListOptions verifies ListOptions defaults.
|
||||
func TestListOptions(t *testing.T) {
|
||||
opts := ListOptions{
|
||||
Status: "open",
|
||||
Type: "task",
|
||||
Priority: 1,
|
||||
}
|
||||
if opts.Status != "open" {
|
||||
t.Errorf("Status = %q, want open", opts.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateOptions verifies CreateOptions fields.
|
||||
func TestCreateOptions(t *testing.T) {
|
||||
opts := CreateOptions{
|
||||
Title: "Test issue",
|
||||
Type: "task",
|
||||
Priority: 2,
|
||||
Description: "A test description",
|
||||
Parent: "gt-abc",
|
||||
}
|
||||
if opts.Title != "Test issue" {
|
||||
t.Errorf("Title = %q, want 'Test issue'", opts.Title)
|
||||
}
|
||||
if opts.Parent != "gt-abc" {
|
||||
t.Errorf("Parent = %q, want gt-abc", opts.Parent)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateOptions verifies UpdateOptions pointer fields.
|
||||
func TestUpdateOptions(t *testing.T) {
|
||||
status := "in_progress"
|
||||
priority := 1
|
||||
opts := UpdateOptions{
|
||||
Status: &status,
|
||||
Priority: &priority,
|
||||
}
|
||||
if *opts.Status != "in_progress" {
|
||||
t.Errorf("Status = %q, want in_progress", *opts.Status)
|
||||
}
|
||||
if *opts.Priority != 1 {
|
||||
t.Errorf("Priority = %d, want 1", *opts.Priority)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsBeadsRepo tests repository detection.
|
||||
func TestIsBeadsRepo(t *testing.T) {
|
||||
// Test with a non-beads directory
|
||||
tmpDir, err := os.MkdirTemp("", "beads-test-*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
b := New(tmpDir)
|
||||
// This should return false since there's no .beads directory
|
||||
// and bd list will fail
|
||||
if b.IsBeadsRepo() {
|
||||
// This might pass if bd handles missing .beads gracefully
|
||||
t.Log("IsBeadsRepo returned true for non-beads directory (bd might initialize)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestWrapError tests error wrapping.
|
||||
func TestWrapError(t *testing.T) {
|
||||
b := New("/test")
|
||||
|
||||
tests := []struct {
|
||||
stderr string
|
||||
wantErr error
|
||||
wantNil bool
|
||||
}{
|
||||
{"not a beads repository", ErrNotARepo, false},
|
||||
{"No .beads directory found", ErrNotARepo, false},
|
||||
{".beads directory not found", ErrNotARepo, false},
|
||||
{"sync conflict detected", ErrSyncConflict, false},
|
||||
{"CONFLICT in file.md", ErrSyncConflict, false},
|
||||
{"Issue not found: gt-xyz", ErrNotFound, false},
|
||||
{"gt-xyz not found", ErrNotFound, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
err := b.wrapError(nil, tt.stderr, []string{"test"})
|
||||
if tt.wantNil {
|
||||
if err != nil {
|
||||
t.Errorf("wrapError(%q) = %v, want nil", tt.stderr, err)
|
||||
}
|
||||
} else {
|
||||
if err != tt.wantErr {
|
||||
t.Errorf("wrapError(%q) = %v, want %v", tt.stderr, err, tt.wantErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Integration test that runs against real bd if available
|
||||
func TestIntegration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
// Find a beads repo (use current directory if it has .beads)
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Walk up to find .beads
|
||||
dir := cwd
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, ".beads")); err == nil {
|
||||
break
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
t.Skip("no .beads directory found in path")
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
|
||||
b := New(dir)
|
||||
|
||||
// Test List
|
||||
t.Run("List", func(t *testing.T) {
|
||||
issues, err := b.List(ListOptions{Status: "open"})
|
||||
if err != nil {
|
||||
t.Fatalf("List failed: %v", err)
|
||||
}
|
||||
t.Logf("Found %d open issues", len(issues))
|
||||
})
|
||||
|
||||
// Test Ready
|
||||
t.Run("Ready", func(t *testing.T) {
|
||||
issues, err := b.Ready()
|
||||
if err != nil {
|
||||
t.Fatalf("Ready failed: %v", err)
|
||||
}
|
||||
t.Logf("Found %d ready issues", len(issues))
|
||||
})
|
||||
|
||||
// Test Blocked
|
||||
t.Run("Blocked", func(t *testing.T) {
|
||||
issues, err := b.Blocked()
|
||||
if err != nil {
|
||||
t.Fatalf("Blocked failed: %v", err)
|
||||
}
|
||||
t.Logf("Found %d blocked issues", len(issues))
|
||||
})
|
||||
|
||||
// Test Show (if we have issues)
|
||||
t.Run("Show", func(t *testing.T) {
|
||||
issues, err := b.List(ListOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("List failed: %v", err)
|
||||
}
|
||||
if len(issues) == 0 {
|
||||
t.Skip("no issues to show")
|
||||
}
|
||||
|
||||
issue, err := b.Show(issues[0].ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Show(%s) failed: %v", issues[0].ID, err)
|
||||
}
|
||||
t.Logf("Showed issue: %s - %s", issue.ID, issue.Title)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user