feat(dep): add --blocks shorthand flag for natural dependency syntax
Agents naturally try to use 'bd dep <blocker> --blocks <blocked>' when establishing blocking relationships - a desire path revealing intuitive mental model for how dependencies should work. When AI agents set up dependency chains, they consistently attempt: bd dep conduit-abc --blocks conduit-xyz This reveals a desire path - the syntax users naturally reach for before reading documentation. Instead of fighting this intuition, we embrace it. - Add --blocks (-b) flag to the bd dep command - Support syntax: bd dep <blocker-id> --blocks <blocked-id> - Equivalent to: bd dep add <blocked-id> <blocker-id> - Full daemon and direct mode support - Cycle detection and child-parent anti-pattern checks - JSON output support for programmatic use This is purely additive. The existing command structure remains: - 'bd dep add' subcommand works exactly as before - All other dep subcommands (remove, list, tree, cycles) unchanged - No breaking changes to existing workflows bd dep bd-xyz --blocks bd-abc # bd-xyz blocks bd-abc bd dep bd-xyz -b bd-abc # Same, using shorthand bd dep add bd-abc bd-xyz # Original syntax still works - Added TestDepBlocksFlag for flag initialization - Added TestDepBlocksFlagFunctionality for semantic correctness - All existing tests pass
This commit is contained in:
@@ -237,8 +237,8 @@ func TestDepCommandsInit(t *testing.T) {
|
||||
t.Fatal("depCmd should be initialized")
|
||||
}
|
||||
|
||||
if depCmd.Use != "dep" {
|
||||
t.Errorf("Expected Use='dep', got %q", depCmd.Use)
|
||||
if depCmd.Use != "dep [issue-id]" {
|
||||
t.Errorf("Expected Use='dep [issue-id]', got %q", depCmd.Use)
|
||||
}
|
||||
|
||||
if depAddCmd == nil {
|
||||
@@ -279,6 +279,103 @@ func TestDepAddFlagAliases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDepBlocksFlag(t *testing.T) {
|
||||
// Test that the --blocks flag exists on depCmd
|
||||
flag := depCmd.Flags().Lookup("blocks")
|
||||
if flag == nil {
|
||||
t.Fatal("depCmd should have --blocks flag")
|
||||
}
|
||||
|
||||
// Test shorthand is -b
|
||||
if flag.Shorthand != "b" {
|
||||
t.Errorf("Expected shorthand='b', got %q", flag.Shorthand)
|
||||
}
|
||||
|
||||
// Test default value is empty string
|
||||
if flag.DefValue != "" {
|
||||
t.Errorf("Expected default blocks='', got %q", flag.DefValue)
|
||||
}
|
||||
|
||||
// Test usage text
|
||||
if !strings.Contains(flag.Usage, "blocks") {
|
||||
t.Errorf("Expected flag usage to mention 'blocks', got %q", flag.Usage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDepBlocksFlagFunctionality(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testDB := filepath.Join(tmpDir, ".beads", "beads.db")
|
||||
s := newTestStore(t, testDB)
|
||||
ctx := context.Background()
|
||||
|
||||
// Create test issues
|
||||
issues := []*types.Issue{
|
||||
{
|
||||
ID: "test-blocks-1",
|
||||
Title: "Blocker Issue",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: "test-blocks-2",
|
||||
Title: "Blocked Issue",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
if err := s.CreateIssue(ctx, issue, "test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add dependency using the same logic as --blocks flag would:
|
||||
// "blocker --blocks blocked" means blocked depends on blocker
|
||||
dep := &types.Dependency{
|
||||
IssueID: "test-blocks-2", // blocked issue
|
||||
DependsOnID: "test-blocks-1", // blocker issue
|
||||
Type: types.DepBlocks,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.AddDependency(ctx, dep, "test"); err != nil {
|
||||
t.Fatalf("AddDependency failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the blocked issue now depends on the blocker
|
||||
deps, err := s.GetDependencies(ctx, "test-blocks-2")
|
||||
if err != nil {
|
||||
t.Fatalf("GetDependencies failed: %v", err)
|
||||
}
|
||||
|
||||
if len(deps) != 1 {
|
||||
t.Fatalf("Expected 1 dependency, got %d", len(deps))
|
||||
}
|
||||
|
||||
if deps[0].ID != "test-blocks-1" {
|
||||
t.Errorf("Expected blocked issue to depend on test-blocks-1, got %s", deps[0].ID)
|
||||
}
|
||||
|
||||
// Verify the blocker has a dependent
|
||||
dependents, err := s.GetDependents(ctx, "test-blocks-1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetDependents failed: %v", err)
|
||||
}
|
||||
|
||||
if len(dependents) != 1 {
|
||||
t.Fatalf("Expected 1 dependent, got %d", len(dependents))
|
||||
}
|
||||
|
||||
if dependents[0].ID != "test-blocks-2" {
|
||||
t.Errorf("Expected test-blocks-1 to have dependent test-blocks-2, got %s", dependents[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDepTreeFormatFlag(t *testing.T) {
|
||||
// Test that the --format flag exists on depTreeCmd
|
||||
flag := depTreeCmd.Flags().Lookup("format")
|
||||
|
||||
Reference in New Issue
Block a user