package config_test import ( "os" "path/filepath" "strings" "testing" "time" "chat-agent-system/internal/config" ) func TestConfigLoadDefaultsAndFile(t *testing.T) { t.Setenv("ENV", "") t.Setenv("DATA_DIR", "") t.Setenv("AGENT_BACKEND", "") t.Setenv("AGENT_TIMEOUT", "") t.Setenv("KB_DB_PORT", "") t.Setenv("KB_DB_PASSWORD", "") t.Setenv("KB_DB_HOST", "") t.Setenv("POSTGRES_SHARED_KB_DSN", "") t.Setenv("APP_DB_DSN", "") t.Setenv("HTTP_ADDR", "") tmpDir := t.TempDir() envPath := filepath.Join(tmpDir, ".env") envContent := `ENV=dev DATA_DIR=./data/test_data AGENT_BACKEND=pi-agent AGENT_TIMEOUT=15m KB_DB_PORT=55432 ` if err := os.WriteFile(envPath, []byte(envContent), 0o644); err != nil { t.Fatalf("failed to write test env: %v", err) } cfg, err := config.Load(envPath) if err != nil { t.Fatalf("unexpected error loading config: %v", err) } if cfg.Env != "dev" { t.Errorf("expected Env 'dev', got %q", cfg.Env) } if cfg.DataDir != "./data/test_data" { t.Errorf("expected DataDir './data/test_data', got %q", cfg.DataDir) } if cfg.AgentBackend != "pi-agent" { t.Errorf("expected AgentBackend 'pi-agent', got %q", cfg.AgentBackend) } if cfg.AgentTimeout != 15*time.Minute { t.Errorf("expected AgentTimeout 15m, got %v", cfg.AgentTimeout) } if cfg.KbDbPort != 55432 { t.Errorf("expected KbDbPort 55432, got %d", cfg.KbDbPort) } } func TestConfigLoadDSNOverrides(t *testing.T) { t.Setenv("APP_DB_DSN", "postgres://app_user:secret_pass@system-db:5432/agent_system_dev?sslmode=disable") t.Setenv("POSTGRES_SHARED_KB_DSN", "postgres://kb_user:kb_pass@shuimu-kb-shared:5432/regulations?sslmode=disable") t.Setenv("HTTP_ADDR", ":9090") t.Setenv("PI_AGENT_RUNNER_URL", "http://pi-agent:18865") cfg, err := config.Load("/dev/null") if err != nil { t.Fatalf("unexpected error loading config: %v", err) } if cfg.ContextDbHost != "system-db" || cfg.ContextDbPort != 5432 || cfg.ContextDbUser != "app_user" || cfg.ContextDbName != "agent_system_dev" { t.Fatalf("unexpected context db config: %+v", cfg) } // 宿主机运行时会将 Compose DNS 归一化为 127.0.0.1(容器内保留原名) wantHost := "shuimu-kb-shared" if _, err := os.Stat("/.dockerenv"); err != nil { wantHost = "127.0.0.1" } if cfg.KbDbHost != wantHost || cfg.KbDbPort != 5432 || cfg.KbDbUser != "kb_user" || cfg.KbDbName != "regulations" { t.Fatalf("unexpected kb db config: host=%s want=%s cfg=%+v", cfg.KbDbHost, wantHost, cfg) } if cfg.HTTPPort != 9090 || cfg.HTTPHost != "0.0.0.0" { t.Fatalf("expected HTTP 0.0.0.0:9090, got %s:%d", cfg.HTTPHost, cfg.HTTPPort) } if cfg.PiAgentRunnerURL != "http://pi-agent:18865" { t.Fatalf("expected pi-agent runner url override, got %q", cfg.PiAgentRunnerURL) } } func TestConfigLoadRuntimeEnvironmentOverrides(t *testing.T) { t.Setenv("ENV", "dev") t.Setenv("AGENT_TIMEOUT", "47m") t.Setenv("AGENTX_API_KEY", "agentx-test-key") t.Setenv("AGENTX_MODEL", "agentx-test-model") t.Setenv("AGENTX_API_BASE", "https://example.invalid/v1") t.Setenv("AGENTX_TIMEOUT", "5m") t.Setenv("PI_AGENT_BRIDGE_BIN", "/test/pi-bridge.mjs") t.Setenv("EMBEDDING_MODEL", "embedding-test-model") t.Setenv("RERANKER_MODEL", "reranker-test-model") cfg, err := config.Load("/dev/null") if err != nil { t.Fatalf("unexpected error loading config: %v", err) } if cfg.AgentTimeout != 47*time.Minute { t.Fatalf("expected environment AgentTimeout 47m, got %v", cfg.AgentTimeout) } if cfg.AgentXAPIKey != "agentx-test-key" || cfg.AgentXModel != "agentx-test-model" || cfg.AgentXAPIBase != "https://example.invalid/v1" || cfg.AgentXTimeout != 5*time.Minute { t.Fatalf("agentx environment overrides were not applied: %+v", cfg.AgentXAPIKey) } if cfg.PiAgentBridgeBin != "/test/pi-bridge.mjs" { t.Fatalf("pi bridge environment override was not applied") } if cfg.EmbeddingModel != "embedding-test-model" || cfg.RerankerModel != "reranker-test-model" { t.Fatalf("retrieval model environment overrides were not applied") } } func TestConfigLoadCORSDefaultsForDev(t *testing.T) { cfg, err := config.Load("/dev/null") if err != nil { t.Fatalf("unexpected error: %v", err) } if len(cfg.CORSAllowedOrigins) == 0 { t.Fatal("expected default CORS origins for dev") } } func TestConfigLoadRejectsAllowInMemoryInMain(t *testing.T) { t.Setenv("ENV", "main") t.Setenv("KB_DB_PASSWORD", "kb-prod-secret-xyz") t.Setenv("CONTEXT_DB_PASSWORD", "ctx-prod-secret-xyz") t.Setenv("ADMIN_PASSWORD", "admin-prod-secret-xyz") t.Setenv("ALLOW_IN_MEMORY", "true") _, err := config.Load("/dev/null") if err == nil { t.Fatal("expected config load to fail when ALLOW_IN_MEMORY=true in main") } } func TestConfigLoadRejectsWeakSecretsOutsideDev(t *testing.T) { t.Setenv("ENV", "main") t.Setenv("KB_DB_PASSWORD", "") t.Setenv("CONTEXT_DB_PASSWORD", "") t.Setenv("ADMIN_PASSWORD", "") _, err := config.Load("/dev/null") if err == nil { t.Fatal("expected config load to fail for missing secrets in main env") } } func TestValidateIndependentVault(t *testing.T) { if err := config.ValidateIndependentVault("", "./data/dev"); err != nil { t.Fatal(err) } if err := config.ValidateIndependentVault("./data/dev", "./data/dev"); err == nil { t.Fatal("runtime data/dev must be rejected") } if err := config.ValidateIndependentVault("./data/dev", "./other"); err == nil { t.Fatal("expected reject data/dev root") } if err := config.ValidateIndependentVault("/var/vault", "/var/vault"); err == nil { t.Fatal("expected reject equal DATA_DIR") } if err := config.ValidateIndependentVault("./data/vault/dev", "./data/dev"); err != nil { t.Fatalf("independent vault root should pass: %v", err) } if err := config.ValidateIndependentVault("/var/vault", "./data/dev"); err != nil { t.Fatal(err) } } func TestConfigLoadRejectsProductionDatabasePlaceholder(t *testing.T) { t.Setenv("ENV", "prod") t.Setenv("KB_DB_PASSWORD", "kb-prod-secret-xyz") t.Setenv("CONTEXT_DB_PASSWORD", "prod_highly_secure_db_pass") t.Setenv("ADMIN_PASSWORD", "admin-prod-secret-xyz") _, err := config.Load("/dev/null") if err == nil { t.Fatal("expected config load to reject the documented production database placeholder") } } func TestFAVFlagsDefaultOff(t *testing.T) { t.Setenv("FAV_DEGRADED_READ", "") t.Setenv("FAV_PUBLISHER_ENABLED", "") cfg, err := config.Load("/dev/null") if err != nil { t.Fatal(err) } if cfg.FAVDegradedRead { t.Fatal("FAV_DEGRADED_READ must default false") } if cfg.FAVPublisherEnabled { t.Fatal("FAV_PUBLISHER_ENABLED must default false") } } func TestFAVFlagsOptInOnly(t *testing.T) { t.Setenv("FAV_DEGRADED_READ", "true") t.Setenv("FAV_PUBLISHER_ENABLED", "1") t.Setenv("VAULT_STATE_ROOT", "/tmp/vault-independent-test") cfg, err := config.Load("/dev/null") if err != nil { t.Fatal(err) } if !cfg.FAVDegradedRead || !cfg.FAVPublisherEnabled { t.Fatalf("expected explicit opt-in: %+v", cfg) } } func TestConfigDefaultKbPasswordEmpty(t *testing.T) { t.Setenv("KB_DB_PASSWORD", "") t.Setenv("KB_DB_HOST", "") t.Setenv("POSTGRES_SHARED_KB_DSN", "") cfg, err := config.Load("/dev/null") if err != nil { t.Fatal(err) } if cfg.KbDbPassword != "" { t.Fatalf("expected empty default KB password, got %q", cfg.KbDbPassword) } if err := cfg.RequireKbPassword(); err == nil { t.Fatal("expected RequireKbPassword to fail when empty") } } func TestConfigLayeredLoadMergesRootPassword(t *testing.T) { t.Setenv("KB_DB_PASSWORD", "") t.Setenv("KB_DB_HOST", "") t.Setenv("KB_DB_PORT", "") t.Setenv("KB_DB_USER", "") t.Setenv("POSTGRES_SHARED_KB_DSN", "") t.Setenv("ENV", "") t.Setenv("DATA_DIR", "") root := t.TempDir() old, err := os.Getwd() if err != nil { t.Fatal(err) } if err := os.Chdir(root); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.Chdir(old) }) if err := os.MkdirAll("configs", 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(".env", []byte("KB_DB_PASSWORD=layered-real-pass\nKB_DB_HOST=shuimu-kb-shared\n"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile("configs/.env.dev", []byte("ENV=dev\nKB_DB_PORT=5432\nKB_DB_USER=regkb\n"), 0o644); err != nil { t.Fatal(err) } cfg, err := config.Load("") if err != nil { t.Fatalf("Load: %v", err) } if cfg.KbDbPassword != "layered-real-pass" { t.Fatalf("expected layered password from root .env, got %q", cfg.KbDbPassword) } if cfg.KbDbPassword == "shared_kb_secret_pass" { t.Fatal("must not use obsolete insecure default password") } wantHost := "127.0.0.1" if _, err := os.Stat("/.dockerenv"); err == nil { wantHost = "shuimu-kb-shared" } if cfg.KbDbHost != wantHost { t.Fatalf("expected host normalize to %s, got %s", wantHost, cfg.KbDbHost) } } func TestNormalizeKBHostForRuntime(t *testing.T) { cfg := &config.Config{KbDbHost: "shuimu-kb-shared"} config.NormalizeKBHostForRuntime(cfg) if _, err := os.Stat("/.dockerenv"); err == nil { if cfg.KbDbHost != "shuimu-kb-shared" { t.Fatalf("in docker must keep compose host, got %s", cfg.KbDbHost) } return } if cfg.KbDbHost != "127.0.0.1" { t.Fatalf("on host expected 127.0.0.1, got %s", cfg.KbDbHost) } } func TestRedactPostgresDSN(t *testing.T) { got := config.RedactPostgresDSN("postgres://regkb:super-secret@127.0.0.1:5432/regulations?sslmode=disable") if got != "postgres://regkb:***@127.0.0.1:5432/regulations?sslmode=disable" { t.Fatalf("unexpected redact: %s", got) } if strings.Contains(got, "super-secret") { t.Fatal("password leaked in redacted DSN") } } func TestLiveLayeredLoadHasNonDefaultPassword(t *testing.T) { if os.Getenv("CHECK_LIVE_KB_ENV") != "1" { t.Skip("set CHECK_LIVE_KB_ENV=1 to assert workspace root .env") } cfg, err := config.Load("") if err != nil { t.Fatal(err) } if cfg.KbDbPassword == "" || cfg.KbDbPassword == "shared_kb_secret_pass" { t.Fatalf("live Load missing real KB password (len=%d)", len(cfg.KbDbPassword)) } if cfg.KbDbHost != "127.0.0.1" && cfg.KbDbHost != "shuimu-kb-shared" { t.Fatalf("unexpected host %s", cfg.KbDbHost) } t.Logf("live kb host=%s port=%d password_set=true", cfg.KbDbHost, cfg.KbDbPort) }