fix(init): respect BEADS_DIR environment variable (#1273)

* fix(sync): read sync.mode from yaml first, then database

bd config set sync.mode writes to config.yaml (because sync.* is a
yaml-only prefix), but GetSyncMode() only read from the database.

This caused dolt-native mode to be ignored - JSONL export still
happened because the database had no sync.mode value.

Now GetSyncMode() checks config.yaml first (via config.GetSyncMode()),
falling back to database for backward compatibility.

Fixes: oss-5ca279

* fix(init): respect BEADS_DIR environment variable

Problem:
- `bd init` ignored BEADS_DIR when checking for existing data
- `bd init` created database at CWD/.beads instead of BEADS_DIR
- Contributor wizard used ~/.beads-planning as default, ignoring BEADS_DIR

Solution:
- Add BEADS_DIR check in checkExistingBeadsData() (matches FindBeadsDir pattern)
- Compute beadsDirForInit early, before initDBPath determination
- Use BEADS_DIR as default in contributor wizard when set
- Preserve precedence: --db > BEADS_DB > BEADS_DIR > default

Impact:
- Users with BEADS_DIR set now get consistent behavior across all bd commands
- ACF-style fork tracking (external .beads directory) now works correctly

Fixes: steveyegge/beads#???

* fix(doctor): respect BEADS_DIR environment variable

Also updates documentation to reflect BEADS_DIR support in init and doctor.

Changes:
- doctor.go: Check BEADS_DIR before falling back to CWD
- doctor_test.go: Add tests for BEADS_DIR path resolution
- WORKTREES.md: Document simplified BEADS_DIR+init workflow
- CONTRIBUTOR_NAMESPACE_ISOLATION.md: Note init/doctor BEADS_DIR support

* test(init): add BEADS_DB > BEADS_DIR precedence test

Verifies that BEADS_DB env var takes precedence over BEADS_DIR
when both are set, ensuring the documented precedence order:
--db > BEADS_DB > BEADS_DIR > default

* chore: fill in GH#1277 placeholder in sync_mode comment
This commit is contained in:
Peter Chanthamynavong
2026-01-24 17:10:05 -08:00
committed by GitHub
parent 810192157c
commit b7d650bd8e
8 changed files with 674 additions and 94 deletions

View File

@@ -1398,3 +1398,120 @@ func TestCheckSyncBranchHookQuick(t *testing.T) {
})
}
}
// TestDoctor_WithBEADS_DIR tests that doctor respects BEADS_DIR environment variable
func TestDoctor_WithBEADS_DIR(t *testing.T) {
// Reset Cobra flags to avoid interference
defer func() {
doctorFix = false
doctorYes = false
doctorInteractive = false
doctorDryRun = false
}()
// Create target directory (where BEADS_DIR points)
targetDir := t.TempDir()
targetBeadsDir := filepath.Join(targetDir, ".beads")
if err := os.Mkdir(targetBeadsDir, 0750); err != nil {
t.Fatal(err)
}
// Create CWD directory (where we run from - should be ignored)
cwdDir := t.TempDir()
// Explicitly do NOT create .beads here - doctor should not check CWD
// Save original working directory
origDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
// Change to CWD (no .beads)
if err := os.Chdir(cwdDir); err != nil {
t.Fatal(err)
}
defer func() {
_ = os.Chdir(origDir)
}()
// Set BEADS_DIR to point to target/.beads
t.Setenv("BEADS_DIR", targetBeadsDir)
// The doctor command's Run function determines path from BEADS_DIR
// We test that by verifying runDiagnostics receives the parent of BEADS_DIR
// Direct call: runDiagnostics uses the path it's given
// Through cobra: Run function computes path from BEADS_DIR
// Test the path resolution logic directly
beadsDir := os.Getenv("BEADS_DIR")
if beadsDir == "" {
t.Fatal("BEADS_DIR should be set")
}
checkPath := filepath.Dir(beadsDir) // Parent of .beads
// Verify we get the target directory, not CWD
if checkPath != targetDir {
t.Errorf("Expected checkPath to be %s, got %s", targetDir, checkPath)
}
// Run diagnostics on the computed path (simulates what cobra Run does)
result := runDiagnostics(checkPath)
// Should find .beads at target location
if len(result.Checks) == 0 {
t.Fatal("Expected at least one check")
}
installCheck := result.Checks[0]
if installCheck.Status != "ok" {
t.Errorf("Expected Installation to pass (found .beads at BEADS_DIR parent), got status=%s, message=%s",
installCheck.Status, installCheck.Message)
}
}
// TestDoctor_ExplicitPathOverridesBEADS_DIR tests that explicit path arg takes precedence
func TestDoctor_ExplicitPathOverridesBEADS_DIR(t *testing.T) {
// Create two directories: one for explicit path, one for BEADS_DIR
explicitDir := t.TempDir()
explicitBeadsDir := filepath.Join(explicitDir, ".beads")
if err := os.Mkdir(explicitBeadsDir, 0750); err != nil {
t.Fatal(err)
}
// Mark explicit with a file so we can verify it was used
if err := os.WriteFile(filepath.Join(explicitBeadsDir, "explicit-marker"), []byte("explicit"), 0644); err != nil {
t.Fatal(err)
}
beadsDirTarget := t.TempDir()
beadsDirBeads := filepath.Join(beadsDirTarget, ".beads")
if err := os.Mkdir(beadsDirBeads, 0750); err != nil {
t.Fatal(err)
}
// Set BEADS_DIR
t.Setenv("BEADS_DIR", beadsDirBeads)
// Test precedence: explicit arg > BEADS_DIR > CWD
// When explicit path is given, BEADS_DIR should be ignored
// Simulate the logic from doctor.go's Run function:
args := []string{explicitDir}
var checkPath string
if len(args) > 0 {
checkPath = args[0]
} else if beadsDir := os.Getenv("BEADS_DIR"); beadsDir != "" {
checkPath = filepath.Dir(beadsDir)
} else {
checkPath = "."
}
// Should use explicit path, not BEADS_DIR
if checkPath != explicitDir {
t.Errorf("Expected explicit path %s to take precedence, got %s", explicitDir, checkPath)
}
// Verify the marker file exists at the chosen path
markerPath := filepath.Join(checkPath, ".beads", "explicit-marker")
if _, err := os.Stat(markerPath); os.IsNotExist(err) {
t.Error("Expected to find explicit-marker in chosen path - wrong directory was selected")
}
}