test: improve coverage for config and git packages

- config: Add tests for GetStringSlice, GetMultiRepoConfig, and nil viper
  behavior. Coverage improved from 65.3% to 84.0%.
- git: Add tests for error paths in RemoveBeadsWorktree, SyncJSONLToWorktree,
  CheckWorktreeHealth, and sparse checkout functions. Coverage improved
  from 72.9% to 82.7%.

Closes: bd-t3b, bd-4h3, bd-ge7

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-28 21:56:42 -08:00
parent 61c61d5957
commit 247672be54
3 changed files with 521 additions and 78 deletions

View File

@@ -231,16 +231,208 @@ func TestAllSettings(t *testing.T) {
if err != nil {
t.Fatalf("Initialize() returned error: %v", err)
}
Set("custom-key", "custom-value")
settings := AllSettings()
if settings == nil {
t.Fatal("AllSettings() returned nil")
}
// Check that our custom key is in the settings
if val, ok := settings["custom-key"]; !ok || val != "custom-value" {
t.Errorf("AllSettings() missing or incorrect custom-key: got %v", val)
}
}
func TestGetStringSlice(t *testing.T) {
err := Initialize()
if err != nil {
t.Fatalf("Initialize() returned error: %v", err)
}
// Test with Set
Set("test-slice", []string{"a", "b", "c"})
got := GetStringSlice("test-slice")
if len(got) != 3 || got[0] != "a" || got[1] != "b" || got[2] != "c" {
t.Errorf("GetStringSlice(test-slice) = %v, want [a b c]", got)
}
// Test with non-existent key - should return empty/nil slice
got = GetStringSlice("nonexistent-key")
if len(got) != 0 {
t.Errorf("GetStringSlice(nonexistent-key) = %v, want empty slice", got)
}
}
func TestGetStringSliceFromConfig(t *testing.T) {
// Create a temporary directory for config file
tmpDir := t.TempDir()
// Create a config file with string slice
configContent := `
repos:
primary: /path/to/primary
additional:
- /path/to/repo1
- /path/to/repo2
`
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(beadsDir, 0750); err != nil {
t.Fatalf("failed to create .beads directory: %v", err)
}
configPath := filepath.Join(beadsDir, "config.yaml")
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
// Change to tmp directory
origDir, err := os.Getwd()
if err != nil {
t.Fatalf("failed to get working directory: %v", err)
}
defer os.Chdir(origDir)
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("failed to change directory: %v", err)
}
// Initialize viper
err = Initialize()
if err != nil {
t.Fatalf("Initialize() returned error: %v", err)
}
// Test that string slice is loaded correctly
got := GetStringSlice("repos.additional")
if len(got) != 2 || got[0] != "/path/to/repo1" || got[1] != "/path/to/repo2" {
t.Errorf("GetStringSlice(repos.additional) = %v, want [/path/to/repo1 /path/to/repo2]", got)
}
}
func TestGetMultiRepoConfig(t *testing.T) {
err := Initialize()
if err != nil {
t.Fatalf("Initialize() returned error: %v", err)
}
// Test when repos.primary is not set (single-repo mode)
config := GetMultiRepoConfig()
if config != nil {
t.Errorf("GetMultiRepoConfig() with no repos.primary = %+v, want nil", config)
}
// Test when repos.primary is set (multi-repo mode)
Set("repos.primary", "/path/to/primary")
Set("repos.additional", []string{"/path/to/repo1", "/path/to/repo2"})
config = GetMultiRepoConfig()
if config == nil {
t.Fatal("GetMultiRepoConfig() returned nil when repos.primary is set")
}
if config.Primary != "/path/to/primary" {
t.Errorf("GetMultiRepoConfig().Primary = %q, want \"/path/to/primary\"", config.Primary)
}
if len(config.Additional) != 2 || config.Additional[0] != "/path/to/repo1" || config.Additional[1] != "/path/to/repo2" {
t.Errorf("GetMultiRepoConfig().Additional = %v, want [/path/to/repo1 /path/to/repo2]", config.Additional)
}
}
func TestGetMultiRepoConfigFromFile(t *testing.T) {
// Create a temporary directory for config file
tmpDir := t.TempDir()
// Create a config file with multi-repo config
configContent := `
repos:
primary: /main/repo
additional:
- /extra/repo1
- /extra/repo2
- /extra/repo3
`
beadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(beadsDir, 0750); err != nil {
t.Fatalf("failed to create .beads directory: %v", err)
}
configPath := filepath.Join(beadsDir, "config.yaml")
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
// Change to tmp directory
origDir, err := os.Getwd()
if err != nil {
t.Fatalf("failed to get working directory: %v", err)
}
defer os.Chdir(origDir)
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("failed to change directory: %v", err)
}
// Initialize viper
err = Initialize()
if err != nil {
t.Fatalf("Initialize() returned error: %v", err)
}
// Test that multi-repo config is loaded correctly
config := GetMultiRepoConfig()
if config == nil {
t.Fatal("GetMultiRepoConfig() returned nil")
}
if config.Primary != "/main/repo" {
t.Errorf("GetMultiRepoConfig().Primary = %q, want \"/main/repo\"", config.Primary)
}
if len(config.Additional) != 3 {
t.Errorf("GetMultiRepoConfig().Additional has %d items, want 3", len(config.Additional))
}
}
func TestNilViperBehavior(t *testing.T) {
// Save the current viper instance
savedV := v
// Set viper to nil to test nil-safety
v = nil
defer func() { v = savedV }()
// All getters should return zero values without panicking
if got := GetString("any-key"); got != "" {
t.Errorf("GetString with nil viper = %q, want \"\"", got)
}
if got := GetBool("any-key"); got != false {
t.Errorf("GetBool with nil viper = %v, want false", got)
}
if got := GetInt("any-key"); got != 0 {
t.Errorf("GetInt with nil viper = %d, want 0", got)
}
if got := GetDuration("any-key"); got != 0 {
t.Errorf("GetDuration with nil viper = %v, want 0", got)
}
if got := GetStringSlice("any-key"); got == nil || len(got) != 0 {
t.Errorf("GetStringSlice with nil viper = %v, want empty slice", got)
}
if got := AllSettings(); got == nil || len(got) != 0 {
t.Errorf("AllSettings with nil viper = %v, want empty map", got)
}
if got := GetMultiRepoConfig(); got != nil {
t.Errorf("GetMultiRepoConfig with nil viper = %+v, want nil", got)
}
// Set should not panic
Set("any-key", "any-value") // Should be a no-op
}

View File

@@ -338,3 +338,254 @@ func TestSparseCheckoutConfiguration(t *testing.T) {
}
})
}
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")
}
})
}