fix(redirect): follow redirect when creating database (GH#bd-0qel)

When bd runs with --no-daemon or during init in a directory that has a
.beads/redirect file, it now correctly follows the redirect to create
the database in the target location instead of locally.

The bug occurred because:
1. init.go hardcoded .beads/beads.db without checking for redirects
2. main.go's fallback path for auto-bootstrap also used local .beads

Both code paths now call beads.FollowRedirect() to resolve the correct
.beads directory before constructing the database path.

Added TestInitWithRedirect to verify the fix.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
joshuavial
2026-01-21 12:28:38 +13:00
committed by Steve Yegge
parent 3824abaf1b
commit d41fb30720
3 changed files with 104 additions and 3 deletions

View File

@@ -139,11 +139,15 @@ With --stealth: configures per-repository git settings for invisible beads usage
//
// Use global dbPath if set via --db flag or BEADS_DB env var (SQLite-only),
// otherwise default to `.beads/beads.db` for SQLite.
// If there's a redirect file, use the redirect target (GH#bd-0qel)
initDBPath := dbPath
if backend == configfile.BackendDolt {
initDBPath = filepath.Join(".beads", "dolt")
} else if initDBPath == "" {
initDBPath = filepath.Join(".beads", beads.CanonicalDatabaseName)
// Check for redirect in local .beads
localBeadsDir := filepath.Join(".", ".beads")
targetBeadsDir := beads.FollowRedirect(localBeadsDir)
initDBPath = filepath.Join(targetBeadsDir, beads.CanonicalDatabaseName)
}
// Migrate old SQLite database files if they exist (SQLite backend only).
@@ -192,7 +196,9 @@ With --stealth: configures per-repository git settings for invisible beads usage
var beadsDir string
// For regular repos, use current directory
beadsDir = filepath.Join(cwd, ".beads")
// But first check if there's a redirect file - if so, use the redirect target (GH#bd-0qel)
localBeadsDir := filepath.Join(cwd, ".beads")
beadsDir = beads.FollowRedirect(localBeadsDir)
// Prevent nested .beads directories
// Check if current working directory is inside a .beads directory

View File

@@ -1465,3 +1465,89 @@ func captureStdout(t *testing.T, fn func() error) string {
}
return buf.String()
}
// TestInitWithRedirect verifies that bd init creates the database in the redirect target,
// not in the local .beads directory. (GH#bd-0qel)
func TestInitWithRedirect(t *testing.T) {
// Reset global state
origDBPath := dbPath
defer func() { dbPath = origDBPath }()
dbPath = ""
// Clear BEADS_DIR to ensure we test the tree search path
origBeadsDir := os.Getenv("BEADS_DIR")
os.Unsetenv("BEADS_DIR")
defer func() {
if origBeadsDir != "" {
os.Setenv("BEADS_DIR", origBeadsDir)
}
}()
// Reset Cobra flags
initCmd.Flags().Set("prefix", "")
initCmd.Flags().Set("quiet", "false")
tmpDir := t.TempDir()
// Create project directory (where we'll run from)
projectDir := filepath.Join(tmpDir, "project")
if err := os.MkdirAll(projectDir, 0755); err != nil {
t.Fatal(err)
}
// Create local .beads with redirect file pointing to target
localBeadsDir := filepath.Join(projectDir, ".beads")
if err := os.MkdirAll(localBeadsDir, 0755); err != nil {
t.Fatal(err)
}
// Create target .beads directory (the redirect destination)
targetBeadsDir := filepath.Join(tmpDir, "canonical", ".beads")
if err := os.MkdirAll(targetBeadsDir, 0755); err != nil {
t.Fatal(err)
}
// Write redirect file - use relative path
redirectPath := filepath.Join(localBeadsDir, beads.RedirectFileName)
// Relative path from project/.beads to canonical/.beads is ../canonical/.beads
if err := os.WriteFile(redirectPath, []byte("../canonical/.beads\n"), 0644); err != nil {
t.Fatal(err)
}
// Change to project directory
t.Chdir(projectDir)
// Run bd init
rootCmd.SetArgs([]string{"init", "--prefix", "redirect-test", "--quiet"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("Init with redirect failed: %v", err)
}
// Verify database was created in TARGET directory, not local
targetDBPath := filepath.Join(targetBeadsDir, "beads.db")
if _, err := os.Stat(targetDBPath); os.IsNotExist(err) {
t.Errorf("Database was NOT created in redirect target: %s", targetDBPath)
}
// Verify database was NOT created in local directory
localDBPath := filepath.Join(localBeadsDir, "beads.db")
if _, err := os.Stat(localDBPath); err == nil {
t.Errorf("Database was incorrectly created in local .beads: %s (should be in redirect target)", localDBPath)
}
// Verify the database is functional
store, err := openExistingTestDB(t, targetDBPath)
if err != nil {
t.Fatalf("Failed to open database in redirect target: %v", err)
}
defer store.Close()
ctx := context.Background()
prefix, err := store.GetConfig(ctx, "issue_prefix")
if err != nil {
t.Fatalf("Failed to get issue prefix from database: %v", err)
}
if prefix != "redirect-test" {
t.Errorf("Expected prefix 'redirect-test', got %q", prefix)
}
}

View File

@@ -532,7 +532,16 @@ var rootCmd = &cobra.Command{
// Invariant: dbPath must always be absolute for filepath.Rel() compatibility
// in daemon sync-branch code path. Use CanonicalizePath for OS-agnostic
// handling (symlinks, case normalization on macOS).
dbPath = utils.CanonicalizePath(filepath.Join(".beads", beads.CanonicalDatabaseName))
//
// IMPORTANT: Use FindBeadsDir() to get the correct .beads directory,
// which follows redirect files. Without this, a redirected .beads
// would create a local database instead of using the redirect target.
// (GH#bd-0qel)
targetBeadsDir := beads.FindBeadsDir()
if targetBeadsDir == "" {
targetBeadsDir = ".beads"
}
dbPath = utils.CanonicalizePath(filepath.Join(targetBeadsDir, beads.CanonicalDatabaseName))
}
}