bd sync: 2025-12-23 20:45:19
This commit is contained in:
@@ -200,12 +200,12 @@ func TestUnset(t *testing.T) {
|
||||
t.Run("removes config value", func(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
defer store.Close()
|
||||
|
||||
|
||||
// Set a value first
|
||||
if err := Set(ctx, store, "beads-metadata"); err != nil {
|
||||
t.Fatalf("Set() error = %v", err)
|
||||
}
|
||||
|
||||
|
||||
// Verify it's set
|
||||
value, err := store.GetConfig(ctx, ConfigKey)
|
||||
if err != nil {
|
||||
@@ -214,12 +214,12 @@ func TestUnset(t *testing.T) {
|
||||
if value != "beads-metadata" {
|
||||
t.Errorf("GetConfig() = %q, want %q", value, "beads-metadata")
|
||||
}
|
||||
|
||||
|
||||
// Unset it
|
||||
if err := Unset(ctx, store); err != nil {
|
||||
t.Fatalf("Unset() error = %v", err)
|
||||
}
|
||||
|
||||
|
||||
// Verify it's gone
|
||||
value, err = store.GetConfig(ctx, ConfigKey)
|
||||
if err != nil {
|
||||
@@ -230,152 +230,3 @@ func TestUnset(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetFromYAML(t *testing.T) {
|
||||
// Save and restore any existing env var
|
||||
origEnv := os.Getenv(EnvVar)
|
||||
defer os.Setenv(EnvVar, origEnv)
|
||||
|
||||
t.Run("returns empty when nothing configured", func(t *testing.T) {
|
||||
os.Unsetenv(EnvVar)
|
||||
branch := GetFromYAML()
|
||||
// GetFromYAML checks env var first, then config.yaml
|
||||
// Without env var set, it should return what's in config.yaml (or empty)
|
||||
// We can't easily mock config.yaml here, so just verify no panic
|
||||
_ = branch
|
||||
})
|
||||
|
||||
t.Run("returns env var value when set", func(t *testing.T) {
|
||||
os.Setenv(EnvVar, "env-sync-branch")
|
||||
defer os.Unsetenv(EnvVar)
|
||||
|
||||
branch := GetFromYAML()
|
||||
if branch != "env-sync-branch" {
|
||||
t.Errorf("GetFromYAML() = %q, want %q", branch, "env-sync-branch")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsConfigured(t *testing.T) {
|
||||
// Save and restore any existing env var
|
||||
origEnv := os.Getenv(EnvVar)
|
||||
defer os.Setenv(EnvVar, origEnv)
|
||||
|
||||
t.Run("returns true when env var is set", func(t *testing.T) {
|
||||
os.Setenv(EnvVar, "test-branch")
|
||||
defer os.Unsetenv(EnvVar)
|
||||
|
||||
if !IsConfigured() {
|
||||
t.Error("IsConfigured() = false when env var is set, want true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("behavior with no env var", func(t *testing.T) {
|
||||
os.Unsetenv(EnvVar)
|
||||
// Just verify no panic - actual value depends on config.yaml
|
||||
_ = IsConfigured()
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsConfiguredWithDB(t *testing.T) {
|
||||
// Save and restore any existing env var
|
||||
origEnv := os.Getenv(EnvVar)
|
||||
defer os.Setenv(EnvVar, origEnv)
|
||||
|
||||
t.Run("returns true when env var is set", func(t *testing.T) {
|
||||
os.Setenv(EnvVar, "test-branch")
|
||||
defer os.Unsetenv(EnvVar)
|
||||
|
||||
if !IsConfiguredWithDB("") {
|
||||
t.Error("IsConfiguredWithDB() = false when env var is set, want true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns false for nonexistent database", func(t *testing.T) {
|
||||
os.Unsetenv(EnvVar)
|
||||
|
||||
result := IsConfiguredWithDB("/nonexistent/path/beads.db")
|
||||
// Should return false because db doesn't exist
|
||||
if result {
|
||||
t.Error("IsConfiguredWithDB() = true for nonexistent db, want false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns false for empty path with no db found", func(t *testing.T) {
|
||||
os.Unsetenv(EnvVar)
|
||||
// When empty path is passed and beads.FindDatabasePath() returns empty,
|
||||
// IsConfiguredWithDB should return false
|
||||
// This tests the code path where dbPath is empty
|
||||
tmpDir, _ := os.MkdirTemp("", "test-no-beads-*")
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
origWd, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(origWd)
|
||||
|
||||
result := IsConfiguredWithDB("")
|
||||
// Should return false because no database exists
|
||||
if result {
|
||||
t.Error("IsConfiguredWithDB('') with no db = true, want false")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetConfigFromDB(t *testing.T) {
|
||||
t.Run("returns empty for nonexistent database", func(t *testing.T) {
|
||||
result := getConfigFromDB("/nonexistent/path/beads.db", ConfigKey)
|
||||
if result != "" {
|
||||
t.Errorf("getConfigFromDB() for nonexistent db = %q, want empty", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns empty when key not found", func(t *testing.T) {
|
||||
// Create a temporary database
|
||||
tmpDir, _ := os.MkdirTemp("", "test-beads-db-*")
|
||||
defer os.RemoveAll(tmpDir)
|
||||
dbPath := tmpDir + "/beads.db"
|
||||
|
||||
// Create a valid SQLite database with the config table
|
||||
store, err := sqlite.New(context.Background(), "file:"+dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
store.Close()
|
||||
|
||||
result := getConfigFromDB(dbPath, "nonexistent.key")
|
||||
if result != "" {
|
||||
t.Errorf("getConfigFromDB() for missing key = %q, want empty", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns value when key exists", func(t *testing.T) {
|
||||
// Create a temporary database
|
||||
tmpDir, _ := os.MkdirTemp("", "test-beads-db-*")
|
||||
defer os.RemoveAll(tmpDir)
|
||||
dbPath := tmpDir + "/beads.db"
|
||||
|
||||
// Create a valid SQLite database with the config table
|
||||
ctx := context.Background()
|
||||
// Use the same connection string format as getConfigFromDB expects
|
||||
store, err := sqlite.New(ctx, "file:"+dbPath+"?_journal_mode=DELETE")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
// Set issue_prefix first (required by storage)
|
||||
if err := store.SetConfig(ctx, "issue_prefix", "bd"); err != nil {
|
||||
store.Close()
|
||||
t.Fatalf("Failed to set issue_prefix: %v", err)
|
||||
}
|
||||
// Set the config value we're testing
|
||||
if err := store.SetConfig(ctx, ConfigKey, "test-sync-branch"); err != nil {
|
||||
store.Close()
|
||||
t.Fatalf("Failed to set config: %v", err)
|
||||
}
|
||||
store.Close()
|
||||
|
||||
result := getConfigFromDB(dbPath, ConfigKey)
|
||||
if result != "test-sync-branch" {
|
||||
t.Errorf("getConfigFromDB() = %q, want %q", result, "test-sync-branch")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,716 +0,0 @@
|
||||
package syncbranch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestIsNonFastForwardError tests the non-fast-forward error detection
|
||||
func TestIsNonFastForwardError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
output string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "non-fast-forward message",
|
||||
output: "error: failed to push some refs to 'origin'\n! [rejected] main -> main (non-fast-forward)",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "fetch first message",
|
||||
output: "error: failed to push some refs to 'origin'\nhint: Updates were rejected because the remote contains work that you do\nhint: not have locally. This is usually caused by another repository pushing\nhint: to the same ref. You may want to first integrate the remote changes\nhint: (e.g., 'git pull ...') before pushing again.\nhint: See the 'Note about fast-forwards' in 'git push --help' for details.\nfetch first",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "rejected behind message",
|
||||
output: "To github.com:user/repo.git\n! [rejected] main -> main (non-fast-forward)\nerror: failed to push some refs\nhint: rejected because behind remote",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "normal push success",
|
||||
output: "Everything up-to-date",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "authentication error",
|
||||
output: "fatal: Authentication failed for 'https://github.com/user/repo.git/'",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "permission denied",
|
||||
output: "ERROR: Permission to user/repo.git denied to user.",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty output",
|
||||
output: "",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isNonFastForwardError(tt.output)
|
||||
if got != tt.want {
|
||||
t.Errorf("isNonFastForwardError(%q) = %v, want %v", tt.output, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHasChangesInWorktree tests change detection in worktree
|
||||
func TestHasChangesInWorktree(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("no changes in clean worktree", func(t *testing.T) {
|
||||
repoDir := setupTestRepo(t)
|
||||
defer os.RemoveAll(repoDir)
|
||||
|
||||
// Create initial commit
|
||||
jsonlPath := filepath.Join(repoDir, ".beads", "issues.jsonl")
|
||||
writeFile(t, jsonlPath, `{"id":"test-1"}`)
|
||||
runGit(t, repoDir, "add", ".")
|
||||
runGit(t, repoDir, "commit", "-m", "initial")
|
||||
|
||||
hasChanges, err := hasChangesInWorktree(ctx, repoDir, jsonlPath)
|
||||
if err != nil {
|
||||
t.Fatalf("hasChangesInWorktree() error = %v", err)
|
||||
}
|
||||
if hasChanges {
|
||||
t.Error("hasChangesInWorktree() = true for clean worktree, want false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("detects uncommitted changes", func(t *testing.T) {
|
||||
repoDir := setupTestRepo(t)
|
||||
defer os.RemoveAll(repoDir)
|
||||
|
||||
// Create initial commit
|
||||
jsonlPath := filepath.Join(repoDir, ".beads", "issues.jsonl")
|
||||
writeFile(t, jsonlPath, `{"id":"test-1"}`)
|
||||
runGit(t, repoDir, "add", ".")
|
||||
runGit(t, repoDir, "commit", "-m", "initial")
|
||||
|
||||
// Modify file without committing
|
||||
writeFile(t, jsonlPath, `{"id":"test-1"}`+"\n"+`{"id":"test-2"}`)
|
||||
|
||||
hasChanges, err := hasChangesInWorktree(ctx, repoDir, jsonlPath)
|
||||
if err != nil {
|
||||
t.Fatalf("hasChangesInWorktree() error = %v", err)
|
||||
}
|
||||
if !hasChanges {
|
||||
t.Error("hasChangesInWorktree() = false with uncommitted changes, want true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("detects new untracked files", func(t *testing.T) {
|
||||
repoDir := setupTestRepo(t)
|
||||
defer os.RemoveAll(repoDir)
|
||||
|
||||
// Create initial commit
|
||||
jsonlPath := filepath.Join(repoDir, ".beads", "issues.jsonl")
|
||||
writeFile(t, jsonlPath, `{"id":"test-1"}`)
|
||||
runGit(t, repoDir, "add", ".")
|
||||
runGit(t, repoDir, "commit", "-m", "initial")
|
||||
|
||||
// Add new file in .beads
|
||||
writeFile(t, filepath.Join(repoDir, ".beads", "metadata.json"), `{}`)
|
||||
|
||||
hasChanges, err := hasChangesInWorktree(ctx, repoDir, jsonlPath)
|
||||
if err != nil {
|
||||
t.Fatalf("hasChangesInWorktree() error = %v", err)
|
||||
}
|
||||
if !hasChanges {
|
||||
t.Error("hasChangesInWorktree() = false with new file, want true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("handles file outside .beads dir", func(t *testing.T) {
|
||||
repoDir := setupTestRepo(t)
|
||||
defer os.RemoveAll(repoDir)
|
||||
|
||||
// Create initial commit
|
||||
jsonlPath := filepath.Join(repoDir, "issues.jsonl") // Not in .beads
|
||||
writeFile(t, jsonlPath, `{"id":"test-1"}`)
|
||||
runGit(t, repoDir, "add", ".")
|
||||
runGit(t, repoDir, "commit", "-m", "initial")
|
||||
|
||||
// Modify file
|
||||
writeFile(t, jsonlPath, `{"id":"test-1"}`+"\n"+`{"id":"test-2"}`)
|
||||
|
||||
hasChanges, err := hasChangesInWorktree(ctx, repoDir, jsonlPath)
|
||||
if err != nil {
|
||||
t.Fatalf("hasChangesInWorktree() error = %v", err)
|
||||
}
|
||||
if !hasChanges {
|
||||
t.Error("hasChangesInWorktree() = false with modified file outside .beads, want true")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCommitInWorktree tests committing changes in worktree
|
||||
func TestCommitInWorktree(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("commits staged changes", func(t *testing.T) {
|
||||
repoDir := setupTestRepo(t)
|
||||
defer os.RemoveAll(repoDir)
|
||||
|
||||
// Create initial commit
|
||||
jsonlPath := filepath.Join(repoDir, ".beads", "issues.jsonl")
|
||||
writeFile(t, jsonlPath, `{"id":"test-1"}`)
|
||||
runGit(t, repoDir, "add", ".")
|
||||
runGit(t, repoDir, "commit", "-m", "initial")
|
||||
|
||||
// Modify file
|
||||
writeFile(t, jsonlPath, `{"id":"test-1"}`+"\n"+`{"id":"test-2"}`)
|
||||
|
||||
// Commit using our function
|
||||
err := commitInWorktree(ctx, repoDir, ".beads/issues.jsonl", "test commit message")
|
||||
if err != nil {
|
||||
t.Fatalf("commitInWorktree() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify commit was made
|
||||
output := getGitOutput(t, repoDir, "log", "-1", "--format=%s")
|
||||
if !strings.Contains(output, "test commit message") {
|
||||
t.Errorf("commit message = %q, want to contain 'test commit message'", output)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("commits entire .beads directory", func(t *testing.T) {
|
||||
repoDir := setupTestRepo(t)
|
||||
defer os.RemoveAll(repoDir)
|
||||
|
||||
// Create initial commit
|
||||
jsonlPath := filepath.Join(repoDir, ".beads", "issues.jsonl")
|
||||
writeFile(t, jsonlPath, `{"id":"test-1"}`)
|
||||
runGit(t, repoDir, "add", ".")
|
||||
runGit(t, repoDir, "commit", "-m", "initial")
|
||||
|
||||
// Add multiple files
|
||||
writeFile(t, filepath.Join(repoDir, ".beads", "metadata.json"), `{"version":"1"}`)
|
||||
writeFile(t, jsonlPath, `{"id":"test-1"}`+"\n"+`{"id":"test-2"}`)
|
||||
|
||||
err := commitInWorktree(ctx, repoDir, ".beads/issues.jsonl", "multi-file commit")
|
||||
if err != nil {
|
||||
t.Fatalf("commitInWorktree() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify both files were committed
|
||||
output := getGitOutput(t, repoDir, "diff", "--name-only", "HEAD~1")
|
||||
if !strings.Contains(output, "issues.jsonl") {
|
||||
t.Error("issues.jsonl not in commit")
|
||||
}
|
||||
if !strings.Contains(output, "metadata.json") {
|
||||
t.Error("metadata.json not in commit")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCopyJSONLToMainRepo tests copying JSONL between worktree and main repo
|
||||
func TestCopyJSONLToMainRepo(t *testing.T) {
|
||||
t.Run("copies JSONL file successfully", func(t *testing.T) {
|
||||
// Setup worktree directory
|
||||
worktreeDir, _ := os.MkdirTemp("", "test-worktree-*")
|
||||
defer os.RemoveAll(worktreeDir)
|
||||
|
||||
// Setup main repo directory
|
||||
mainRepoDir, _ := os.MkdirTemp("", "test-mainrepo-*")
|
||||
defer os.RemoveAll(mainRepoDir)
|
||||
|
||||
// Create .beads directories
|
||||
os.MkdirAll(filepath.Join(worktreeDir, ".beads"), 0750)
|
||||
os.MkdirAll(filepath.Join(mainRepoDir, ".beads"), 0750)
|
||||
|
||||
// Write content to worktree JSONL
|
||||
worktreeContent := `{"id":"test-1","title":"Test Issue"}`
|
||||
if err := os.WriteFile(filepath.Join(worktreeDir, ".beads", "issues.jsonl"), []byte(worktreeContent), 0600); err != nil {
|
||||
t.Fatalf("Failed to write worktree JSONL: %v", err)
|
||||
}
|
||||
|
||||
mainJSONLPath := filepath.Join(mainRepoDir, ".beads", "issues.jsonl")
|
||||
|
||||
err := copyJSONLToMainRepo(worktreeDir, ".beads/issues.jsonl", mainJSONLPath)
|
||||
if err != nil {
|
||||
t.Fatalf("copyJSONLToMainRepo() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify content was copied
|
||||
copied, err := os.ReadFile(mainJSONLPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read copied file: %v", err)
|
||||
}
|
||||
if string(copied) != worktreeContent {
|
||||
t.Errorf("copied content = %q, want %q", string(copied), worktreeContent)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns nil when worktree JSONL does not exist", func(t *testing.T) {
|
||||
worktreeDir, _ := os.MkdirTemp("", "test-worktree-*")
|
||||
defer os.RemoveAll(worktreeDir)
|
||||
|
||||
mainRepoDir, _ := os.MkdirTemp("", "test-mainrepo-*")
|
||||
defer os.RemoveAll(mainRepoDir)
|
||||
|
||||
mainJSONLPath := filepath.Join(mainRepoDir, ".beads", "issues.jsonl")
|
||||
|
||||
err := copyJSONLToMainRepo(worktreeDir, ".beads/issues.jsonl", mainJSONLPath)
|
||||
if err != nil {
|
||||
t.Errorf("copyJSONLToMainRepo() for nonexistent file = %v, want nil", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("also copies metadata.json if present", func(t *testing.T) {
|
||||
worktreeDir, _ := os.MkdirTemp("", "test-worktree-*")
|
||||
defer os.RemoveAll(worktreeDir)
|
||||
|
||||
mainRepoDir, _ := os.MkdirTemp("", "test-mainrepo-*")
|
||||
defer os.RemoveAll(mainRepoDir)
|
||||
|
||||
// Create .beads directories
|
||||
os.MkdirAll(filepath.Join(worktreeDir, ".beads"), 0750)
|
||||
os.MkdirAll(filepath.Join(mainRepoDir, ".beads"), 0750)
|
||||
|
||||
// Write JSONL and metadata to worktree
|
||||
if err := os.WriteFile(filepath.Join(worktreeDir, ".beads", "issues.jsonl"), []byte(`{}`), 0600); err != nil {
|
||||
t.Fatalf("Failed to write worktree JSONL: %v", err)
|
||||
}
|
||||
metadataContent := `{"prefix":"bd"}`
|
||||
if err := os.WriteFile(filepath.Join(worktreeDir, ".beads", "metadata.json"), []byte(metadataContent), 0600); err != nil {
|
||||
t.Fatalf("Failed to write metadata: %v", err)
|
||||
}
|
||||
|
||||
mainJSONLPath := filepath.Join(mainRepoDir, ".beads", "issues.jsonl")
|
||||
|
||||
err := copyJSONLToMainRepo(worktreeDir, ".beads/issues.jsonl", mainJSONLPath)
|
||||
if err != nil {
|
||||
t.Fatalf("copyJSONLToMainRepo() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify metadata was also copied
|
||||
metadata, err := os.ReadFile(filepath.Join(mainRepoDir, ".beads", "metadata.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read metadata: %v", err)
|
||||
}
|
||||
if string(metadata) != metadataContent {
|
||||
t.Errorf("metadata content = %q, want %q", string(metadata), metadataContent)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestGetRemoteForBranch tests remote detection for branches
|
||||
func TestGetRemoteForBranch(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("returns origin as default", func(t *testing.T) {
|
||||
repoDir := setupTestRepo(t)
|
||||
defer os.RemoveAll(repoDir)
|
||||
|
||||
// Create initial commit
|
||||
writeFile(t, filepath.Join(repoDir, ".beads", "issues.jsonl"), `{"id":"test-1"}`)
|
||||
runGit(t, repoDir, "add", ".")
|
||||
runGit(t, repoDir, "commit", "-m", "initial")
|
||||
|
||||
remote := getRemoteForBranch(ctx, repoDir, "nonexistent-branch")
|
||||
if remote != "origin" {
|
||||
t.Errorf("getRemoteForBranch() = %q, want 'origin'", remote)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns configured remote", func(t *testing.T) {
|
||||
repoDir := setupTestRepo(t)
|
||||
defer os.RemoveAll(repoDir)
|
||||
|
||||
// Create initial commit
|
||||
writeFile(t, filepath.Join(repoDir, ".beads", "issues.jsonl"), `{"id":"test-1"}`)
|
||||
runGit(t, repoDir, "add", ".")
|
||||
runGit(t, repoDir, "commit", "-m", "initial")
|
||||
|
||||
// Configure a custom remote for a branch
|
||||
runGit(t, repoDir, "config", "branch.test-branch.remote", "upstream")
|
||||
|
||||
remote := getRemoteForBranch(ctx, repoDir, "test-branch")
|
||||
if remote != "upstream" {
|
||||
t.Errorf("getRemoteForBranch() = %q, want 'upstream'", remote)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestGetRepoRoot tests repository root detection
|
||||
func TestGetRepoRoot(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("returns repo root for regular repository", func(t *testing.T) {
|
||||
repoDir := setupTestRepo(t)
|
||||
defer os.RemoveAll(repoDir)
|
||||
|
||||
// Create initial commit
|
||||
writeFile(t, filepath.Join(repoDir, ".beads", "issues.jsonl"), `{"id":"test-1"}`)
|
||||
runGit(t, repoDir, "add", ".")
|
||||
runGit(t, repoDir, "commit", "-m", "initial")
|
||||
|
||||
// Change to repo directory
|
||||
origWd, _ := os.Getwd()
|
||||
os.Chdir(repoDir)
|
||||
defer os.Chdir(origWd)
|
||||
|
||||
root, err := GetRepoRoot(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRepoRoot() error = %v", err)
|
||||
}
|
||||
|
||||
// Resolve symlinks for comparison
|
||||
expectedRoot, _ := filepath.EvalSymlinks(repoDir)
|
||||
actualRoot, _ := filepath.EvalSymlinks(root)
|
||||
|
||||
if actualRoot != expectedRoot {
|
||||
t.Errorf("GetRepoRoot() = %q, want %q", actualRoot, expectedRoot)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns error for non-git directory", func(t *testing.T) {
|
||||
tmpDir, _ := os.MkdirTemp("", "non-git-*")
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
origWd, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(origWd)
|
||||
|
||||
_, err := GetRepoRoot(ctx)
|
||||
if err == nil {
|
||||
t.Error("GetRepoRoot() expected error for non-git directory")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns repo root from subdirectory", func(t *testing.T) {
|
||||
repoDir := setupTestRepo(t)
|
||||
defer os.RemoveAll(repoDir)
|
||||
|
||||
// Create initial commit
|
||||
writeFile(t, filepath.Join(repoDir, ".beads", "issues.jsonl"), `{"id":"test-1"}`)
|
||||
runGit(t, repoDir, "add", ".")
|
||||
runGit(t, repoDir, "commit", "-m", "initial")
|
||||
|
||||
// Create and change to subdirectory
|
||||
subDir := filepath.Join(repoDir, "subdir", "nested")
|
||||
os.MkdirAll(subDir, 0750)
|
||||
|
||||
origWd, _ := os.Getwd()
|
||||
os.Chdir(subDir)
|
||||
defer os.Chdir(origWd)
|
||||
|
||||
root, err := GetRepoRoot(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRepoRoot() error = %v", err)
|
||||
}
|
||||
|
||||
// Resolve symlinks for comparison
|
||||
expectedRoot, _ := filepath.EvalSymlinks(repoDir)
|
||||
actualRoot, _ := filepath.EvalSymlinks(root)
|
||||
|
||||
if actualRoot != expectedRoot {
|
||||
t.Errorf("GetRepoRoot() from subdirectory = %q, want %q", actualRoot, expectedRoot)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("handles worktree correctly", func(t *testing.T) {
|
||||
// Create main repo
|
||||
mainRepoDir := setupTestRepo(t)
|
||||
defer os.RemoveAll(mainRepoDir)
|
||||
|
||||
// Create initial commit
|
||||
writeFile(t, filepath.Join(mainRepoDir, ".beads", "issues.jsonl"), `{"id":"test-1"}`)
|
||||
runGit(t, mainRepoDir, "add", ".")
|
||||
runGit(t, mainRepoDir, "commit", "-m", "initial")
|
||||
|
||||
// Create a worktree
|
||||
worktreeDir, _ := os.MkdirTemp("", "test-worktree-*")
|
||||
defer os.RemoveAll(worktreeDir)
|
||||
runGit(t, mainRepoDir, "worktree", "add", worktreeDir, "-b", "feature")
|
||||
|
||||
// Test from worktree - should return main repo root
|
||||
origWd, _ := os.Getwd()
|
||||
os.Chdir(worktreeDir)
|
||||
defer os.Chdir(origWd)
|
||||
|
||||
root, err := GetRepoRoot(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRepoRoot() from worktree error = %v", err)
|
||||
}
|
||||
|
||||
// Should return the main repo root, not the worktree
|
||||
expectedRoot, _ := filepath.EvalSymlinks(mainRepoDir)
|
||||
actualRoot, _ := filepath.EvalSymlinks(root)
|
||||
|
||||
if actualRoot != expectedRoot {
|
||||
t.Errorf("GetRepoRoot() from worktree = %q, want main repo %q", actualRoot, expectedRoot)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestHasGitRemote tests remote detection
|
||||
func TestHasGitRemote(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("returns false for repo without remote", func(t *testing.T) {
|
||||
repoDir := setupTestRepo(t)
|
||||
defer os.RemoveAll(repoDir)
|
||||
|
||||
// Create initial commit
|
||||
writeFile(t, filepath.Join(repoDir, ".beads", "issues.jsonl"), `{"id":"test-1"}`)
|
||||
runGit(t, repoDir, "add", ".")
|
||||
runGit(t, repoDir, "commit", "-m", "initial")
|
||||
|
||||
origWd, _ := os.Getwd()
|
||||
os.Chdir(repoDir)
|
||||
defer os.Chdir(origWd)
|
||||
|
||||
if HasGitRemote(ctx) {
|
||||
t.Error("HasGitRemote() = true for repo without remote, want false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns true for repo with remote", func(t *testing.T) {
|
||||
repoDir := setupTestRepo(t)
|
||||
defer os.RemoveAll(repoDir)
|
||||
|
||||
// Create initial commit
|
||||
writeFile(t, filepath.Join(repoDir, ".beads", "issues.jsonl"), `{"id":"test-1"}`)
|
||||
runGit(t, repoDir, "add", ".")
|
||||
runGit(t, repoDir, "commit", "-m", "initial")
|
||||
|
||||
// Add a remote
|
||||
runGit(t, repoDir, "remote", "add", "origin", "https://github.com/test/repo.git")
|
||||
|
||||
origWd, _ := os.Getwd()
|
||||
os.Chdir(repoDir)
|
||||
defer os.Chdir(origWd)
|
||||
|
||||
if !HasGitRemote(ctx) {
|
||||
t.Error("HasGitRemote() = false for repo with remote, want true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns false for non-git directory", func(t *testing.T) {
|
||||
tmpDir, _ := os.MkdirTemp("", "non-git-*")
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
origWd, _ := os.Getwd()
|
||||
os.Chdir(tmpDir)
|
||||
defer os.Chdir(origWd)
|
||||
|
||||
if HasGitRemote(ctx) {
|
||||
t.Error("HasGitRemote() = true for non-git directory, want false")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestGetCurrentBranch tests current branch detection
|
||||
func TestGetCurrentBranch(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("returns current branch name", func(t *testing.T) {
|
||||
repoDir := setupTestRepo(t)
|
||||
defer os.RemoveAll(repoDir)
|
||||
|
||||
// Create initial commit
|
||||
writeFile(t, filepath.Join(repoDir, ".beads", "issues.jsonl"), `{"id":"test-1"}`)
|
||||
runGit(t, repoDir, "add", ".")
|
||||
runGit(t, repoDir, "commit", "-m", "initial")
|
||||
|
||||
origWd, _ := os.Getwd()
|
||||
os.Chdir(repoDir)
|
||||
defer os.Chdir(origWd)
|
||||
|
||||
branch, err := GetCurrentBranch(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetCurrentBranch() error = %v", err)
|
||||
}
|
||||
|
||||
// The default branch is usually "master" or "main" depending on git config
|
||||
if branch != "master" && branch != "main" {
|
||||
// Could also be a user-defined default, just verify it's not empty
|
||||
if branch == "" {
|
||||
t.Error("GetCurrentBranch() returned empty string")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns correct branch after checkout", func(t *testing.T) {
|
||||
repoDir := setupTestRepo(t)
|
||||
defer os.RemoveAll(repoDir)
|
||||
|
||||
// Create initial commit
|
||||
writeFile(t, filepath.Join(repoDir, ".beads", "issues.jsonl"), `{"id":"test-1"}`)
|
||||
runGit(t, repoDir, "add", ".")
|
||||
runGit(t, repoDir, "commit", "-m", "initial")
|
||||
|
||||
// Create and checkout new branch
|
||||
runGit(t, repoDir, "checkout", "-b", "feature-branch")
|
||||
|
||||
origWd, _ := os.Getwd()
|
||||
os.Chdir(repoDir)
|
||||
defer os.Chdir(origWd)
|
||||
|
||||
branch, err := GetCurrentBranch(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetCurrentBranch() error = %v", err)
|
||||
}
|
||||
|
||||
if branch != "feature-branch" {
|
||||
t.Errorf("GetCurrentBranch() = %q, want 'feature-branch'", branch)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestFormatVanishedIssues tests the forensic logging formatter
|
||||
func TestFormatVanishedIssues(t *testing.T) {
|
||||
t.Run("formats vanished issues correctly", func(t *testing.T) {
|
||||
localIssues := map[string]issueSummary{
|
||||
"bd-1": {ID: "bd-1", Title: "First Issue"},
|
||||
"bd-2": {ID: "bd-2", Title: "Second Issue"},
|
||||
"bd-3": {ID: "bd-3", Title: "Third Issue"},
|
||||
}
|
||||
mergedIssues := map[string]issueSummary{
|
||||
"bd-1": {ID: "bd-1", Title: "First Issue"},
|
||||
}
|
||||
|
||||
lines := formatVanishedIssues(localIssues, mergedIssues, 3, 1)
|
||||
|
||||
// Should contain header
|
||||
found := false
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "Mass deletion forensic log") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("formatVanishedIssues() missing header")
|
||||
}
|
||||
|
||||
// Should list vanished issues
|
||||
foundBd2 := false
|
||||
foundBd3 := false
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "bd-2") {
|
||||
foundBd2 = true
|
||||
}
|
||||
if strings.Contains(line, "bd-3") {
|
||||
foundBd3 = true
|
||||
}
|
||||
}
|
||||
if !foundBd2 || !foundBd3 {
|
||||
t.Errorf("formatVanishedIssues() missing vanished issues: bd-2=%v, bd-3=%v", foundBd2, foundBd3)
|
||||
}
|
||||
|
||||
// Should show totals
|
||||
foundTotal := false
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "Total vanished: 2") {
|
||||
foundTotal = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundTotal {
|
||||
t.Error("formatVanishedIssues() missing total count")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("truncates long titles", func(t *testing.T) {
|
||||
longTitle := strings.Repeat("A", 100)
|
||||
localIssues := map[string]issueSummary{
|
||||
"bd-1": {ID: "bd-1", Title: longTitle},
|
||||
}
|
||||
mergedIssues := map[string]issueSummary{}
|
||||
|
||||
lines := formatVanishedIssues(localIssues, mergedIssues, 1, 0)
|
||||
|
||||
// Find the line with bd-1 and check title is truncated
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "bd-1") {
|
||||
if len(line) > 80 { // Line should be reasonably short
|
||||
// Verify it ends with "..."
|
||||
if !strings.Contains(line, "...") {
|
||||
t.Error("formatVanishedIssues() should truncate long titles with '...'")
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCheckDivergence tests the public CheckDivergence function
|
||||
func TestCheckDivergence(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("returns no divergence when remote does not exist", func(t *testing.T) {
|
||||
repoDir := setupTestRepo(t)
|
||||
defer os.RemoveAll(repoDir)
|
||||
|
||||
// Create initial commit
|
||||
writeFile(t, filepath.Join(repoDir, ".beads", "issues.jsonl"), `{"id":"test-1"}`)
|
||||
runGit(t, repoDir, "add", ".")
|
||||
runGit(t, repoDir, "commit", "-m", "initial")
|
||||
|
||||
// Add remote but don't create the branch on it
|
||||
runGit(t, repoDir, "remote", "add", "origin", repoDir) // Use self as remote
|
||||
|
||||
info, err := CheckDivergence(ctx, repoDir, "beads-sync")
|
||||
if err != nil {
|
||||
// Expected to fail since remote branch doesn't exist
|
||||
return
|
||||
}
|
||||
|
||||
// If it succeeds, verify no divergence
|
||||
if info.IsDiverged {
|
||||
t.Error("CheckDivergence() should not report divergence when remote doesn't exist")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// helper to run git with error handling (already exists but needed for this file)
|
||||
func runGitHelper(t *testing.T, dir string, args ...string) {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = dir
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("git %v failed: %v\n%s", args, err, output)
|
||||
}
|
||||
}
|
||||
@@ -1,416 +0,0 @@
|
||||
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", "master")
|
||||
|
||||
// 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", "master")
|
||||
|
||||
// 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", "master")
|
||||
|
||||
// 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", "master")
|
||||
|
||||
// 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", "master")
|
||||
|
||||
// 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", "master")
|
||||
|
||||
// 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", "master")
|
||||
|
||||
// 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", "master")
|
||||
|
||||
// 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
|
||||
runGit(t, tmpDir, "init")
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user