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:
committed by
GitHub
parent
810192157c
commit
b7d650bd8e
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
@@ -579,6 +580,9 @@ func TestInitNoDbMode(t *testing.T) {
|
||||
dbPath = ""
|
||||
noDb = false
|
||||
|
||||
// Reset Cobra flags - critical for --no-db to work correctly
|
||||
rootCmd.PersistentFlags().Set("no-db", "false")
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
t.Chdir(tmpDir)
|
||||
|
||||
@@ -602,13 +606,36 @@ func TestInitNoDbMode(t *testing.T) {
|
||||
// Initialize with --no-db flag
|
||||
rootCmd.SetArgs([]string{"init", "--no-db", "--no-daemon", "--prefix", "test", "--quiet"})
|
||||
|
||||
t.Logf("DEBUG: noDb before Execute=%v", noDb)
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("Init with --no-db failed: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("DEBUG: noDb after Execute=%v", noDb)
|
||||
|
||||
// Debug: Check where files were created
|
||||
beadsDirEnv := os.Getenv("BEADS_DIR")
|
||||
t.Logf("DEBUG: tmpDir=%s", tmpDir)
|
||||
t.Logf("DEBUG: BEADS_DIR=%s", beadsDirEnv)
|
||||
t.Logf("DEBUG: CWD=%s", func() string { cwd, _ := os.Getwd(); return cwd }())
|
||||
|
||||
// Check what files exist in tmpDir
|
||||
entries, _ := os.ReadDir(tmpDir)
|
||||
t.Logf("DEBUG: entries in tmpDir: %v", entries)
|
||||
if beadsDirEnv != "" {
|
||||
beadsEntries, err := os.ReadDir(beadsDirEnv)
|
||||
t.Logf("DEBUG: entries in BEADS_DIR: %v (err: %v)", beadsEntries, err)
|
||||
}
|
||||
|
||||
// Verify issues.jsonl was created
|
||||
jsonlPath := filepath.Join(tmpDir, ".beads", "issues.jsonl")
|
||||
if _, err := os.Stat(jsonlPath); os.IsNotExist(err) {
|
||||
// Also check at BEADS_DIR directly
|
||||
beadsDirJsonlPath := filepath.Join(beadsDirEnv, "issues.jsonl")
|
||||
if _, err2 := os.Stat(beadsDirJsonlPath); err2 == nil {
|
||||
t.Logf("DEBUG: issues.jsonl found at BEADS_DIR path: %s", beadsDirJsonlPath)
|
||||
}
|
||||
t.Error("issues.jsonl was not created in --no-db mode")
|
||||
}
|
||||
|
||||
@@ -1643,3 +1670,384 @@ func TestInitWithRedirectToExistingDatabase(t *testing.T) {
|
||||
t.Errorf("Canonical database prefix should still be 'existing', got %q (was overwritten!)", prefix)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BEADS_DIR Tests
|
||||
// =============================================================================
|
||||
// These tests verify that bd init respects the BEADS_DIR environment variable
|
||||
// for both safety checks and database creation.
|
||||
|
||||
// TestCheckExistingBeadsData_WithBEADS_DIR verifies that checkExistingBeadsData
|
||||
// uses BEADS_DIR instead of CWD when the environment variable is set.
|
||||
// This tests requirements FR-001, FR-004.
|
||||
func TestCheckExistingBeadsData_WithBEADS_DIR(t *testing.T) {
|
||||
// Save and restore BEADS_DIR
|
||||
origBeadsDir := os.Getenv("BEADS_DIR")
|
||||
defer func() {
|
||||
if origBeadsDir != "" {
|
||||
os.Setenv("BEADS_DIR", origBeadsDir)
|
||||
} else {
|
||||
os.Unsetenv("BEADS_DIR")
|
||||
}
|
||||
beads.ResetCaches()
|
||||
git.ResetCaches()
|
||||
}()
|
||||
|
||||
t.Run("TC-002: BEADS_DIR set, no existing DB", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create BEADS_DIR location (no database)
|
||||
beadsDirPath := filepath.Join(tmpDir, "external", ".beads")
|
||||
os.MkdirAll(beadsDirPath, 0755)
|
||||
|
||||
os.Setenv("BEADS_DIR", beadsDirPath)
|
||||
beads.ResetCaches()
|
||||
|
||||
// Should succeed because BEADS_DIR has no database
|
||||
err := checkExistingBeadsData("test")
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error when BEADS_DIR has no database, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TC-003: BEADS_DIR set, CWD has .beads, should ignore CWD", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create CWD with existing database (should be ignored)
|
||||
cwdBeadsDir := filepath.Join(tmpDir, "cwd", ".beads")
|
||||
os.MkdirAll(cwdBeadsDir, 0755)
|
||||
cwdDBPath := filepath.Join(cwdBeadsDir, beads.CanonicalDatabaseName)
|
||||
store, err := sqlite.New(context.Background(), cwdDBPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store.Close()
|
||||
|
||||
// Create BEADS_DIR location (no database)
|
||||
beadsDirPath := filepath.Join(tmpDir, "external", ".beads")
|
||||
os.MkdirAll(beadsDirPath, 0755)
|
||||
|
||||
// Set BEADS_DIR - should check external, not CWD
|
||||
os.Setenv("BEADS_DIR", beadsDirPath)
|
||||
beads.ResetCaches()
|
||||
|
||||
// Change to CWD with database
|
||||
origWd, _ := os.Getwd()
|
||||
os.Chdir(filepath.Join(tmpDir, "cwd"))
|
||||
defer os.Chdir(origWd)
|
||||
|
||||
// Should succeed because BEADS_DIR has no database (CWD ignored)
|
||||
err = checkExistingBeadsData("test")
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error when BEADS_DIR has no database (CWD should be ignored), got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TC-004: BEADS_DIR set, target exists with DB, should error", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create BEADS_DIR with existing database
|
||||
beadsDirPath := filepath.Join(tmpDir, "external", ".beads")
|
||||
os.MkdirAll(beadsDirPath, 0755)
|
||||
dbPath := filepath.Join(beadsDirPath, beads.CanonicalDatabaseName)
|
||||
store, err := sqlite.New(context.Background(), dbPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store.Close()
|
||||
|
||||
os.Setenv("BEADS_DIR", beadsDirPath)
|
||||
beads.ResetCaches()
|
||||
|
||||
// Should error because BEADS_DIR already has database
|
||||
err = checkExistingBeadsData("test")
|
||||
if err == nil {
|
||||
t.Error("Expected error when BEADS_DIR already has database")
|
||||
}
|
||||
// FR-005: Error message should reference the BEADS_DIR path
|
||||
if !strings.Contains(err.Error(), beadsDirPath) {
|
||||
t.Errorf("Expected error to mention BEADS_DIR path %s, got: %v", beadsDirPath, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestInit_WithBEADS_DIR verifies that bd init creates the database at BEADS_DIR
|
||||
// when the environment variable is set.
|
||||
// This tests requirements FR-002.
|
||||
func TestInit_WithBEADS_DIR(t *testing.T) {
|
||||
// Skip on Windows - init has platform-specific behaviors
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Skipping BEADS_DIR test on Windows")
|
||||
}
|
||||
|
||||
// Reset global state
|
||||
origDBPath := dbPath
|
||||
defer func() { dbPath = origDBPath }()
|
||||
dbPath = ""
|
||||
|
||||
// Save and restore BEADS_DIR
|
||||
origBeadsDir := os.Getenv("BEADS_DIR")
|
||||
defer func() {
|
||||
if origBeadsDir != "" {
|
||||
os.Setenv("BEADS_DIR", origBeadsDir)
|
||||
} else {
|
||||
os.Unsetenv("BEADS_DIR")
|
||||
}
|
||||
beads.ResetCaches()
|
||||
git.ResetCaches()
|
||||
}()
|
||||
|
||||
// Reset Cobra flags
|
||||
initCmd.Flags().Set("prefix", "")
|
||||
initCmd.Flags().Set("quiet", "false")
|
||||
initCmd.Flags().Set("backend", "")
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create external BEADS_DIR location
|
||||
beadsDirPath := filepath.Join(tmpDir, "external", ".beads")
|
||||
os.MkdirAll(filepath.Dir(beadsDirPath), 0755) // Create parent, not .beads itself
|
||||
|
||||
os.Setenv("BEADS_DIR", beadsDirPath)
|
||||
beads.ResetCaches()
|
||||
git.ResetCaches()
|
||||
|
||||
// Change to a different working directory
|
||||
cwdPath := filepath.Join(tmpDir, "workdir")
|
||||
os.MkdirAll(cwdPath, 0755)
|
||||
t.Chdir(cwdPath)
|
||||
|
||||
// Run bd init with quiet flag
|
||||
rootCmd.SetArgs([]string{"init", "--prefix", "beadsdir-test", "--quiet"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("Init with BEADS_DIR failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify database was created at BEADS_DIR, not CWD
|
||||
expectedDBPath := filepath.Join(beadsDirPath, beads.CanonicalDatabaseName)
|
||||
if _, err := os.Stat(expectedDBPath); os.IsNotExist(err) {
|
||||
t.Errorf("Database was not created at BEADS_DIR path: %s", expectedDBPath)
|
||||
}
|
||||
|
||||
// Verify database was NOT created at CWD
|
||||
cwdDBPath := filepath.Join(cwdPath, ".beads", beads.CanonicalDatabaseName)
|
||||
if _, err := os.Stat(cwdDBPath); err == nil {
|
||||
t.Errorf("Database should NOT have been created at CWD: %s", cwdDBPath)
|
||||
}
|
||||
|
||||
// Verify database has correct prefix
|
||||
store, err := openExistingTestDB(t, expectedDBPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open database at BEADS_DIR: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
prefix, err := store.GetConfig(ctx, "issue_prefix")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get prefix from database: %v", err)
|
||||
}
|
||||
if prefix != "beadsdir-test" {
|
||||
t.Errorf("Expected prefix 'beadsdir-test', got %q", prefix)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInit_WithBEADS_DIR_DoltBackend verifies that bd init with Dolt backend
|
||||
// creates the database at BEADS_DIR when the environment variable is set.
|
||||
// This tests requirements FR-002 for Dolt backend.
|
||||
func TestInit_WithBEADS_DIR_DoltBackend(t *testing.T) {
|
||||
// Skip on Windows
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Skipping BEADS_DIR Dolt test on Windows")
|
||||
}
|
||||
|
||||
// Check if dolt is available
|
||||
if _, err := exec.LookPath("dolt"); err != nil {
|
||||
t.Skip("Dolt not installed, skipping Dolt backend test")
|
||||
}
|
||||
|
||||
// Reset global state
|
||||
origDBPath := dbPath
|
||||
defer func() { dbPath = origDBPath }()
|
||||
dbPath = ""
|
||||
|
||||
// Save and restore BEADS_DIR
|
||||
origBeadsDir := os.Getenv("BEADS_DIR")
|
||||
defer func() {
|
||||
if origBeadsDir != "" {
|
||||
os.Setenv("BEADS_DIR", origBeadsDir)
|
||||
} else {
|
||||
os.Unsetenv("BEADS_DIR")
|
||||
}
|
||||
beads.ResetCaches()
|
||||
git.ResetCaches()
|
||||
}()
|
||||
|
||||
// Reset Cobra flags
|
||||
initCmd.Flags().Set("prefix", "")
|
||||
initCmd.Flags().Set("quiet", "false")
|
||||
initCmd.Flags().Set("backend", "")
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Create external BEADS_DIR location
|
||||
beadsDirPath := filepath.Join(tmpDir, "external", ".beads")
|
||||
os.MkdirAll(filepath.Dir(beadsDirPath), 0755)
|
||||
|
||||
os.Setenv("BEADS_DIR", beadsDirPath)
|
||||
beads.ResetCaches()
|
||||
git.ResetCaches()
|
||||
|
||||
// Change to a different working directory
|
||||
cwdPath := filepath.Join(tmpDir, "workdir")
|
||||
os.MkdirAll(cwdPath, 0755)
|
||||
t.Chdir(cwdPath)
|
||||
|
||||
// Run bd init with Dolt backend
|
||||
rootCmd.SetArgs([]string{"init", "--prefix", "dolt-test", "--backend", "dolt", "--quiet"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("Init with BEADS_DIR and Dolt backend failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify Dolt database was created at BEADS_DIR
|
||||
expectedDoltPath := filepath.Join(beadsDirPath, "dolt")
|
||||
if info, err := os.Stat(expectedDoltPath); os.IsNotExist(err) {
|
||||
t.Errorf("Dolt database was not created at BEADS_DIR path: %s", expectedDoltPath)
|
||||
} else if !info.IsDir() {
|
||||
t.Errorf("Expected Dolt path to be a directory: %s", expectedDoltPath)
|
||||
}
|
||||
|
||||
// Verify database was NOT created at CWD
|
||||
cwdDoltPath := filepath.Join(cwdPath, ".beads", "dolt")
|
||||
if _, err := os.Stat(cwdDoltPath); err == nil {
|
||||
t.Errorf("Dolt database should NOT have been created at CWD: %s", cwdDoltPath)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInit_WithoutBEADS_DIR_NoBehaviorChange verifies that existing behavior
|
||||
// is unchanged when BEADS_DIR is not set.
|
||||
// This tests requirement NFR-001.
|
||||
func TestInit_WithoutBEADS_DIR_NoBehaviorChange(t *testing.T) {
|
||||
// Reset global state
|
||||
origDBPath := dbPath
|
||||
defer func() { dbPath = origDBPath }()
|
||||
dbPath = ""
|
||||
|
||||
// Ensure BEADS_DIR is not set
|
||||
origBeadsDir := os.Getenv("BEADS_DIR")
|
||||
os.Unsetenv("BEADS_DIR")
|
||||
defer func() {
|
||||
if origBeadsDir != "" {
|
||||
os.Setenv("BEADS_DIR", origBeadsDir)
|
||||
}
|
||||
beads.ResetCaches()
|
||||
git.ResetCaches()
|
||||
}()
|
||||
|
||||
beads.ResetCaches()
|
||||
git.ResetCaches()
|
||||
|
||||
// Reset Cobra flags
|
||||
initCmd.Flags().Set("prefix", "")
|
||||
initCmd.Flags().Set("quiet", "false")
|
||||
initCmd.Flags().Set("backend", "")
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
t.Chdir(tmpDir)
|
||||
|
||||
// Run bd init
|
||||
rootCmd.SetArgs([]string{"init", "--prefix", "no-beadsdir", "--quiet"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("Init without BEADS_DIR failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify database was created at CWD/.beads (default behavior)
|
||||
expectedDBPath := filepath.Join(tmpDir, ".beads", beads.CanonicalDatabaseName)
|
||||
if _, err := os.Stat(expectedDBPath); os.IsNotExist(err) {
|
||||
t.Errorf("Database was not created at default CWD/.beads path: %s", expectedDBPath)
|
||||
}
|
||||
|
||||
// Verify database has correct prefix
|
||||
store, err := openExistingTestDB(t, expectedDBPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open database: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
prefix, err := store.GetConfig(ctx, "issue_prefix")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get prefix from database: %v", err)
|
||||
}
|
||||
if prefix != "no-beadsdir" {
|
||||
t.Errorf("Expected prefix 'no-beadsdir', got %q", prefix)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInit_BEADS_DB_OverridesBEADS_DIR verifies precedence: BEADS_DB > BEADS_DIR
|
||||
// This ensures that explicit database path env var takes precedence over directory env var.
|
||||
func TestInit_BEADS_DB_OverridesBEADS_DIR(t *testing.T) {
|
||||
// Reset global state
|
||||
origDBPath := dbPath
|
||||
defer func() { dbPath = origDBPath }()
|
||||
dbPath = ""
|
||||
|
||||
beads.ResetCaches()
|
||||
git.ResetCaches()
|
||||
|
||||
// Reset Cobra flags
|
||||
initCmd.Flags().Set("prefix", "")
|
||||
initCmd.Flags().Set("quiet", "false")
|
||||
initCmd.Flags().Set("backend", "")
|
||||
|
||||
// Create two target locations
|
||||
beadsDirTarget := t.TempDir() // Where BEADS_DIR points (should be ignored)
|
||||
beadsDBTarget := t.TempDir() // Where BEADS_DB points (should be used)
|
||||
|
||||
beadsDirBeads := filepath.Join(beadsDirTarget, ".beads")
|
||||
if err := os.MkdirAll(beadsDirBeads, 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
beadsDBPath := filepath.Join(beadsDBTarget, "override.db")
|
||||
|
||||
// Set both env vars - BEADS_DB should take precedence
|
||||
t.Setenv("BEADS_DIR", beadsDirBeads)
|
||||
t.Setenv("BEADS_DB", beadsDBPath)
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
t.Chdir(tmpDir)
|
||||
|
||||
// Run bd init
|
||||
rootCmd.SetArgs([]string{"init", "--prefix", "precedence", "--quiet"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("Init with BEADS_DB + BEADS_DIR failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify database was created at BEADS_DB location (not BEADS_DIR)
|
||||
if _, err := os.Stat(beadsDBPath); os.IsNotExist(err) {
|
||||
t.Errorf("Database was NOT created at BEADS_DB path: %s", beadsDBPath)
|
||||
}
|
||||
|
||||
// Verify database was NOT created at BEADS_DIR location
|
||||
beadsDirDBPath := filepath.Join(beadsDirBeads, beads.CanonicalDatabaseName)
|
||||
if _, err := os.Stat(beadsDirDBPath); err == nil {
|
||||
t.Errorf("Database was incorrectly created at BEADS_DIR path: %s (BEADS_DB should override)", beadsDirDBPath)
|
||||
}
|
||||
|
||||
// Verify the database has correct prefix
|
||||
store, err := openExistingTestDB(t, beadsDBPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open database: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
prefix, err := store.GetConfig(ctx, "issue_prefix")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get prefix from database: %v", err)
|
||||
}
|
||||
if prefix != "precedence" {
|
||||
t.Errorf("Expected prefix 'precedence', got %q", prefix)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user