package config import ( "os" "path/filepath" "strings" "testing" ) func TestIsYamlOnlyKey(t *testing.T) { tests := []struct { key string expected bool }{ // Exact matches {"no-db", true}, {"no-daemon", true}, {"no-auto-flush", true}, {"json", true}, {"auto-start-daemon", true}, {"flush-debounce", true}, {"git.author", true}, {"git.no-gpg-sign", true}, // Prefix matches {"routing.mode", true}, {"routing.custom-key", true}, {"sync.branch", true}, {"sync.require_confirmation_on_mass_delete", true}, {"directory.labels", true}, {"repos.primary", true}, {"external_projects.beads", true}, // SQLite keys (should return false) {"jira.url", false}, {"jira.project", false}, {"linear.api_key", false}, {"github.org", false}, {"custom.setting", false}, {"status.custom", false}, {"issue_prefix", false}, } for _, tt := range tests { t.Run(tt.key, func(t *testing.T) { got := IsYamlOnlyKey(tt.key) if got != tt.expected { t.Errorf("IsYamlOnlyKey(%q) = %v, want %v", tt.key, got, tt.expected) } }) } } func TestUpdateYamlKey(t *testing.T) { tests := []struct { name string content string key string value string expected string }{ { name: "update commented key", content: "# no-db: false\nother: value", key: "no-db", value: "true", expected: "no-db: true\nother: value", }, { name: "update existing key", content: "no-db: false\nother: value", key: "no-db", value: "true", expected: "no-db: true\nother: value", }, { name: "add new key", content: "other: value", key: "no-db", value: "true", expected: "other: value\n\nno-db: true", }, { name: "preserve indentation", content: " # no-db: false\nother: value", key: "no-db", value: "true", expected: " no-db: true\nother: value", }, { name: "handle string value", content: "# actor: \"\"\nother: value", key: "actor", value: "steve", expected: "actor: \"steve\"\nother: value", }, { name: "handle duration value", content: "# flush-debounce: \"5s\"", key: "flush-debounce", value: "30s", expected: "flush-debounce: 30s", }, { name: "quote special characters", content: "other: value", key: "actor", value: "user: name", expected: "other: value\n\nactor: \"user: name\"", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := updateYamlKey(tt.content, tt.key, tt.value) if err != nil { t.Fatalf("updateYamlKey() error = %v", err) } if got != tt.expected { t.Errorf("updateYamlKey() =\n%q\nwant:\n%q", got, tt.expected) } }) } } func TestFormatYamlValue(t *testing.T) { tests := []struct { value string expected string }{ {"true", "true"}, {"false", "false"}, {"TRUE", "true"}, {"FALSE", "false"}, {"123", "123"}, {"3.14", "3.14"}, {"30s", "30s"}, {"5m", "5m"}, {"simple", "\"simple\""}, {"has space", "\"has space\""}, {"has:colon", "\"has:colon\""}, {"has#hash", "\"has#hash\""}, {" leading", "\" leading\""}, } for _, tt := range tests { t.Run(tt.value, func(t *testing.T) { got := formatYamlValue(tt.value) if got != tt.expected { t.Errorf("formatYamlValue(%q) = %q, want %q", tt.value, got, tt.expected) } }) } } func TestNormalizeYamlKey(t *testing.T) { tests := []struct { input string expected string }{ {"sync.branch", "sync-branch"}, // alias should be normalized {"sync-branch", "sync-branch"}, // already canonical {"no-db", "no-db"}, // no alias, unchanged {"json", "json"}, // no alias, unchanged {"routing.mode", "routing.mode"}, // no alias for this one } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { got := normalizeYamlKey(tt.input) if got != tt.expected { t.Errorf("normalizeYamlKey(%q) = %q, want %q", tt.input, got, tt.expected) } }) } } func TestSetYamlConfig_KeyNormalization(t *testing.T) { // Create a temp directory with .beads/config.yaml tmpDir, err := os.MkdirTemp("", "beads-yaml-key-norm-*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) beadsDir := filepath.Join(tmpDir, ".beads") if err := os.MkdirAll(beadsDir, 0755); err != nil { t.Fatalf("Failed to create .beads dir: %v", err) } configPath := filepath.Join(beadsDir, "config.yaml") initialConfig := `# Beads Config sync-branch: old-value ` if err := os.WriteFile(configPath, []byte(initialConfig), 0644); err != nil { t.Fatalf("Failed to write config.yaml: %v", err) } // Change to temp directory for the test oldWd, _ := os.Getwd() if err := os.Chdir(tmpDir); err != nil { t.Fatalf("Failed to chdir: %v", err) } defer os.Chdir(oldWd) // Test SetYamlConfig with aliased key (sync.branch should write as sync-branch) if err := SetYamlConfig("sync.branch", "new-value"); err != nil { t.Fatalf("SetYamlConfig() error = %v", err) } // Read back and verify content, err := os.ReadFile(configPath) if err != nil { t.Fatalf("Failed to read config.yaml: %v", err) } contentStr := string(content) // Should update the existing sync-branch line, not add sync.branch if !strings.Contains(contentStr, "sync-branch: \"new-value\"") { t.Errorf("config.yaml should contain 'sync-branch: \"new-value\"', got:\n%s", contentStr) } if strings.Contains(contentStr, "sync.branch") { t.Errorf("config.yaml should NOT contain 'sync.branch' (should be normalized to sync-branch), got:\n%s", contentStr) } } func TestSetYamlConfig(t *testing.T) { // Create a temp directory with .beads/config.yaml tmpDir, err := os.MkdirTemp("", "beads-yaml-test-*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) beadsDir := filepath.Join(tmpDir, ".beads") if err := os.MkdirAll(beadsDir, 0755); err != nil { t.Fatalf("Failed to create .beads dir: %v", err) } configPath := filepath.Join(beadsDir, "config.yaml") initialConfig := `# Beads Config # no-db: false other-setting: value ` if err := os.WriteFile(configPath, []byte(initialConfig), 0644); err != nil { t.Fatalf("Failed to write config.yaml: %v", err) } // Change to temp directory for the test oldWd, _ := os.Getwd() if err := os.Chdir(tmpDir); err != nil { t.Fatalf("Failed to chdir: %v", err) } defer os.Chdir(oldWd) // Test SetYamlConfig if err := SetYamlConfig("no-db", "true"); err != nil { t.Fatalf("SetYamlConfig() error = %v", err) } // Read back and verify content, err := os.ReadFile(configPath) if err != nil { t.Fatalf("Failed to read config.yaml: %v", err) } contentStr := string(content) if !strings.Contains(contentStr, "no-db: true") { t.Errorf("config.yaml should contain 'no-db: true', got:\n%s", contentStr) } if strings.Contains(contentStr, "# no-db") { t.Errorf("config.yaml should not have commented no-db, got:\n%s", contentStr) } if !strings.Contains(contentStr, "other-setting: value") { t.Errorf("config.yaml should preserve other settings, got:\n%s", contentStr) } }