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) {
|
||||
// Cannot use t.Parallel() due to t.Setenv
|
||||
rigPath := t.TempDir()
|
||||
|
||||
@@ -70,13 +70,16 @@ func (r *Rig) Summary() RigSummary {
|
||||
}
|
||||
|
||||
// BeadsPath returns the path to use for beads operations.
|
||||
// Returns the mayor/rig clone path if available (has proper sync-branch config),
|
||||
// otherwise falls back to the rig root path.
|
||||
// This ensures beads commands read from a location with git-synced beads data.
|
||||
// Always returns the rig root path where .beads/ contains either:
|
||||
// - A local beads database (when repo doesn't track .beads/)
|
||||
// - 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 {
|
||||
if r.HasMayor {
|
||||
return r.Path + "/mayor/rig"
|
||||
}
|
||||
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