Files
beads/cmd/bd/reinit_test.go
Steve Yegge c4c5c8063a Fix: Change default JSONL filename from beads.jsonl to issues.jsonl
The canonical beads database name is issues.jsonl. Tens of thousands of users
have issues.jsonl, and beads.jsonl was only used by the Beads project itself
due to git history pollution.

Changes:
- Updated bd doctor to warn about beads.jsonl instead of issues.jsonl
- Changed default config from beads.jsonl to issues.jsonl
- Reversed precedence in checkGitForIssues to prefer issues.jsonl
- Updated git merge driver config to use issues.jsonl
- Updated all tests to expect issues.jsonl as the default

issues.jsonl is now the canonical default; beads.jsonl is legacy

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 23:34:22 -08:00

451 lines
13 KiB
Go

package main
import (
"context"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
"github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types"
)
// TestDatabaseReinitialization tests all database reinitialization scenarios
// covered in DATABASE_REINIT_BUG.md
func TestDatabaseReinitialization(t *testing.T) {
// Skip on Windows due to git hook autoimport flakiness
if runtime.GOOS == "windows" {
t.Skip("Skipping on Windows: git hook autoimport is flaky in CI")
}
// Skip in Nix build environment where git isn't available
if os.Getenv("NIX_BUILD_TOP") != "" {
t.Skip("Skipping test in Nix build environment (git not available)")
}
// Check if git is available
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not available in PATH, skipping test")
}
t.Run("fresh_clone_auto_import", testFreshCloneAutoImport)
t.Run("database_removal_scenario", testDatabaseRemovalScenario)
t.Run("legacy_filename_support", testLegacyFilenameSupport)
t.Run("precedence_test", testPrecedenceTest)
t.Run("init_safety_check", testInitSafetyCheck)
}
// testFreshCloneAutoImport verifies auto-import works on fresh clone
func testFreshCloneAutoImport(t *testing.T) {
dir := t.TempDir()
// Initialize git repo
runCmd(t, dir, "git", "init")
runCmd(t, dir, "git", "config", "user.email", "test@example.com")
runCmd(t, dir, "git", "config", "user.name", "Test User")
// Create .beads directory with issues.jsonl (canonical name)
beadsDir := filepath.Join(dir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatalf("Failed to create .beads dir: %v", err)
}
// Create test issue data
issue := &types.Issue{
ID: "test-1",
Title: "Test issue",
Description: "Test description",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
if err := writeJSONL(jsonlPath, []*types.Issue{issue}); err != nil {
t.Fatalf("Failed to write JSONL: %v", err)
}
// Commit to git (use forward slashes for git path)
runCmd(t, dir, "git", "add", ".beads/issues.jsonl")
runCmd(t, dir, "git", "commit", "-m", "Initial commit")
// Remove database to simulate fresh clone
dbPath := filepath.Join(beadsDir, "test.db")
os.Remove(dbPath)
// Run bd init with auto-import disabled to test checkGitForIssues
store, err := sqlite.New(context.Background(), dbPath)
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer store.Close()
ctx := context.Background()
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
t.Fatalf("Failed to set prefix: %v", err)
}
// Test checkGitForIssues detects issues.jsonl
originalDir, _ := os.Getwd()
os.Chdir(dir)
defer os.Chdir(originalDir)
count, path := checkGitForIssues()
if count != 1 {
t.Errorf("Expected 1 issue in git, got %d", count)
}
// Normalize path for comparison (handle both forward and backslash)
expectedPath := normalizeGitPath(".beads/issues.jsonl")
if normalizeGitPath(path) != expectedPath {
t.Errorf("Expected path %s, got %s", expectedPath, path)
}
// Import from git
if err := importFromGit(ctx, dbPath, store, path); err != nil {
t.Fatalf("Import failed: %v", err)
}
// Verify issue was imported
stats, err := store.GetStatistics(ctx)
if err != nil {
t.Fatalf("Failed to get stats: %v", err)
}
if stats.TotalIssues != 1 {
t.Errorf("Expected 1 issue after import, got %d", stats.TotalIssues)
}
}
// testDatabaseRemovalScenario tests the primary bug scenario
func testDatabaseRemovalScenario(t *testing.T) {
dir := t.TempDir()
// Initialize git repo
runCmd(t, dir, "git", "init")
runCmd(t, dir, "git", "config", "user.email", "test@example.com")
runCmd(t, dir, "git", "config", "user.name", "Test User")
// Create .beads directory with issues.jsonl (canonical name)
beadsDir := filepath.Join(dir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatalf("Failed to create .beads dir: %v", err)
}
// Create multiple test issues
issues := []*types.Issue{
{
ID: "test-1",
Title: "First issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
},
{
ID: "test-2",
Title: "Second issue",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeBug,
},
}
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
if err := writeJSONL(jsonlPath, issues); err != nil {
t.Fatalf("Failed to write JSONL: %v", err)
}
// Commit to git
runCmd(t, dir, "git", "add", ".beads/issues.jsonl")
runCmd(t, dir, "git", "commit", "-m", "Add issues")
// Simulate rm -rf .beads/
os.RemoveAll(beadsDir)
os.MkdirAll(beadsDir, 0755)
// Change to test directory
originalDir, _ := os.Getwd()
os.Chdir(dir)
defer os.Chdir(originalDir)
// Test checkGitForIssues finds issues.jsonl (canonical name)
count, path := checkGitForIssues()
if count != 2 {
t.Errorf("Expected 2 issues in git, got %d", count)
}
expectedPath := normalizeGitPath(".beads/issues.jsonl")
if normalizeGitPath(path) != expectedPath {
t.Errorf("Expected %s, got %s", expectedPath, path)
}
// Initialize database and import
dbPath := filepath.Join(beadsDir, "test.db")
store, err := sqlite.New(context.Background(), dbPath)
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer store.Close()
ctx := context.Background()
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
t.Fatalf("Failed to set prefix: %v", err)
}
if err := importFromGit(ctx, dbPath, store, path); err != nil {
t.Fatalf("Import failed: %v", err)
}
// Verify correct filename was detected
if filepath.Base(path) != "issues.jsonl" {
t.Errorf("Should have imported from issues.jsonl, got %s", path)
}
// Verify stats show >0 issues
stats, err := store.GetStatistics(ctx)
if err != nil {
t.Fatalf("Failed to get stats: %v", err)
}
if stats.TotalIssues != 2 {
t.Errorf("Expected 2 issues, got %d", stats.TotalIssues)
}
}
// testLegacyFilenameSupport tests issues.jsonl fallback
func testLegacyFilenameSupport(t *testing.T) {
dir := t.TempDir()
// Initialize git repo
runCmd(t, dir, "git", "init")
runCmd(t, dir, "git", "config", "user.email", "test@example.com")
runCmd(t, dir, "git", "config", "user.name", "Test User")
// Create .beads directory with issues.jsonl (legacy)
beadsDir := filepath.Join(dir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatalf("Failed to create .beads dir: %v", err)
}
issue := &types.Issue{
ID: "test-1",
Title: "Legacy issue",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
}
// Use legacy filename
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
if err := writeJSONL(jsonlPath, []*types.Issue{issue}); err != nil {
t.Fatalf("Failed to write JSONL: %v", err)
}
// Commit to git
runCmd(t, dir, "git", "add", ".beads/issues.jsonl")
runCmd(t, dir, "git", "commit", "-m", "Add legacy issue")
// Change to test directory
originalDir, _ := os.Getwd()
os.Chdir(dir)
defer os.Chdir(originalDir)
// Test checkGitForIssues finds issues.jsonl
count, path := checkGitForIssues()
if count != 1 {
t.Errorf("Expected 1 issue in git, got %d", count)
}
expectedPath := normalizeGitPath(".beads/issues.jsonl")
if normalizeGitPath(path) != expectedPath {
t.Errorf("Expected %s, got %s", expectedPath, path)
}
// Initialize and import
dbPath := filepath.Join(beadsDir, "test.db")
store, err := sqlite.New(context.Background(), dbPath)
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer store.Close()
ctx := context.Background()
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
t.Fatalf("Failed to set prefix: %v", err)
}
if err := importFromGit(ctx, dbPath, store, path); err != nil {
t.Fatalf("Import failed: %v", err)
}
// Verify import succeeded
stats, err := store.GetStatistics(ctx)
if err != nil {
t.Fatalf("Failed to get stats: %v", err)
}
if stats.TotalIssues != 1 {
t.Errorf("Expected 1 issue, got %d", stats.TotalIssues)
}
}
// testPrecedenceTest verifies issues.jsonl is preferred over beads.jsonl
func testPrecedenceTest(t *testing.T) {
dir := t.TempDir()
// Initialize git repo
runCmd(t, dir, "git", "init")
runCmd(t, dir, "git", "config", "user.email", "test@example.com")
runCmd(t, dir, "git", "config", "user.name", "Test User")
// Create .beads directory with BOTH files
beadsDir := filepath.Join(dir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatalf("Failed to create .beads dir: %v", err)
}
// Create issues.jsonl with 2 issues (canonical, should be preferred)
canonicalIssues := []*types.Issue{
{ID: "test-1", Title: "From issues.jsonl", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
{ID: "test-2", Title: "Also from issues.jsonl", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
}
if err := writeJSONL(filepath.Join(beadsDir, "issues.jsonl"), canonicalIssues); err != nil {
t.Fatalf("Failed to write issues.jsonl: %v", err)
}
// Create beads.jsonl with 1 issue (should be ignored)
legacyIssues := []*types.Issue{
{ID: "test-99", Title: "From beads.jsonl", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask},
}
if err := writeJSONL(filepath.Join(beadsDir, "beads.jsonl"), legacyIssues); err != nil {
t.Fatalf("Failed to write beads.jsonl: %v", err)
}
// Commit both files
runCmd(t, dir, "git", "add", ".beads/")
runCmd(t, dir, "git", "commit", "-m", "Add both files")
// Change to test directory
originalDir, _ := os.Getwd()
os.Chdir(dir)
defer os.Chdir(originalDir)
// Test checkGitForIssues prefers issues.jsonl
count, path := checkGitForIssues()
if count != 2 {
t.Errorf("Expected 2 issues (from issues.jsonl), got %d", count)
}
expectedPath := normalizeGitPath(".beads/issues.jsonl")
if normalizeGitPath(path) != expectedPath {
t.Errorf("Expected issues.jsonl to be preferred, got %s", path)
}
}
// testInitSafetyCheck tests the safety check that prevents silent data loss
func testInitSafetyCheck(t *testing.T) {
dir := t.TempDir()
// Initialize git repo
runCmd(t, dir, "git", "init")
runCmd(t, dir, "git", "config", "user.email", "test@example.com")
runCmd(t, dir, "git", "config", "user.name", "Test User")
// Create .beads directory with issues.jsonl (canonical name)
beadsDir := filepath.Join(dir, ".beads")
if err := os.MkdirAll(beadsDir, 0755); err != nil {
t.Fatalf("Failed to create .beads dir: %v", err)
}
issue := &types.Issue{
ID: "test-1",
Title: "Test issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
if err := writeJSONL(jsonlPath, []*types.Issue{issue}); err != nil {
t.Fatalf("Failed to write JSONL: %v", err)
}
// Commit to git
runCmd(t, dir, "git", "add", ".beads/issues.jsonl")
runCmd(t, dir, "git", "commit", "-m", "Add issue")
// Change to test directory
originalDir, _ := os.Getwd()
os.Chdir(dir)
defer os.Chdir(originalDir)
// Create empty database (simulating failed import)
dbPath := filepath.Join(beadsDir, "test.db")
store, err := sqlite.New(context.Background(), dbPath)
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
ctx := context.Background()
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
t.Fatalf("Failed to set prefix: %v", err)
}
// Verify safety check would detect the problem
stats, err := store.GetStatistics(ctx)
if err != nil {
t.Fatalf("Failed to get stats: %v", err)
}
if stats.TotalIssues == 0 {
// Database is empty - check if git has issues
recheck, recheckPath := checkGitForIssues()
if recheck == 0 {
t.Error("Safety check should have detected issues in git")
}
expectedPath := normalizeGitPath(".beads/issues.jsonl")
if normalizeGitPath(recheckPath) != expectedPath {
t.Errorf("Safety check found wrong path: %s", recheckPath)
}
// This would trigger the error exit in real init.go
t.Logf("Safety check correctly detected %d issues in git at %s", recheck, recheckPath)
} else {
t.Error("Database should be empty for this test")
}
store.Close()
}
// Helper functions
// runCmd runs a command and fails the test if it returns an error
func runCmd(t *testing.T, dir string, name string, args ...string) {
t.Helper()
cmd := exec.Command(name, args...)
cmd.Dir = dir
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("Command %s %v failed: %v\nOutput: %s", name, args, err, output)
}
}
// writeJSONL writes issues to a JSONL file
func writeJSONL(path string, issues []*types.Issue) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
for _, issue := range issues {
if err := enc.Encode(issue); err != nil {
return err
}
}
return nil
}
// normalizeGitPath converts a path to use forward slashes for git compatibility
// Git always uses forward slashes internally, even on Windows
func normalizeGitPath(path string) string {
if runtime.GOOS == windowsOS {
return filepath.ToSlash(path)
}
return path
}