Ctrl K
ComunidadGuías…

internal/config/clone_test.go

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

Archivo original ↗Preguntar a KiBot
package config

import (
	"reflect"
	"testing"

	"github.com/router-for-me/CLIProxyAPI/v8/internal/registry"
	"gopkg.in/yaml.v3"
)

func TestCloneForRuntimeNil(t *testing.T) {
	var cfg *Config
	if got := cfg.CloneForRuntime(); got != nil {
		t.Fatalf("CloneForRuntime() = %#v, want nil", got)
	}
}

func TestParseConfigBytes_AntigravitySensitiveWords(t *testing.T) {
	cfg, errParse := ParseConfigBytes([]byte(`antigravity:
  sensitive-words:
    - "API"
    - "proxy"
`))
	if errParse != nil {
		t.Fatalf("ParseConfigBytes() error = %v", errParse)
	}
	want := []string{"API", "proxy"}
	if !reflect.DeepEqual(cfg.Antigravity.SensitiveWords, want) {
		t.Fatalf("Antigravity.SensitiveWords = %#v, want %#v", cfg.Antigravity.SensitiveWords, want)
	}
}

func TestParseConfigBytes_AntigravityConnectionPool(t *testing.T) {
	cfg, errParse := ParseConfigBytes([]byte(`antigravity:
  connection-pool:
    enabled: false
    idle-conn-timeout: "15s"
    max-idle-conns-per-host: 4
`))
	if errParse != nil {
		t.Fatalf("ParseConfigBytes() error = %v", errParse)
	}
	if cfg.Antigravity.ConnectionPool.Enabled == nil || *cfg.Antigravity.ConnectionPool.Enabled != false {
		t.Fatalf("Enabled = %v, want false", cfg.Antigravity.ConnectionPool.Enabled)
	}
	if cfg.Antigravity.ConnectionPool.IdleConnTimeout != "15s" {
		t.Fatalf("IdleConnTimeout = %q, want 15s", cfg.Antigravity.ConnectionPool.IdleConnTimeout)
	}
	if cfg.Antigravity.ConnectionPool.MaxIdleConnsPerHost == nil || *cfg.Antigravity.ConnectionPool.MaxIdleConnsPerHost != 4 {
		t.Fatalf("MaxIdleConnsPerHost = %v, want 4", cfg.Antigravity.ConnectionPool.MaxIdleConnsPerHost)
	}
}

func TestCloneForRuntimeDeepCopiesConfig(t *testing.T) {
	cfg := sampleCloneRuntimeConfig()

	clone := cfg.CloneForRuntime()
	if clone == nil {
		t.Fatal("CloneForRuntime() = nil")
	}
	if clone == cfg {
		t.Fatal("CloneForRuntime() returned original pointer")
	}

	mutateOriginalConfig(cfg)

	if clone.Home.Host != "home.local" {
		t.Fatalf("clone.Home.Host = %q, want home.local", clone.Home.Host)
	}
	if clone.APIKeys[0] != "client-key" {
		t.Fatalf("clone.APIKeys[0] = %q, want client-key", clone.APIKeys[0])
	}
	if clone.OAuthExcludedModels["codex"][0] != "hidden-model" {
		t.Fatalf("clone.OAuthExcludedModels[codex][0] = %q, want hidden-model", clone.OAuthExcludedModels["codex"][0])
	}
	if clone.OAuthModelAlias["codex"][0].Alias != "client-model" {
		t.Fatalf("clone.OAuthModelAlias[codex][0].Alias = %q, want client-model", clone.OAuthModelAlias["codex"][0].Alias)
	}
	if got := pluginRawScalar(t, clone.Plugins.Configs["sample"].Raw, "mode"); got != "first" {
		t.Fatalf("clone plugin raw mode = %q, want first", got)
	}
	if clone.OpenAICompatibility[0].Models[0].Thinking.Levels[0] != "low" {
		t.Fatalf("clone thinking level = %q, want low", clone.OpenAICompatibility[0].Models[0].Thinking.Levels[0])
	}
	if got := clone.Payload.Default[0].Params["object"].(map[string]any)["key"]; got != "value" {
		t.Fatalf("clone payload object key = %#v, want value", got)
	}

	clone.APIKeys[0] = "clone-client-key"
	clone.OAuthExcludedModels["codex"][0] = "clone-hidden-model"
	clone.OAuthModelAlias["codex"][0].Alias = "clone-client-model"
	clone.OpenAICompatibility[0].Models[0].Thinking.Levels[0] = "clone-low"
	clone.Payload.Default[0].Params["object"].(map[string]any)["key"] = "clone-value"
	plugin := clone.Plugins.Configs["sample"]
	setPluginRawScalar(t, &plugin.Raw, "mode", "third")
	clone.Plugins.Configs["sample"] = plugin

	if cfg.APIKeys[0] != "mutated-client-key" {
		t.Fatalf("cfg.APIKeys[0] = %q, want mutated-client-key", cfg.APIKeys[0])
	}
	if cfg.OAuthExcludedModels["codex"][0] != "mutated-hidden-model" {
		t.Fatalf("cfg.OAuthExcludedModels[codex][0] = %q, want mutated-hidden-model", cfg.OAuthExcludedModels["codex"][0])
	}
	if cfg.OAuthModelAlias["codex"][0].Alias != "mutated-client-model" {
		t.Fatalf("cfg.OAuthModelAlias[codex][0].Alias = %q, want mutated-client-model", cfg.OAuthModelAlias["codex"][0].Alias)
	}
	if got := pluginRawScalar(t, cfg.Plugins.Configs["sample"].Raw, "mode"); got != "second" {
		t.Fatalf("cfg plugin raw mode = %q, want second", got)
	}
	if cfg.OpenAICompatibility[0].Models[0].Thinking.Levels[0] != "mutated-low" {
		t.Fatalf("cfg thinking level = %q, want mutated-low", cfg.OpenAICompatibility[0].Models[0].Thinking.Levels[0])
	}
	if got := cfg.Payload.Default[0].Params["object"].(map[string]any)["key"]; got != "mutated-value" {
		t.Fatalf("cfg payload object key = %#v, want mutated-value", got)
	}
}

func TestCloneForRuntimeDoesNotShareReferenceFields(t *testing.T) {
	cfg := sampleCloneRuntimeConfig()
	clone := cfg.CloneForRuntime()

	assertNoSharedRuntimeReferences(t, reflect.ValueOf(cfg), reflect.ValueOf(clone), "Config")
}

func sampleCloneRuntimeConfig() *Config {
	cacheStrict := true
	bypassStrict := false
	pluginEnabled := false
	cacheUserID := true

	return &Config{
		SDKConfig: SDKConfig{
			APIKeys: []string{"client-key"},
			Streaming: StreamingConfig{
				KeepAliveSeconds:      3,
				BootstrapRetries:      2,
				SendKeepAliveBeforeFT: true,
			},
		},
		Home: HomeConfig{
			Enabled: true,
			Host:    "home.local",
			Port:    8081,
			TLS: HomeTLSConfig{
				Enable:              true,
				ServerName:          "home.local",
				CACert:              "ca",
				ClientCert:          "cert",
				ClientKey:           "key",
				UseTargetServerName: true,
			},
		},
		Plugins: PluginsConfig{
			Enabled:      true,
			Dir:          "plugins",
			StoreSources: []string{"https://plugins.example/store.json"},
			Configs: map[string]PluginInstanceConfig{
				"sample": {
					Enabled:  &pluginEnabled,
					Priority: 10,
					Raw:      samplePluginRawNode("first"),
				},
			},
		},
		AntigravitySignatureCacheEnabled: &cacheStrict,
		AntigravitySignatureBypassStrict: &bypassStrict,
		Antigravity: AntigravityConfig{
			SensitiveWords:        []string{"Codex"},
			SensitiveWordsReplace: map[string]string{"You are Codex": "You are an assistant"},
		},
		Devin: DevinConfig{
			SensitiveWords:        []string{"DevinWord"},
			SensitiveWordsReplace: map[string]string{"Claude Code": "Devin"},
		},
		GeminiKey: []GeminiKey{{
			APIKey:         "gemini-key",
			Models:         []GeminiModel{{Name: "gemini-upstream", Alias: "gemini-upstream-alias"}},
			Headers:        map[string]string{"X-Gemini": "one"},
			ExcludedModels: []string{"gemini-hidden"},
		}},
		CodexKey: []CodexKey{{
			APIKey:         "codex-key",
			Models:         []CodexModel{{Name: "codex-upstream", Alias: "codex-client"}},
			Headers:        map[string]string{"X-Codex": "one"},
			ExcludedModels: []string{"codex-hidden-key"},
		}},
		ClaudeKey: []ClaudeKey{{
			APIKey:         "claude-key",
			Models:         []ClaudeModel{{Name: "claude-upstream", Alias: "claude-client"}},
			Headers:        map[string]string{"X-Claude": "one"},
			ExcludedModels: []string{"claude-hidden"},
			Cloak: &CloakConfig{
				SensitiveWords:        []string{"secret"},
				SensitiveWordsReplace: map[string]string{"Claude Code": "Claude"},
				CacheUserID:           &cacheUserID,
			},
		}},
		OpenAICompatibility: []OpenAICompatibility{{
			Name:          "compat",
			APIKeyEntries: []OpenAICompatibilityAPIKey{{APIKey: "compat-key", ProxyURL: "http://proxy.local"}},
			Models: []OpenAICompatibilityModel{{
				Name:     "compat-upstream",
				Alias:    "compat-client",
				Thinking: &registry.ThinkingSupport{Levels: []string{"low", "high"}},
			}},
			Headers: map[string]string{"X-Compat": "one"},
		}},
		VertexCompatAPIKey: []VertexCompatKey{{
			APIKey:         "vertex-key",
			Headers:        map[string]string{"X-Vertex": "one"},
			Models:         []VertexCompatModel{{Name: "vertex-upstream", Alias: "vertex-client"}},
			ExcludedModels: []string{"vertex-hidden"},
		}},
		OAuthExcludedModels: map[string][]string{
			"codex": {"hidden-model"},
		},
		OAuthModelAlias: map[string][]OAuthModelAlias{
			"codex": {{Name: "upstream-model", Alias: "client-model", Fork: true}},
		},
		Payload: PayloadConfig{
			Default: []PayloadRule{{
				Models: []PayloadModelRule{{
					Name:    "model-*",
					Headers: map[string]string{"X-Tier": "gold"},
					Match:   []map[string]any{{"tier": "gold"}},
					Exist:   []string{"$.messages"},
				}},
				Params: map[string]any{
					"object": map[string]any{"key": "value"},
					"array":  []any{"first", map[string]any{"nested": "value"}},
				},
			}},
			Filter: []PayloadFilterRule{{
				Models: []PayloadModelRule{{Name: "model-*"}},
				Params: []string{"$.secret"},
			}},
		},
	}
}

func mutateOriginalConfig(cfg *Config) {
	cfg.Home.Host = "mutated-home.local"
	cfg.APIKeys[0] = "mutated-client-key"
	cfg.OAuthExcludedModels["codex"][0] = "mutated-hidden-model"
	cfg.OAuthModelAlias["codex"][0].Alias = "mutated-client-model"
	cfg.OpenAICompatibility[0].Models[0].Thinking.Levels[0] = "mutated-low"
	cfg.Payload.Default[0].Params["object"].(map[string]any)["key"] = "mutated-value"
	plugin := cfg.Plugins.Configs["sample"]
	setPluginRawScalar(nil, &plugin.Raw, "mode", "second")
	cfg.Plugins.Configs["sample"] = plugin
}

func samplePluginRawNode(mode string) yaml.Node {
	modeValue := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: mode, Anchor: "modeAnchor"}
	return yaml.Node{
		Kind: yaml.MappingNode,
		Tag:  "!!map",
		Content: []*yaml.Node{
			{Kind: yaml.ScalarNode, Tag: "!!str", Value: "enabled"},
			{Kind: yaml.ScalarNode, Tag: "!!bool", Value: "false"},
			{Kind: yaml.ScalarNode, Tag: "!!str", Value: "mode"},
			modeValue,
			{Kind: yaml.ScalarNode, Tag: "!!str", Value: "mode-alias"},
			{Kind: yaml.AliasNode, Alias: modeValue},
		},
	}
}

func pluginRawScalar(t *testing.T, node yaml.Node, key string) string {
	t.Helper()
	for i := 0; i+1 < len(node.Content); i += 2 {
		if node.Content[i] != nil && node.Content[i].Value == key && node.Content[i+1] != nil {
			return node.Content[i+1].Value
		}
	}
	t.Fatalf("raw plugin node missing key %q", key)
	return ""
}

func setPluginRawScalar(t *testing.T, node *yaml.Node, key, value string) {
	if t != nil {
		t.Helper()
	}
	for i := 0; i+1 < len(node.Content); i += 2 {
		if node.Content[i] != nil && node.Content[i].Value == key && node.Content[i+1] != nil {
			node.Content[i+1].Value = value
			return
		}
	}
	if t != nil {
		t.Fatalf("raw plugin node missing key %q", key)
	}
}

func assertNoSharedRuntimeReferences(t *testing.T, original, clone reflect.Value, path string) {
	t.Helper()
	if !original.IsValid() || !clone.IsValid() {
		return
	}
	if original.Kind() == reflect.Interface {
		if original.IsNil() || clone.IsNil() {
			return
		}
		assertNoSharedRuntimeReferences(t, original.Elem(), clone.Elem(), path)
		return
	}
	if original.Kind() != clone.Kind() {
		t.Fatalf("%s kind mismatch: %s != %s", path, original.Kind(), clone.Kind())
	}

	switch original.Kind() {
	case reflect.Pointer:
		if original.IsNil() || clone.IsNil() {
			return
		}
		if original.Pointer() == clone.Pointer() {
			t.Fatalf("%s shares pointer %x", path, original.Pointer())
		}
		assertNoSharedRuntimeReferences(t, original.Elem(), clone.Elem(), path+"->"+original.Type().Elem().String())
	case reflect.Map:
		if original.IsNil() || clone.IsNil() {
			return
		}
		if original.Pointer() == clone.Pointer() {
			t.Fatalf("%s shares map pointer %x", path, original.Pointer())
		}
		iter := original.MapRange()
		for iter.Next() {
			key := iter.Key()
			assertNoSharedRuntimeReferences(t, iter.Value(), clone.MapIndex(key), path+"["+keyForPath(key)+"]")
		}
	case reflect.Slice:
		if original.IsNil() || clone.IsNil() {
			return
		}
		if original.Pointer() == clone.Pointer() {
			t.Fatalf("%s shares slice pointer %x", path, original.Pointer())
		}
		for i := 0; i < original.Len(); i++ {
			assertNoSharedRuntimeReferences(t, original.Index(i), clone.Index(i), path+"[]")
		}
	case reflect.Struct:
		for i := 0; i < original.NumField(); i++ {
			field := original.Type().Field(i)
			assertNoSharedRuntimeReferences(t, original.Field(i), clone.Field(i), path+"."+field.Name)
		}
	}
}

func keyForPath(key reflect.Value) string {
	if key.Kind() == reflect.String {
		return key.String()
	}
	return key.Type().String()
}

func TestParseConfigBytes_ProxyConnectionPool(t *testing.T) {
	cfg, err := ParseConfigBytes([]byte("api-keys:\n  codex:\n    - name: pool-group\n      base-url: https://ollama.com/v1\n      connection-pool:\n        enabled: true\n        idle-conn-timeout: 15s\n        max-idle-conns-per-host: 4\n      keys:\n        - api-key: pool-key\n"))
	if err != nil {
		t.Fatal(err)
	}
	if len(cfg.CodexKey) != 1 {
		t.Fatalf("codex entries = %d, want 1", len(cfg.CodexKey))
	}
	pool := cfg.CodexKey[0].ConnectionPool
	if pool == nil || pool.Enabled == nil || !*pool.Enabled || pool.IdleConnTimeout != "15s" || pool.MaxIdleConnsPerHost == nil || *pool.MaxIdleConnsPerHost != 4 {
		t.Fatalf("unexpected pool config: %+v", pool)
	}
	clone := cfg.CloneForRuntime()
	if clone.CodexKey[0].ConnectionPool == pool {
		t.Fatal("clone shares the pool block")
	}
	*clone.CodexKey[0].ConnectionPool.Enabled = false
	if !*cfg.CodexKey[0].ConnectionPool.Enabled {
		t.Fatal("clone shares enabled pointer")
	}
}

func TestV8Validate_SensitiveWordsReplace(t *testing.T) {
	v8YAML := []byte(`config-version: 8
oauth:
  providers:
    antigravity:
      sensitive-words:
        - "Codex"
      sensitive-words-replace:
        "You are Codex, an agent based on GPT-5.": "You are a helpful coding assistant."
    devin:
      sensitive-words:
        - "API"
      sensitive-words-replace:
        "Claude Code": "Devin Assistant"
api-keys:
  claude:
    - name: claude-1
      keys:
        - api-key: sk-ant-test
          cloak:
            sensitive-words:
              - "proxy"
            sensitive-words-replace:
              "Claude Code": "Claude"
`)
	if err := ValidateV8Config(v8YAML); err != nil {
		t.Fatalf("ValidateV8Config() error = %v", err)
	}
	cfg, errParse := ParseConfigBytes(v8YAML)
	if errParse != nil {
		t.Fatalf("ParseConfigBytes() error = %v", errParse)
	}
	if got := cfg.Antigravity.SensitiveWordsReplace["You are Codex, an agent based on GPT-5."]; got != "You are a helpful coding assistant." {
		t.Fatalf("Antigravity.SensitiveWordsReplace = %#v", cfg.Antigravity.SensitiveWordsReplace)
	}
	if got := cfg.Devin.SensitiveWordsReplace["Claude Code"]; got != "Devin Assistant" {
		t.Fatalf("Devin.SensitiveWordsReplace = %#v", cfg.Devin.SensitiveWordsReplace)
	}
	if len(cfg.ClaudeKey) != 1 || cfg.ClaudeKey[0].Cloak == nil || cfg.ClaudeKey[0].Cloak.SensitiveWordsReplace["Claude Code"] != "Claude" {
		t.Fatalf("ClaudeKey[0].Cloak = %#v", cfg.ClaudeKey)
	}
}