fix(config): normalize yaml config keys to canonical format (#732)
fix(config): normalize yaml config keys to canonical format Fixes sync.branch vs sync-branch config key mismatch.
This commit is contained in:
@@ -74,14 +74,34 @@ func IsYamlOnlyKey(key string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keyAliases maps alternative key names to their canonical yaml form.
|
||||||
|
// This ensures consistency when users use different formats (dot vs hyphen).
|
||||||
|
var keyAliases = map[string]string{
|
||||||
|
"sync.branch": "sync-branch",
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeYamlKey converts a key to its canonical yaml format.
|
||||||
|
// Some keys have aliases (e.g., sync.branch -> sync-branch) to handle
|
||||||
|
// different input formats consistently.
|
||||||
|
func normalizeYamlKey(key string) string {
|
||||||
|
if canonical, ok := keyAliases[key]; ok {
|
||||||
|
return canonical
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
// SetYamlConfig sets a configuration value in the project's config.yaml file.
|
// SetYamlConfig sets a configuration value in the project's config.yaml file.
|
||||||
// It handles both adding new keys and updating existing (possibly commented) keys.
|
// It handles both adding new keys and updating existing (possibly commented) keys.
|
||||||
|
// Keys are normalized to their canonical yaml format (e.g., sync.branch -> sync-branch).
|
||||||
func SetYamlConfig(key, value string) error {
|
func SetYamlConfig(key, value string) error {
|
||||||
configPath, err := findProjectConfigYaml()
|
configPath, err := findProjectConfigYaml()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize key to canonical yaml format
|
||||||
|
normalizedKey := normalizeYamlKey(key)
|
||||||
|
|
||||||
// Read existing config
|
// Read existing config
|
||||||
content, err := os.ReadFile(configPath) //nolint:gosec // configPath is from findProjectConfigYaml
|
content, err := os.ReadFile(configPath) //nolint:gosec // configPath is from findProjectConfigYaml
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -89,7 +109,7 @@ func SetYamlConfig(key, value string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update or add the key
|
// Update or add the key
|
||||||
newContent, err := updateYamlKey(string(content), key, value)
|
newContent, err := updateYamlKey(string(content), normalizedKey, value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -194,12 +214,8 @@ func formatYamlValue(value string) string {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
// String values that need quoting
|
// For all other string-like values, quote to preserve YAML string semantics
|
||||||
if needsQuoting(value) {
|
|
||||||
return fmt.Sprintf("%q", value)
|
return fmt.Sprintf("%q", value)
|
||||||
}
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isNumeric(s string) bool {
|
func isNumeric(s string) bool {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func TestUpdateYamlKey(t *testing.T) {
|
|||||||
content: "# actor: \"\"\nother: value",
|
content: "# actor: \"\"\nother: value",
|
||||||
key: "actor",
|
key: "actor",
|
||||||
value: "steve",
|
value: "steve",
|
||||||
expected: "actor: steve\nother: value",
|
expected: "actor: \"steve\"\nother: value",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "handle duration value",
|
name: "handle duration value",
|
||||||
@@ -136,8 +136,8 @@ func TestFormatYamlValue(t *testing.T) {
|
|||||||
{"3.14", "3.14"},
|
{"3.14", "3.14"},
|
||||||
{"30s", "30s"},
|
{"30s", "30s"},
|
||||||
{"5m", "5m"},
|
{"5m", "5m"},
|
||||||
{"simple", "simple"},
|
{"simple", "\"simple\""},
|
||||||
{"has space", "has space"},
|
{"has space", "\"has space\""},
|
||||||
{"has:colon", "\"has:colon\""},
|
{"has:colon", "\"has:colon\""},
|
||||||
{"has#hash", "\"has#hash\""},
|
{"has#hash", "\"has#hash\""},
|
||||||
{" leading", "\" leading\""},
|
{" leading", "\" leading\""},
|
||||||
@@ -153,6 +153,77 @@ func TestFormatYamlValue(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func TestSetYamlConfig(t *testing.T) {
|
||||||
// Create a temp directory with .beads/config.yaml
|
// Create a temp directory with .beads/config.yaml
|
||||||
tmpDir, err := os.MkdirTemp("", "beads-yaml-test-*")
|
tmpDir, err := os.MkdirTemp("", "beads-yaml-test-*")
|
||||||
|
|||||||
Reference in New Issue
Block a user