Files
beads/cmd/bd/worktree_daemon_test.go
beads/crew/dave ac24a63187 fix: make tests resilient to project .beads/redirect
Tests were failing because beads.FindDatabasePath() follows the
project's .beads/redirect file, causing tests to find unexpected
databases. Fixed by:

- Setting BEADS_DIR in tests that need isolation from git repo detection
- Clearing BEADS_DIR in TestMain to prevent global contamination
- Updating migration test schema to include owner column

This ensures tests work correctly in crew directories that have
redirect files pointing to shared .beads directories.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Executed-By: beads/crew/dave
Rig: beads
Role: crew
2026-01-10 22:38:04 -08:00

466 lines
14 KiB
Go

package main
import (
"database/sql"
"os"
"os/exec"
"testing"
"github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/git"
// Import SQLite driver for test database creation
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
)
// TestShouldDisableDaemonForWorktree tests the worktree daemon disable logic.
// The function should return true (disable daemon) when:
// - In a git worktree AND sync-branch is NOT configured
// The function should return false (allow daemon) when:
// - Not in a worktree (regular repo)
// - In a worktree but sync-branch IS configured
func TestShouldDisableDaemonForWorktree(t *testing.T) {
// Initialize config for tests
if err := config.Initialize(); err != nil {
t.Fatalf("Failed to initialize config: %v", err)
}
// Save and restore environment variables
origSyncBranch := os.Getenv("BEADS_SYNC_BRANCH")
defer func() {
if origSyncBranch != "" {
os.Setenv("BEADS_SYNC_BRANCH", origSyncBranch)
} else {
os.Unsetenv("BEADS_SYNC_BRANCH")
}
}()
t.Run("returns false in regular repo without sync-branch", func(t *testing.T) {
// Create a regular git repo (not a worktree) using existing helper
repoPath, cleanup := setupGitRepo(t)
defer cleanup()
_ = repoPath // repoPath is the current directory after setupGitRepo
// No sync-branch configured
os.Unsetenv("BEADS_SYNC_BRANCH")
result := shouldDisableDaemonForWorktree()
if result {
t.Error("Expected shouldDisableDaemonForWorktree() to return false in regular repo")
}
})
t.Run("returns false in regular repo with sync-branch", func(t *testing.T) {
// Create a regular git repo (not a worktree) using existing helper
_, cleanup := setupGitRepo(t)
defer cleanup()
// Sync-branch configured
os.Setenv("BEADS_SYNC_BRANCH", "beads-metadata")
result := shouldDisableDaemonForWorktree()
if result {
t.Error("Expected shouldDisableDaemonForWorktree() to return false in regular repo with sync-branch")
}
})
t.Run("returns true in worktree without sync-branch", func(t *testing.T) {
// Create a git repo with a worktree
mainDir, worktreeDir := setupWorktreeTestRepo(t)
// Change to the worktree directory
origDir, _ := os.Getwd()
defer func() {
_ = os.Chdir(origDir)
// Reset git caches after changing directory
git.ResetCaches()
// Reinitialize config to restore original state
_ = config.Initialize()
}()
if err := os.Chdir(worktreeDir); err != nil {
t.Fatalf("Failed to change to worktree dir: %v", err)
}
git.ResetCaches()
// Reset git caches after changing directory (required for IsWorktree to re-detect)
git.ResetCaches()
// Set BEADS_DIR to the test's .beads directory to prevent
// git repo detection from finding the project's .beads
origBeadsDir := os.Getenv("BEADS_DIR")
os.Setenv("BEADS_DIR", mainDir+"/.beads")
defer func() {
if origBeadsDir != "" {
os.Setenv("BEADS_DIR", origBeadsDir)
} else {
os.Unsetenv("BEADS_DIR")
}
}()
// No sync-branch configured
os.Unsetenv("BEADS_SYNC_BRANCH")
// Reinitialize config to pick up the new directory's config.yaml
if err := config.Initialize(); err != nil {
t.Fatalf("Failed to reinitialize config: %v", err)
}
// Debug: verify we're actually in a worktree
isWorktree := isGitWorktree()
t.Logf("isGitWorktree() = %v, worktreeDir = %s", isWorktree, worktreeDir)
result := shouldDisableDaemonForWorktree()
if !result {
t.Errorf("Expected shouldDisableDaemonForWorktree() to return true in worktree without sync-branch (isWorktree=%v)", isWorktree)
}
// Cleanup
cleanupTestWorktree(t, mainDir, worktreeDir)
})
t.Run("returns false in worktree with sync-branch configured", func(t *testing.T) {
// Create a git repo with a worktree
mainDir, worktreeDir := setupWorktreeTestRepo(t)
// Change to the worktree directory
origDir, _ := os.Getwd()
defer func() {
_ = os.Chdir(origDir)
git.ResetCaches()
_ = config.Initialize()
}()
if err := os.Chdir(worktreeDir); err != nil {
t.Fatalf("Failed to change to worktree dir: %v", err)
}
git.ResetCaches()
// Reset git caches after changing directory
git.ResetCaches()
// Reinitialize config to pick up the new directory's config.yaml
if err := config.Initialize(); err != nil {
t.Fatalf("Failed to reinitialize config: %v", err)
}
// Sync-branch configured via environment variable
os.Setenv("BEADS_SYNC_BRANCH", "beads-metadata")
result := shouldDisableDaemonForWorktree()
if result {
t.Error("Expected shouldDisableDaemonForWorktree() to return false in worktree with sync-branch")
}
// Cleanup
cleanupTestWorktree(t, mainDir, worktreeDir)
})
t.Run("returns false in worktree with sync-branch in database config", func(t *testing.T) {
// Create a git repo with a worktree AND a database with sync.branch config
mainDir, worktreeDir := setupWorktreeTestRepoWithDB(t, "beads-metadata")
// Change to the worktree directory
origDir, _ := os.Getwd()
defer func() {
_ = os.Chdir(origDir)
git.ResetCaches()
_ = config.Initialize()
}()
if err := os.Chdir(worktreeDir); err != nil {
t.Fatalf("Failed to change to worktree dir: %v", err)
}
git.ResetCaches()
// Reset git caches after changing directory
git.ResetCaches()
// Reinitialize config to pick up the new directory's config.yaml
if err := config.Initialize(); err != nil {
t.Fatalf("Failed to reinitialize config: %v", err)
}
// NO env var or config.yaml sync-branch - only database config
os.Unsetenv("BEADS_SYNC_BRANCH")
result := shouldDisableDaemonForWorktree()
if result {
t.Error("Expected shouldDisableDaemonForWorktree() to return false in worktree with sync-branch in database")
}
// Cleanup
cleanupTestWorktree(t, mainDir, worktreeDir)
})
}
// TestShouldAutoStartDaemonWorktreeIntegration tests that shouldAutoStartDaemon
// respects the worktree+sync-branch logic.
func TestShouldAutoStartDaemonWorktreeIntegration(t *testing.T) {
// Initialize config for tests
if err := config.Initialize(); err != nil {
t.Fatalf("Failed to initialize config: %v", err)
}
// Save and restore environment variables
origNoDaemon := os.Getenv("BEADS_NO_DAEMON")
origAutoStart := os.Getenv("BEADS_AUTO_START_DAEMON")
origSyncBranch := os.Getenv("BEADS_SYNC_BRANCH")
defer func() {
restoreTestEnv("BEADS_NO_DAEMON", origNoDaemon)
restoreTestEnv("BEADS_AUTO_START_DAEMON", origAutoStart)
restoreTestEnv("BEADS_SYNC_BRANCH", origSyncBranch)
}()
t.Run("disables auto-start in worktree without sync-branch", func(t *testing.T) {
// Create a git repo with a worktree
mainDir, worktreeDir := setupWorktreeTestRepo(t)
// Change to the worktree directory
origDir, _ := os.Getwd()
defer func() {
_ = os.Chdir(origDir)
git.ResetCaches()
_ = config.Initialize()
}()
if err := os.Chdir(worktreeDir); err != nil {
t.Fatalf("Failed to change to worktree dir: %v", err)
}
git.ResetCaches()
// Reset git caches after changing directory
git.ResetCaches()
// Set BEADS_DIR to the test's .beads directory to prevent
// git repo detection from finding the project's .beads
origBeadsDir := os.Getenv("BEADS_DIR")
os.Setenv("BEADS_DIR", mainDir+"/.beads")
defer func() {
if origBeadsDir != "" {
os.Setenv("BEADS_DIR", origBeadsDir)
} else {
os.Unsetenv("BEADS_DIR")
}
}()
// Clear all daemon-related env vars
os.Unsetenv("BEADS_NO_DAEMON")
os.Unsetenv("BEADS_AUTO_START_DAEMON")
os.Unsetenv("BEADS_SYNC_BRANCH")
// Reinitialize config to pick up the new directory's config.yaml
if err := config.Initialize(); err != nil {
t.Fatalf("Failed to reinitialize config: %v", err)
}
result := shouldAutoStartDaemon()
if result {
t.Error("Expected shouldAutoStartDaemon() to return false in worktree without sync-branch")
}
// Cleanup
cleanupTestWorktree(t, mainDir, worktreeDir)
})
t.Run("enables auto-start in worktree with sync-branch", func(t *testing.T) {
// Create a git repo with a worktree
mainDir, worktreeDir := setupWorktreeTestRepo(t)
// Change to the worktree directory
origDir, _ := os.Getwd()
defer func() {
_ = os.Chdir(origDir)
git.ResetCaches()
_ = config.Initialize()
}()
if err := os.Chdir(worktreeDir); err != nil {
t.Fatalf("Failed to change to worktree dir: %v", err)
}
git.ResetCaches()
// Reset git caches after changing directory
git.ResetCaches()
// Reinitialize config to pick up the new directory's config.yaml
if err := config.Initialize(); err != nil {
t.Fatalf("Failed to reinitialize config: %v", err)
}
// Clear daemon env vars but set sync-branch
os.Unsetenv("BEADS_NO_DAEMON")
os.Unsetenv("BEADS_AUTO_START_DAEMON")
os.Setenv("BEADS_SYNC_BRANCH", "beads-metadata")
result := shouldAutoStartDaemon()
if !result {
t.Error("Expected shouldAutoStartDaemon() to return true in worktree with sync-branch")
}
// Cleanup
cleanupTestWorktree(t, mainDir, worktreeDir)
})
t.Run("BEADS_NO_DAEMON still takes precedence in worktree", func(t *testing.T) {
// Create a git repo with a worktree
mainDir, worktreeDir := setupWorktreeTestRepo(t)
// Change to the worktree directory
origDir, _ := os.Getwd()
defer func() {
_ = os.Chdir(origDir)
git.ResetCaches()
_ = config.Initialize()
}()
if err := os.Chdir(worktreeDir); err != nil {
t.Fatalf("Failed to change to worktree dir: %v", err)
}
git.ResetCaches()
// Reset git caches after changing directory
git.ResetCaches()
// Reinitialize config to pick up the new directory's config.yaml
if err := config.Initialize(); err != nil {
t.Fatalf("Failed to reinitialize config: %v", err)
}
// Set BEADS_NO_DAEMON (should override everything)
os.Setenv("BEADS_NO_DAEMON", "1")
os.Setenv("BEADS_SYNC_BRANCH", "beads-metadata")
result := shouldAutoStartDaemon()
if result {
t.Error("Expected BEADS_NO_DAEMON=1 to disable auto-start even with sync-branch")
}
// Cleanup
cleanupTestWorktree(t, mainDir, worktreeDir)
})
}
// Helper functions for worktree daemon tests
func restoreTestEnv(key, value string) {
if value != "" {
os.Setenv(key, value)
} else {
os.Unsetenv(key)
}
}
// setupWorktreeTestRepo creates a git repo with a worktree for testing.
// Returns the main repo directory and worktree directory.
// Caller is responsible for cleanup via cleanupTestWorktree.
//
// IMPORTANT: This function also reinitializes the config package to use the
// temp directory's config, avoiding interference from the beads project's own config.
func setupWorktreeTestRepo(t *testing.T) (mainDir, worktreeDir string) {
t.Helper()
// Create main repo directory
mainDir = t.TempDir()
// Initialize git repo with 'main' as default branch (modern git convention)
cmd := exec.Command("git", "init", "--initial-branch=main")
cmd.Dir = mainDir
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("Failed to init git repo: %v\n%s", err, output)
}
// Configure git user for commits
cmd = exec.Command("git", "config", "user.email", "test@test.com")
cmd.Dir = mainDir
_ = cmd.Run()
cmd = exec.Command("git", "config", "user.name", "Test User")
cmd.Dir = mainDir
_ = cmd.Run()
// Create .beads directory with empty config (no sync-branch)
beadsDir := mainDir + "/.beads"
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatalf("Failed to create .beads dir: %v", err)
}
// Create minimal config.yaml without sync-branch
configContent := "# Test config\nissue-prefix: \"test\"\n"
if err := os.WriteFile(beadsDir+"/config.yaml", []byte(configContent), 0644); err != nil {
t.Fatalf("Failed to create config.yaml: %v", err)
}
// Create initial commit (required for worktrees)
if err := os.WriteFile(mainDir+"/README.md", []byte("# Test\n"), 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
cmd = exec.Command("git", "add", ".")
cmd.Dir = mainDir
_ = cmd.Run()
cmd = exec.Command("git", "commit", "-m", "Initial commit")
cmd.Dir = mainDir
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("Failed to create initial commit: %v\n%s", err, output)
}
// Create a branch for the worktree
cmd = exec.Command("git", "branch", "feature-branch")
cmd.Dir = mainDir
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("Failed to create branch: %v\n%s", err, output)
}
// Create worktree directory (must be outside main repo)
worktreeDir = t.TempDir()
// Add worktree
cmd = exec.Command("git", "worktree", "add", worktreeDir, "feature-branch")
cmd.Dir = mainDir
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("Failed to create worktree: %v\n%s", err, output)
}
return mainDir, worktreeDir
}
// cleanupTestWorktree removes a worktree created by setupWorktreeTestRepo.
func cleanupTestWorktree(t *testing.T, mainDir, worktreeDir string) {
t.Helper()
// Remove worktree
cmd := exec.Command("git", "worktree", "remove", worktreeDir, "--force")
cmd.Dir = mainDir
_ = cmd.Run() // Best effort cleanup
}
// setupWorktreeTestRepoWithDB creates a git repo with a worktree AND a database
// that has sync.branch configured. This tests the database config path.
func setupWorktreeTestRepoWithDB(t *testing.T, syncBranch string) (mainDir, worktreeDir string) {
t.Helper()
// First create the basic worktree repo
mainDir, worktreeDir = setupWorktreeTestRepo(t)
// Now create a database with sync.branch config
beadsDir := mainDir + "/.beads"
dbPath := beadsDir + "/beads.db"
// Create a minimal SQLite database with the config table and sync.branch value
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
// Create config table
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT)`)
if err != nil {
t.Fatalf("Failed to create config table: %v", err)
}
// Insert sync.branch config
_, err = db.Exec(`INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)`, "sync.branch", syncBranch)
if err != nil {
t.Fatalf("Failed to insert sync.branch config: %v", err)
}
return mainDir, worktreeDir
}