feat: add 'convoy' issue type with reactive completion (bd-hj0s)

- Add TypeConvoy to issue types for cross-project tracking
- Implement reactive completion: when all tracked issues close,
  convoy auto-closes with reason "All tracked issues completed"
- Uses 'tracks' dependency type (non-blocking, cross-prefix capable)
- Update help text for --type flag in list/create commands
- Add test for convoy reactive completion behavior

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Executed-By: beads/crew/dave
Rig: beads
Role: crew
This commit is contained in:
Steve Yegge
2025-12-30 00:04:43 -08:00
parent 160feb95ea
commit b63df91230
6 changed files with 247 additions and 3 deletions

View File

@@ -1469,6 +1469,100 @@ func TestDeleteConfig(t *testing.T) {
}
}
func TestConvoyReactiveCompletion(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create a convoy
convoy := &types.Issue{
Title: "Test Convoy",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeConvoy,
}
err := store.CreateIssue(ctx, convoy, "test-user")
if err != nil {
t.Fatalf("CreateIssue convoy failed: %v", err)
}
// Create two issues to track
issue1 := &types.Issue{
Title: "Tracked Issue 1",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
err = store.CreateIssue(ctx, issue1, "test-user")
if err != nil {
t.Fatalf("CreateIssue issue1 failed: %v", err)
}
issue2 := &types.Issue{
Title: "Tracked Issue 2",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
err = store.CreateIssue(ctx, issue2, "test-user")
if err != nil {
t.Fatalf("CreateIssue issue2 failed: %v", err)
}
// Add tracking dependencies: convoy tracks issue1 and issue2
dep1 := &types.Dependency{
IssueID: convoy.ID,
DependsOnID: issue1.ID,
Type: types.DepTracks,
}
err = store.AddDependency(ctx, dep1, "test-user")
if err != nil {
t.Fatalf("AddDependency for issue1 failed: %v", err)
}
dep2 := &types.Dependency{
IssueID: convoy.ID,
DependsOnID: issue2.ID,
Type: types.DepTracks,
}
err = store.AddDependency(ctx, dep2, "test-user")
if err != nil {
t.Fatalf("AddDependency for issue2 failed: %v", err)
}
// Close first issue - convoy should still be open
err = store.CloseIssue(ctx, issue1.ID, "Done", "test-user")
if err != nil {
t.Fatalf("CloseIssue issue1 failed: %v", err)
}
convoyAfter1, err := store.GetIssue(ctx, convoy.ID)
if err != nil {
t.Fatalf("GetIssue convoy after issue1 closed failed: %v", err)
}
if convoyAfter1.Status == types.StatusClosed {
t.Error("Convoy should NOT be closed after only first tracked issue is closed")
}
// Close second issue - convoy should auto-close now
err = store.CloseIssue(ctx, issue2.ID, "Done", "test-user")
if err != nil {
t.Fatalf("CloseIssue issue2 failed: %v", err)
}
convoyAfter2, err := store.GetIssue(ctx, convoy.ID)
if err != nil {
t.Fatalf("GetIssue convoy after issue2 closed failed: %v", err)
}
if convoyAfter2.Status != types.StatusClosed {
t.Errorf("Convoy should be auto-closed when all tracked issues are closed, got status: %v", convoyAfter2.Status)
}
if convoyAfter2.CloseReason != "All tracked issues completed" {
t.Errorf("Convoy close reason should be 'All tracked issues completed', got: %q", convoyAfter2.CloseReason)
}
}
func TestIsClosed(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()