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:
kraitsura
2026-01-03 23:18:30 -08:00
committed by Steve Yegge
parent f92090344a
commit 44626262e2
2 changed files with 256 additions and 7 deletions

View File

@@ -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")