Files
beads/cmd/bd/sync_test.go
Steve Yegge dfcbb7d3af Optimize sync_test.go: eliminate redundant git init calls
- Add setupGitRepo(), setupGitRepoWithBranch(), and setupMinimalGitRepo() helpers
- Refactor 19 test functions to use shared git repo setup
- Reduces duplicate git initialization boilerplate by ~300 lines
- All tests pass with improved maintainability

Related to bd-ktng
2025-11-23 22:54:05 -08:00

434 lines
12 KiB
Go

package main
import (
"context"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/syncbranch"
)
func TestIsGitRepo_InGitRepo(t *testing.T) {
// This test assumes we're running in the beads git repo
if !isGitRepo() {
t.Skip("not in a git repository")
}
}
func TestIsGitRepo_NotInGitRepo(t *testing.T) {
tmpDir := t.TempDir()
originalWd, _ := os.Getwd()
defer os.Chdir(originalWd)
os.Chdir(tmpDir)
if isGitRepo() {
t.Error("expected false when not in git repo")
}
}
func TestGitHasUpstream_NoUpstream(t *testing.T) {
_, cleanup := setupGitRepo(t)
defer cleanup()
// Should not have upstream
if gitHasUpstream() {
t.Error("expected false when no upstream configured")
}
}
func TestGitHasChanges_NoFile(t *testing.T) {
ctx := context.Background()
_, cleanup := setupGitRepo(t)
defer cleanup()
// Check - should have no changes (test.txt was committed by setupGitRepo)
hasChanges, err := gitHasChanges(ctx, "test.txt")
if err != nil {
t.Fatalf("gitHasChanges() error = %v", err)
}
if hasChanges {
t.Error("expected no changes for committed file")
}
}
func TestGitHasChanges_ModifiedFile(t *testing.T) {
ctx := context.Background()
tmpDir, cleanup := setupGitRepo(t)
defer cleanup()
// Modify the file
testFile := filepath.Join(tmpDir, "test.txt")
os.WriteFile(testFile, []byte("modified"), 0644)
// Check - should have changes
hasChanges, err := gitHasChanges(ctx, "test.txt")
if err != nil {
t.Fatalf("gitHasChanges() error = %v", err)
}
if !hasChanges {
t.Error("expected changes for modified file")
}
}
func TestGitHasUnmergedPaths_CleanRepo(t *testing.T) {
_, cleanup := setupGitRepo(t)
defer cleanup()
// Should not have unmerged paths
hasUnmerged, err := gitHasUnmergedPaths()
if err != nil {
t.Fatalf("gitHasUnmergedPaths() error = %v", err)
}
if hasUnmerged {
t.Error("expected no unmerged paths in clean repo")
}
}
func TestGitCommit_Success(t *testing.T) {
ctx := context.Background()
_, cleanup := setupGitRepo(t)
defer cleanup()
// Create a new file
testFile := "new.txt"
os.WriteFile(testFile, []byte("content"), 0644)
// Commit the file
err := gitCommit(ctx, testFile, "test commit")
if err != nil {
t.Fatalf("gitCommit() error = %v", err)
}
// Verify file is committed
hasChanges, err := gitHasChanges(ctx, testFile)
if err != nil {
t.Fatalf("gitHasChanges() error = %v", err)
}
if hasChanges {
t.Error("expected no changes after commit")
}
}
func TestGitCommit_AutoMessage(t *testing.T) {
ctx := context.Background()
_, cleanup := setupGitRepo(t)
defer cleanup()
// Create a new file
testFile := "new.txt"
os.WriteFile(testFile, []byte("content"), 0644)
// Commit with auto-generated message (empty string)
err := gitCommit(ctx, testFile, "")
if err != nil {
t.Fatalf("gitCommit() error = %v", err)
}
// Verify it committed (message generation worked)
cmd := exec.Command("git", "log", "-1", "--pretty=%B")
output, _ := cmd.Output()
if len(output) == 0 {
t.Error("expected commit message to be generated")
}
}
func TestCountIssuesInJSONL_NonExistent(t *testing.T) {
count, err := countIssuesInJSONL("/nonexistent/path.jsonl")
if err == nil {
t.Error("expected error for nonexistent file")
}
if count != 0 {
t.Errorf("count = %d, want 0 on error", count)
}
}
func TestCountIssuesInJSONL_EmptyFile(t *testing.T) {
tmpDir := t.TempDir()
jsonlPath := filepath.Join(tmpDir, "empty.jsonl")
os.WriteFile(jsonlPath, []byte(""), 0644)
count, err := countIssuesInJSONL(jsonlPath)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if count != 0 {
t.Errorf("count = %d, want 0", count)
}
}
func TestCountIssuesInJSONL_MultipleIssues(t *testing.T) {
tmpDir := t.TempDir()
jsonlPath := filepath.Join(tmpDir, "issues.jsonl")
content := `{"id":"bd-1"}
{"id":"bd-2"}
{"id":"bd-3"}
`
os.WriteFile(jsonlPath, []byte(content), 0644)
count, err := countIssuesInJSONL(jsonlPath)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if count != 3 {
t.Errorf("count = %d, want 3", count)
}
}
func TestCountIssuesInJSONL_WithMalformedLines(t *testing.T) {
tmpDir := t.TempDir()
jsonlPath := filepath.Join(tmpDir, "mixed.jsonl")
content := `{"id":"bd-1"}
not valid json
{"id":"bd-2"}
{"id":"bd-3"}
`
os.WriteFile(jsonlPath, []byte(content), 0644)
count, err := countIssuesInJSONL(jsonlPath)
// countIssuesInJSONL returns error on malformed JSON
if err == nil {
t.Error("expected error for malformed JSON")
}
// Should have counted the first valid issue before hitting error
if count != 1 {
t.Errorf("count = %d, want 1 (before malformed line)", count)
}
}
func TestGetCurrentBranch(t *testing.T) {
ctx := context.Background()
_, cleanup := setupGitRepo(t)
defer cleanup()
// Get current branch
branch, err := getCurrentBranch(ctx)
if err != nil {
t.Fatalf("getCurrentBranch() error = %v", err)
}
// Default branch is usually main or master
if branch != "main" && branch != "master" {
t.Logf("got branch %s (expected main or master, but this can vary)", branch)
}
}
func TestMergeSyncBranch_NoSyncBranchConfigured(t *testing.T) {
ctx := context.Background()
_, cleanup := setupGitRepo(t)
defer cleanup()
// Try to merge without sync.branch configured (or database)
err := mergeSyncBranch(ctx, false)
if err == nil {
t.Error("expected error when sync.branch not configured")
}
// Error could be about missing database or missing sync.branch config
if err != nil && !strings.Contains(err.Error(), "sync.branch") && !strings.Contains(err.Error(), "database") {
t.Errorf("expected error about sync.branch or database, got: %v", err)
}
}
func TestMergeSyncBranch_OnSyncBranch(t *testing.T) {
ctx := context.Background()
tmpDir, cleanup := setupGitRepo(t)
defer cleanup()
// Create sync branch
exec.Command("git", "checkout", "-b", "beads-metadata").Run()
// Initialize bd database and set sync.branch
beadsDir := filepath.Join(tmpDir, ".beads")
os.MkdirAll(beadsDir, 0755)
// This test will fail with store access issues, so we just verify the branch check
// The actual merge functionality is tested in integration tests
currentBranch, _ := getCurrentBranch(ctx)
if currentBranch != "beads-metadata" {
t.Skipf("test setup failed, current branch is %s", currentBranch)
}
}
func TestMergeSyncBranch_DirtyWorkingTree(t *testing.T) {
_, cleanup := setupGitRepo(t)
defer cleanup()
// Create uncommitted changes
os.WriteFile("test.txt", []byte("modified"), 0644)
// This test verifies the dirty working tree check would work
// (We can't test the full merge without database setup)
statusCmd := exec.Command("git", "status", "--porcelain")
output, _ := statusCmd.Output()
if len(output) == 0 {
t.Error("expected dirty working tree for test setup")
}
}
func TestGetSyncBranch_EnvOverridesDB(t *testing.T) {
ctx := context.Background()
// Save and restore global store state
oldStore := store
storeMutex.Lock()
oldStoreActive := storeActive
storeMutex.Unlock()
oldDBPath := dbPath
// Use an in-memory SQLite store for testing
testStore, err := sqlite.New(context.Background(), "file::memory:?mode=memory&cache=private")
if err != nil {
t.Fatalf("failed to create test store: %v", err)
}
defer testStore.Close()
// Seed DB config and globals
if err := testStore.SetConfig(ctx, "sync.branch", "db-branch"); err != nil {
t.Fatalf("failed to set sync.branch in db: %v", err)
}
storeMutex.Lock()
store = testStore
storeActive = true
storeMutex.Unlock()
dbPath = "" // avoid FindDatabasePath in ensureStoreActive
// Set environment override
if err := os.Setenv(syncbranch.EnvVar, "env-branch"); err != nil {
t.Fatalf("failed to set %s: %v", syncbranch.EnvVar, err)
}
defer os.Unsetenv(syncbranch.EnvVar)
// Ensure we restore globals after the test
defer func() {
storeMutex.Lock()
store = oldStore
storeActive = oldStoreActive
storeMutex.Unlock()
dbPath = oldDBPath
}()
branch, err := getSyncBranch(ctx)
if err != nil {
t.Fatalf("getSyncBranch() error = %v", err)
}
if branch != "env-branch" {
t.Errorf("getSyncBranch() = %q, want %q (env override)", branch, "env-branch")
}
}
func TestIsInRebase_NotInRebase(t *testing.T) {
_, cleanup := setupGitRepo(t)
defer cleanup()
// Should not be in rebase
if isInRebase() {
t.Error("expected false when not in rebase")
}
}
func TestIsInRebase_InRebase(t *testing.T) {
tmpDir, cleanup := setupGitRepo(t)
defer cleanup()
// Simulate rebase by creating rebase-merge directory
os.MkdirAll(filepath.Join(tmpDir, ".git", "rebase-merge"), 0755)
// Should detect rebase
if !isInRebase() {
t.Error("expected true when .git/rebase-merge exists")
}
}
func TestIsInRebase_InRebaseApply(t *testing.T) {
tmpDir, cleanup := setupMinimalGitRepo(t)
defer cleanup()
// Simulate non-interactive rebase by creating rebase-apply directory
os.MkdirAll(filepath.Join(tmpDir, ".git", "rebase-apply"), 0755)
// Should detect rebase
if !isInRebase() {
t.Error("expected true when .git/rebase-apply exists")
}
}
func TestHasJSONLConflict_NoConflict(t *testing.T) {
_, cleanup := setupGitRepo(t)
defer cleanup()
// Should not have JSONL conflict
if hasJSONLConflict() {
t.Error("expected false when no conflicts")
}
}
func TestHasJSONLConflict_OnlyJSONLConflict(t *testing.T) {
tmpDir, cleanup := setupGitRepoWithBranch(t, "main")
defer cleanup()
// Create initial commit with beads.jsonl
beadsDir := filepath.Join(tmpDir, ".beads")
os.MkdirAll(beadsDir, 0755)
os.WriteFile(filepath.Join(beadsDir, "beads.jsonl"), []byte(`{"id":"bd-1","title":"original"}`), 0644)
exec.Command("git", "add", ".").Run()
exec.Command("git", "commit", "-m", "add beads.jsonl").Run()
// Create a second commit on main (modify same issue)
os.WriteFile(filepath.Join(beadsDir, "beads.jsonl"), []byte(`{"id":"bd-1","title":"main-version"}`), 0644)
exec.Command("git", "add", ".").Run()
exec.Command("git", "commit", "-m", "main change").Run()
// Create a branch from the first commit
exec.Command("git", "checkout", "-b", "feature", "HEAD~1").Run()
os.WriteFile(filepath.Join(beadsDir, "beads.jsonl"), []byte(`{"id":"bd-1","title":"feature-version"}`), 0644)
exec.Command("git", "add", ".").Run()
exec.Command("git", "commit", "-m", "feature change").Run()
// Attempt rebase onto main (will conflict)
exec.Command("git", "rebase", "main").Run()
// Should detect JSONL conflict during rebase
if !hasJSONLConflict() {
t.Error("expected true when only beads.jsonl has conflict during rebase")
}
}
func TestHasJSONLConflict_MultipleConflicts(t *testing.T) {
tmpDir, cleanup := setupGitRepoWithBranch(t, "main")
defer cleanup()
// Create initial commit with beads.jsonl and another file
beadsDir := filepath.Join(tmpDir, ".beads")
os.MkdirAll(beadsDir, 0755)
os.WriteFile(filepath.Join(beadsDir, "beads.jsonl"), []byte(`{"id":"bd-1","title":"original"}`), 0644)
os.WriteFile("other.txt", []byte("line1\nline2\nline3"), 0644)
exec.Command("git", "add", ".").Run()
exec.Command("git", "commit", "-m", "add initial files").Run()
// Create a second commit on main (modify both files)
os.WriteFile(filepath.Join(beadsDir, "beads.jsonl"), []byte(`{"id":"bd-1","title":"main-version"}`), 0644)
os.WriteFile("other.txt", []byte("line1\nmain-version\nline3"), 0644)
exec.Command("git", "add", ".").Run()
exec.Command("git", "commit", "-m", "main change").Run()
// Create a branch from the first commit
exec.Command("git", "checkout", "-b", "feature", "HEAD~1").Run()
os.WriteFile(filepath.Join(beadsDir, "beads.jsonl"), []byte(`{"id":"bd-1","title":"feature-version"}`), 0644)
os.WriteFile("other.txt", []byte("line1\nfeature-version\nline3"), 0644)
exec.Command("git", "add", ".").Run()
exec.Command("git", "commit", "-m", "feature change").Run()
// Attempt rebase (will conflict on both files)
exec.Command("git", "rebase", "main").Run()
// Should NOT auto-resolve when multiple files conflict
if hasJSONLConflict() {
t.Error("expected false when multiple files have conflicts (should not auto-resolve)")
}
}