diff --git a/internal/config/yaml_config.go b/internal/config/yaml_config.go index db659c98..cda4f695 100644 --- a/internal/config/yaml_config.go +++ b/internal/config/yaml_config.go @@ -74,14 +74,34 @@ func IsYamlOnlyKey(key string) bool { 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. // 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 { configPath, err := findProjectConfigYaml() if err != nil { return err } + // Normalize key to canonical yaml format + normalizedKey := normalizeYamlKey(key) + // Read existing config content, err := os.ReadFile(configPath) //nolint:gosec // configPath is from findProjectConfigYaml if err != nil { @@ -89,7 +109,7 @@ func SetYamlConfig(key, value string) error { } // Update or add the key - newContent, err := updateYamlKey(string(content), key, value) + newContent, err := updateYamlKey(string(content), normalizedKey, value) if err != nil { return err } @@ -194,12 +214,8 @@ func formatYamlValue(value string) string { return value } - // String values that need quoting - if needsQuoting(value) { - return fmt.Sprintf("%q", value) - } - - return value + // For all other string-like values, quote to preserve YAML string semantics + return fmt.Sprintf("%q", value) } func isNumeric(s string) bool { diff --git a/internal/config/yaml_config_test.go b/internal/config/yaml_config_test.go index 6fefe8f3..9f09b6aa 100644 --- a/internal/config/yaml_config_test.go +++ b/internal/config/yaml_config_test.go @@ -92,7 +92,7 @@ func TestUpdateYamlKey(t *testing.T) { content: "# actor: \"\"\nother: value", key: "actor", value: "steve", - expected: "actor: steve\nother: value", + expected: "actor: \"steve\"\nother: value", }, { name: "handle duration value", @@ -136,8 +136,8 @@ func TestFormatYamlValue(t *testing.T) { {"3.14", "3.14"}, {"30s", "30s"}, {"5m", "5m"}, - {"simple", "simple"}, - {"has space", "has space"}, + {"simple", "\"simple\""}, + {"has space", "\"has space\""}, {"has:colon", "\"has:colon\""}, {"has#hash", "\"has#hash\""}, {" 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) { // Create a temp directory with .beads/config.yaml tmpDir, err := os.MkdirTemp("", "beads-yaml-test-*")