package config import ( "os" "path/filepath" "reflect" "testing" ) // 本文件锁定 GUARDRAIL_MAX_INPUT_CHARS 的配置侧契约(切片 G2):键名一致、三态可解。 // 派生与两条 WARN 的落点在组合根(cmd/cyrene/wire_guardrail.go 的 // deriveGuardrailInputLimit),这里只保证「键真的能配上、值真的能进来」。 // TestGuardrailMaxInputCharsEnvMatchesTag 钉住键名与 env 标签**逐字一致**: // 组合根的 WARN 用 GuardrailMaxInputCharsEnv 点出「改哪个键」,两处一旦漂移, // 日志就会把部署方指到不存在的键上。 func TestGuardrailMaxInputCharsEnvMatchesTag(t *testing.T) { t.Parallel() field, ok := reflect.TypeOf(GuardrailConfig{}).FieldByName("MaxInputChars") if !ok { t.Fatal("GuardrailConfig 里没有 MaxInputChars 字段") } if got := field.Tag.Get("env"); got != GuardrailMaxInputCharsEnv { t.Fatalf("MaxInputChars 的 env 标签 = %q, want %q", got, GuardrailMaxInputCharsEnv) } } // TestLoad_GuardrailMaxInputCharsTriState 钉住三态取值真的能配进来: // 未设置 ⇒ 0(= 按推导预算派生);进程环境或 .env 给出正数 ⇒ 覆盖值; // 负数 ⇒ **原样**进来(非法判定在组合根,那里负责记 WARN 并回落,不在这一层报错, // 免得一个笔误让整个进程起不来)。 func TestLoad_GuardrailMaxInputCharsTriState(t *testing.T) { dir := t.TempDir() t.Setenv("JWT_SECRET", "0123456789012345678901234567890123456789") unsetenvForTest(t, GuardrailMaxInputCharsEnv) cfg, err := Load(filepath.Join(dir, "absent.env")) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Guardrail.MaxInputChars != 0 { t.Fatalf("未配置时 = %d, want 0(0 = 按推导预算派生)", cfg.Guardrail.MaxInputChars) } // .env 里写了也算配上(与进程环境同一优先级链)。 envFile := filepath.Join(dir, "test.env") if err := os.WriteFile(envFile, []byte(GuardrailMaxInputCharsEnv+"=1000\n"), 0o600); err != nil { t.Fatalf("写 .env: %v", err) } cfg, err = Load(envFile) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Guardrail.MaxInputChars != 1000 { t.Fatalf(".env 配置时 = %d, want 1000", cfg.Guardrail.MaxInputChars) } // 负数原样进来:非法值的处理在组合根(WARN + 回落派生值),这一层不拦。 t.Setenv(GuardrailMaxInputCharsEnv, "-1") cfg, err = Load(envFile) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Guardrail.MaxInputChars != -1 { t.Fatalf("负数配置时 = %d, want -1(原样进来,判定在组合根)", cfg.Guardrail.MaxInputChars) } }