fix(rig): return rig root from BeadsPath() to respect redirect system
BeadsPath() was incorrectly returning <rig>/mayor/rig when HasMayor was true, bypassing the redirect system at <rig>/.beads/redirect. This caused beads operations to fail when the user's repo doesn't have tracked beads. The redirect architecture is: - <rig>/.beads/redirect -> mayor/rig/.beads (when repo tracks .beads/) - <rig>/.beads/ contains local database (when repo doesn't track .beads/) By always returning the rig root, all callers now go through the redirect system which is set up by initBeads() during rig creation. Affected callers (all now work correctly): - internal/refinery/manager.go - Queue() for merge requests - internal/swarm/manager.go - swarm operations - internal/cmd/swarm.go - swarm CLI commands - internal/cmd/status.go - rig status display - internal/cmd/mq_next.go - merge queue operations - internal/cmd/mq_list.go - merge queue listing - internal/cmd/rig_dock.go - dock/undock operations Fixes #317 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -308,6 +308,89 @@ func TestEnsureGitignoreEntry_AppendsToExisting(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInitBeads_TrackedBeads_CreatesRedirect(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// When the cloned repo has tracked beads (mayor/rig/.beads exists),
|
||||||
|
// initBeads should create a redirect file at <rig>/.beads/redirect
|
||||||
|
// pointing to mayor/rig/.beads instead of creating a local database.
|
||||||
|
rigPath := t.TempDir()
|
||||||
|
|
||||||
|
// Simulate tracked beads in the cloned repo
|
||||||
|
mayorBeadsDir := filepath.Join(rigPath, "mayor", "rig", ".beads")
|
||||||
|
if err := os.MkdirAll(mayorBeadsDir, 0755); err != nil {
|
||||||
|
t.Fatalf("mkdir mayor beads: %v", err)
|
||||||
|
}
|
||||||
|
// Create a config file to simulate a real beads directory
|
||||||
|
if err := os.WriteFile(filepath.Join(mayorBeadsDir, "config.yaml"), []byte("prefix: gt\n"), 0644); err != nil {
|
||||||
|
t.Fatalf("write mayor config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manager := &Manager{}
|
||||||
|
if err := manager.initBeads(rigPath, "gt"); err != nil {
|
||||||
|
t.Fatalf("initBeads: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify redirect file was created
|
||||||
|
redirectPath := filepath.Join(rigPath, ".beads", "redirect")
|
||||||
|
content, err := os.ReadFile(redirectPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reading redirect file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "mayor/rig/.beads\n"
|
||||||
|
if string(content) != expected {
|
||||||
|
t.Errorf("redirect content = %q, want %q", string(content), expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no local database was created (no config.yaml at rig level)
|
||||||
|
rigConfigPath := filepath.Join(rigPath, ".beads", "config.yaml")
|
||||||
|
if _, err := os.Stat(rigConfigPath); !os.IsNotExist(err) {
|
||||||
|
t.Errorf("expected no config.yaml at rig level when using redirect, but it exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitBeads_LocalBeads_CreatesDatabase(t *testing.T) {
|
||||||
|
// Cannot use t.Parallel() due to t.Setenv
|
||||||
|
// When the cloned repo does NOT have tracked beads (no mayor/rig/.beads),
|
||||||
|
// initBeads should create a local database at <rig>/.beads/
|
||||||
|
rigPath := t.TempDir()
|
||||||
|
|
||||||
|
// Create mayor/rig directory but WITHOUT .beads (no tracked beads)
|
||||||
|
mayorRigDir := filepath.Join(rigPath, "mayor", "rig")
|
||||||
|
if err := os.MkdirAll(mayorRigDir, 0755); err != nil {
|
||||||
|
t.Fatalf("mkdir mayor/rig: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use fake bd that succeeds
|
||||||
|
script := `#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
if [[ "$1" == "init" ]]; then
|
||||||
|
# Simulate successful bd init
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
`
|
||||||
|
binDir := writeFakeBD(t, script)
|
||||||
|
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||||
|
|
||||||
|
manager := &Manager{}
|
||||||
|
if err := manager.initBeads(rigPath, "gt"); err != nil {
|
||||||
|
t.Fatalf("initBeads: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify NO redirect file was created
|
||||||
|
redirectPath := filepath.Join(rigPath, ".beads", "redirect")
|
||||||
|
if _, err := os.Stat(redirectPath); !os.IsNotExist(err) {
|
||||||
|
t.Errorf("expected no redirect file for local beads, but it exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify .beads directory was created
|
||||||
|
beadsDir := filepath.Join(rigPath, ".beads")
|
||||||
|
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
|
||||||
|
t.Errorf("expected .beads directory to be created")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestInitBeadsWritesConfigOnFailure(t *testing.T) {
|
func TestInitBeadsWritesConfigOnFailure(t *testing.T) {
|
||||||
// Cannot use t.Parallel() due to t.Setenv
|
// Cannot use t.Parallel() due to t.Setenv
|
||||||
rigPath := t.TempDir()
|
rigPath := t.TempDir()
|
||||||
|
|||||||
@@ -70,13 +70,16 @@ func (r *Rig) Summary() RigSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BeadsPath returns the path to use for beads operations.
|
// BeadsPath returns the path to use for beads operations.
|
||||||
// Returns the mayor/rig clone path if available (has proper sync-branch config),
|
// Always returns the rig root path where .beads/ contains either:
|
||||||
// otherwise falls back to the rig root path.
|
// - A local beads database (when repo doesn't track .beads/)
|
||||||
// This ensures beads commands read from a location with git-synced beads data.
|
// - A redirect file pointing to mayor/rig/.beads (when repo tracks .beads/)
|
||||||
|
//
|
||||||
|
// The redirect is set up by initBeads() during rig creation and followed
|
||||||
|
// automatically by the bd CLI and beads.ResolveBeadsDir().
|
||||||
|
//
|
||||||
|
// This ensures we never write to the user's repo clone (mayor/rig/) and
|
||||||
|
// all beads operations go through the redirect system.
|
||||||
func (r *Rig) BeadsPath() string {
|
func (r *Rig) BeadsPath() string {
|
||||||
if r.HasMayor {
|
|
||||||
return r.Path + "/mayor/rig"
|
|
||||||
}
|
|
||||||
return r.Path
|
return r.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
146
internal/rig/types_test.go
Normal file
146
internal/rig/types_test.go
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package rig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBeadsPath_AlwaysReturnsRigRoot(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// BeadsPath should always return the rig root path, regardless of HasMayor.
|
||||||
|
// The redirect system at <rig>/.beads/redirect handles finding the actual
|
||||||
|
// beads location (either local at <rig>/.beads/ or tracked at mayor/rig/.beads/).
|
||||||
|
//
|
||||||
|
// This ensures:
|
||||||
|
// 1. We don't write files to the user's repo clone (mayor/rig/)
|
||||||
|
// 2. The redirect architecture is respected
|
||||||
|
// 3. All code paths use the same beads resolution logic
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
rig Rig
|
||||||
|
wantPath string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "rig with mayor only",
|
||||||
|
rig: Rig{
|
||||||
|
Name: "testrig",
|
||||||
|
Path: "/home/user/gt/testrig",
|
||||||
|
HasMayor: true,
|
||||||
|
},
|
||||||
|
wantPath: "/home/user/gt/testrig",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rig with witness only",
|
||||||
|
rig: Rig{
|
||||||
|
Name: "testrig",
|
||||||
|
Path: "/home/user/gt/testrig",
|
||||||
|
HasWitness: true,
|
||||||
|
},
|
||||||
|
wantPath: "/home/user/gt/testrig",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rig with refinery only",
|
||||||
|
rig: Rig{
|
||||||
|
Name: "testrig",
|
||||||
|
Path: "/home/user/gt/testrig",
|
||||||
|
HasRefinery: true,
|
||||||
|
},
|
||||||
|
wantPath: "/home/user/gt/testrig",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rig with no agents",
|
||||||
|
rig: Rig{
|
||||||
|
Name: "testrig",
|
||||||
|
Path: "/home/user/gt/testrig",
|
||||||
|
},
|
||||||
|
wantPath: "/home/user/gt/testrig",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rig with mayor and witness",
|
||||||
|
rig: Rig{
|
||||||
|
Name: "testrig",
|
||||||
|
Path: "/home/user/gt/testrig",
|
||||||
|
HasMayor: true,
|
||||||
|
HasWitness: true,
|
||||||
|
},
|
||||||
|
wantPath: "/home/user/gt/testrig",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rig with mayor and refinery",
|
||||||
|
rig: Rig{
|
||||||
|
Name: "testrig",
|
||||||
|
Path: "/home/user/gt/testrig",
|
||||||
|
HasMayor: true,
|
||||||
|
HasRefinery: true,
|
||||||
|
},
|
||||||
|
wantPath: "/home/user/gt/testrig",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rig with witness and refinery",
|
||||||
|
rig: Rig{
|
||||||
|
Name: "testrig",
|
||||||
|
Path: "/home/user/gt/testrig",
|
||||||
|
HasWitness: true,
|
||||||
|
HasRefinery: true,
|
||||||
|
},
|
||||||
|
wantPath: "/home/user/gt/testrig",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rig with all agents",
|
||||||
|
rig: Rig{
|
||||||
|
Name: "fullrig",
|
||||||
|
Path: "/tmp/gt/fullrig",
|
||||||
|
HasMayor: true,
|
||||||
|
HasWitness: true,
|
||||||
|
HasRefinery: true,
|
||||||
|
},
|
||||||
|
wantPath: "/tmp/gt/fullrig",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rig with polecats",
|
||||||
|
rig: Rig{
|
||||||
|
Name: "testrig",
|
||||||
|
Path: "/home/user/gt/testrig",
|
||||||
|
HasMayor: true,
|
||||||
|
Polecats: []string{"polecat1", "polecat2"},
|
||||||
|
},
|
||||||
|
wantPath: "/home/user/gt/testrig",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rig with crew",
|
||||||
|
rig: Rig{
|
||||||
|
Name: "testrig",
|
||||||
|
Path: "/home/user/gt/testrig",
|
||||||
|
HasMayor: true,
|
||||||
|
Crew: []string{"crew1", "crew2"},
|
||||||
|
},
|
||||||
|
wantPath: "/home/user/gt/testrig",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
got := tt.rig.BeadsPath()
|
||||||
|
if got != tt.wantPath {
|
||||||
|
t.Errorf("BeadsPath() = %q, want %q", got, tt.wantPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultBranch_FallsBackToMain(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// DefaultBranch should return "main" when config cannot be loaded
|
||||||
|
rig := Rig{
|
||||||
|
Name: "testrig",
|
||||||
|
Path: "/nonexistent/path",
|
||||||
|
}
|
||||||
|
|
||||||
|
got := rig.DefaultBranch()
|
||||||
|
if got != "main" {
|
||||||
|
t.Errorf("DefaultBranch() = %q, want %q", got, "main")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user