internal/config/cloak_save_test.go
Rama main · réplica de lectura del repositorio original.
package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestSaveConfigPreserveComments_ClaudeCloakUpdates(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "config.yaml")
initialYAML := `claude-api-key:
- api-key: sk-ant-test
cloak:
mode: always
strict-mode: true
cache-user-id: true
`
if errWrite := os.WriteFile(configPath, []byte(initialYAML), 0o600); errWrite != nil {
t.Fatalf("os.WriteFile() error = %v", errWrite)
}
// Test case 1: Disable strict-mode, clear mode, remove cache-user-id.
cfg := &Config{
ClaudeKey: []ClaudeKey{
{
APIKey: "sk-ant-test",
Cloak: &CloakConfig{
Mode: "",
StrictMode: false,
CacheUserID: nil,
},
},
},
}
if errSave := SaveConfigPreserveComments(configPath, cfg); errSave != nil {
t.Fatalf("SaveConfigPreserveComments() error = %v", errSave)
}
savedBytes, errRead := os.ReadFile(configPath)
if errRead != nil {
t.Fatalf("os.ReadFile() error = %v", errRead)
}
savedText := string(savedBytes)
// In issue #5935, mode: always, strict-mode: true, and cache-user-id: true remained in config.yaml
// even when strict-mode was set to false, mode was cleared, and cache-user-id was disabled.
if strings.Contains(savedText, "mode: always") {
t.Errorf("saved YAML still contains 'mode: always' after clearing mode; got:\n%s", savedText)
}
if strings.Contains(savedText, "strict-mode: true") {
t.Errorf("saved YAML still contains 'strict-mode: true' after setting strict-mode to false; got:\n%s", savedText)
}
if strings.Contains(savedText, "cache-user-id: true") {
t.Errorf("saved YAML still contains 'cache-user-id: true' after disabling cache-user-id; got:\n%s", savedText)
}
// Loading the saved config must reflect the updated false/empty values, while preserving explicit Cloak.
loadedCfg, errLoad := LoadConfigOptional(configPath, false)
if errLoad != nil {
t.Fatalf("LoadConfigOptional() error = %v", errLoad)
}
if len(loadedCfg.ClaudeKey) == 0 {
t.Fatalf("loaded ClaudeKey is empty")
}
cloak := loadedCfg.ClaudeKey[0].Cloak
if cloak == nil {
t.Fatalf("loaded Cloak is nil, want non-nil explicit cloak config")
}
if cloak.Mode != "" {
t.Errorf("loaded Cloak.Mode = %q, want empty", cloak.Mode)
}
if cloak.StrictMode {
t.Errorf("loaded Cloak.StrictMode = true, want false")
}
if cloak.CacheUserID != nil && *cloak.CacheUserID {
t.Errorf("loaded Cloak.CacheUserID = true, want nil or false")
}
}
func TestSaveConfigPreserveComments_ClaudeCloakExplicitFalse(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "config.yaml")
initialYAML := `claude-api-key:
- api-key: sk-ant-test
cloak:
mode: always
strict-mode: true
cache-user-id: true
`
if errWrite := os.WriteFile(configPath, []byte(initialYAML), 0o600); errWrite != nil {
t.Fatalf("os.WriteFile() error = %v", errWrite)
}
cacheFalse := false
cfg := &Config{
ClaudeKey: []ClaudeKey{
{
APIKey: "sk-ant-test",
Cloak: &CloakConfig{
Mode: "auto",
StrictMode: false,
CacheUserID: &cacheFalse,
},
},
},
}
if errSave := SaveConfigPreserveComments(configPath, cfg); errSave != nil {
t.Fatalf("SaveConfigPreserveComments() error = %v", errSave)
}
savedBytes, errRead := os.ReadFile(configPath)
if errRead != nil {
t.Fatalf("os.ReadFile() error = %v", errRead)
}
savedText := string(savedBytes)
if !strings.Contains(savedText, "mode: auto") {
t.Errorf("saved YAML missing 'mode: auto'; got:\n%s", savedText)
}
if strings.Contains(savedText, "strict-mode: true") {
t.Errorf("saved YAML still contains 'strict-mode: true'; got:\n%s", savedText)
}
if !strings.Contains(savedText, "cache-user-id: false") {
t.Errorf("saved YAML missing explicit 'cache-user-id: false'; got:\n%s", savedText)
}
loadedCfg, errLoad := LoadConfigOptional(configPath, false)
if errLoad != nil {
t.Fatalf("LoadConfigOptional() error = %v", errLoad)
}
cloak := loadedCfg.ClaudeKey[0].Cloak
if cloak == nil {
t.Fatalf("loaded Cloak is nil, want non-nil")
}
if cloak.Mode != "auto" {
t.Errorf("loaded Cloak.Mode = %q, want %q", cloak.Mode, "auto")
}
if cloak.StrictMode {
t.Errorf("loaded Cloak.StrictMode = true, want false")
}
if cloak.CacheUserID == nil || *cloak.CacheUserID != false {
t.Errorf("loaded Cloak.CacheUserID = %v, want false", cloak.CacheUserID)
}
}
func TestSaveConfigPreserveComments_ClaudeCloakExplicitFalseWhenFieldPreviouslyAbsent(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "config.yaml")
// Initial YAML only has mode: always. cache-user-id is completely absent.
initialYAML := `claude-api-key:
- api-key: sk-ant-test
cloak:
mode: always
`
if errWrite := os.WriteFile(configPath, []byte(initialYAML), 0o600); errWrite != nil {
t.Fatalf("os.WriteFile() error = %v", errWrite)
}
cacheFalse := false
cfg := &Config{
ClaudeKey: []ClaudeKey{
{
APIKey: "sk-ant-test",
Cloak: &CloakConfig{
Mode: "always",
CacheUserID: &cacheFalse,
},
},
},
}
if errSave := SaveConfigPreserveComments(configPath, cfg); errSave != nil {
t.Fatalf("SaveConfigPreserveComments() error = %v", errSave)
}
savedBytes, errRead := os.ReadFile(configPath)
if errRead != nil {
t.Fatalf("os.ReadFile() error = %v", errRead)
}
savedText := string(savedBytes)
// An explicit pointer false for cache-user-id must be added to YAML even if previously absent.
if !strings.Contains(savedText, "cache-user-id: false") {
t.Errorf("saved YAML missing newly-added 'cache-user-id: false'; got:\n%s", savedText)
}
loadedCfg, errLoad := LoadConfigOptional(configPath, false)
if errLoad != nil {
t.Fatalf("LoadConfigOptional() error = %v", errLoad)
}
cloak := loadedCfg.ClaudeKey[0].Cloak
if cloak == nil {
t.Fatalf("loaded Cloak is nil, want non-nil")
}
if cloak.CacheUserID == nil || *cloak.CacheUserID != false {
t.Errorf("loaded Cloak.CacheUserID = %v, want explicit false", cloak.CacheUserID)
}
}
func TestSaveConfigPreserveComments_ClaudeCloakRemovedWhenNil(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "config.yaml")
initialYAML := `claude-api-key:
- api-key: sk-ant-test
cloak:
mode: always
strict-mode: true
cache-user-id: true
`
if errWrite := os.WriteFile(configPath, []byte(initialYAML), 0o600); errWrite != nil {
t.Fatalf("os.WriteFile() error = %v", errWrite)
}
// Cloak explicitly set to nil (removed)
cfg := &Config{
ClaudeKey: []ClaudeKey{
{
APIKey: "sk-ant-test",
Cloak: nil,
},
},
}
if errSave := SaveConfigPreserveComments(configPath, cfg); errSave != nil {
t.Fatalf("SaveConfigPreserveComments() error = %v", errSave)
}
savedBytes, errRead := os.ReadFile(configPath)
if errRead != nil {
t.Fatalf("os.ReadFile() error = %v", errRead)
}
savedText := string(savedBytes)
if strings.Contains(savedText, "cloak:") {
t.Errorf("saved YAML still contains 'cloak:' after nil removal; got:\n%s", savedText)
}
loadedCfg, errLoad := LoadConfigOptional(configPath, false)
if errLoad != nil {
t.Fatalf("LoadConfigOptional() error = %v", errLoad)
}
if loadedCfg.ClaudeKey[0].Cloak != nil {
t.Errorf("loaded Cloak = %+v, want nil", loadedCfg.ClaudeKey[0].Cloak)
}
}
func TestSaveConfigPreserveComments_HeadersPrunedAndNonTargetMappingsPreserved(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "config.yaml")
initialYAML := `remote-management:
allow-remote: true
claude-api-key:
- api-key: sk-ant-test
headers:
Custom-A: valA
Custom-B: valB
`
if errWrite := os.WriteFile(configPath, []byte(initialYAML), 0o600); errWrite != nil {
t.Fatalf("os.WriteFile() error = %v", errWrite)
}
cfg := &Config{
RemoteManagement: RemoteManagement{
AllowRemote: true,
},
ClaudeKey: []ClaudeKey{
{
APIKey: "sk-ant-test",
Headers: map[string]string{
"Custom-A": "valA",
},
},
},
}
if errSave := SaveConfigPreserveComments(configPath, cfg); errSave != nil {
t.Fatalf("SaveConfigPreserveComments() error = %v", errSave)
}
savedBytes, errRead := os.ReadFile(configPath)
if errRead != nil {
t.Fatalf("os.ReadFile() error = %v", errRead)
}
savedText := string(savedBytes)
// Deleted header Custom-B must be pruned from claude-api-key.headers
if strings.Contains(savedText, "Custom-B") {
t.Errorf("saved YAML still contains deleted header 'Custom-B'; got:\n%s", savedText)
}
if !strings.Contains(savedText, "Custom-A: valA") {
t.Errorf("saved YAML missing retained header 'Custom-A: valA'; got:\n%s", savedText)
}
// Non-target root mapping (remote-management) must be preserved
if !strings.Contains(savedText, "allow-remote: true") {
t.Errorf("saved YAML lost 'allow-remote: true'; got:\n%s", savedText)
}
}
func TestSaveConfigPreserveComments_SensitiveWordsReplaceEmptyValueAndPruning(t *testing.T) {
for _, v8Layout := range []bool{false, true} {
name := "legacy"
initialYAML := "port: 8317\nclaude-api-key:\n - api-key: sk-ant-test\n"
if v8Layout {
name = "v8"
initialYAML = "config-version: 8\nserver:\n port: 8317\napi-keys:\n claude:\n - name: claude-1\n keys:\n - api-key: sk-ant-test\n"
}
t.Run(name, func(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "config.yaml")
if errWrite := os.WriteFile(configPath, []byte(initialYAML), 0o600); errWrite != nil {
t.Fatalf("os.WriteFile() error = %v", errWrite)
}
cfg, errLoad := LoadConfigOptional(configPath, false)
if errLoad != nil {
t.Fatalf("initial LoadConfigOptional() error = %v", errLoad)
}
// Step 1: Save empty-string deletion entries in all three scopes.
cfg.Antigravity.SensitiveWordsReplace = map[string]string{
"You are Codex, an agent based on GPT-5.": "",
"TempAntigrav": "Keep",
}
cfg.Devin.SensitiveWordsReplace = map[string]string{
"DropDevinPhrase": "",
}
cfg.ClaudeKey[0].Cloak = &CloakConfig{
SensitiveWordsReplace: map[string]string{
"DropClaudePhrase": "",
"OldClaudeTarget": "OldRepl",
},
}
if errSave := SaveConfigPreserveComments(configPath, cfg); errSave != nil {
t.Fatalf("SaveConfigPreserveComments() step 1 error = %v", errSave)
}
reloaded, errReload := LoadConfigOptional(configPath, false)
if errReload != nil {
t.Fatalf("reload step 1 error = %v", errReload)
}
if val, ok := reloaded.Antigravity.SensitiveWordsReplace["You are Codex, an agent based on GPT-5."]; !ok || val != "" {
t.Fatalf("Antigravity empty replacement lost: %#v", reloaded.Antigravity.SensitiveWordsReplace)
}
if val, ok := reloaded.Devin.SensitiveWordsReplace["DropDevinPhrase"]; !ok || val != "" {
t.Fatalf("Devin empty replacement lost: %#v", reloaded.Devin.SensitiveWordsReplace)
}
if reloaded.ClaudeKey[0].Cloak == nil {
t.Fatal("Claude Cloak is nil after step 1")
}
if val, ok := reloaded.ClaudeKey[0].Cloak.SensitiveWordsReplace["DropClaudePhrase"]; !ok || val != "" {
t.Fatalf("Claude empty replacement lost: %#v", reloaded.ClaudeKey[0].Cloak.SensitiveWordsReplace)
}
// Step 2: Prune individual keys and clear Devin replacements completely.
delete(reloaded.Antigravity.SensitiveWordsReplace, "TempAntigrav")
reloaded.Devin.SensitiveWordsReplace = nil
delete(reloaded.ClaudeKey[0].Cloak.SensitiveWordsReplace, "OldClaudeTarget")
if errSave2 := SaveConfigPreserveComments(configPath, reloaded); errSave2 != nil {
t.Fatalf("SaveConfigPreserveComments() step 2 error = %v", errSave2)
}
reloaded2, errReload2 := LoadConfigOptional(configPath, false)
if errReload2 != nil {
t.Fatalf("reload step 2 error = %v", errReload2)
}
if _, exists := reloaded2.Antigravity.SensitiveWordsReplace["TempAntigrav"]; exists {
t.Fatalf("deleted Antigravity key persisted: %#v", reloaded2.Antigravity.SensitiveWordsReplace)
}
if len(reloaded2.Devin.SensitiveWordsReplace) != 0 {
t.Fatalf("cleared Devin replacements persisted: %#v", reloaded2.Devin.SensitiveWordsReplace)
}
if _, exists := reloaded2.ClaudeKey[0].Cloak.SensitiveWordsReplace["OldClaudeTarget"]; exists {
t.Fatalf("deleted Claude replacement key persisted: %#v", reloaded2.ClaudeKey[0].Cloak.SensitiveWordsReplace)
}
})
}
}