fix: validate project files in FindBeadsDir (bd-420) (#424)
FindBeadsDir() now checks for actual beads project files before returning a .beads directory. This prevents false positives when ~/.beads/ exists only for daemon registry (registry.json). Changes: - Add hasBeadsProjectFiles() helper that checks for: - metadata.json or config.yaml (project config) - *.db files (excluding backups and vc.db) - *.jsonl files (JSONL-only mode) - Update FindBeadsDir() to validate directories during tree search - Add comprehensive tests for project file detection - Update version_tracking_test.go to create project files Fixes #420 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -124,12 +124,18 @@ func TestTrackBdVersion_NoBeadsDir(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTrackBdVersion_FirstRun(t *testing.T) {
|
||||
// Create temp .beads directory
|
||||
// Create temp .beads directory with a project file (bd-420)
|
||||
// FindBeadsDir now requires actual project files, not just directory existence
|
||||
tmpDir := t.TempDir()
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create .beads: %v", err)
|
||||
}
|
||||
// Create a database file so FindBeadsDir finds this directory
|
||||
dbPath := filepath.Join(beadsDir, "beads.db")
|
||||
if err := os.WriteFile(dbPath, []byte{}, 0644); err != nil {
|
||||
t.Fatalf("Failed to create db file: %v", err)
|
||||
}
|
||||
|
||||
// Change to temp directory
|
||||
origWd, _ := os.Getwd()
|
||||
|
||||
@@ -230,9 +230,45 @@ func FindDatabasePath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// hasBeadsProjectFiles checks if a .beads directory contains actual project files.
|
||||
// Returns true if the directory contains any of:
|
||||
// - metadata.json or config.yaml (project configuration)
|
||||
// - Any *.db file (excluding backups and vc.db)
|
||||
// - Any *.jsonl file (JSONL-only mode or git-tracked issues)
|
||||
//
|
||||
// Returns false for directories that only contain daemon registry files (bd-420).
|
||||
// This prevents FindBeadsDir from returning ~/.beads/ which only has registry.json.
|
||||
func hasBeadsProjectFiles(beadsDir string) bool {
|
||||
// Check for project configuration files
|
||||
if _, err := os.Stat(filepath.Join(beadsDir, "metadata.json")); err == nil {
|
||||
return true
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(beadsDir, "config.yaml")); err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for database files (excluding backups and vc.db)
|
||||
dbMatches, _ := filepath.Glob(filepath.Join(beadsDir, "*.db"))
|
||||
for _, match := range dbMatches {
|
||||
baseName := filepath.Base(match)
|
||||
if !strings.Contains(baseName, ".backup") && baseName != "vc.db" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check for JSONL files (JSONL-only mode or fresh clone)
|
||||
jsonlMatches, _ := filepath.Glob(filepath.Join(beadsDir, "*.jsonl"))
|
||||
if len(jsonlMatches) > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// FindBeadsDir finds the .beads/ directory in the current directory tree
|
||||
// Returns empty string if not found. Supports both database and JSONL-only mode.
|
||||
// Stops at the git repository root to avoid finding unrelated directories (bd-c8x).
|
||||
// Validates that the directory contains actual project files (bd-420).
|
||||
// This is useful for commands that need to detect beads projects without requiring a database.
|
||||
func FindBeadsDir() string {
|
||||
// 1. Check BEADS_DIR environment variable (preferred)
|
||||
@@ -255,7 +291,10 @@ func FindBeadsDir() string {
|
||||
for dir := cwd; dir != "/" && dir != "."; dir = filepath.Dir(dir) {
|
||||
beadsDir := filepath.Join(dir, ".beads")
|
||||
if info, err := os.Stat(beadsDir); err == nil && info.IsDir() {
|
||||
return beadsDir
|
||||
// Validate directory contains actual project files (bd-420)
|
||||
if hasBeadsProjectFiles(beadsDir) {
|
||||
return beadsDir
|
||||
}
|
||||
}
|
||||
|
||||
// Stop at git root to avoid finding unrelated directories (bd-c8x)
|
||||
|
||||
@@ -282,6 +282,134 @@ func TestFindJSONLPathSkipsDeletions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestHasBeadsProjectFiles verifies that hasBeadsProjectFiles correctly
|
||||
// distinguishes between project directories and daemon-only directories (bd-420)
|
||||
func TestHasBeadsProjectFiles(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
files []string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "empty directory",
|
||||
files: []string{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "daemon registry only",
|
||||
files: []string{"registry.json", "registry.lock"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "has database",
|
||||
files: []string{"beads.db"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "has issues.jsonl",
|
||||
files: []string{"issues.jsonl"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "has metadata.json",
|
||||
files: []string{"metadata.json"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "has config.yaml",
|
||||
files: []string{"config.yaml"},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "ignores backup db",
|
||||
files: []string{"beads.backup.db"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "ignores vc.db",
|
||||
files: []string{"vc.db"},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "real db with backup",
|
||||
files: []string{"beads.db", "beads.backup.db"},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "beads-project-test-*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create test files
|
||||
for _, file := range tt.files {
|
||||
path := filepath.Join(tmpDir, file)
|
||||
if err := os.WriteFile(path, []byte("{}"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
result := hasBeadsProjectFiles(tmpDir)
|
||||
if result != tt.expected {
|
||||
t.Errorf("hasBeadsProjectFiles() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindBeadsDirSkipsDaemonRegistry verifies that FindBeadsDir skips
|
||||
// directories containing only daemon registry files (bd-420)
|
||||
func TestFindBeadsDirSkipsDaemonRegistry(t *testing.T) {
|
||||
// Save original state
|
||||
originalEnv := os.Getenv("BEADS_DIR")
|
||||
originalWd, _ := os.Getwd()
|
||||
defer func() {
|
||||
if originalEnv != "" {
|
||||
os.Setenv("BEADS_DIR", originalEnv)
|
||||
} else {
|
||||
os.Unsetenv("BEADS_DIR")
|
||||
}
|
||||
os.Chdir(originalWd)
|
||||
}()
|
||||
os.Unsetenv("BEADS_DIR")
|
||||
|
||||
// Create temp directory structure
|
||||
tmpDir, err := os.MkdirTemp("", "beads-daemon-test-*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create .beads with only daemon registry files (should be skipped)
|
||||
beadsDir := filepath.Join(tmpDir, ".beads")
|
||||
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(beadsDir, "registry.json"), []byte("[]"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Change to temp dir
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Should NOT find the daemon-only directory
|
||||
result := FindBeadsDir()
|
||||
if result != "" {
|
||||
// Resolve symlinks for comparison
|
||||
resultResolved, _ := filepath.EvalSymlinks(result)
|
||||
beadsDirResolved, _ := filepath.EvalSymlinks(beadsDir)
|
||||
if resultResolved == beadsDirResolved {
|
||||
t.Errorf("FindBeadsDir() should skip daemon-only directory, got %q", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindDatabasePathHomeDefault(t *testing.T) {
|
||||
// This test verifies that if no database is found, it falls back to home directory
|
||||
// We can't reliably test this without modifying the home directory, so we'll skip
|
||||
|
||||
Reference in New Issue
Block a user