package config import ( "os" "testing" ) // reviewTimeoutEnvUnset 让 MEMORY_REVIEW_TIMEOUT_SEC 在本次用例中**真的未设置**。 // // 为什么不用 t.Setenv(key, ""):空串在 decode.go 的 lookupRaw 里**照样命中 env 分支** // (`os.LookupEnv` 对空值返回 ok=true)⇒ 配置层读出 0,于是**看不见 tag 上的字面 // 默认值** —— 而那正是本切片的回归面(tag 上再加回 `default:"20"` 必须变红)。 // 未设置才会走到 tag 默认那一档。 func reviewTimeoutEnvUnset(t *testing.T) { t.Helper() old, had := os.LookupEnv("MEMORY_REVIEW_TIMEOUT_SEC") t.Cleanup(func() { if had { _ = os.Setenv("MEMORY_REVIEW_TIMEOUT_SEC", old) return } _ = os.Unsetenv("MEMORY_REVIEW_TIMEOUT_SEC") }) if err := os.Unsetenv("MEMORY_REVIEW_TIMEOUT_SEC"); err != nil { t.Fatalf("unset MEMORY_REVIEW_TIMEOUT_SEC: %v", err) } } // TestLoadMemoryReviewConfigDefaults 锁定 D3「入库前审查」(design/23 §15.1 D3) // 的 env 契约默认值。它们必须与 memory 包的兜底默认值一致 —— 两处不一致会让 // 「没配这个键」与「配了但解析失败」走向不同的行为。 // // 🔴 TimeoutSec 是**例外的那一个,而且是刻意的**(切片 M16,2026-10-13): // 它**没有**字面默认值,未配置时就是 0,含义是「没配」而不是「0 秒」。 // 单条审查预算的唯一真源在 memory 包的 `defaultReviewTimeout`(90s), // 全链的生效值 = `ReviewConfig.Timeout` 经 `normalizeReviewConfig` 归一化之后的值。 // // 为什么不在这里断言 90:那会让本仓重新长出**两个**真源(env tag 一处、 // memory 常量一处),而 `.env` 冻结口径(AGENTS §10)本来就把这类「上限/调优」 // 参数指向**代码默认值**。断言生效值的那条测试在 memory 包 // (TestReviewTimeoutEffectiveValueIsTheOnlyDefault,走完整组装路径)—— // 本包不能引 memory 包(memory → config 已成立,反向会成环)。 func TestLoadMemoryReviewConfigDefaults(t *testing.T) { reviewTimeoutEnvUnset(t) cfg, err := LoadMemoryReviewConfig("") if err != nil { t.Fatalf("LoadMemoryReviewConfig() error: %v", err) } if !cfg.Enabled { t.Error("MEMORY_REVIEW_ENABLED 默认应为 true(主人 2026-10-05 裁定「入库前审查」;T0 零模型调用)") } if cfg.QueueSize != 256 { t.Errorf("MEMORY_REVIEW_QUEUE_SIZE = %d, want 256", cfg.QueueSize) } if cfg.ScanLimit != 200 { t.Errorf("MEMORY_REVIEW_SCAN_LIMIT = %d, want 200(§15 D3-4 每条审查有界)", cfg.ScanLimit) } if cfg.TimeoutSec != 0 { t.Errorf("MEMORY_REVIEW_TIMEOUT_SEC = %d, want 0(未配置)—— "+ "这里出现非零值说明 tag 上又被加回了字面默认值,memory 侧的 "+ "defaultReviewTimeout 会再次变成死代码(M16 修的正是这个形态)", cfg.TimeoutSec) } } // TestLoadMemoryReviewConfigEnvOverrides 锁定键名与类型:写进 .env 的旋钮真的 // 生效(键名写错会静默沿用默认值,这类错误只能靠这条用例抓住)。 // // TimeoutSec 另钉一条口径(M16):**正数原样透传、≤0 原样透传**。配置层不做 // 「坏值回落」——「0 或负数 ⇒ 用默认 90s」是 memory 包 normalizeReviewConfig // 的语义,全链只有那一处决定它(组合根不许在这里补默认值)。 func TestLoadMemoryReviewConfigEnvOverrides(t *testing.T) { t.Setenv("MEMORY_REVIEW_ENABLED", "false") t.Setenv("MEMORY_REVIEW_QUEUE_SIZE", "8") t.Setenv("MEMORY_REVIEW_SCAN_LIMIT", "32") t.Setenv("MEMORY_REVIEW_TIMEOUT_SEC", "3") cfg, err := LoadMemoryReviewConfig("") if err != nil { t.Fatalf("LoadMemoryReviewConfig() error: %v", err) } if cfg.Enabled || cfg.QueueSize != 8 || cfg.ScanLimit != 32 || cfg.TimeoutSec != 3 { t.Fatalf("MEMORY_REVIEW_* 未生效:%+v", cfg) } for _, tc := range []struct { raw string want int }{ {"0", 0}, // 显式 0 = 用默认(下游归一化),不许在这里被改成 90 {"-5", -5}, // 负数同样是「用默认」,也不在这里报错(AGENTS:不让笔误把进程弄起不来) } { t.Setenv("MEMORY_REVIEW_TIMEOUT_SEC", tc.raw) got, err := LoadMemoryReviewConfig("") if err != nil { t.Fatalf("LoadMemoryReviewConfig(%q) error: %v", tc.raw, err) } if got.TimeoutSec != tc.want { t.Fatalf("MEMORY_REVIEW_TIMEOUT_SEC=%q 读出 %d, want %d(配置层不回落,回落只在 memory 包)", tc.raw, got.TimeoutSec, tc.want) } } }