Add Beads library API for Go integration
Expose full Storage interface and all types through public beads.go API, enabling external Go projects (like VC) to import Beads directly instead of spawning CLI processes. Changes: - Expanded beads.go with all public types (Issue, Dependency, Comment, etc.) - Added all constants (Status, IssueType, DependencyType, EventType) - Created comprehensive integration tests (beads_integration_test.go) - Added library usage example at examples/library-usage/ - Documented library integration in README.md Test coverage: 96.4% on public API, 14 integration tests, all passing. Closes bd-58, bd-59 Amp-Thread-ID: https://ampcode.com/threads/T-f0093c79-7422-45e2-b0ed-0ddfebc9ffea Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
41
README.md
41
README.md
@@ -777,6 +777,47 @@ bd ensures only one daemon runs per repository using file locking:
|
||||
|
||||
The lock file is the single source of truth for daemon status. Commands like `bd daemon --status` check lock availability rather than PID file contents, eliminating race conditions where multiple daemons could start simultaneously.
|
||||
|
||||
## Using Beads as a Library
|
||||
|
||||
For Go projects that need deeper integration, import Beads directly instead of spawning CLI processes:
|
||||
|
||||
```go
|
||||
import "github.com/steveyegge/beads"
|
||||
|
||||
// Find and open the database
|
||||
dbPath := beads.FindDatabasePath()
|
||||
store, err := beads.NewSQLiteStorage(dbPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
// Get ready work
|
||||
ctx := context.Background()
|
||||
ready, err := store.GetReadyWork(ctx, beads.WorkFilter{
|
||||
Status: beads.StatusOpen,
|
||||
Limit: 10,
|
||||
})
|
||||
|
||||
// Create issues programmatically
|
||||
issue := &beads.Issue{
|
||||
Title: "New task",
|
||||
Status: beads.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: beads.TypeTask,
|
||||
}
|
||||
store.CreateIssue(ctx, issue, "my-app")
|
||||
```
|
||||
|
||||
**Why use Beads as a library?**
|
||||
- ✅ **Direct API access** - No process spawn overhead
|
||||
- ✅ **Type safety** - Compile-time checking
|
||||
- ✅ **Performance** - Native Go calls
|
||||
- ✅ **Transactions** - Full control over database operations
|
||||
- ✅ **Error handling** - Proper Go error types
|
||||
|
||||
See [examples/library-usage/](examples/library-usage/) for a complete working example.
|
||||
|
||||
## Extending bd
|
||||
|
||||
Applications can extend bd's SQLite database with their own tables. See [EXTENDING.md](EXTENDING.md) for the full guide.
|
||||
|
||||
42
beads.go
42
beads.go
@@ -18,10 +18,21 @@ import (
|
||||
|
||||
// Core types for working with issues
|
||||
type (
|
||||
Issue = types.Issue
|
||||
Status = types.Status
|
||||
IssueType = types.IssueType
|
||||
WorkFilter = types.WorkFilter
|
||||
Issue = types.Issue
|
||||
Status = types.Status
|
||||
IssueType = types.IssueType
|
||||
Dependency = types.Dependency
|
||||
DependencyType = types.DependencyType
|
||||
Comment = types.Comment
|
||||
Event = types.Event
|
||||
EventType = types.EventType
|
||||
Label = types.Label
|
||||
BlockedIssue = types.BlockedIssue
|
||||
TreeNode = types.TreeNode
|
||||
Statistics = types.Statistics
|
||||
IssueFilter = types.IssueFilter
|
||||
WorkFilter = types.WorkFilter
|
||||
EpicStatus = types.EpicStatus
|
||||
)
|
||||
|
||||
// Status constants
|
||||
@@ -41,6 +52,29 @@ const (
|
||||
TypeChore = types.TypeChore
|
||||
)
|
||||
|
||||
// DependencyType constants
|
||||
const (
|
||||
DepBlocks = types.DepBlocks
|
||||
DepRelated = types.DepRelated
|
||||
DepParentChild = types.DepParentChild
|
||||
DepDiscoveredFrom = types.DepDiscoveredFrom
|
||||
)
|
||||
|
||||
// EventType constants
|
||||
const (
|
||||
EventCreated = types.EventCreated
|
||||
EventUpdated = types.EventUpdated
|
||||
EventStatusChanged = types.EventStatusChanged
|
||||
EventCommented = types.EventCommented
|
||||
EventClosed = types.EventClosed
|
||||
EventReopened = types.EventReopened
|
||||
EventDependencyAdded = types.EventDependencyAdded
|
||||
EventDependencyRemoved = types.EventDependencyRemoved
|
||||
EventLabelAdded = types.EventLabelAdded
|
||||
EventLabelRemoved = types.EventLabelRemoved
|
||||
EventCompacted = types.EventCompacted
|
||||
)
|
||||
|
||||
// Storage provides the minimal interface for extension orchestration
|
||||
type Storage = storage.Storage
|
||||
|
||||
|
||||
480
beads_integration_test.go
Normal file
480
beads_integration_test.go
Normal file
@@ -0,0 +1,480 @@
|
||||
package beads_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/beads"
|
||||
)
|
||||
|
||||
// TestLibraryIntegration tests the full public API that external users will use
|
||||
func TestLibraryIntegration(t *testing.T) {
|
||||
// Setup: Create a temporary database
|
||||
tmpDir, err := os.MkdirTemp("", "beads-integration-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
store, err := beads.NewSQLiteStorage(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSQLiteStorage failed: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test 1: Create issue
|
||||
t.Run("CreateIssue", func(t *testing.T) {
|
||||
issue := &beads.Issue{
|
||||
Title: "Test task",
|
||||
Description: "Integration test",
|
||||
Status: beads.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: beads.TypeTask,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := store.CreateIssue(ctx, issue, "test-actor")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateIssue failed: %v", err)
|
||||
}
|
||||
|
||||
if issue.ID == "" {
|
||||
t.Error("Issue ID should be auto-generated")
|
||||
}
|
||||
|
||||
t.Logf("Created issue: %s", issue.ID)
|
||||
})
|
||||
|
||||
// Test 2: Get issue
|
||||
t.Run("GetIssue", func(t *testing.T) {
|
||||
// Create an issue first
|
||||
issue := &beads.Issue{
|
||||
Title: "Get test",
|
||||
Status: beads.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: beads.TypeBug,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
store.CreateIssue(ctx, issue, "test-actor")
|
||||
|
||||
// Get it back
|
||||
retrieved, err := store.GetIssue(ctx, issue.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssue failed: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Title != issue.Title {
|
||||
t.Errorf("Expected title %q, got %q", issue.Title, retrieved.Title)
|
||||
}
|
||||
if retrieved.IssueType != beads.TypeBug {
|
||||
t.Errorf("Expected type bug, got %v", retrieved.IssueType)
|
||||
}
|
||||
})
|
||||
|
||||
// Test 3: Update issue
|
||||
t.Run("UpdateIssue", func(t *testing.T) {
|
||||
issue := &beads.Issue{
|
||||
Title: "Update test",
|
||||
Status: beads.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: beads.TypeTask,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
store.CreateIssue(ctx, issue, "test-actor")
|
||||
|
||||
// Update status
|
||||
updates := map[string]interface{}{
|
||||
"status": beads.StatusInProgress,
|
||||
"assignee": "test-user",
|
||||
}
|
||||
|
||||
err := store.UpdateIssue(ctx, issue.ID, updates, "test-actor")
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateIssue failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify update
|
||||
updated, _ := store.GetIssue(ctx, issue.ID)
|
||||
if updated.Status != beads.StatusInProgress {
|
||||
t.Errorf("Expected status in_progress, got %v", updated.Status)
|
||||
}
|
||||
if updated.Assignee != "test-user" {
|
||||
t.Errorf("Expected assignee test-user, got %q", updated.Assignee)
|
||||
}
|
||||
})
|
||||
|
||||
// Test 4: Add dependency
|
||||
t.Run("AddDependency", func(t *testing.T) {
|
||||
issue1 := &beads.Issue{
|
||||
Title: "Parent task",
|
||||
Status: beads.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: beads.TypeTask,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
issue2 := &beads.Issue{
|
||||
Title: "Child task",
|
||||
Status: beads.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: beads.TypeTask,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
store.CreateIssue(ctx, issue1, "test-actor")
|
||||
store.CreateIssue(ctx, issue2, "test-actor")
|
||||
|
||||
// Add dependency: issue2 blocks issue1
|
||||
dep := &beads.Dependency{
|
||||
IssueID: issue1.ID,
|
||||
DependsOnID: issue2.ID,
|
||||
Type: beads.DepBlocks,
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: "test-actor",
|
||||
}
|
||||
|
||||
err := store.AddDependency(ctx, dep, "test-actor")
|
||||
if err != nil {
|
||||
t.Fatalf("AddDependency failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify dependency
|
||||
deps, _ := store.GetDependencies(ctx, issue1.ID)
|
||||
if len(deps) != 1 {
|
||||
t.Fatalf("Expected 1 dependency, got %d", len(deps))
|
||||
}
|
||||
if deps[0].ID != issue2.ID {
|
||||
t.Errorf("Expected dependency on %s, got %s", issue2.ID, deps[0].ID)
|
||||
}
|
||||
})
|
||||
|
||||
// Test 5: Add label
|
||||
t.Run("AddLabel", func(t *testing.T) {
|
||||
issue := &beads.Issue{
|
||||
Title: "Label test",
|
||||
Status: beads.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: beads.TypeFeature,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
store.CreateIssue(ctx, issue, "test-actor")
|
||||
|
||||
err := store.AddLabel(ctx, issue.ID, "urgent", "test-actor")
|
||||
if err != nil {
|
||||
t.Fatalf("AddLabel failed: %v", err)
|
||||
}
|
||||
|
||||
labels, _ := store.GetLabels(ctx, issue.ID)
|
||||
if len(labels) != 1 {
|
||||
t.Fatalf("Expected 1 label, got %d", len(labels))
|
||||
}
|
||||
if labels[0] != "urgent" {
|
||||
t.Errorf("Expected label 'urgent', got %q", labels[0])
|
||||
}
|
||||
})
|
||||
|
||||
// Test 6: Add comment
|
||||
t.Run("AddComment", func(t *testing.T) {
|
||||
issue := &beads.Issue{
|
||||
Title: "Comment test",
|
||||
Status: beads.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: beads.TypeTask,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
store.CreateIssue(ctx, issue, "test-actor")
|
||||
|
||||
comment, err := store.AddIssueComment(ctx, issue.ID, "test-user", "Test comment")
|
||||
if err != nil {
|
||||
t.Fatalf("AddIssueComment failed: %v", err)
|
||||
}
|
||||
|
||||
if comment.Text != "Test comment" {
|
||||
t.Errorf("Expected comment text 'Test comment', got %q", comment.Text)
|
||||
}
|
||||
|
||||
comments, _ := store.GetIssueComments(ctx, issue.ID)
|
||||
if len(comments) != 1 {
|
||||
t.Fatalf("Expected 1 comment, got %d", len(comments))
|
||||
}
|
||||
})
|
||||
|
||||
// Test 7: Get ready work
|
||||
t.Run("GetReadyWork", func(t *testing.T) {
|
||||
// Create some issues
|
||||
for i := 0; i < 3; i++ {
|
||||
issue := &beads.Issue{
|
||||
Title: "Ready work test",
|
||||
Status: beads.StatusOpen,
|
||||
Priority: i,
|
||||
IssueType: beads.TypeTask,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
store.CreateIssue(ctx, issue, "test-actor")
|
||||
}
|
||||
|
||||
ready, err := store.GetReadyWork(ctx, beads.WorkFilter{
|
||||
Status: beads.StatusOpen,
|
||||
Limit: 5,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GetReadyWork failed: %v", err)
|
||||
}
|
||||
|
||||
if len(ready) == 0 {
|
||||
t.Error("Expected some ready work, got none")
|
||||
}
|
||||
|
||||
t.Logf("Found %d ready issues", len(ready))
|
||||
})
|
||||
|
||||
// Test 8: Get statistics
|
||||
t.Run("GetStatistics", func(t *testing.T) {
|
||||
stats, err := store.GetStatistics(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetStatistics failed: %v", err)
|
||||
}
|
||||
|
||||
if stats.TotalIssues == 0 {
|
||||
t.Error("Expected some total issues, got 0")
|
||||
}
|
||||
|
||||
t.Logf("Stats: Total=%d, Open=%d, InProgress=%d, Closed=%d",
|
||||
stats.TotalIssues, stats.OpenIssues, stats.InProgressIssues, stats.ClosedIssues)
|
||||
})
|
||||
|
||||
// Test 9: Close issue
|
||||
t.Run("CloseIssue", func(t *testing.T) {
|
||||
issue := &beads.Issue{
|
||||
Title: "Close test",
|
||||
Status: beads.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: beads.TypeTask,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
store.CreateIssue(ctx, issue, "test-actor")
|
||||
|
||||
err := store.CloseIssue(ctx, issue.ID, "Completed", "test-actor")
|
||||
if err != nil {
|
||||
t.Fatalf("CloseIssue failed: %v", err)
|
||||
}
|
||||
|
||||
closed, _ := store.GetIssue(ctx, issue.ID)
|
||||
if closed.Status != beads.StatusClosed {
|
||||
t.Errorf("Expected status closed, got %v", closed.Status)
|
||||
}
|
||||
if closed.ClosedAt == nil {
|
||||
t.Error("Expected ClosedAt to be set")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestDependencyTypes ensures all dependency type constants are exported
|
||||
func TestDependencyTypes(t *testing.T) {
|
||||
types := []beads.DependencyType{
|
||||
beads.DepBlocks,
|
||||
beads.DepRelated,
|
||||
beads.DepParentChild,
|
||||
beads.DepDiscoveredFrom,
|
||||
}
|
||||
|
||||
for _, dt := range types {
|
||||
if dt == "" {
|
||||
t.Errorf("Dependency type should not be empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestStatusConstants ensures all status constants are exported
|
||||
func TestStatusConstants(t *testing.T) {
|
||||
statuses := []beads.Status{
|
||||
beads.StatusOpen,
|
||||
beads.StatusInProgress,
|
||||
beads.StatusClosed,
|
||||
beads.StatusBlocked,
|
||||
}
|
||||
|
||||
for _, s := range statuses {
|
||||
if s == "" {
|
||||
t.Errorf("Status should not be empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssueTypeConstants ensures all issue type constants are exported
|
||||
func TestIssueTypeConstants(t *testing.T) {
|
||||
types := []beads.IssueType{
|
||||
beads.TypeBug,
|
||||
beads.TypeFeature,
|
||||
beads.TypeTask,
|
||||
beads.TypeEpic,
|
||||
beads.TypeChore,
|
||||
}
|
||||
|
||||
for _, it := range types {
|
||||
if it == "" {
|
||||
t.Errorf("IssueType should not be empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatchCreateIssues tests creating multiple issues at once
|
||||
func TestBatchCreateIssues(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "beads-batch-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
store, err := beads.NewSQLiteStorage(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSQLiteStorage failed: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create multiple issues
|
||||
issues := make([]*beads.Issue, 5)
|
||||
for i := 0; i < 5; i++ {
|
||||
issues[i] = &beads.Issue{
|
||||
Title: "Batch test",
|
||||
Status: beads.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: beads.TypeTask,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
err = store.CreateIssues(ctx, issues, "test-actor")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateIssues failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify all got IDs
|
||||
for i, issue := range issues {
|
||||
if issue.ID == "" {
|
||||
t.Errorf("Issue %d should have ID set", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindDatabasePathIntegration tests the database discovery
|
||||
func TestFindDatabasePathIntegration(t *testing.T) {
|
||||
// Save original working directory
|
||||
originalWd, _ := os.Getwd()
|
||||
defer os.Chdir(originalWd)
|
||||
|
||||
// Create temporary directory with .beads
|
||||
tmpDir, err := os.MkdirTemp("", "beads-find-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
os.MkdirAll(beadsDir, 0o755)
|
||||
|
||||
dbPath := filepath.Join(beadsDir, "test.db")
|
||||
f, _ := os.Create(dbPath)
|
||||
f.Close()
|
||||
|
||||
// Change to temp directory
|
||||
os.Chdir(tmpDir)
|
||||
|
||||
// Should find the database
|
||||
found := beads.FindDatabasePath()
|
||||
if found == "" {
|
||||
t.Error("Expected to find database, got empty string")
|
||||
}
|
||||
|
||||
t.Logf("Found database at: %s", found)
|
||||
}
|
||||
|
||||
// TestRoundTripIssue tests creating, updating, and retrieving an issue
|
||||
func TestRoundTripIssue(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "beads-roundtrip-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
store, err := beads.NewSQLiteStorage(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSQLiteStorage failed: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create issue with all fields
|
||||
original := &beads.Issue{
|
||||
Title: "Complete issue",
|
||||
Description: "Full description",
|
||||
Design: "Design notes",
|
||||
AcceptanceCriteria: "Acceptance criteria",
|
||||
Notes: "Implementation notes",
|
||||
Status: beads.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: beads.TypeFeature,
|
||||
Assignee: "developer",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = store.CreateIssue(ctx, original, "test-actor")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateIssue failed: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve and verify all fields
|
||||
retrieved, err := store.GetIssue(ctx, original.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssue failed: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Title != original.Title {
|
||||
t.Errorf("Title mismatch: expected %q, got %q", original.Title, retrieved.Title)
|
||||
}
|
||||
if retrieved.Description != original.Description {
|
||||
t.Errorf("Description mismatch")
|
||||
}
|
||||
if retrieved.Design != original.Design {
|
||||
t.Errorf("Design mismatch")
|
||||
}
|
||||
if retrieved.AcceptanceCriteria != original.AcceptanceCriteria {
|
||||
t.Errorf("AcceptanceCriteria mismatch")
|
||||
}
|
||||
if retrieved.Notes != original.Notes {
|
||||
t.Errorf("Notes mismatch")
|
||||
}
|
||||
if retrieved.Status != original.Status {
|
||||
t.Errorf("Status mismatch")
|
||||
}
|
||||
if retrieved.Priority != original.Priority {
|
||||
t.Errorf("Priority mismatch")
|
||||
}
|
||||
if retrieved.IssueType != original.IssueType {
|
||||
t.Errorf("IssueType mismatch")
|
||||
}
|
||||
if retrieved.Assignee != original.Assignee {
|
||||
t.Errorf("Assignee mismatch")
|
||||
}
|
||||
}
|
||||
199
examples/library-usage/README.md
Normal file
199
examples/library-usage/README.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# Beads Library Usage Example
|
||||
|
||||
This example demonstrates using Beads as a Go library in external projects (like VC).
|
||||
|
||||
## Why Use Beads as a Library?
|
||||
|
||||
Instead of spawning `bd` CLI processes:
|
||||
- ✅ **Direct API access** - Call functions directly instead of parsing JSON output
|
||||
- ✅ **Type safety** - Compile-time checking of types and interfaces
|
||||
- ✅ **Performance** - No process spawn overhead
|
||||
- ✅ **Transactions** - Access to database transactions for complex operations
|
||||
- ✅ **Shared database** - Multiple components can use same database connection
|
||||
- ✅ **Error handling** - Proper Go error types instead of parsing stderr
|
||||
|
||||
## Installation
|
||||
|
||||
In your Go project:
|
||||
|
||||
```bash
|
||||
go get github.com/steveyegge/beads@latest
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/steveyegge/beads"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Find and open database
|
||||
dbPath := beads.FindDatabasePath()
|
||||
store, err := beads.NewSQLiteStorage(dbPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
// Get ready work
|
||||
ready, err := store.GetReadyWork(ctx, beads.WorkFilter{
|
||||
Status: beads.StatusOpen,
|
||||
Limit: 10,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Process ready issues...
|
||||
}
|
||||
```
|
||||
|
||||
## Running This Example
|
||||
|
||||
```bash
|
||||
# From this directory
|
||||
cd examples/library-usage
|
||||
|
||||
# Make sure there's a Beads database
|
||||
bd init --prefix demo
|
||||
|
||||
# Run the example
|
||||
go run main.go
|
||||
```
|
||||
|
||||
## Available Operations
|
||||
|
||||
The `beads.Storage` interface provides:
|
||||
|
||||
### Issues
|
||||
- `CreateIssue(ctx, issue, actor)` - Create a new issue
|
||||
- `CreateIssues(ctx, issues, actor)` - Batch create issues
|
||||
- `GetIssue(ctx, id)` - Get issue by ID
|
||||
- `UpdateIssue(ctx, id, updates, actor)` - Update issue fields
|
||||
- `CloseIssue(ctx, id, reason, actor)` - Close an issue
|
||||
- `SearchIssues(ctx, query, filter)` - Search with filters
|
||||
|
||||
### Dependencies
|
||||
- `AddDependency(ctx, dep, actor)` - Add dependency between issues
|
||||
- `RemoveDependency(ctx, issueID, dependsOnID, actor)` - Remove dependency
|
||||
- `GetDependencies(ctx, issueID)` - Get what this issue depends on
|
||||
- `GetDependents(ctx, issueID)` - Get what depends on this issue
|
||||
- `GetDependencyTree(ctx, issueID, maxDepth, showAllPaths)` - Visualize tree
|
||||
|
||||
### Labels
|
||||
- `AddLabel(ctx, issueID, label, actor)` - Add label to issue
|
||||
- `RemoveLabel(ctx, issueID, label, actor)` - Remove label
|
||||
- `GetLabels(ctx, issueID)` - Get all labels for an issue
|
||||
- `GetIssuesByLabel(ctx, label)` - Find issues with label
|
||||
|
||||
### Ready Work & Blocking
|
||||
- `GetReadyWork(ctx, filter)` - Find issues with no blockers
|
||||
- `GetBlockedIssues(ctx)` - Find blocked issues with blocker info
|
||||
- `GetEpicsEligibleForClosure(ctx)` - Find completable epics
|
||||
|
||||
### Comments & Events
|
||||
- `AddIssueComment(ctx, issueID, author, text)` - Add comment
|
||||
- `GetIssueComments(ctx, issueID)` - Get all comments
|
||||
- `GetEvents(ctx, issueID, limit)` - Get audit trail
|
||||
|
||||
### Statistics
|
||||
- `GetStatistics(ctx)` - Get aggregate metrics
|
||||
|
||||
## Types
|
||||
|
||||
All types are exported via the `beads` package:
|
||||
|
||||
```go
|
||||
// Core types
|
||||
beads.Issue
|
||||
beads.Status (Open, InProgress, Closed, Blocked)
|
||||
beads.IssueType (Bug, Feature, Task, Epic, Chore)
|
||||
beads.Priority (0-4)
|
||||
|
||||
// Relationships
|
||||
beads.Dependency
|
||||
beads.DependencyType (Blocks, Related, ParentChild, DiscoveredFrom)
|
||||
|
||||
// Metadata
|
||||
beads.Label
|
||||
beads.Comment
|
||||
beads.Event
|
||||
|
||||
// Queries
|
||||
beads.IssueFilter
|
||||
beads.WorkFilter
|
||||
beads.BlockedIssue
|
||||
beads.EpicStatus
|
||||
beads.Statistics
|
||||
```
|
||||
|
||||
## VC Integration Example
|
||||
|
||||
For VC (VibeCoder), the integration would look like:
|
||||
|
||||
```go
|
||||
// In VC's storage layer
|
||||
type VCStorage struct {
|
||||
beads beads.Storage
|
||||
}
|
||||
|
||||
func NewVCStorage(dbPath string) (*VCStorage, error) {
|
||||
store, err := beads.NewSQLiteStorage(dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &VCStorage{beads: store}, nil
|
||||
}
|
||||
|
||||
// Claim ready work for executor
|
||||
func (s *VCStorage) ClaimWork(ctx context.Context, executorID string) (*beads.Issue, error) {
|
||||
ready, err := s.beads.GetReadyWork(ctx, beads.WorkFilter{
|
||||
Status: beads.StatusOpen,
|
||||
Limit: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(ready) == 0 {
|
||||
return nil, nil // No work available
|
||||
}
|
||||
|
||||
issue := ready[0]
|
||||
|
||||
// Claim it
|
||||
updates := map[string]interface{}{
|
||||
"status": beads.StatusInProgress,
|
||||
"assignee": executorID,
|
||||
}
|
||||
|
||||
if err := s.beads.UpdateIssue(ctx, issue.ID, updates, executorID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return issue, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Context** - Always pass `context.Context` for cancellation support
|
||||
2. **Actor** - Provide meaningful actor strings for audit trail
|
||||
3. **Error handling** - Check all errors; database operations can fail
|
||||
4. **Close** - Always `defer store.Close()` after opening
|
||||
5. **Transactions** - For complex multi-step operations, consider using the underlying database connection directly
|
||||
|
||||
## See Also
|
||||
|
||||
- [EXTENDING.md](../../EXTENDING.md) - Detailed extension guide
|
||||
- [beads.go](../../beads.go) - Public API source
|
||||
- [internal/storage/storage.go](../../internal/storage/storage.go) - Storage interface
|
||||
22
examples/library-usage/go.mod
Normal file
22
examples/library-usage/go.mod
Normal file
@@ -0,0 +1,22 @@
|
||||
module example.com/beads-library-demo
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require github.com/steveyegge/beads v0.0.0
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.38.2 // indirect
|
||||
)
|
||||
|
||||
// For local development, replace with local path
|
||||
replace github.com/steveyegge/beads => ../..
|
||||
49
examples/library-usage/go.sum
Normal file
49
examples/library-usage/go.sum
Normal file
@@ -0,0 +1,49 @@
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
||||
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
129
examples/library-usage/main.go
Normal file
129
examples/library-usage/main.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// Package main demonstrates using Beads as a Go library
|
||||
//
|
||||
// This example shows how an external project (like VC) can import and use
|
||||
// Beads programmatically instead of spawning CLI processes.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/beads"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Find the Beads database (looks for .beads/*.db in current/parent dirs)
|
||||
dbPath := beads.FindDatabasePath()
|
||||
if dbPath == "" {
|
||||
log.Fatal("No Beads database found. Run 'bd init' first.")
|
||||
}
|
||||
|
||||
fmt.Printf("Using database: %s\n\n", dbPath)
|
||||
|
||||
// Open the database
|
||||
store, err := beads.NewSQLiteStorage(dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open storage: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
// Example 1: Get ready work
|
||||
fmt.Println("=== Ready Work ===")
|
||||
ready, err := store.GetReadyWork(ctx, beads.WorkFilter{
|
||||
Status: beads.StatusOpen,
|
||||
Limit: 5,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get ready work: %v", err)
|
||||
}
|
||||
|
||||
for _, issue := range ready {
|
||||
fmt.Printf("- %s: %s (priority %d)\n", issue.ID, issue.Title, issue.Priority)
|
||||
}
|
||||
|
||||
// Example 2: Create an issue
|
||||
fmt.Println("\n=== Creating Issue ===")
|
||||
newIssue := &beads.Issue{
|
||||
ID: "", // Empty = auto-generate
|
||||
Title: "Example library-created issue",
|
||||
Description: "This issue was created programmatically using Beads as a library",
|
||||
Status: beads.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: beads.TypeTask,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := store.CreateIssue(ctx, newIssue, "library-example"); err != nil {
|
||||
log.Fatalf("Failed to create issue: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Created issue: %s\n", newIssue.ID)
|
||||
|
||||
// Example 3: Add a dependency
|
||||
fmt.Println("\n=== Adding Dependency ===")
|
||||
dep := &beads.Dependency{
|
||||
IssueID: newIssue.ID,
|
||||
DependsOnID: "bd-1", // Assumes bd-1 exists
|
||||
Type: beads.DepDiscoveredFrom,
|
||||
CreatedAt: time.Now(),
|
||||
CreatedBy: "library-example",
|
||||
}
|
||||
|
||||
if err := store.AddDependency(ctx, dep, "library-example"); err != nil {
|
||||
// Don't fail if bd-1 doesn't exist
|
||||
fmt.Printf("Note: Could not add dependency (bd-1 may not exist): %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Added dependency: %s discovered-from bd-1\n", newIssue.ID)
|
||||
}
|
||||
|
||||
// Example 4: Add a label
|
||||
fmt.Println("\n=== Adding Label ===")
|
||||
if err := store.AddLabel(ctx, newIssue.ID, "library-usage", "library-example"); err != nil {
|
||||
log.Fatalf("Failed to add label: %v", err)
|
||||
}
|
||||
fmt.Printf("Added label 'library-usage' to %s\n", newIssue.ID)
|
||||
|
||||
// Example 5: Add a comment
|
||||
fmt.Println("\n=== Adding Comment ===")
|
||||
comment, err := store.AddIssueComment(ctx, newIssue.ID, "library-example", "This is a programmatic comment")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to add comment: %v", err)
|
||||
}
|
||||
fmt.Printf("Added comment #%d\n", comment.ID)
|
||||
|
||||
// Example 6: Update issue status
|
||||
fmt.Println("\n=== Updating Status ===")
|
||||
updates := map[string]interface{}{
|
||||
"status": beads.StatusInProgress,
|
||||
}
|
||||
if err := store.UpdateIssue(ctx, newIssue.ID, updates, "library-example"); err != nil {
|
||||
log.Fatalf("Failed to update issue: %v", err)
|
||||
}
|
||||
fmt.Printf("Updated %s status to in_progress\n", newIssue.ID)
|
||||
|
||||
// Example 7: Get statistics
|
||||
fmt.Println("\n=== Statistics ===")
|
||||
stats, err := store.GetStatistics(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get statistics: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Total issues: %d\n", stats.TotalIssues)
|
||||
fmt.Printf("Open: %d | In Progress: %d | Closed: %d | Blocked: %d | Ready: %d\n",
|
||||
stats.OpenIssues, stats.InProgressIssues, stats.ClosedIssues,
|
||||
stats.BlockedIssues, stats.ReadyIssues)
|
||||
|
||||
// Example 8: Close the issue
|
||||
fmt.Println("\n=== Closing Issue ===")
|
||||
if err := store.CloseIssue(ctx, newIssue.ID, "Completed demo", "library-example"); err != nil {
|
||||
log.Fatalf("Failed to close issue: %v", err)
|
||||
}
|
||||
fmt.Printf("Closed issue %s\n", newIssue.ID)
|
||||
|
||||
fmt.Println("\n✅ Library usage demo complete!")
|
||||
}
|
||||
169
examples/library-usage/main_test.go
Normal file
169
examples/library-usage/main_test.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/beads"
|
||||
)
|
||||
|
||||
// TestExampleCompiles ensures the example code compiles and basic API works
|
||||
func TestExampleCompiles(t *testing.T) {
|
||||
// Create temporary database for testing
|
||||
tmpDir, err := os.MkdirTemp("", "beads-example-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
dbPath := filepath.Join(tmpDir, "test.db")
|
||||
|
||||
// Open storage
|
||||
store, err := beads.NewSQLiteStorage(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open storage: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create an issue (from example code)
|
||||
newIssue := &beads.Issue{
|
||||
ID: "",
|
||||
Title: "Test library-created issue",
|
||||
Description: "This verifies the library example works",
|
||||
Status: beads.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: beads.TypeTask,
|
||||
}
|
||||
|
||||
if err := store.CreateIssue(ctx, newIssue, "test"); err != nil {
|
||||
t.Fatalf("Failed to create issue: %v", err)
|
||||
}
|
||||
|
||||
if newIssue.ID == "" {
|
||||
t.Error("Issue ID should be auto-generated")
|
||||
}
|
||||
|
||||
// Get ready work (from example code)
|
||||
ready, err := store.GetReadyWork(ctx, beads.WorkFilter{
|
||||
Status: beads.StatusOpen,
|
||||
Limit: 5,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get ready work: %v", err)
|
||||
}
|
||||
|
||||
if len(ready) == 0 {
|
||||
t.Error("Expected at least one ready issue")
|
||||
}
|
||||
|
||||
// Add label (from example code)
|
||||
if err := store.AddLabel(ctx, newIssue.ID, "test-label", "test"); err != nil {
|
||||
t.Fatalf("Failed to add label: %v", err)
|
||||
}
|
||||
|
||||
labels, err := store.GetLabels(ctx, newIssue.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get labels: %v", err)
|
||||
}
|
||||
|
||||
if len(labels) != 1 || labels[0] != "test-label" {
|
||||
t.Errorf("Expected label 'test-label', got %v", labels)
|
||||
}
|
||||
|
||||
// Update status (from example code)
|
||||
updates := map[string]interface{}{
|
||||
"status": beads.StatusInProgress,
|
||||
}
|
||||
if err := store.UpdateIssue(ctx, newIssue.ID, updates, "test"); err != nil {
|
||||
t.Fatalf("Failed to update issue: %v", err)
|
||||
}
|
||||
|
||||
// Get statistics (from example code)
|
||||
stats, err := store.GetStatistics(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get statistics: %v", err)
|
||||
}
|
||||
|
||||
if stats.TotalIssues == 0 {
|
||||
t.Error("Expected at least one total issue")
|
||||
}
|
||||
|
||||
// Close issue (from example code)
|
||||
if err := store.CloseIssue(ctx, newIssue.ID, "Test complete", "test"); err != nil {
|
||||
t.Fatalf("Failed to close issue: %v", err)
|
||||
}
|
||||
|
||||
// Verify closed
|
||||
closed, err := store.GetIssue(ctx, newIssue.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get issue: %v", err)
|
||||
}
|
||||
|
||||
if closed.Status != beads.StatusClosed {
|
||||
t.Errorf("Expected status closed, got %v", closed.Status)
|
||||
}
|
||||
|
||||
t.Log("✅ All example operations work correctly")
|
||||
}
|
||||
|
||||
// TestDependencyConstants ensures all constants are accessible
|
||||
func TestDependencyConstants(t *testing.T) {
|
||||
// Test that all constants from the example are accessible
|
||||
_ = beads.StatusOpen
|
||||
_ = beads.StatusInProgress
|
||||
_ = beads.StatusClosed
|
||||
_ = beads.StatusBlocked
|
||||
|
||||
_ = beads.TypeBug
|
||||
_ = beads.TypeFeature
|
||||
_ = beads.TypeTask
|
||||
_ = beads.TypeEpic
|
||||
_ = beads.TypeChore
|
||||
|
||||
_ = beads.DepBlocks
|
||||
_ = beads.DepRelated
|
||||
_ = beads.DepParentChild
|
||||
_ = beads.DepDiscoveredFrom
|
||||
}
|
||||
|
||||
// TestFindDatabasePath tests database discovery
|
||||
func TestFindDatabasePath(t *testing.T) {
|
||||
// Create temp directory with .beads
|
||||
tmpDir, err := os.MkdirTemp("", "beads-finddb-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0o755); err != nil {
|
||||
t.Fatalf("Failed to create .beads dir: %v", err)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(beadsDir, "test.db")
|
||||
f, err := os.Create(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create db file: %v", err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
// Change to temp directory
|
||||
originalWd, _ := os.Getwd()
|
||||
defer os.Chdir(originalWd)
|
||||
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("Failed to chdir: %v", err)
|
||||
}
|
||||
|
||||
// Test FindDatabasePath
|
||||
found := beads.FindDatabasePath()
|
||||
if found == "" {
|
||||
t.Error("Expected to find database, got empty string")
|
||||
}
|
||||
|
||||
t.Logf("Found database: %s", found)
|
||||
}
|
||||
Reference in New Issue
Block a user