User authorized lifting CURRENT FAV whole-branch merge protection on 2026-09-23. Keep FAV_DEGRADED_READ/FAV_PUBLISHER_ENABLED default false. Drop duplicate goose 00034 (local_dev already has 00036). Preserve agentx workspace admin routes and pin-export wiring.
318 lines
9.9 KiB
Go
318 lines
9.9 KiB
Go
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)
|
|
}
|