Ctrl K
ComunidadGuías…

internal/config/config_v8_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package config

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"sync"
	"testing"

	log "github.com/sirupsen/logrus"
	logtest "github.com/sirupsen/logrus/hooks/test"
	"gopkg.in/yaml.v3"
)

func TestV8ExampleLoadsAndRoundTrips(t *testing.T) {
	example, err := os.ReadFile(filepath.Join("..", "..", "config.example.yaml"))
	if err != nil {
		t.Fatal(err)
	}
	active, err := ParseConfigBytes(example)
	if err != nil {
		t.Fatalf("load active v8 example: %v", err)
	}
	if active.Port != 8317 || len(active.APIKeys) != 3 || active.RequestRetry != 3 || !active.QuotaExceeded.AntigravityCredits {
		t.Fatal("v8 template lost its default configuration")
	}
	if err = ValidateV8Config(example); err != nil {
		t.Fatalf("active template must use the v8 layout: %v", err)
	}
	for _, count := range []int{len(active.GeminiKey), len(active.CodexKey), len(active.ClaudeKey), len(active.VertexCompatAPIKey), len(active.XAIKey), len(active.MetaKey), len(active.InteractionsKey), len(active.OpenAICompatibility)} {
		if count != 0 {
			t.Fatal("placeholder upstream credentials must remain commented")
		}
	}
	if _, changed, errNormalize := NormalizeConfigLayout(example, false); errNormalize != nil || changed {
		t.Fatalf("v8 template unexpectedly needs conflict cleanup: changed=%v error=%v", changed, errNormalize)
	}
	// Validate the provider examples exactly as operators uncomment them.
	text := strings.ReplaceAll(string(example), "\r\n", "\n")
	_, block, found := strings.Cut(text, "# BEGIN API KEY EXAMPLES\n")
	if !found {
		t.Fatal("config.example.yaml is missing the API-key examples")
	}
	block, _, found = strings.Cut(block, "# END API KEY EXAMPLES")
	if !found {
		t.Fatal("config.example.yaml is missing the end of the API-key examples")
	}
	var uncommented strings.Builder
	for _, line := range strings.Split(strings.TrimSuffix(block, "\n"), "\n") {
		if line == "" {
			continue
		}
		if !strings.HasPrefix(line, "#") {
			t.Fatal("placeholder upstream examples must remain commented")
		}
		uncommented.WriteString(strings.TrimPrefix(strings.TrimPrefix(line, "#"), " "))
		uncommented.WriteByte('\n')
	}
	data := []byte(text + "\n" + uncommented.String())
	if err = ValidateV8Config(data); err != nil {
		t.Fatal(err)
	}
	cfg, err := ParseConfigBytes(data)
	if err != nil {
		t.Fatal(err)
	}
	if cfg.Port != 8317 || len(cfg.APIKeys) != 3 || len(cfg.GeminiKey) != 3 || len(cfg.CodexKey) != 1 || len(cfg.ClaudeKey) != 2 || len(cfg.VertexCompatAPIKey) != 1 || len(cfg.XAIKey) != 1 || len(cfg.MetaKey) != 1 || len(cfg.InteractionsKey) != 1 || len(cfg.OpenAICompatibility) != 1 {
		t.Fatal("v8 example fields did not reach runtime config")
	}
	if !cfg.QuotaExceeded.AntigravityCredits || cfg.QuotaExceeded.SwitchProject || cfg.QuotaExceeded.SwitchPreviewModel {
		t.Fatal("legacy-only quota examples must remain commented")
	}
	path := filepath.Join(t.TempDir(), "config.yaml")
	if err = os.WriteFile(path, data, 0600); err != nil {
		t.Fatal(err)
	}
	if err = SaveConfigPreserveComments(path, cfg); err != nil {
		t.Fatal(err)
	}
	reloaded, err := LoadConfig(path)
	if err != nil {
		t.Fatal(err)
	}
	var beforeNode, afterNode yaml.Node
	if err = beforeNode.Encode(cfg); err != nil {
		t.Fatal(err)
	}
	if err = afterNode.Encode(reloaded); err != nil {
		t.Fatal(err)
	}
	if !nodesStructurallyEqual(&beforeNode, &afterNode) {
		t.Fatal("runtime values changed after saving v8 example")
	}
	saved, _ := os.ReadFile(path)
	var node yaml.Node
	if err = yaml.Unmarshal(saved, &node); err != nil {
		t.Fatal(err)
	}
	if groups := yamlPath(node.Content[0], "api-keys.gemini"); groups == nil || len(groups.Content) != 2 || yamlPath(groups.Content[0], "name").Value != "gemini-1" {
		t.Fatal("save lost upstream group identity")
	}
}

func TestV8PresencePrecedenceAndCleanup(t *testing.T) {
	for _, tc := range []struct {
		name, raw string
		retry     int
		cooling   bool
		keys      int
		unchanged bool
	}{
		{"legacy", "request-retry: 4\ndisable-cooling: true\napi-keys: [old]\n", 4, true, 1, true},
		{"mixed explicit zero", "request-retry: 4\ndisable-cooling: true\napi-keys: [old]\nrouting:\n  retry: {request-retry: 0}\n  cooldown: {disable-cooling: false}\naccess: {api-keys: []}\n", 0, false, 0, false},
		{"version does not force migration", "config-version: 8\nrequest-retry: 4\ndisable-cooling: true\napi-keys: [old]\n", 4, true, 1, true},
		{"partial new block", "request-retry: 4\ndisable-cooling: true\napi-keys: [old]\nrouting: {retry: {max-retry-credentials: 2}}\n", 4, true, 1, true},
	} {
		t.Run(tc.name, func(t *testing.T) {
			path := filepath.Join(t.TempDir(), "config.yaml")
			if err := os.WriteFile(path, []byte(tc.raw), 0600); err != nil {
				t.Fatal(err)
			}
			cfg, err := LoadConfig(path)
			if err != nil {
				t.Fatal(err)
			}
			if cfg.RequestRetry != tc.retry || cfg.DisableCooling != tc.cooling || len(cfg.APIKeys) != tc.keys {
				t.Fatalf("unexpected effective config: retry=%d cooling=%v keys=%d", cfg.RequestRetry, cfg.DisableCooling, len(cfg.APIKeys))
			}
			saved, _ := os.ReadFile(path)
			if tc.unchanged && string(saved) != tc.raw {
				t.Fatal("legacy config was rewritten")
			}
			if !tc.unchanged {
				var doc yaml.Node
				_ = yaml.Unmarshal(saved, &doc)
				if yamlPath(doc.Content[0], "request-retry") != nil || legacyPath(doc.Content[0], "api-keys") != nil || yamlPath(doc.Content[0], "disable-cooling") != nil {
					t.Fatal("conflicting legacy fields remain")
				}
			}
		})
	}
}

func TestV8KeyInheritance(t *testing.T) {
	for _, provider := range []string{"gemini", "interactions", "vertex", "codex", "claude", "xai", "meta"} {
		t.Run(provider, func(t *testing.T) {
			raw := "request-retry: 9\napi-keys:\n  " + provider + ":\n" + `    - name: shared
      base-url: https://example.invalid
      priority: 7
      prefix: group
      proxy-url: direct
      headers: {X-Group: yes}
      models: [{name: model, alias: alias}]
      excluded-models: [blocked]
      disable-cooling: true
      request-retry: 3
      keys:
        - api-key: inherited
          priority: null
          headers: null
          disable-cooling: null
          request-retry: null
        - api-key: overridden
          weight: 0
          priority: 0
          prefix: ''
          proxy-url: ''
          headers: {}
          models: []
          excluded-models: []
          disable-cooling: false
          request-retry: 0
        - api-key: global-retry
          request-retry: -1
`
			cfg, err := ParseConfigBytes([]byte(raw))
			if err != nil {
				t.Fatal(err)
			}
			var effective yaml.Node
			if err = effective.Encode(cfg); err != nil {
				t.Fatal(err)
			}
			var old string
			for _, family := range v8KeyFamilies {
				if family.current == provider {
					old = family.old
				}
			}
			keys := yamlPath(&effective, old)
			if keys == nil || len(keys.Content) != 3 {
				t.Fatal("group did not expand to three keys")
			}
			first, second, third := keys.Content[0], keys.Content[1], keys.Content[2]
			if yamlPath(first, "priority").Value != "7" || yamlPath(first, "request-retry").Value != "3" || yamlPath(first, "disable-cooling").Value != "true" || yamlPath(first, "headers") == nil {
				t.Fatal("null did not inherit")
			}
			if yamlPath(second, "request-retry").Value != "0" || yamlPath(second, "disable-cooling").Value != "false" || yamlPath(second, "weight").Value != "0" || yamlPath(third, "request-retry").Value != "-1" {
				t.Fatal("explicit overrides lost")
			}
			for _, name := range []string{"headers", "models", "excluded-models"} {
				if node := yamlPath(second, name); node != nil && len(node.Content) != 0 {
					t.Fatalf("empty %s inherited group value", name)
				}
			}
		})
	}
}

func TestV8MigrationPreservesLegacySemantics(t *testing.T) {
	raw := []byte(`host: 127.0.0.1
port: 8317
api-keys: [client]
request-retry: 0
ws-auth: false
quota-exceeded: {switch-project: true, switch-preview-model: true, antigravity-credits: true}
codex-api-key:
  - api-key: a
    base-url: https://example.invalid
    headers: {X-Test: first}
    request-retry: 0
    disable-cooling: false
  - api-key: b
    base-url: https://example.invalid
    headers: {X-Test: second}
    request-retry: -1
openai-compatibility:
  - name: compatible
    base-url: https://example.invalid
    api-key-entries: [{api-key: a, weight: 0}, {api-key: b, proxy-url: direct}]
`)
	before, err := ParseConfigBytes(raw)
	if err != nil {
		t.Fatal(err)
	}
	migrated, _, err := NormalizeConfigLayout(raw, true)
	if err != nil {
		t.Fatal(err)
	}
	if err = ValidateV8Config(migrated); err != nil {
		t.Fatal(err)
	}
	after, err := ParseConfigBytes(migrated)
	if err != nil {
		t.Fatal(err)
	}
	// Scope metadata is intentionally added when fields move under oauth.providers.
	after.OAuthOnlyFields = nil
	if !reflect.DeepEqual(before, after) {
		t.Fatal("migration changed effective config")
	}
	var doc yaml.Node
	_ = yaml.Unmarshal(migrated, &doc)
	groups := yamlPath(doc.Content[0], "api-keys.codex")
	if len(groups.Content) != 2 {
		t.Fatal("different shared settings were combined")
	}
	if !after.QuotaExceeded.SwitchProject || !after.QuotaExceeded.SwitchPreviewModel {
		t.Fatal("legacy-only options lost")
	}
}

func TestV8MigrationCommentsUnknownLegacySections(t *testing.T) {
	raw := []byte(`home:
  enabled: true
  host: ignored.example
enable-gemini-cli-endpoint: true
forgotten-setting:
  items: [first, second]
proxy-url: old
`)
	unchanged, changed, err := NormalizeConfigLayout(raw, false)
	if err != nil || changed || string(unchanged) != string(raw) {
		t.Fatalf("read-only normalization changed the legacy file: changed=%v error=%v", changed, err)
	}
	migrated, changed, err := NormalizeConfigLayout(raw, true)
	if err != nil || !changed {
		t.Fatalf("migrate legacy file: changed=%v error=%v", changed, err)
	}
	var doc yaml.Node
	if err = yaml.Unmarshal(migrated, &doc); err != nil {
		t.Fatal(err)
	}
	for _, key := range []string{"home", "enable-gemini-cli-endpoint", "forgotten-setting", "proxy-url"} {
		if yamlPath(doc.Content[0], key) != nil {
			t.Fatalf("legacy section %s remained active after migration", key)
		}
	}
	for _, text := range []string{"# home:", "#     enabled: true", "#     host: ignored.example", "# enable-gemini-cli-endpoint: true", "# forgotten-setting:", "#     items: [first, second]"} {
		if !strings.Contains(string(migrated), text) {
			t.Fatalf("unknown legacy section was not preserved as a comment: %s\n%s", text, migrated)
		}
	}
	if err = ValidateV8Config(migrated); err != nil {
		t.Fatalf("migrated file is invalid: %v", err)
	}
	cfg, err := ParseConfigBytes(migrated)
	if err != nil || cfg.ProxyURL != "old" || cfg.Home.Enabled {
		t.Fatalf("migration changed effective settings: cfg=%+v error=%v", cfg, err)
	}
	remigrated, _, err := NormalizeConfigLayout(migrated, true)
	if err != nil || strings.Count(string(remigrated), "# home:") != 1 || strings.Count(string(remigrated), "# forgotten-setting:") != 1 {
		t.Fatalf("repeated migration lost or duplicated comments: %v\n%s", err, remigrated)
	}
}

func TestV8MigrationCommentsUnknownNestedFields(t *testing.T) {
	raw := []byte(`server: {port: 8317}
routing: {strategy: fill-first, session-affinity: true}
oauth:
  providers:
    codex:
      disable-codex-cloaking: true
      retired-setting: {mode: old}
`)
	unchanged, changed, err := NormalizeConfigLayout(raw, false)
	if err != nil || changed || string(unchanged) != string(raw) {
		t.Fatalf("read-only normalization changed existing config: changed=%v error=%v", changed, err)
	}
	migrated, _, err := NormalizeConfigLayout(raw, true)
	if err != nil {
		t.Fatal(err)
	}
	if err = ValidateV8Config(migrated); err != nil {
		t.Fatalf("migrated config is invalid: %v\n%s", err, migrated)
	}
	var doc yaml.Node
	if err = yaml.Unmarshal(migrated, &doc); err != nil {
		t.Fatal(err)
	}
	if yamlPath(doc.Content[0], "oauth.providers.codex.retired-setting") != nil || !strings.Contains(string(migrated), "# oauth.providers.codex.retired-setting:") {
		t.Fatalf("unknown nested field was not preserved as a comment: %s", migrated)
	}
	cfg, err := ParseConfigBytes(migrated)
	if err != nil || cfg.Routing.Strategy != "fill-first" || !cfg.Routing.SessionAffinity || !cfg.Codex.DisableCodexCloaking {
		t.Fatalf("migration changed known settings: cfg=%+v error=%v", cfg, err)
	}
	remigrated, _, err := NormalizeConfigLayout(migrated, true)
	if err != nil || strings.Count(string(remigrated), "# oauth.providers.codex.retired-setting:") != 1 {
		t.Fatalf("repeated migration lost or duplicated the comment: %v\n%s", err, remigrated)
	}
}

func TestV8MigrationCommentsUnknownLegacySectionsWarnsConsole(t *testing.T) {
	logger := log.StandardLogger()
	previousHooks := logger.ReplaceHooks(make(log.LevelHooks))
	previousLevel := logger.GetLevel()
	previousOut := logger.Out
	hook := logtest.NewLocal(logger)
	logger.SetLevel(log.WarnLevel)
	t.Cleanup(func() {
		logger.ReplaceHooks(previousHooks)
		logger.SetLevel(previousLevel)
		logger.SetOutput(previousOut)
	})

	raw := []byte(`host: "127.0.0.1"
port: 8317
some-obsolete-legacy-block:
  alpha: 1
  beta: two
another-legacy-key:
  gamma: 3
`)
	migrated, changed, errMigrate := NormalizeConfigLayout(raw, true)
	if errMigrate != nil || !changed {
		t.Fatalf("NormalizeConfigLayout() error = %v, changed = %v", errMigrate, changed)
	}
	if !strings.Contains(string(migrated), "# some-obsolete-legacy-block:") || !strings.Contains(string(migrated), "# another-legacy-key:") {
		t.Fatalf("expected unknown sections to be commented out, got: %s", string(migrated))
	}

	foundSections := make(map[string]bool)
	for _, entry := range hook.AllEntries() {
		if entry.Level == log.WarnLevel {
			if strings.Contains(entry.Message, "some-obsolete-legacy-block") {
				foundSections["some-obsolete-legacy-block"] = true
			}
			if strings.Contains(entry.Message, "another-legacy-key") {
				foundSections["another-legacy-key"] = true
			}
			if !strings.Contains(entry.Message, "unrecognized") || !strings.Contains(entry.Message, "commented out") {
				t.Fatalf("expected warning message to convey 'unrecognized' and 'commented out', got: %s", entry.Message)
			}
			if strings.Contains(entry.Message, "server") || strings.Contains(entry.Message, "host") || strings.Contains(entry.Message, "port") {
				t.Fatalf("known section falsely reported in warning: %s", entry.Message)
			}
		}
	}
	if !foundSections["some-obsolete-legacy-block"] || !foundSections["another-legacy-key"] {
		t.Fatalf("expected warnings for all unmapped sections, found: %+v", foundSections)
	}

	// Repeated migration should not repeat warnings
	hook.Reset()
	remigrated, _, errRemigrate := NormalizeConfigLayout(migrated, true)
	if errRemigrate != nil {
		t.Fatalf("repeated NormalizeConfigLayout() error = %v", errRemigrate)
	}
	if len(hook.AllEntries()) != 0 {
		t.Fatalf("expected no warnings on repeated migration, got: %+v", hook.AllEntries())
	}
	_ = remigrated

	// Test warning hook customization
	var hookBuf bytes.Buffer
	var hookMu sync.Mutex
	SetV8MigrationWarnFunc(func(section, msg string) {
		log.Warn(msg)
		hookMu.Lock()
		_, _ = fmt.Fprintf(&hookBuf, "HOOK: %s -> %s\n", section, msg)
		hookMu.Unlock()
	})
	t.Cleanup(func() {
		SetV8MigrationWarnFunc(nil)
	})

	hook.Reset()
	_, _, errMigrateHook := NormalizeConfigLayout(raw, true)
	if errMigrateHook != nil {
		t.Fatalf("NormalizeConfigLayout() under custom hook error = %v", errMigrateHook)
	}

	hookOutput := hookBuf.String()
	if !strings.Contains(hookOutput, "HOOK: some-obsolete-legacy-block") || !strings.Contains(hookOutput, "HOOK: another-legacy-key") {
		t.Fatalf("expected custom hook to capture warnings, got: %s", hookOutput)
	}

	// Known-only config produces no warnings
	hook.Reset()
	hookBuf.Reset()
	knownRaw := []byte(`host: "127.0.0.1"
port: 8317
debug: true
`)
	_, _, errKnown := NormalizeConfigLayout(knownRaw, true)
	if errKnown != nil {
		t.Fatalf("NormalizeConfigLayout() error = %v", errKnown)
	}
	if len(hook.AllEntries()) != 0 || hookBuf.Len() != 0 {
		t.Fatalf("expected no warnings for purely known configuration, got logs=%+v hook=%s", hook.AllEntries(), hookBuf.String())
	}
}

func TestV8MigrationConcurrentOutputSwitch(t *testing.T) {
	raw := []byte(`host: "127.0.0.1"
port: 8317
some-concurrent-legacy-block:
  data: true
`)
	var warnMu sync.Mutex
	var buf bytes.Buffer
	customWarn := func(section, msg string) {
		warnMu.Lock()
		_, _ = fmt.Fprintf(&buf, "%s: %s\n", section, msg)
		warnMu.Unlock()
	}
	SetV8MigrationWarnFunc(customWarn)
	t.Cleanup(func() {
		SetV8MigrationWarnFunc(nil)
	})

	var wg sync.WaitGroup
	for i := 0; i < 20; i++ {
		wg.Add(2)
		go func() {
			defer wg.Done()
			SetV8MigrationWarnFunc(customWarn)
			SetV8MigrationWarnFunc(nil)
		}()
		go func() {
			defer wg.Done()
			_, _, _ = NormalizeConfigLayout(raw, true)
		}()
	}
	wg.Wait()
}

func TestV8SaveCommentsObsoleteSections(t *testing.T) {
	path := filepath.Join(t.TempDir(), "config.yaml")
	raw := `auth: {old: true}
ampcode: {old: true}
amp-upstream-url: https://old.example
amp-upstream-api-key: old-secret
generative-language-api-key: old-key
home: {enabled: true}
proxy-url: old
`
	if err := os.WriteFile(path, []byte(raw), 0600); err != nil {
		t.Fatal(err)
	}
	cfg, err := LoadConfig(path)
	if err != nil {
		t.Fatal(err)
	}
	if err = SaveConfigPreserveComments(path, cfg, true); err != nil {
		t.Fatal(err)
	}
	saved, err := os.ReadFile(path)
	if err != nil {
		t.Fatal(err)
	}
	if err = ValidateV8Config(saved); err != nil {
		t.Fatalf("saved migration is invalid: %v", err)
	}
	for _, key := range []string{"auth", "ampcode", "amp-upstream-url", "amp-upstream-api-key", "generative-language-api-key", "home"} {
		if !strings.Contains(string(saved), "# "+key+":") {
			t.Errorf("obsolete setting %s was discarded rather than commented", key)
		}
	}
	if !strings.Contains(string(saved), "# amp-upstream-api-key: old-secret") {
		t.Fatal("obsolete setting lost its value")
	}
}

func TestV8MigrationPreservesEmptyLegacyContainers(t *testing.T) {
	for _, section := range []configPath{
		{"tls", "server.tls"}, {"remote-management", "management"},
		{"pprof", "observability.pprof"}, {"discovery", "server.discovery"},
		{"discovery.interfaces", "server.discovery.interfaces"},
		{"credential-concurrency", "credentials.concurrency"}, {"credential-in-flight", "credentials.in-flight"},
		{"streaming", "requests.streaming"}, {"payload", "requests.payload"},
		{"codex", "oauth.providers.codex"}, {"codex.live-media-relay", "oauth.providers.codex.live-media-relay"},
		{"codex-header-defaults", "oauth.providers.codex.header-defaults"},
		{"claude", "upstream.claude"}, {"claude-code", "upstream.claude"},
		{"claude-header-defaults", "upstream.claude.header-defaults"},
		{"antigravity", "oauth.providers.antigravity"}, {"antigravity.connection-pool", "oauth.providers.antigravity.connection-pool"},
		{"xai", "upstream.xai"}, {"devin", "oauth.providers.devin"},
	} {
		for _, empty := range []string{"{}", "null"} {
			t.Run(section.old+"/"+empty, func(t *testing.T) {
				var doc, value yaml.Node
				if err := yaml.Unmarshal([]byte("port: 8317\nplugins: {configs: {sample: {enabled: false, options: {}}}}\n"), &doc); err != nil {
					t.Fatal(err)
				}
				if err := yaml.Unmarshal([]byte(empty), &value); err != nil {
					t.Fatal(err)
				}
				value.Content[0].LineComment = "Keep this empty block comment"
				setYAMLPath(doc.Content[0], section.old, value.Content[0])
				raw, err := yaml.Marshal(&doc)
				if err != nil {
					t.Fatal(err)
				}
				before, err := ParseConfigBytes(raw)
				if err != nil {
					t.Fatal(err)
				}
				if unchanged, changed, errNormalize := NormalizeConfigLayout(raw, false); errNormalize != nil || changed || string(unchanged) != string(raw) {
					t.Fatalf("legacy-only load changed the file: %v", errNormalize)
				}
				migrated, _, err := NormalizeConfigLayout(raw, true)
				if err != nil {
					t.Fatal(err)
				}
				if err = ValidateV8Config(migrated); err != nil {
					t.Fatalf("migration left an invalid block: %v", err)
				}
				after, err := ParseConfigBytes(migrated)
				if err != nil {
					t.Fatal(err)
				}
				var beforeNode, afterNode yaml.Node
				if err = beforeNode.Encode((*legacyConfig)(before)); err != nil {
					t.Fatal(err)
				}
				if err = afterNode.Encode((*legacyConfig)(after)); err != nil {
					t.Fatal(err)
				}
				if !nodesStructurallyEqual(&beforeNode, &afterNode) {
					t.Fatal("migration changed effective defaults or plugin options")
				}
				if err = yaml.Unmarshal(migrated, &doc); err != nil {
					t.Fatal(err)
				}
				moved := yamlPath(doc.Content[0], section.current)
				if yamlPath(doc.Content[0], section.old) != nil || moved == nil || moved.Kind != yaml.MappingNode || len(moved.Content) != 0 {
					t.Fatal("empty legacy container was not moved to its v8 path")
				}
				if !strings.Contains(string(migrated), "Keep this empty block comment") {
					t.Fatal("migration dropped the empty block comment")
				}
			})
		}
	}
}

func TestV8EmptyLegacyContainersKeepNewValues(t *testing.T) {
	raw := []byte(`port: 8317
tls: null
codex: {disable-codex-cloaking: true, live-media-relay: {}}
server: {tls: {enable: true, cert: server.crt, key: server.key}}
oauth: {providers: {codex: {live-media-relay: {max-sessions: 12}}}}
`)
	for _, migrate := range []bool{false, true} {
		data, _, err := NormalizeConfigLayout(raw, migrate)
		if err != nil {
			t.Fatal(err)
		}
		cfg, err := ParseConfigBytes(data)
		if err != nil {
			t.Fatal(err)
		}
		if !cfg.TLS.Enable || cfg.TLS.Cert != "server.crt" || cfg.TLS.Key != "server.key" || cfg.Codex.LiveMediaRelay.MaxSessions != 12 || !cfg.Codex.DisableCodexCloaking {
			t.Fatal("empty legacy block overwrote new values or a non-empty sibling")
		}
		var doc yaml.Node
		if err = yaml.Unmarshal(data, &doc); err != nil {
			t.Fatal(err)
		}
		if yamlPath(doc.Content[0], "tls") != nil || yamlPath(doc.Content[0], "codex.live-media-relay") != nil {
			t.Fatal("conflicting empty legacy blocks were not removed")
		}
		if !migrate && yamlPath(doc.Content[0], "codex.disable-codex-cloaking") == nil {
			t.Fatal("conflict cleanup migrated a non-conflicting legacy sibling")
		}
	}
}

func TestV8PrivateIPAliasPrecedenceAndMigration(t *testing.T) {
	for _, tc := range []struct {
		name, raw string
		want      bool
	}{
		{"allow", "codex: {live-media-relay: {allow-private-remote-ips: true}}\n", false},
		{"deny", "codex: {live-media-relay: {allow-private-remote-ips: false}}\n", true},
		{"new wins", "codex: {live-media-relay: {allow-private-remote-ips: false}}\noauth: {providers: {codex: {live-media-relay: {disable-private-remote-ips: false}}}}\n", false},
	} {
		t.Run(tc.name, func(t *testing.T) {
			cfg, err := ParseConfigBytes([]byte(tc.raw))
			if err != nil {
				t.Fatal(err)
			}
			if cfg.Codex.LiveMediaRelay.DisablePrivateRemoteIPs != tc.want {
				t.Fatal("wrong private-IP policy")
			}
			path := filepath.Join(t.TempDir(), "config.yaml")
			if err = os.WriteFile(path, []byte(tc.raw), 0600); err != nil {
				t.Fatal(err)
			}
			if err = SaveConfigPreserveComments(path, cfg, true); err != nil {
				t.Fatal(err)
			}
			after, err := LoadConfig(path)
			if err != nil {
				t.Fatal(err)
			}
			if after.Codex.LiveMediaRelay.DisablePrivateRemoteIPs != tc.want {
				t.Fatal("migration inverted the policy")
			}
			data, err := os.ReadFile(path)
			if err != nil || strings.Contains(string(data), "allow-private-remote-ips") {
				t.Fatalf("legacy alias remained: %v", err)
			}
		})
	}
}

func TestV8SaveLegacyAdapterAndManualFallback(t *testing.T) {
	path := filepath.Join(t.TempDir(), "config.yaml")
	raw := "config-version: 8\nrouting: {retry: {request-retry: 1}}\noauth: {providers: {aistudio: {ws-auth: true}}}\n"
	if err := os.WriteFile(path, []byte(raw), 0600); err != nil {
		t.Fatal(err)
	}
	cfg, err := LoadConfig(path)
	if err != nil {
		t.Fatal(err)
	}
	cfg.RequestRetry, cfg.WebsocketAuth = 0, false
	if err = SaveConfigPreserveComments(path, cfg); err != nil {
		t.Fatal(err)
	}
	reloaded, err := LoadConfig(path)
	if err != nil {
		t.Fatal(err)
	}
	if reloaded.RequestRetry != 0 || reloaded.WebsocketAuth {
		t.Fatal("legacy API write was shadowed by stale v8 values")
	}
	if err = os.WriteFile(path, []byte("config-version: 8\nrequest-retry: 5\nws-auth: true\n"), 0600); err != nil {
		t.Fatal(err)
	}
	reloaded, err = LoadConfig(path)
	if err != nil {
		t.Fatal(err)
	}
	if reloaded.RequestRetry != 5 || !reloaded.WebsocketAuth {
		t.Fatal("manual legacy fallback failed")
	}
}

func TestV8RejectsInvalidGroups(t *testing.T) {
	for _, raw := range []string{
		"api-keys: {codex: [{name: a, keys: [{api-key: a, weight: 1.5}]}]}",
		"api-keys: {codex: [{name: a, keys: [{api-key: a, base-url: https://invalid}]}]}",
		"api-keys: {codex: [{name: a, keys: {api-key: a}}]}",
		"server: true", "config-version: 9", "server: {port: bad}",
	} {
		t.Run(raw, func(t *testing.T) {
			if _, err := ParseConfigBytes([]byte(raw)); err == nil {
				t.Fatal("accepted invalid v8 config")
			}
		})
	}
}

func TestV8ValidationRejectsLegacyWriteLayout(t *testing.T) {
	for _, raw := range []string{
		"debug: true", "server: {port: 8317}\nport: 8318", "api-keys: [client]",
		"codex-api-key: []", "codex: {}", "quota-exceeded: {antigravity-credits: true}",
		"home: {enabled: true}", "enable-gemini-cli-endpoint: true", "unknown-root: true",
		"<<: {debug: true}",
	} {
		t.Run(raw, func(t *testing.T) {
			if _, err := ParseConfigBytes([]byte(raw)); err != nil {
				t.Fatalf("legacy file compatibility failed: %v", err)
			}
			if err := ValidateV8Config([]byte(raw)); err == nil {
				t.Fatal("v8 API accepted the legacy write layout")
			}
		})
	}
}

func TestV8SecretHashUsesNewPath(t *testing.T) {
	path := filepath.Join(t.TempDir(), "config.yaml")
	if err := os.WriteFile(path, []byte("management: {secret-key: test-secret}\n"), 0600); err != nil {
		t.Fatal(err)
	}
	cfg, err := LoadConfig(path)
	if err != nil {
		t.Fatal(err)
	}
	saved, _ := os.ReadFile(path)
	if !looksLikeBcrypt(cfg.RemoteManagement.SecretKey) || strings.Contains(string(saved), "test-secret") || strings.Contains(string(saved), "remote-management") {
		t.Fatal("secret was not hashed at the v8 path")
	}
}

func TestV8LegacyClientKeysDoNotOverwriteUpstreamGroups(t *testing.T) {
	path := filepath.Join(t.TempDir(), "config.yaml")
	if err := os.WriteFile(path, []byte("api-keys: {codex: [{name: upstream, base-url: 'https://example.invalid', keys: [{api-key: upstream-key}]}]}\n"), 0600); err != nil {
		t.Fatal(err)
	}
	cfg, err := LoadConfig(path)
	if err != nil {
		t.Fatal(err)
	}
	cfg.APIKeys = []string{"client-key"}
	if err = SaveConfigPreserveComments(path, cfg); err != nil {
		t.Fatal(err)
	}
	cfg, err = LoadConfig(path)
	if err != nil {
		t.Fatal(err)
	}
	if len(cfg.APIKeys) != 1 || cfg.APIKeys[0] != "client-key" || len(cfg.CodexKey) != 1 {
		t.Fatal("client/upstream key collision lost credentials")
	}
}

func TestV8AliasesAndMergeKeys(t *testing.T) {
	raw := []byte(`routing: &routing
  strategy: round-robin
  retry: {request-retry: 0}
codex-api-key:
  - &key
    api-key: first
    base-url: https://example.invalid
    request-retry: 0
  - <<: *key
    api-key: second
api-keys:
  gemini:
    - &upstream
      name: first
      base-url: https://example.invalid
      request-retry: 2
      keys: [{api-key: one}]
    - <<: *upstream
      name: second
      keys: [{api-key: two, request-retry: 0}]
`)
	before, err := ParseConfigBytes(raw)
	if err != nil {
		t.Fatal(err)
	}
	migrated, _, err := NormalizeConfigLayout(raw, true)
	if err != nil {
		t.Fatal(err)
	}
	after, err := ParseConfigBytes(migrated)
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(before, after) || len(after.CodexKey) != 2 || len(after.GeminiKey) != 2 {
		t.Fatal("alias migration changed effective credentials")
	}
	if *after.GeminiKey[0].RequestRetry != 2 || *after.GeminiKey[1].RequestRetry != 0 {
		t.Fatal("merge override precedence changed")
	}
}

func TestV8LegacyAddsExplicitZeroRetryOverride(t *testing.T) {
	for _, raw := range []string{
		"request-retry: 3\napi-keys: {codex: [{name: upstream, base-url: 'https://example.invalid', keys: [{api-key: upstream-key}]}]}\n",
		"request-retry: 3\ncodex-api-key: [{base-url: 'https://example.invalid', api-key: upstream-key}]\n",
	} {
		path := filepath.Join(t.TempDir(), "config.yaml")
		if err := os.WriteFile(path, []byte(raw), 0600); err != nil {
			t.Fatal(err)
		}
		cfg, err := LoadConfig(path)
		if err != nil {
			t.Fatal(err)
		}
		cfg.CodexKey[0].RequestRetry = new(0)
		if err = SaveConfigPreserveComments(path, cfg); err != nil {
			t.Fatal(err)
		}
		cfg, err = LoadConfig(path)
		if err != nil {
			t.Fatal(err)
		}
		if cfg.CodexKey[0].RequestRetry == nil || *cfg.CodexKey[0].RequestRetry != 0 {
			t.Fatal("new zero retry override was discarded")
		}
	}
}

func TestV8LegacyNullRouting(t *testing.T) {
	for _, raw := range []string{
		"port: 8317\nrouting: null\n",
		"port: 8317\nrouting: ~\n",
		"port: 8317\nrouting:\n",
		"port: 8317\nrouting: null\nrequest-retry: 3\n",
		"server: {port: 8317}\nrouting: null\n",
	} {
		t.Run(strings.TrimSpace(raw), func(t *testing.T) {
			cfg, err := ParseConfigBytes([]byte(raw))
			if err != nil {
				t.Fatalf("parse legacy null routing: %v", err)
			}
			if cfg.Port != 8317 || !reflect.DeepEqual(cfg.Routing, RoutingConfig{}) {
				t.Fatal("null routing did not preserve default routing")
			}
			path := filepath.Join(t.TempDir(), "config.yaml")
			if err = os.WriteFile(path, []byte(raw), 0600); err != nil {
				t.Fatal(err)
			}
			loaded, err := LoadConfig(path)
			if err != nil {
				t.Fatal(err)
			}
			data, err := os.ReadFile(path)
			if err != nil || string(data) != raw {
				t.Fatalf("loading legacy null routing rewrote the file: %v", err)
			}
			if err = SaveConfigPreserveComments(path, loaded, true); err != nil {
				t.Fatalf("migrate null routing: %v", err)
			}
			reloaded, err := LoadConfig(path)
			if err != nil || !reflect.DeepEqual(loaded.Routing, reloaded.Routing) || loaded.RequestRetry != reloaded.RequestRetry {
				t.Fatalf("migrating null routing changed effective settings: %v", err)
			}
		})
	}
	for _, raw := range []string{"routing: false", "routing: []", "routing: {retry: false}", "server: null"} {
		if _, err := ParseConfigBytes([]byte(raw)); err == nil {
			t.Errorf("accepted invalid container: %s", raw)
		}
	}
}

func TestV8SecretHashResolvesReferences(t *testing.T) {
	const secret = "test-management-reference-secret"
	for _, tc := range []struct {
		name        string
		raw         string
		path        string
		allowRemote bool
	}{
		{"v8 alias", "defaults: &management\n  secret-key: " + secret + "\n  allow-remote: true\nmanagement: *management\nother: *management\n", "management", true},
		{"v8 merge", "defaults: &management\n  secret-key: " + secret + "\n  allow-remote: true\nmanagement: {<<: *management, allow-remote: false}\nother: *management\n", "management", false},
		{"v8 scalar alias", "password: &password " + secret + "\nmanagement: {secret-key: *password, allow-remote: true}\n", "management", true},
		{"v8 root merge", "defaults: &root\n  management: {secret-key: " + secret + ", allow-remote: true}\n<<: *root\n", "management", true},
		{"v8 wins legacy", "remote-management: {secret-key: stale-secret, allow-remote: false}\ndefaults: &management {secret-key: " + secret + ", allow-remote: true}\nmanagement: *management\n", "management", true},
		{"legacy alias", "defaults: &management\n  secret-key: " + secret + "\n  allow-remote: true\nremote-management: *management\nother: *management\n", "remote-management", true},
	} {
		t.Run(tc.name, func(t *testing.T) {
			path := filepath.Join(t.TempDir(), "config.yaml")
			raw := tc.raw + "# Keep this comment\nport: 8317\n"
			if err := os.WriteFile(path, []byte(raw), 0600); err != nil {
				t.Fatal(err)
			}
			cfg, err := LoadConfig(path)
			if err != nil {
				t.Fatal(err)
			}
			saved, err := os.ReadFile(path)
			if err != nil {
				t.Fatal(err)
			}
			var doc yaml.Node
			if err = yaml.Unmarshal(saved, &doc); err != nil {
				t.Fatal(err)
			}
			effective := expandConfigAliases(doc.Content[0])
			stored := yamlPath(effective, tc.path+".secret-key")
			if stored == nil || stored.Value != cfg.RemoteManagement.SecretKey || !looksLikeBcrypt(stored.Value) {
				t.Fatal("effective management secret was not hashed at the correct path")
			}
			if cfg.RemoteManagement.AllowRemote != tc.allowRemote || cfg.Port != 8317 || !strings.Contains(string(saved), "# Keep this comment") {
				t.Fatal("hashing changed unrelated settings or comments")
			}
			if other := yamlPath(effective, "other.secret-key"); other != nil && other.Value != secret {
				t.Fatal("hashing mutated another use of the shared anchor")
			}
			if tc.path == "management" && yamlPath(effective, "remote-management.secret-key") != nil {
				t.Fatal("hashing left a conflicting legacy secret")
			}
			reloaded, err := LoadConfig(path)
			if err != nil {
				t.Fatal(err)
			}
			second, err := os.ReadFile(path)
			if err != nil || string(second) != string(saved) || reloaded.RemoteManagement.SecretKey != cfg.RemoteManagement.SecretKey || reloaded.RemoteManagement.AllowRemote != tc.allowRemote {
				t.Fatalf("second load changed the persisted hash or settings: %v", err)
			}
		})
	}
}

func TestV8ExampleDocumentsSessionAffinityCompactionGuidance(t *testing.T) {
	example, errRead := os.ReadFile(filepath.Join("..", "..", "config.example.yaml"))
	if errRead != nil {
		t.Fatal(errRead)
	}
	var doc yaml.Node
	if errUnmarshal := yaml.Unmarshal(example, &doc); errUnmarshal != nil {
		t.Fatal(errUnmarshal)
	}
	routing := yamlPath(doc.Content[0], "routing")
	if routing == nil {
		t.Fatal("config.example.yaml is missing routing block")
	}
	idx := findMapKeyIndex(routing, "session-affinity")
	if idx < 0 {
		t.Fatal("config.example.yaml is missing routing.session-affinity")
	}
	comment := routing.Content[idx].HeadComment
	for _, required := range []string{
		"Merkle LCP prefix/compaction matching",
		"2 shared trailing turns",
		"replaces all earlier turns with a summary",
		"send X-Session-ID",
	} {
		if !strings.Contains(comment, required) {
			t.Fatalf("routing.session-affinity comment in config.example.yaml is missing %q; got:\n%s", required, comment)
		}
	}
}