Files
beads/internal/git/worktree_test.go
Steve Yegge 26b8013908 fix(sync): merge instead of overwrite in SyncJSONLToWorktree (bd-52q)
When syncing JSONL to worktree, if the worktree has more issues than
local, merge them instead of blindly overwriting. This prevents fresh
clones from accidentally deleting remote issues when they sync with
fewer issues than the sync branch.

Root cause of GitHub #464: A fresh clone with sync-branch configured
would start with an empty database (since JSONL is on sync-branch, not
HEAD). When syncing, the local 1-issue JSONL would overwrite the
remotes 10-issue JSONL, and the subsequent 3-way merge would see this
as local deleted 9 issues causing deletion to win.

The fix compares issue counts and triggers a merge when local has fewer
issues than the worktree (remote). Uses 3-way merge with empty base to
combine both sets of issues.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 14:33:34 -08:00

748 lines
23 KiB
Go

package git
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// setupTestRepo creates a temporary git repository for testing
func setupTestRepo(t *testing.T) (repoPath string, cleanup func()) {
t.Helper()
tmpDir := t.TempDir()
repoPath = filepath.Join(tmpDir, "test-repo")
// Create repo directory
if err := os.MkdirAll(repoPath, 0750); err != nil {
t.Fatalf("Failed to create test repo directory: %v", err)
}
// Initialize git repo
cmd := exec.Command("git", "init")
cmd.Dir = repoPath
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("Failed to init git repo: %v\nOutput: %s", err, string(output))
}
// Configure git user for commits
cmd = exec.Command("git", "config", "user.email", "test@example.com")
cmd.Dir = repoPath
if err := cmd.Run(); err != nil {
t.Fatalf("Failed to set git user.email: %v", err)
}
cmd = exec.Command("git", "config", "user.name", "Test User")
cmd.Dir = repoPath
if err := cmd.Run(); err != nil {
t.Fatalf("Failed to set git user.name: %v", err)
}
// Create .beads directory and a test file
beadsDir := filepath.Join(repoPath, ".beads")
if err := os.MkdirAll(beadsDir, 0750); err != nil {
t.Fatalf("Failed to create .beads directory: %v", err)
}
testFile := filepath.Join(beadsDir, "test.jsonl")
if err := os.WriteFile(testFile, []byte("test data\n"), 0644); err != nil {
t.Fatalf("Failed to write test file: %v", err)
}
// Create a file outside .beads to test sparse checkout
otherFile := filepath.Join(repoPath, "other.txt")
if err := os.WriteFile(otherFile, []byte("other data\n"), 0644); err != nil {
t.Fatalf("Failed to write other file: %v", err)
}
// Initial commit
cmd = exec.Command("git", "add", ".")
cmd.Dir = repoPath
if err := cmd.Run(); err != nil {
t.Fatalf("Failed to git add: %v", err)
}
cmd = exec.Command("git", "commit", "-m", "Initial commit")
cmd.Dir = repoPath
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("Failed to commit: %v\nOutput: %s", err, string(output))
}
cleanup = func() {
// Cleanup is handled by t.TempDir()
}
return repoPath, cleanup
}
func TestCreateBeadsWorktree(t *testing.T) {
repoPath, cleanup := setupTestRepo(t)
defer cleanup()
wm := NewWorktreeManager(repoPath)
worktreePath := filepath.Join(t.TempDir(), "beads-worktree")
t.Run("creates new branch worktree", func(t *testing.T) {
err := wm.CreateBeadsWorktree("beads-metadata", worktreePath)
if err != nil {
t.Fatalf("CreateBeadsWorktree failed: %v", err)
}
// Verify worktree exists
if _, err := os.Stat(worktreePath); os.IsNotExist(err) {
t.Errorf("Worktree directory was not created")
}
// Verify .git file exists
gitFile := filepath.Join(worktreePath, ".git")
if _, err := os.Stat(gitFile); os.IsNotExist(err) {
t.Errorf("Worktree .git file was not created")
}
// Verify .beads directory exists in worktree
beadsDir := filepath.Join(worktreePath, ".beads")
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
t.Errorf(".beads directory not found in worktree")
}
// Verify sparse checkout: other.txt should NOT exist
otherFile := filepath.Join(worktreePath, "other.txt")
if _, err := os.Stat(otherFile); err == nil {
t.Errorf("Sparse checkout failed: other.txt should not exist in worktree")
}
})
t.Run("idempotent - calling twice succeeds", func(t *testing.T) {
worktreePath2 := filepath.Join(t.TempDir(), "beads-worktree-idempotent")
// Create once
if err := wm.CreateBeadsWorktree("beads-metadata-idempotent", worktreePath2); err != nil {
t.Fatalf("First CreateBeadsWorktree failed: %v", err)
}
// Create again with same path (should succeed and be a no-op)
if err := wm.CreateBeadsWorktree("beads-metadata-idempotent", worktreePath2); err != nil {
t.Errorf("Second CreateBeadsWorktree failed (should be idempotent): %v", err)
}
// Verify worktree still exists and is valid
if valid, err := wm.isValidWorktree(worktreePath2); err != nil || !valid {
t.Errorf("Worktree should still be valid after idempotent call: valid=%v, err=%v", valid, err)
}
})
}
func TestRemoveBeadsWorktree(t *testing.T) {
repoPath, cleanup := setupTestRepo(t)
defer cleanup()
wm := NewWorktreeManager(repoPath)
worktreePath := filepath.Join(t.TempDir(), "beads-worktree")
// Create worktree first
if err := wm.CreateBeadsWorktree("beads-metadata", worktreePath); err != nil {
t.Fatalf("CreateBeadsWorktree failed: %v", err)
}
// Verify it exists
if _, err := os.Stat(worktreePath); os.IsNotExist(err) {
t.Fatalf("Worktree was not created")
}
// Remove it
if err := wm.RemoveBeadsWorktree(worktreePath); err != nil {
t.Fatalf("RemoveBeadsWorktree failed: %v", err)
}
// Verify it's gone
if _, err := os.Stat(worktreePath); err == nil {
t.Errorf("Worktree directory still exists after removal")
}
}
func TestCheckWorktreeHealth(t *testing.T) {
repoPath, cleanup := setupTestRepo(t)
defer cleanup()
wm := NewWorktreeManager(repoPath)
t.Run("healthy worktree passes check", func(t *testing.T) {
worktreePath := filepath.Join(t.TempDir(), "beads-worktree")
if err := wm.CreateBeadsWorktree("beads-metadata", worktreePath); err != nil {
t.Fatalf("CreateBeadsWorktree failed: %v", err)
}
if err := wm.CheckWorktreeHealth(worktreePath); err != nil {
t.Errorf("CheckWorktreeHealth failed for healthy worktree: %v", err)
}
})
t.Run("non-existent path fails check", func(t *testing.T) {
nonExistentPath := filepath.Join(t.TempDir(), "does-not-exist")
err := wm.CheckWorktreeHealth(nonExistentPath)
if err == nil {
t.Error("CheckWorktreeHealth should fail for non-existent path")
}
if !strings.Contains(err.Error(), "does not exist") {
t.Errorf("Expected 'does not exist' error, got: %v", err)
}
})
t.Run("invalid worktree fails check", func(t *testing.T) {
invalidPath := filepath.Join(t.TempDir(), "invalid-worktree")
if err := os.MkdirAll(invalidPath, 0750); err != nil {
t.Fatalf("Failed to create invalid path: %v", err)
}
err := wm.CheckWorktreeHealth(invalidPath)
if err == nil {
t.Error("CheckWorktreeHealth should fail for invalid worktree")
}
})
}
func TestSyncJSONLToWorktree(t *testing.T) {
repoPath, cleanup := setupTestRepo(t)
defer cleanup()
wm := NewWorktreeManager(repoPath)
worktreePath := filepath.Join(t.TempDir(), "beads-worktree")
// Create worktree
if err := wm.CreateBeadsWorktree("beads-metadata", worktreePath); err != nil {
t.Fatalf("CreateBeadsWorktree failed: %v", err)
}
// Update the JSONL in the main repo
mainJSONL := filepath.Join(repoPath, ".beads", "test.jsonl")
newData := []byte("updated data\n")
if err := os.WriteFile(mainJSONL, newData, 0644); err != nil {
t.Fatalf("Failed to update main JSONL: %v", err)
}
// Sync to worktree
if err := wm.SyncJSONLToWorktree(worktreePath, ".beads/test.jsonl"); err != nil {
t.Fatalf("SyncJSONLToWorktree failed: %v", err)
}
// Verify the data was synced
worktreeJSONL := filepath.Join(worktreePath, ".beads", "test.jsonl")
data, err := os.ReadFile(worktreeJSONL)
if err != nil {
t.Fatalf("Failed to read worktree JSONL: %v", err)
}
if string(data) != string(newData) {
t.Errorf("JSONL data mismatch.\nExpected: %s\nGot: %s", string(newData), string(data))
}
}
func TestBranchExists(t *testing.T) {
repoPath, cleanup := setupTestRepo(t)
defer cleanup()
wm := NewWorktreeManager(repoPath)
t.Run("main branch exists", func(t *testing.T) {
// Get the default branch name (might be 'main' or 'master')
cmd := exec.Command("git", "branch", "--show-current")
cmd.Dir = repoPath
output, err := cmd.Output()
if err != nil {
t.Fatalf("Failed to get current branch: %v", err)
}
currentBranch := strings.TrimSpace(string(output))
exists := wm.branchExists(currentBranch)
if !exists {
t.Errorf("Current branch %s should exist", currentBranch)
}
})
t.Run("non-existent branch returns false", func(t *testing.T) {
exists := wm.branchExists("does-not-exist-branch")
if exists {
t.Error("Non-existent branch should return false")
}
})
}
func TestIsValidWorktree(t *testing.T) {
repoPath, cleanup := setupTestRepo(t)
defer cleanup()
wm := NewWorktreeManager(repoPath)
t.Run("created worktree is valid", func(t *testing.T) {
worktreePath := filepath.Join(t.TempDir(), "beads-worktree")
if err := wm.CreateBeadsWorktree("beads-metadata", worktreePath); err != nil {
t.Fatalf("CreateBeadsWorktree failed: %v", err)
}
valid, err := wm.isValidWorktree(worktreePath)
if err != nil {
t.Fatalf("isValidWorktree failed: %v", err)
}
if !valid {
t.Error("Created worktree should be valid")
}
})
t.Run("non-worktree path is invalid", func(t *testing.T) {
invalidPath := filepath.Join(t.TempDir(), "not-a-worktree")
if err := os.MkdirAll(invalidPath, 0750); err != nil {
t.Fatalf("Failed to create test directory: %v", err)
}
valid, err := wm.isValidWorktree(invalidPath)
if err != nil {
t.Fatalf("isValidWorktree failed: %v", err)
}
if valid {
t.Error("Non-worktree path should be invalid")
}
})
}
func TestSparseCheckoutConfiguration(t *testing.T) {
repoPath, cleanup := setupTestRepo(t)
defer cleanup()
wm := NewWorktreeManager(repoPath)
worktreePath := filepath.Join(t.TempDir(), "beads-worktree")
// Create worktree
if err := wm.CreateBeadsWorktree("beads-metadata", worktreePath); err != nil {
t.Fatalf("CreateBeadsWorktree failed: %v", err)
}
t.Run("sparse checkout includes .beads", func(t *testing.T) {
if err := wm.verifySparseCheckout(worktreePath); err != nil {
t.Errorf("verifySparseCheckout failed: %v", err)
}
})
t.Run("can reconfigure sparse checkout", func(t *testing.T) {
if err := wm.configureSparseCheckout(worktreePath); err != nil {
t.Errorf("configureSparseCheckout failed: %v", err)
}
// Verify it's still correct
if err := wm.verifySparseCheckout(worktreePath); err != nil {
t.Errorf("verifySparseCheckout failed after reconfigure: %v", err)
}
})
}
func TestRemoveBeadsWorktreeManualCleanup(t *testing.T) {
repoPath, cleanup := setupTestRepo(t)
defer cleanup()
wm := NewWorktreeManager(repoPath)
worktreePath := filepath.Join(t.TempDir(), "beads-worktree")
// Create worktree
if err := wm.CreateBeadsWorktree("beads-metadata", worktreePath); err != nil {
t.Fatalf("CreateBeadsWorktree failed: %v", err)
}
// Manually corrupt the worktree to force manual cleanup path
// Remove the .git file which will cause git worktree remove to fail
gitFile := filepath.Join(worktreePath, ".git")
if err := os.Remove(gitFile); err != nil {
t.Fatalf("Failed to remove .git file: %v", err)
}
// Now remove should use the manual cleanup path
err := wm.RemoveBeadsWorktree(worktreePath)
if err != nil {
t.Errorf("RemoveBeadsWorktree should succeed with manual cleanup: %v", err)
}
// Verify directory is gone
if _, err := os.Stat(worktreePath); err == nil {
t.Error("Worktree directory should be removed")
}
}
func TestRemoveBeadsWorktreeNonExistent(t *testing.T) {
repoPath, cleanup := setupTestRepo(t)
defer cleanup()
wm := NewWorktreeManager(repoPath)
nonExistentPath := filepath.Join(t.TempDir(), "does-not-exist")
// Removing a non-existent worktree should succeed (no-op)
err := wm.RemoveBeadsWorktree(nonExistentPath)
if err != nil {
t.Errorf("RemoveBeadsWorktree should succeed for non-existent path: %v", err)
}
}
func TestSyncJSONLToWorktreeErrors(t *testing.T) {
repoPath, cleanup := setupTestRepo(t)
defer cleanup()
wm := NewWorktreeManager(repoPath)
worktreePath := filepath.Join(t.TempDir(), "beads-worktree")
// Create worktree
if err := wm.CreateBeadsWorktree("beads-metadata", worktreePath); err != nil {
t.Fatalf("CreateBeadsWorktree failed: %v", err)
}
t.Run("fails when source file does not exist", func(t *testing.T) {
err := wm.SyncJSONLToWorktree(worktreePath, ".beads/nonexistent.jsonl")
if err == nil {
t.Error("SyncJSONLToWorktree should fail when source file does not exist")
}
if !strings.Contains(err.Error(), "failed to read source JSONL") {
t.Errorf("Expected 'failed to read source JSONL' error, got: %v", err)
}
})
}
func TestCreateBeadsWorktreeWithExistingBranch(t *testing.T) {
repoPath, cleanup := setupTestRepo(t)
defer cleanup()
wm := NewWorktreeManager(repoPath)
// Create a branch first
branchName := "existing-branch"
cmd := exec.Command("git", "branch", branchName)
cmd.Dir = repoPath
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("Failed to create branch: %v\nOutput: %s", err, string(output))
}
// Now create worktree with this existing branch
worktreePath := filepath.Join(t.TempDir(), "beads-worktree")
if err := wm.CreateBeadsWorktree(branchName, worktreePath); err != nil {
t.Fatalf("CreateBeadsWorktree failed with existing branch: %v", err)
}
// Verify worktree was created
if _, err := os.Stat(worktreePath); os.IsNotExist(err) {
t.Error("Worktree directory was not created")
}
// Verify .beads exists
beadsDir := filepath.Join(worktreePath, ".beads")
if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
t.Error(".beads directory not found in worktree")
}
}
func TestCreateBeadsWorktreeInvalidPath(t *testing.T) {
repoPath, cleanup := setupTestRepo(t)
defer cleanup()
wm := NewWorktreeManager(repoPath)
worktreePath := filepath.Join(t.TempDir(), "beads-worktree")
// Create a file where the worktree directory should be (but not a valid worktree)
if err := os.WriteFile(worktreePath, []byte("not a worktree"), 0644); err != nil {
t.Fatalf("Failed to create blocking file: %v", err)
}
// CreateBeadsWorktree should handle this - it should remove the invalid path
err := wm.CreateBeadsWorktree("beads-metadata", worktreePath)
if err != nil {
t.Fatalf("CreateBeadsWorktree should handle invalid path: %v", err)
}
// Verify worktree was created
if _, err := os.Stat(worktreePath); os.IsNotExist(err) {
t.Error("Worktree directory was not created")
}
// Verify it's now a valid worktree (directory, not file)
info, err := os.Stat(worktreePath)
if err != nil {
t.Fatalf("Failed to stat worktree path: %v", err)
}
if !info.IsDir() {
t.Error("Worktree path should be a directory")
}
}
func TestCheckWorktreeHealthWithBrokenSparseCheckout(t *testing.T) {
repoPath, cleanup := setupTestRepo(t)
defer cleanup()
wm := NewWorktreeManager(repoPath)
worktreePath := filepath.Join(t.TempDir(), "beads-worktree")
// Create worktree
if err := wm.CreateBeadsWorktree("beads-metadata", worktreePath); err != nil {
t.Fatalf("CreateBeadsWorktree failed: %v", err)
}
// Read the .git file to find the git directory
gitFile := filepath.Join(worktreePath, ".git")
gitContent, err := os.ReadFile(gitFile)
if err != nil {
t.Fatalf("Failed to read .git file: %v", err)
}
// Parse "gitdir: /path/to/git/dir"
gitDirLine := strings.TrimSpace(string(gitContent))
gitDir := strings.TrimPrefix(gitDirLine, "gitdir: ")
// Corrupt the sparse-checkout file
sparseFile := filepath.Join(gitDir, "info", "sparse-checkout")
if err := os.WriteFile(sparseFile, []byte("invalid\n"), 0644); err != nil {
t.Fatalf("Failed to corrupt sparse-checkout: %v", err)
}
// CheckWorktreeHealth should detect the problem and attempt to fix it
err = wm.CheckWorktreeHealth(worktreePath)
if err != nil {
t.Errorf("CheckWorktreeHealth should repair broken sparse checkout: %v", err)
}
// Verify sparse checkout was repaired
if err := wm.verifySparseCheckout(worktreePath); err != nil {
t.Errorf("Sparse checkout should be repaired: %v", err)
}
}
func TestVerifySparseCheckoutErrors(t *testing.T) {
repoPath, cleanup := setupTestRepo(t)
defer cleanup()
wm := NewWorktreeManager(repoPath)
t.Run("fails with missing .git file", func(t *testing.T) {
invalidPath := filepath.Join(t.TempDir(), "no-git-file")
if err := os.MkdirAll(invalidPath, 0750); err != nil {
t.Fatalf("Failed to create test directory: %v", err)
}
err := wm.verifySparseCheckout(invalidPath)
if err == nil {
t.Error("verifySparseCheckout should fail with missing .git file")
}
})
t.Run("fails with invalid .git file format", func(t *testing.T) {
invalidPath := filepath.Join(t.TempDir(), "invalid-git-file")
if err := os.MkdirAll(invalidPath, 0750); err != nil {
t.Fatalf("Failed to create test directory: %v", err)
}
// Create an invalid .git file (missing "gitdir: " prefix)
gitFile := filepath.Join(invalidPath, ".git")
if err := os.WriteFile(gitFile, []byte("invalid format"), 0644); err != nil {
t.Fatalf("Failed to create invalid .git file: %v", err)
}
err := wm.verifySparseCheckout(invalidPath)
if err == nil {
t.Error("verifySparseCheckout should fail with invalid .git file format")
}
if !strings.Contains(err.Error(), "invalid .git file format") {
t.Errorf("Expected 'invalid .git file format' error, got: %v", err)
}
})
}
func TestConfigureSparseCheckoutErrors(t *testing.T) {
repoPath, cleanup := setupTestRepo(t)
defer cleanup()
wm := NewWorktreeManager(repoPath)
t.Run("fails with missing .git file", func(t *testing.T) {
invalidPath := filepath.Join(t.TempDir(), "no-git-file")
if err := os.MkdirAll(invalidPath, 0750); err != nil {
t.Fatalf("Failed to create test directory: %v", err)
}
err := wm.configureSparseCheckout(invalidPath)
if err == nil {
t.Error("configureSparseCheckout should fail with missing .git file")
}
})
t.Run("fails with invalid .git file format", func(t *testing.T) {
invalidPath := filepath.Join(t.TempDir(), "invalid-git-file")
if err := os.MkdirAll(invalidPath, 0750); err != nil {
t.Fatalf("Failed to create test directory: %v", err)
}
// Create an invalid .git file
gitFile := filepath.Join(invalidPath, ".git")
if err := os.WriteFile(gitFile, []byte("invalid format"), 0644); err != nil {
t.Fatalf("Failed to create invalid .git file: %v", err)
}
err := wm.configureSparseCheckout(invalidPath)
if err == nil {
t.Error("configureSparseCheckout should fail with invalid .git file format")
}
})
}
// TestSyncJSONLToWorktreeMerge tests the merge behavior when worktree has more issues
// than the local repo (bd-52q fix for GitHub #464)
func TestSyncJSONLToWorktreeMerge(t *testing.T) {
repoPath, cleanup := setupTestRepo(t)
defer cleanup()
wm := NewWorktreeManager(repoPath)
worktreePath := filepath.Join(t.TempDir(), "beads-worktree")
// Create worktree
if err := wm.CreateBeadsWorktree("beads-metadata", worktreePath); err != nil {
t.Fatalf("CreateBeadsWorktree failed: %v", err)
}
t.Run("merges when worktree has more issues than local", func(t *testing.T) {
// Set up: worktree has 3 issues (simulating remote state)
worktreeJSONL := filepath.Join(worktreePath, ".beads", "issues.jsonl")
worktreeData := `{"id":"bd-001","title":"Issue 1","status":"open","created_at":"2025-01-01T00:00:00Z","created_by":"user1"}
{"id":"bd-002","title":"Issue 2","status":"open","created_at":"2025-01-01T00:00:01Z","created_by":"user1"}
{"id":"bd-003","title":"Issue 3","status":"open","created_at":"2025-01-01T00:00:02Z","created_by":"user1"}
`
if err := os.WriteFile(worktreeJSONL, []byte(worktreeData), 0644); err != nil {
t.Fatalf("Failed to write worktree JSONL: %v", err)
}
// Local has only 1 issue (simulating fresh clone that hasn't synced)
mainJSONL := filepath.Join(repoPath, ".beads", "issues.jsonl")
mainData := `{"id":"bd-004","title":"New Issue","status":"open","created_at":"2025-01-02T00:00:00Z","created_by":"user2"}
`
if err := os.WriteFile(mainJSONL, []byte(mainData), 0644); err != nil {
t.Fatalf("Failed to write main JSONL: %v", err)
}
// Sync should MERGE, not overwrite
if err := wm.SyncJSONLToWorktree(worktreePath, ".beads/issues.jsonl"); err != nil {
t.Fatalf("SyncJSONLToWorktree failed: %v", err)
}
// Read the result
resultData, err := os.ReadFile(worktreeJSONL)
if err != nil {
t.Fatalf("Failed to read result JSONL: %v", err)
}
// Should have all 4 issues (3 from worktree + 1 from local)
resultCount := countJSONLIssues(resultData)
if resultCount != 4 {
t.Errorf("Expected 4 issues after merge, got %d\nContent:\n%s", resultCount, string(resultData))
}
// Verify specific issues are present
resultStr := string(resultData)
for _, id := range []string{"bd-001", "bd-002", "bd-003", "bd-004"} {
if !strings.Contains(resultStr, id) {
t.Errorf("Expected issue %s to be in merged result", id)
}
}
})
t.Run("overwrites when local has same or more issues", func(t *testing.T) {
// Set up: worktree has 2 issues
worktreeJSONL := filepath.Join(worktreePath, ".beads", "issues.jsonl")
worktreeData := `{"id":"bd-010","title":"Old 1","status":"open","created_at":"2025-01-01T00:00:00Z","created_by":"user1"}
{"id":"bd-011","title":"Old 2","status":"open","created_at":"2025-01-01T00:00:01Z","created_by":"user1"}
`
if err := os.WriteFile(worktreeJSONL, []byte(worktreeData), 0644); err != nil {
t.Fatalf("Failed to write worktree JSONL: %v", err)
}
// Local has 3 issues (more than worktree)
mainJSONL := filepath.Join(repoPath, ".beads", "issues.jsonl")
mainData := `{"id":"bd-020","title":"New 1","status":"open","created_at":"2025-01-02T00:00:00Z","created_by":"user2"}
{"id":"bd-021","title":"New 2","status":"open","created_at":"2025-01-02T00:00:01Z","created_by":"user2"}
{"id":"bd-022","title":"New 3","status":"open","created_at":"2025-01-02T00:00:02Z","created_by":"user2"}
`
if err := os.WriteFile(mainJSONL, []byte(mainData), 0644); err != nil {
t.Fatalf("Failed to write main JSONL: %v", err)
}
// Sync should OVERWRITE (local is authoritative when it has more)
if err := wm.SyncJSONLToWorktree(worktreePath, ".beads/issues.jsonl"); err != nil {
t.Fatalf("SyncJSONLToWorktree failed: %v", err)
}
// Read the result
resultData, err := os.ReadFile(worktreeJSONL)
if err != nil {
t.Fatalf("Failed to read result JSONL: %v", err)
}
// Should have exactly 3 issues (from local)
resultCount := countJSONLIssues(resultData)
if resultCount != 3 {
t.Errorf("Expected 3 issues after overwrite, got %d", resultCount)
}
// Should have local issues, not worktree issues
resultStr := string(resultData)
if strings.Contains(resultStr, "bd-010") || strings.Contains(resultStr, "bd-011") {
t.Error("Old worktree issues should have been overwritten")
}
if !strings.Contains(resultStr, "bd-020") || !strings.Contains(resultStr, "bd-021") || !strings.Contains(resultStr, "bd-022") {
t.Error("New local issues should be present")
}
})
}
func TestCountJSONLIssues(t *testing.T) {
tests := []struct {
name string
data string
expected int
}{
{
name: "empty file",
data: "",
expected: 0,
},
{
name: "single issue",
data: `{"id":"bd-001","title":"Test"}`,
expected: 1,
},
{
name: "multiple issues",
data: `{"id":"bd-001","title":"Test 1"}
{"id":"bd-002","title":"Test 2"}
{"id":"bd-003","title":"Test 3"}`,
expected: 3,
},
{
name: "with blank lines",
data: `{"id":"bd-001","title":"Test 1"}
{"id":"bd-002","title":"Test 2"}
`,
expected: 2,
},
{
name: "non-JSON lines ignored",
data: "# comment\n{\"id\":\"bd-001\"}\nnot json",
expected: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
count := countJSONLIssues([]byte(tt.data))
if count != tt.expected {
t.Errorf("countJSONLIssues() = %d, want %d", count, tt.expected)
}
})
}
}