- bd repo add/remove now writes to .beads/config.yaml instead of database - bd repo remove deletes hydrated issues from the removed repo - Added internal/config/repos.go for YAML config manipulation - Added DeleteIssuesBySourceRepo for cleanup on remove Fixes config disconnect where bd repo add wrote to DB but hydration read from YAML. Breaking change: bd repo add no longer accepts optional alias argument. Co-authored-by: Dylan Conlin <dylan.conlin@gmail.com> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
287 lines
7.3 KiB
Go
287 lines
7.3 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestGetReposFromYAML_Empty(t *testing.T) {
|
|
// Create temp dir with empty config.yaml
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
if err := os.WriteFile(configPath, []byte("# empty config\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
repos, err := GetReposFromYAML(configPath)
|
|
if err != nil {
|
|
t.Fatalf("GetReposFromYAML failed: %v", err)
|
|
}
|
|
|
|
if repos.Primary != "" {
|
|
t.Errorf("expected empty primary, got %q", repos.Primary)
|
|
}
|
|
if len(repos.Additional) != 0 {
|
|
t.Errorf("expected empty additional, got %v", repos.Additional)
|
|
}
|
|
}
|
|
|
|
func TestGetReposFromYAML_WithRepos(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
config := `repos:
|
|
primary: "."
|
|
additional:
|
|
- ~/beads-planning
|
|
- /path/to/other
|
|
`
|
|
if err := os.WriteFile(configPath, []byte(config), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
repos, err := GetReposFromYAML(configPath)
|
|
if err != nil {
|
|
t.Fatalf("GetReposFromYAML failed: %v", err)
|
|
}
|
|
|
|
if repos.Primary != "." {
|
|
t.Errorf("expected primary='.', got %q", repos.Primary)
|
|
}
|
|
if len(repos.Additional) != 2 {
|
|
t.Fatalf("expected 2 additional repos, got %d", len(repos.Additional))
|
|
}
|
|
if repos.Additional[0] != "~/beads-planning" {
|
|
t.Errorf("expected first additional='~/beads-planning', got %q", repos.Additional[0])
|
|
}
|
|
if repos.Additional[1] != "/path/to/other" {
|
|
t.Errorf("expected second additional='/path/to/other', got %q", repos.Additional[1])
|
|
}
|
|
}
|
|
|
|
func TestSetReposInYAML_NewFile(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
|
|
repos := &ReposConfig{
|
|
Primary: ".",
|
|
Additional: []string{"~/test-repo"},
|
|
}
|
|
|
|
if err := SetReposInYAML(configPath, repos); err != nil {
|
|
t.Fatalf("SetReposInYAML failed: %v", err)
|
|
}
|
|
|
|
// Verify by reading back
|
|
readRepos, err := GetReposFromYAML(configPath)
|
|
if err != nil {
|
|
t.Fatalf("GetReposFromYAML failed: %v", err)
|
|
}
|
|
|
|
if readRepos.Primary != "." {
|
|
t.Errorf("expected primary='.', got %q", readRepos.Primary)
|
|
}
|
|
if len(readRepos.Additional) != 1 || readRepos.Additional[0] != "~/test-repo" {
|
|
t.Errorf("expected additional=['~/test-repo'], got %v", readRepos.Additional)
|
|
}
|
|
}
|
|
|
|
func TestSetReposInYAML_PreservesOtherConfig(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
|
|
// Write initial config with other settings
|
|
initial := `issue-prefix: "test"
|
|
sync-branch: "beads-sync"
|
|
json: false
|
|
`
|
|
if err := os.WriteFile(configPath, []byte(initial), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Add repos
|
|
repos := &ReposConfig{
|
|
Primary: ".",
|
|
Additional: []string{"~/test-repo"},
|
|
}
|
|
if err := SetReposInYAML(configPath, repos); err != nil {
|
|
t.Fatalf("SetReposInYAML failed: %v", err)
|
|
}
|
|
|
|
// Verify content still has other settings
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
content := string(data)
|
|
|
|
// Check that original settings are preserved
|
|
if !contains(content, "issue-prefix") {
|
|
t.Error("issue-prefix setting was lost")
|
|
}
|
|
if !contains(content, "sync-branch") {
|
|
t.Error("sync-branch setting was lost")
|
|
}
|
|
if !contains(content, "json") {
|
|
t.Error("json setting was lost")
|
|
}
|
|
|
|
// Check that repos section was added
|
|
if !contains(content, "repos:") {
|
|
t.Error("repos section not found")
|
|
}
|
|
}
|
|
|
|
func TestAddRepo(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
if err := os.WriteFile(configPath, []byte("# config\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Add first repo
|
|
if err := AddRepo(configPath, "~/first-repo"); err != nil {
|
|
t.Fatalf("AddRepo failed: %v", err)
|
|
}
|
|
|
|
repos, err := GetReposFromYAML(configPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if repos.Primary != "." {
|
|
t.Errorf("expected primary='.', got %q", repos.Primary)
|
|
}
|
|
if len(repos.Additional) != 1 || repos.Additional[0] != "~/first-repo" {
|
|
t.Errorf("unexpected additional: %v", repos.Additional)
|
|
}
|
|
|
|
// Add second repo
|
|
if err := AddRepo(configPath, "/path/to/second"); err != nil {
|
|
t.Fatalf("AddRepo failed: %v", err)
|
|
}
|
|
|
|
repos, err = GetReposFromYAML(configPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(repos.Additional) != 2 {
|
|
t.Fatalf("expected 2 additional repos, got %d", len(repos.Additional))
|
|
}
|
|
}
|
|
|
|
func TestAddRepo_Duplicate(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
if err := os.WriteFile(configPath, []byte("# config\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Add repo
|
|
if err := AddRepo(configPath, "~/test-repo"); err != nil {
|
|
t.Fatalf("AddRepo failed: %v", err)
|
|
}
|
|
|
|
// Try to add same repo again - should fail
|
|
err := AddRepo(configPath, "~/test-repo")
|
|
if err == nil {
|
|
t.Error("expected error for duplicate repo, got nil")
|
|
}
|
|
}
|
|
|
|
func TestRemoveRepo(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
config := `repos:
|
|
primary: "."
|
|
additional:
|
|
- ~/first
|
|
- ~/second
|
|
`
|
|
if err := os.WriteFile(configPath, []byte(config), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Remove first repo
|
|
if err := RemoveRepo(configPath, "~/first"); err != nil {
|
|
t.Fatalf("RemoveRepo failed: %v", err)
|
|
}
|
|
|
|
repos, err := GetReposFromYAML(configPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(repos.Additional) != 1 || repos.Additional[0] != "~/second" {
|
|
t.Errorf("unexpected additional after remove: %v", repos.Additional)
|
|
}
|
|
|
|
// Remove last repo - should clear primary too
|
|
if err := RemoveRepo(configPath, "~/second"); err != nil {
|
|
t.Fatalf("RemoveRepo failed: %v", err)
|
|
}
|
|
|
|
repos, err = GetReposFromYAML(configPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if repos.Primary != "" {
|
|
t.Errorf("expected empty primary after removing all repos, got %q", repos.Primary)
|
|
}
|
|
if len(repos.Additional) != 0 {
|
|
t.Errorf("expected empty additional after removing all repos, got %v", repos.Additional)
|
|
}
|
|
}
|
|
|
|
func TestRemoveRepo_NotFound(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yaml")
|
|
if err := os.WriteFile(configPath, []byte("# config\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err := RemoveRepo(configPath, "~/nonexistent")
|
|
if err == nil {
|
|
t.Error("expected error for nonexistent repo, got nil")
|
|
}
|
|
}
|
|
|
|
func TestFindConfigYAMLPath(t *testing.T) {
|
|
// Create temp dir with .beads/config.yaml
|
|
tmpDir := t.TempDir()
|
|
beadsDir := filepath.Join(tmpDir, ".beads")
|
|
if err := os.MkdirAll(beadsDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
configPath := filepath.Join(beadsDir, "config.yaml")
|
|
if err := os.WriteFile(configPath, []byte("# config\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Change to the temp dir
|
|
oldWd, _ := os.Getwd()
|
|
defer func() {
|
|
if err := os.Chdir(oldWd); err != nil {
|
|
t.Logf("warning: failed to restore working directory: %v", err)
|
|
}
|
|
}()
|
|
if err := os.Chdir(tmpDir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
found, err := FindConfigYAMLPath()
|
|
if err != nil {
|
|
t.Fatalf("FindConfigYAMLPath failed: %v", err)
|
|
}
|
|
|
|
// Verify path ends with .beads/config.yaml
|
|
if filepath.Base(found) != "config.yaml" {
|
|
t.Errorf("expected path ending with config.yaml, got %s", found)
|
|
}
|
|
if filepath.Base(filepath.Dir(found)) != ".beads" {
|
|
t.Errorf("expected path in .beads dir, got %s", found)
|
|
}
|
|
}
|
|
|
|
func contains(s, substr string) bool {
|
|
return len(s) >= len(substr) && (s == substr || len(s) > 0 && (s[0:len(substr)] == substr || contains(s[1:], substr)))
|
|
}
|