Modern git (2.28+) uses 'main' as default branch, not 'master'. Tests were failing because they assumed 'master' branch exists. Changes: - Use 'git init --initial-branch=main' instead of bare 'git init' - Change 'git checkout master' to 'git checkout main' - Add git.ResetCaches() after os.Chdir() to clear cached git state - Ensures test isolation when changing directories
417 lines
13 KiB
Go
417 lines
13 KiB
Go
package syncbranch
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestCommitToSyncBranch tests the main commit function
|
|
func TestCommitToSyncBranch(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
t.Run("commits changes to sync branch", func(t *testing.T) {
|
|
// Setup: create a repo with a sync branch
|
|
repoDir := setupTestRepoWithRemote(t)
|
|
defer os.RemoveAll(repoDir)
|
|
|
|
syncBranch := "beads-sync"
|
|
jsonlPath := filepath.Join(repoDir, ".beads", "issues.jsonl")
|
|
|
|
// Create sync branch
|
|
runGit(t, repoDir, "checkout", "-b", syncBranch)
|
|
writeFile(t, jsonlPath, `{"id":"test-1"}`)
|
|
runGit(t, repoDir, "add", ".")
|
|
runGit(t, repoDir, "commit", "-m", "initial sync branch commit")
|
|
runGit(t, repoDir, "checkout", "main")
|
|
|
|
// Write new content to commit
|
|
writeFile(t, jsonlPath, `{"id":"test-1"}`+"\n"+`{"id":"test-2"}`)
|
|
|
|
result, err := CommitToSyncBranch(ctx, repoDir, syncBranch, jsonlPath, false)
|
|
if err != nil {
|
|
t.Fatalf("CommitToSyncBranch() error = %v", err)
|
|
}
|
|
|
|
if !result.Committed {
|
|
t.Error("CommitToSyncBranch() Committed = false, want true")
|
|
}
|
|
if result.Branch != syncBranch {
|
|
t.Errorf("CommitToSyncBranch() Branch = %q, want %q", result.Branch, syncBranch)
|
|
}
|
|
if !strings.Contains(result.Message, "bd sync:") {
|
|
t.Errorf("CommitToSyncBranch() Message = %q, want to contain 'bd sync:'", result.Message)
|
|
}
|
|
})
|
|
|
|
t.Run("returns not committed when no changes", func(t *testing.T) {
|
|
repoDir := setupTestRepoWithRemote(t)
|
|
defer os.RemoveAll(repoDir)
|
|
|
|
syncBranch := "beads-sync"
|
|
jsonlPath := filepath.Join(repoDir, ".beads", "issues.jsonl")
|
|
|
|
// Create sync branch with content
|
|
runGit(t, repoDir, "checkout", "-b", syncBranch)
|
|
writeFile(t, jsonlPath, `{"id":"test-1"}`)
|
|
runGit(t, repoDir, "add", ".")
|
|
runGit(t, repoDir, "commit", "-m", "initial")
|
|
runGit(t, repoDir, "checkout", "main")
|
|
|
|
// Write the same content that's in the sync branch
|
|
writeFile(t, jsonlPath, `{"id":"test-1"}`)
|
|
|
|
// Commit with same content (no changes)
|
|
result, err := CommitToSyncBranch(ctx, repoDir, syncBranch, jsonlPath, false)
|
|
if err != nil {
|
|
t.Fatalf("CommitToSyncBranch() error = %v", err)
|
|
}
|
|
|
|
if result.Committed {
|
|
t.Error("CommitToSyncBranch() Committed = true when no changes, want false")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestPullFromSyncBranch tests pulling changes from sync branch
|
|
func TestPullFromSyncBranch(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
t.Run("handles sync branch not on remote", func(t *testing.T) {
|
|
repoDir := setupTestRepoWithRemote(t)
|
|
defer os.RemoveAll(repoDir)
|
|
|
|
syncBranch := "beads-sync"
|
|
jsonlPath := filepath.Join(repoDir, ".beads", "issues.jsonl")
|
|
|
|
// Create local sync branch but don't set up remote tracking
|
|
runGit(t, repoDir, "checkout", "-b", syncBranch)
|
|
writeFile(t, jsonlPath, `{"id":"test-1"}`)
|
|
runGit(t, repoDir, "add", ".")
|
|
runGit(t, repoDir, "commit", "-m", "local sync")
|
|
runGit(t, repoDir, "checkout", "main")
|
|
|
|
// Pull should handle the case where remote doesn't have the branch
|
|
result, err := PullFromSyncBranch(ctx, repoDir, syncBranch, jsonlPath, false)
|
|
// This tests the fetch failure path since "origin" points to self without the sync branch
|
|
// It should either succeed (not pulled) or fail gracefully
|
|
if err != nil {
|
|
// Expected - fetch will fail since origin doesn't have sync branch
|
|
return
|
|
}
|
|
if result.Pulled && !result.FastForwarded && !result.Merged {
|
|
// Pulled but no change - acceptable
|
|
_ = result
|
|
}
|
|
})
|
|
|
|
t.Run("pulls when already up to date", func(t *testing.T) {
|
|
repoDir := setupTestRepoWithRemote(t)
|
|
defer os.RemoveAll(repoDir)
|
|
|
|
syncBranch := "beads-sync"
|
|
jsonlPath := filepath.Join(repoDir, ".beads", "issues.jsonl")
|
|
|
|
// Create sync branch and simulate it being tracked
|
|
runGit(t, repoDir, "checkout", "-b", syncBranch)
|
|
writeFile(t, jsonlPath, `{"id":"test-1"}`)
|
|
runGit(t, repoDir, "add", ".")
|
|
runGit(t, repoDir, "commit", "-m", "sync commit")
|
|
// Set up a fake remote ref at the same commit
|
|
runGit(t, repoDir, "update-ref", "refs/remotes/origin/"+syncBranch, "HEAD")
|
|
runGit(t, repoDir, "checkout", "main")
|
|
|
|
// Pull when already at remote HEAD
|
|
result, err := PullFromSyncBranch(ctx, repoDir, syncBranch, jsonlPath, false)
|
|
if err != nil {
|
|
// Might fail on fetch step, that's acceptable
|
|
return
|
|
}
|
|
// Should have pulled successfully (even if no new content)
|
|
if result.Pulled {
|
|
// Good - it recognized it's up to date
|
|
}
|
|
})
|
|
|
|
t.Run("copies JSONL to main repo after sync", func(t *testing.T) {
|
|
repoDir := setupTestRepoWithRemote(t)
|
|
defer os.RemoveAll(repoDir)
|
|
|
|
syncBranch := "beads-sync"
|
|
jsonlPath := filepath.Join(repoDir, ".beads", "issues.jsonl")
|
|
|
|
// Create sync branch with content
|
|
runGit(t, repoDir, "checkout", "-b", syncBranch)
|
|
writeFile(t, jsonlPath, `{"id":"sync-1"}`)
|
|
runGit(t, repoDir, "add", ".")
|
|
runGit(t, repoDir, "commit", "-m", "sync commit")
|
|
runGit(t, repoDir, "update-ref", "refs/remotes/origin/"+syncBranch, "HEAD")
|
|
runGit(t, repoDir, "checkout", "main")
|
|
|
|
// Remove local JSONL to verify it gets copied back
|
|
os.Remove(jsonlPath)
|
|
|
|
result, err := PullFromSyncBranch(ctx, repoDir, syncBranch, jsonlPath, false)
|
|
if err != nil {
|
|
return // Acceptable in test env
|
|
}
|
|
|
|
if result.Pulled {
|
|
// Verify JSONL was copied to main repo
|
|
if _, err := os.Stat(jsonlPath); os.IsNotExist(err) {
|
|
t.Error("PullFromSyncBranch() did not copy JSONL to main repo")
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("handles fast-forward case", func(t *testing.T) {
|
|
repoDir := setupTestRepoWithRemote(t)
|
|
defer os.RemoveAll(repoDir)
|
|
|
|
syncBranch := "beads-sync"
|
|
jsonlPath := filepath.Join(repoDir, ".beads", "issues.jsonl")
|
|
|
|
// Create sync branch with base commit
|
|
runGit(t, repoDir, "checkout", "-b", syncBranch)
|
|
writeFile(t, jsonlPath, `{"id":"base"}`)
|
|
runGit(t, repoDir, "add", ".")
|
|
runGit(t, repoDir, "commit", "-m", "base")
|
|
baseCommit := strings.TrimSpace(getGitOutput(t, repoDir, "rev-parse", "HEAD"))
|
|
|
|
// Add another commit and set as remote
|
|
writeFile(t, jsonlPath, `{"id":"base"}`+"\n"+`{"id":"remote"}`)
|
|
runGit(t, repoDir, "add", ".")
|
|
runGit(t, repoDir, "commit", "-m", "remote commit")
|
|
runGit(t, repoDir, "update-ref", "refs/remotes/origin/"+syncBranch, "HEAD")
|
|
|
|
// Reset back to base (so remote is ahead)
|
|
runGit(t, repoDir, "reset", "--hard", baseCommit)
|
|
runGit(t, repoDir, "checkout", "main")
|
|
|
|
// Pull should fast-forward
|
|
result, err := PullFromSyncBranch(ctx, repoDir, syncBranch, jsonlPath, false)
|
|
if err != nil {
|
|
return // Acceptable with self-remote
|
|
}
|
|
|
|
// Just verify result is populated correctly
|
|
_ = result.FastForwarded
|
|
_ = result.Merged
|
|
})
|
|
}
|
|
|
|
// TestResetToRemote tests resetting sync branch to remote state
|
|
// Note: Full remote tests are in cmd/bd tests; this tests the basic flow
|
|
func TestResetToRemote(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
t.Run("returns error when fetch fails", func(t *testing.T) {
|
|
repoDir := setupTestRepoWithRemote(t)
|
|
defer os.RemoveAll(repoDir)
|
|
|
|
syncBranch := "beads-sync"
|
|
jsonlPath := filepath.Join(repoDir, ".beads", "issues.jsonl")
|
|
|
|
// Create local sync branch without remote
|
|
runGit(t, repoDir, "checkout", "-b", syncBranch)
|
|
writeFile(t, jsonlPath, `{"id":"local-1"}`)
|
|
runGit(t, repoDir, "add", ".")
|
|
runGit(t, repoDir, "commit", "-m", "local commit")
|
|
runGit(t, repoDir, "checkout", "main")
|
|
|
|
// ResetToRemote should fail since remote branch doesn't exist
|
|
err := ResetToRemote(ctx, repoDir, syncBranch, jsonlPath)
|
|
if err == nil {
|
|
// If it succeeds without remote, that's also acceptable
|
|
// (the remote is set to self, might not have sync branch)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestPushSyncBranch tests the push function
|
|
// Note: Full push tests require actual remote; this tests basic error handling
|
|
func TestPushSyncBranch(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
t.Run("handles missing worktree gracefully", func(t *testing.T) {
|
|
repoDir := setupTestRepoWithRemote(t)
|
|
defer os.RemoveAll(repoDir)
|
|
|
|
syncBranch := "beads-sync"
|
|
|
|
// Create sync branch
|
|
runGit(t, repoDir, "checkout", "-b", syncBranch)
|
|
writeFile(t, filepath.Join(repoDir, ".beads", "issues.jsonl"), `{"id":"test-1"}`)
|
|
runGit(t, repoDir, "add", ".")
|
|
runGit(t, repoDir, "commit", "-m", "initial")
|
|
runGit(t, repoDir, "checkout", "main")
|
|
|
|
// PushSyncBranch should handle the worktree creation
|
|
err := PushSyncBranch(ctx, repoDir, syncBranch)
|
|
// Will fail because origin doesn't have the branch, but should not panic
|
|
if err != nil {
|
|
// Expected - push will fail since origin doesn't have the branch set up
|
|
if !strings.Contains(err.Error(), "push failed") {
|
|
// Some other error - acceptable in test env
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestRunCmdWithTimeoutMessage tests the timeout message function
|
|
func TestRunCmdWithTimeoutMessage(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
t.Run("runs command and returns output", func(t *testing.T) {
|
|
cmd := exec.CommandContext(ctx, "echo", "hello")
|
|
output, err := runCmdWithTimeoutMessage(ctx, "test message", 5*time.Second, cmd)
|
|
if err != nil {
|
|
t.Fatalf("runCmdWithTimeoutMessage() error = %v", err)
|
|
}
|
|
if !strings.Contains(string(output), "hello") {
|
|
t.Errorf("runCmdWithTimeoutMessage() output = %q, want to contain 'hello'", output)
|
|
}
|
|
})
|
|
|
|
t.Run("returns error for failing command", func(t *testing.T) {
|
|
cmd := exec.CommandContext(ctx, "false") // Always exits with 1
|
|
_, err := runCmdWithTimeoutMessage(ctx, "test message", 5*time.Second, cmd)
|
|
if err == nil {
|
|
t.Error("runCmdWithTimeoutMessage() expected error for failing command")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestPreemptiveFetchAndFastForward tests the pre-emptive fetch function
|
|
func TestPreemptiveFetchAndFastForward(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
t.Run("returns nil when remote branch does not exist", func(t *testing.T) {
|
|
repoDir := setupTestRepoWithRemote(t)
|
|
defer os.RemoveAll(repoDir)
|
|
|
|
// Create sync branch locally but don't push
|
|
runGit(t, repoDir, "checkout", "-b", "beads-sync")
|
|
writeFile(t, filepath.Join(repoDir, ".beads", "issues.jsonl"), `{"id":"test-1"}`)
|
|
runGit(t, repoDir, "add", ".")
|
|
runGit(t, repoDir, "commit", "-m", "initial")
|
|
|
|
err := preemptiveFetchAndFastForward(ctx, repoDir, "beads-sync", "origin")
|
|
if err != nil {
|
|
t.Errorf("preemptiveFetchAndFastForward() error = %v, want nil (not an error when remote doesn't exist)", err)
|
|
}
|
|
})
|
|
|
|
t.Run("no-op when local equals remote", func(t *testing.T) {
|
|
repoDir := setupTestRepoWithRemote(t)
|
|
defer os.RemoveAll(repoDir)
|
|
|
|
syncBranch := "beads-sync"
|
|
|
|
// Create sync branch
|
|
runGit(t, repoDir, "checkout", "-b", syncBranch)
|
|
writeFile(t, filepath.Join(repoDir, ".beads", "issues.jsonl"), `{"id":"test-1"}`)
|
|
runGit(t, repoDir, "add", ".")
|
|
runGit(t, repoDir, "commit", "-m", "initial")
|
|
// Set remote ref at same commit
|
|
runGit(t, repoDir, "update-ref", "refs/remotes/origin/"+syncBranch, "HEAD")
|
|
|
|
err := preemptiveFetchAndFastForward(ctx, repoDir, syncBranch, "origin")
|
|
// Should succeed since we're already in sync
|
|
if err != nil {
|
|
// Might fail on fetch step with self-remote, acceptable
|
|
return
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestFetchAndRebaseInWorktree tests the fetch and rebase function
|
|
func TestFetchAndRebaseInWorktree(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
t.Run("returns error when fetch fails", func(t *testing.T) {
|
|
repoDir := setupTestRepoWithRemote(t)
|
|
defer os.RemoveAll(repoDir)
|
|
|
|
syncBranch := "beads-sync"
|
|
|
|
// Create sync branch locally
|
|
runGit(t, repoDir, "checkout", "-b", syncBranch)
|
|
writeFile(t, filepath.Join(repoDir, ".beads", "issues.jsonl"), `{"id":"test-1"}`)
|
|
runGit(t, repoDir, "add", ".")
|
|
runGit(t, repoDir, "commit", "-m", "initial")
|
|
|
|
// fetchAndRebaseInWorktree should fail since remote doesn't have the branch
|
|
err := fetchAndRebaseInWorktree(ctx, repoDir, syncBranch, "origin")
|
|
if err == nil {
|
|
// If it succeeds, it means the test setup allowed it (self remote)
|
|
return
|
|
}
|
|
// Expected to fail
|
|
if !strings.Contains(err.Error(), "fetch failed") {
|
|
// Some other error - still acceptable
|
|
}
|
|
})
|
|
}
|
|
|
|
// Helper: setup a test repo with a (fake) remote
|
|
func setupTestRepoWithRemote(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
tmpDir, err := os.MkdirTemp("", "bd-test-repo-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
|
|
// Initialize git repo with 'main' as default branch (modern git convention)
|
|
runGit(t, tmpDir, "init", "--initial-branch=main")
|
|
runGit(t, tmpDir, "config", "user.email", "test@test.com")
|
|
runGit(t, tmpDir, "config", "user.name", "Test User")
|
|
|
|
// Create initial commit
|
|
writeFile(t, filepath.Join(tmpDir, "README.md"), "# Test Repo")
|
|
runGit(t, tmpDir, "add", ".")
|
|
runGit(t, tmpDir, "commit", "-m", "initial commit")
|
|
|
|
// Create .beads directory
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0750); err != nil {
|
|
os.RemoveAll(tmpDir)
|
|
t.Fatalf("Failed to create .beads dir: %v", err)
|
|
}
|
|
|
|
// Add a fake remote (just for configuration purposes)
|
|
runGit(t, tmpDir, "remote", "add", "origin", tmpDir)
|
|
|
|
return tmpDir
|
|
}
|
|
|