package config import ( "os" "path/filepath" "testing" "time" ) func TestLoadPrefersEnvLLMAPIKey(t *testing.T) { t.Setenv("LLM_API_KEY", "env-priority-key") configPath := writeTestConfig(t, ` llm: provider: ark api_key: "config-file-key" `) cfg, err := Load(configPath) if err != nil { t.Fatalf("Load() error = %v", err) } if cfg.LLM.APIKey != "env-priority-key" { t.Fatalf("expected env api key, got %q", cfg.LLM.APIKey) } } func TestLoadFallsBackToConfigLLMAPIKey(t *testing.T) { t.Setenv("LLM_API_KEY", "") configPath := writeTestConfig(t, ` llm: provider: ark api_key: "config-file-key" `) cfg, err := Load(configPath) if err != nil { t.Fatalf("Load() error = %v", err) } if cfg.LLM.APIKey != "config-file-key" { t.Fatalf("expected config api key, got %q", cfg.LLM.APIKey) } } func TestLoadPrefersEnvRetrievalAndQdrantSettings(t *testing.T) { t.Setenv("SILICONFLOW_API_KEY", "siliconflow-env-key") t.Setenv("SILICONFLOW_EMBEDDING_MODEL", "env-embedding") t.Setenv("SILICONFLOW_RERANKER_MODEL", "env-reranker") t.Setenv("QDRANT_API_KEY", "qdrant-env-key") t.Setenv("QDRANT_URL", "qdrant-env:6334") t.Setenv("QDRANT_COLLECTION", "env_collection") configPath := writeTestConfig(t, ` qdrant: url: qdrant-config:6334 api_key: "qdrant-config-key" collection: "config_collection" retrieval: provider: siliconflow api_key: "retrieval-config-key" embedding_model: "config-embedding" reranker_model: "config-reranker" `) cfg, err := Load(configPath) if err != nil { t.Fatalf("Load() error = %v", err) } if cfg.Retrieval.APIKey != "siliconflow-env-key" { t.Fatalf("expected env retrieval api key, got %q", cfg.Retrieval.APIKey) } if cfg.Retrieval.EmbeddingModel != "env-embedding" { t.Fatalf("expected env embedding model, got %q", cfg.Retrieval.EmbeddingModel) } if cfg.Retrieval.RerankerModel != "env-reranker" { t.Fatalf("expected env reranker model, got %q", cfg.Retrieval.RerankerModel) } if cfg.Qdrant.APIKey != "qdrant-env-key" { t.Fatalf("expected env qdrant api key, got %q", cfg.Qdrant.APIKey) } if cfg.Qdrant.URL != "qdrant-env:6334" { t.Fatalf("expected env qdrant url, got %q", cfg.Qdrant.URL) } if cfg.Qdrant.Collection != "env_collection" { t.Fatalf("expected env qdrant collection, got %q", cfg.Qdrant.Collection) } } func TestLoadAppliesBrandLibraryDefaults(t *testing.T) { configPath := writeTestConfig(t, ` brand_library: {} `) cfg, err := Load(configPath) if err != nil { t.Fatalf("Load() error = %v", err) } if cfg.BrandLibrary.FreeBrandLimit != 1 { t.Fatalf("expected default free brand limit 1, got %d", cfg.BrandLibrary.FreeBrandLimit) } if cfg.BrandLibrary.PaidBrandLimit != 2 { t.Fatalf("expected default paid brand limit 2, got %d", cfg.BrandLibrary.PaidBrandLimit) } if cfg.BrandLibrary.MaxKeywords != 5 { t.Fatalf("expected default max keywords 5, got %d", cfg.BrandLibrary.MaxKeywords) } if cfg.BrandLibrary.MaxQuestionsPerKeyword != 5 { t.Fatalf("expected default max questions per keyword 5, got %d", cfg.BrandLibrary.MaxQuestionsPerKeyword) } } func TestLoadAppliesSchedulerHTTPDefaultsAndDisableSentinel(t *testing.T) { defaultConfigPath := writeTestConfig(t, ` scheduler: {} `) cfg, err := Load(defaultConfigPath) if err != nil { t.Fatalf("Load() default error = %v", err) } if cfg.Scheduler.HTTPHost != "127.0.0.1" { t.Fatalf("expected default scheduler http host 127.0.0.1, got %q", cfg.Scheduler.HTTPHost) } if cfg.Scheduler.HTTPPort != 8081 { t.Fatalf("expected default scheduler http port 8081, got %d", cfg.Scheduler.HTTPPort) } disabledConfigPath := writeTestConfig(t, ` scheduler: http_port: -1 `) cfg, err = Load(disabledConfigPath) if err != nil { t.Fatalf("Load() disabled error = %v", err) } if cfg.Scheduler.HTTPPort != -1 { t.Fatalf("expected scheduler http port -1 to disable server, got %d", cfg.Scheduler.HTTPPort) } } func TestLoadPrefersEnvSchedulerMetricsSettings(t *testing.T) { t.Setenv("SCHEDULER_HTTP_HOST", "0.0.0.0") t.Setenv("SCHEDULER_INTERNAL_METRICS_TOKEN", "env-metrics-token") configPath := writeTestConfig(t, ` scheduler: http_host: 127.0.0.1 internal_metrics_token: config-token `) cfg, err := Load(configPath) if err != nil { t.Fatalf("Load() error = %v", err) } if cfg.Scheduler.HTTPHost != "0.0.0.0" { t.Fatalf("expected env scheduler http host, got %q", cfg.Scheduler.HTTPHost) } if cfg.Scheduler.InternalMetricsToken != "env-metrics-token" { t.Fatalf("expected env scheduler metrics token, got %q", cfg.Scheduler.InternalMetricsToken) } } func TestLoadPrefersEnvJWTSettings(t *testing.T) { t.Setenv("JWT_SECRET", "env-jwt-secret") t.Setenv("JWT_ACCESS_TTL", "20m") t.Setenv("JWT_REFRESH_TTL", "720h") configPath := writeTestConfig(t, ` jwt: secret: config-jwt-secret access_ttl: 15m refresh_ttl: 168h `) cfg, err := Load(configPath) if err != nil { t.Fatalf("Load() error = %v", err) } if cfg.JWT.Secret != "env-jwt-secret" { t.Fatalf("expected env jwt secret, got %q", cfg.JWT.Secret) } if cfg.JWT.AccessTTL != 20*time.Minute { t.Fatalf("expected env jwt access ttl 20m, got %s", cfg.JWT.AccessTTL) } if cfg.JWT.RefreshTTL != 720*time.Hour { t.Fatalf("expected env jwt refresh ttl 720h, got %s", cfg.JWT.RefreshTTL) } } func TestLoadAppliesMembershipDefaults(t *testing.T) { configPath := writeTestConfig(t, ` membership: {} `) cfg, err := Load(configPath) if err != nil { t.Fatalf("Load() error = %v", err) } if cfg.Membership.DefaultPlanCode != "free" { t.Fatalf("expected default plan code free, got %q", cfg.Membership.DefaultPlanCode) } if len(cfg.Membership.Plans) != 3 { t.Fatalf("expected 3 default plans, got %d", len(cfg.Membership.Plans)) } free, ok := cfg.Membership.FindPlan("free") if !ok { t.Fatalf("expected free plan to exist") } if free.ArticleGeneration != 3 { t.Fatalf("expected free article generation 3, got %d", free.ArticleGeneration) } if free.ImageStorageBytes != 10*1024*1024 { t.Fatalf("expected free image storage 10MB, got %d", free.ImageStorageBytes) } if free.CompanyLimit != 1 { t.Fatalf("expected free company limit 1, got %d", free.CompanyLimit) } if free.SubscriptionDuration != 72*time.Hour { t.Fatalf("expected free subscription duration 72h, got %s", free.SubscriptionDuration) } if !free.ContactAdminOnExpiry { t.Fatalf("expected free plan to require admin contact on expiry") } pro, ok := cfg.Membership.FindPlan("pro") if !ok { t.Fatalf("expected pro plan to exist") } if pro.ArticleGeneration != 400 { t.Fatalf("expected pro article generation 400, got %d", pro.ArticleGeneration) } if pro.CompanyLimit != 2 { t.Fatalf("expected pro company limit 2, got %d", pro.CompanyLimit) } } func writeTestConfig(t *testing.T, body string) string { t.Helper() dir := t.TempDir() path := filepath.Join(dir, "config.yaml") if err := os.WriteFile(path, []byte(body), 0o600); err != nil { t.Fatalf("write config: %v", err) } return path }