package config import ( "os" "path/filepath" "reflect" "testing" ) // 本文件钉住上下文预算的推导(design/04 附录 A.3):budget = min(窗口 × 比例, // 窗口 − 输出保留),以及 CONTEXT_BUDGET_RATIO 的解析与回落口径。 // 默认模型的真实窗口(facts-for-cyrene.md)与两条便于阅读的取值。 const ( oneMillionWindow = 1048576 maxTokens384K = 384 * 1024 ) // budgetCase 是推导用例的一条断言:输入 + 期望的结论要点。 type budgetCase struct { name string in ContextBudgetInput wantTokens int wantMode ContextBudgetMode wantWindow int wantDeclared bool wantReserve int } // TestDeriveContextBudget_Derived 钉住推导侧的五种情形(任务书 §4 必测 1 的 // ①②③,外加比例回落与输出保留封顶)。 func TestDeriveContextBudget_Derived(t *testing.T) { t.Parallel() ratio := DefaultContextBudgetRatio cases := []budgetCase{ { // ① 1M × 2/3 = 699050 token(文档里写作「683K」,那是 Ki:699050/1024 ≈ 683)。 name: "1M 窗口按 2/3 推导", in: ContextBudgetInput{Window: oneMillionWindow, Ratio: ratio}, wantTokens: 699050, wantMode: ContextBudgetDerived, wantWindow: oneMillionWindow, wantDeclared: true, wantReserve: MinContextOutputReserve, }, { // ② 该组 maxTokens=384K ⇒ 输出保留顶到 384K ⇒ min(699050, 1M−384K=655360)。 name: "带 384K 输出保留时取窗口减输出的那一侧", in: ContextBudgetInput{Window: oneMillionWindow, GroupMaxTokens: maxTokens384K, Ratio: ratio}, wantTokens: 655360, wantMode: ContextBudgetDerived, wantWindow: oneMillionWindow, wantDeclared: true, wantReserve: maxTokens384K, }, { // ③ 未声明窗口 ⇒ 回落 131072,131072 × 2/3 = 87381(截尾), // 且 WindowDeclared=false(组合根据此记恰好一条 WARN)。 name: "未声明窗口回落 128Ki 并提示", in: ContextBudgetInput{Ratio: ratio}, wantTokens: 87381, wantMode: ContextBudgetDerived, wantWindow: DefaultModelContextWindow, wantDeclared: false, wantReserve: MinContextOutputReserve, }, { name: "非法比例回落到 2/3", in: ContextBudgetInput{Window: oneMillionWindow, Ratio: 3}, wantTokens: 699050, wantMode: ContextBudgetDerived, wantWindow: oneMillionWindow, wantDeclared: true, wantReserve: MinContextOutputReserve, }, { // maxTokens 被声明得比窗口还大(配置矛盾)时,输出保留封顶在窗口一半, // 预算绝不退化成 0 或负数。 name: "输出保留不超过窗口一半", in: ContextBudgetInput{Window: 32768, GroupMaxTokens: maxTokens384K, Ratio: ratio}, wantTokens: 16384, wantMode: ContextBudgetDerived, wantWindow: 32768, wantDeclared: true, wantReserve: 16384, }, } for _, c := range cases { assertBudgetCase(t, c) } } // TestDeriveContextBudget_OverrideAndOff 钉住显式覆盖与关闭(任务书 §4 必测 1 的 // ④⑤):两者都**不推导**,窗口声明与否都不影响结论。 func TestDeriveContextBudget_OverrideAndOff(t *testing.T) { t.Parallel() ratio := DefaultContextBudgetRatio cases := []budgetCase{ { name: "显式覆盖原样使用", in: ContextBudgetInput{Window: oneMillionWindow, Override: 4096, OverrideConfigured: true, Ratio: ratio}, wantTokens: 4096, wantMode: ContextBudgetOverride, }, { name: "非零覆盖不必同步标志位也生效", in: ContextBudgetInput{Window: oneMillionWindow, Override: 600, Ratio: ratio}, wantTokens: 600, wantMode: ContextBudgetOverride, }, { name: "显式 0 = 关闭压缩(不推导)", in: ContextBudgetInput{Window: oneMillionWindow, Override: 0, OverrideConfigured: true, Ratio: ratio}, wantTokens: 0, wantMode: ContextBudgetOff, }, { name: "负值 = 关闭压缩(不推导)", in: ContextBudgetInput{Window: oneMillionWindow, Override: -1, OverrideConfigured: true, Ratio: ratio}, wantTokens: -1, wantMode: ContextBudgetOff, }, } for _, c := range cases { c := c t.Run(c.name, func(t *testing.T) { t.Parallel() got := DeriveContextBudget(c.in) if got.Tokens != c.wantTokens || got.Mode != c.wantMode { t.Fatalf("DeriveContextBudget = {tokens:%d mode:%s}, want {tokens:%d mode:%s}", got.Tokens, got.Mode, c.wantTokens, c.wantMode) } if got.WindowDeclared || got.Window != 0 || got.OutputReserve != 0 { t.Fatalf("覆盖/关闭态不该带推导结论:%+v", got) } }) } } // assertBudgetCase 校验一条推导用例:结论要点 + 推导侧特有的窗口与保留量。 func assertBudgetCase(t *testing.T, c budgetCase) { t.Helper() t.Run(c.name, func(t *testing.T) { t.Parallel() got := DeriveContextBudget(c.in) if got.Tokens != c.wantTokens || got.Mode != c.wantMode { t.Fatalf("DeriveContextBudget = {tokens:%d mode:%s}, want {tokens:%d mode:%s}", got.Tokens, got.Mode, c.wantTokens, c.wantMode) } if got.WindowDeclared != c.wantDeclared { t.Fatalf("WindowDeclared = %v, want %v", got.WindowDeclared, c.wantDeclared) } if got.Window != c.wantWindow || got.OutputReserve != c.wantReserve { t.Fatalf("window/reserve = %d/%d, want %d/%d", got.Window, got.OutputReserve, c.wantWindow, c.wantReserve) } }) } // TestDeriveContextBudget_OneMillionWindowBeatsTheOld32K 是主人裁决的直接体现: // 1M 窗口下推导出的预算必须是旧默认 32K 的二十倍以上 —— 否则「既丢上下文、又反复 // 烧缓存」(design/04 附录 A.2 第 2 条)的老毛病没被治好。 func TestDeriveContextBudget_OneMillionWindowBeatsTheOld32K(t *testing.T) { t.Parallel() const oldDefault = 32768 got := DeriveContextBudget(ContextBudgetInput{Window: oneMillionWindow}) if got.Tokens <= oldDefault*20 { t.Fatalf("1M 窗口推导出的预算 = %d,仍贴着旧默认 %d(推导没生效)", got.Tokens, oldDefault) } } // TestParseContextBudgetRatio 钉住比例的两种写法与非法值判定(非法值由调用方记 // WARN 并回落默认,绝不静默生效)。 func TestParseContextBudgetRatio(t *testing.T) { t.Parallel() ok := []struct { raw string want float64 }{ {"0.5", 0.5}, {"0.6667", 0.6667}, {" 2/3 ", 2.0 / 3.0}, {"3/4", 0.75}, {"1", 1}, } for _, c := range ok { got, err := ParseContextBudgetRatio(c.raw) if err != nil { t.Fatalf("ParseContextBudgetRatio(%q) 报错: %v", c.raw, err) } if got != c.want { t.Fatalf("ParseContextBudgetRatio(%q) = %v, want %v", c.raw, got, c.want) } } for _, raw := range []string{"maybe", "0", "-0.5", "1.5", "2/0", "/3", "3/"} { if _, err := ParseContextBudgetRatio(raw); err == nil { t.Fatalf("ParseContextBudgetRatio(%q) 竟然通过了 —— 非法比例必须被报告,不得静默生效", raw) } } } // TestLoadContextBudgetRatio 钉住读取链:进程环境 > .env > 默认,且非法值返回 // 错误(调用方据此记 WARN 回落默认,并带上原始取值)。 func TestLoadContextBudgetRatio(t *testing.T) { dir := t.TempDir() envFile := filepath.Join(dir, "test.env") if err := os.WriteFile(envFile, []byte(ContextBudgetRatioEnv+"=0.5\n"), 0o600); err != nil { t.Fatalf("写 .env: %v", err) } unsetenvForTest(t, ContextBudgetRatioEnv) got, err := LoadContextBudgetRatio(envFile) if err != nil { t.Fatalf("读 .env 里的比例: %v", err) } if got != 0.5 { t.Fatalf(".env 里的比例 = %v, want 0.5", got) } // 进程环境优先于 .env。 t.Setenv(ContextBudgetRatioEnv, "2/3") got, err = LoadContextBudgetRatio(envFile) if err != nil { t.Fatalf("读进程环境里的比例: %v", err) } if got != 2.0/3.0 { t.Fatalf("进程环境里的比例 = %v, want 2/3", got) } // 非法值:返回错误,并回落到默认(错误里带原始取值,便于 WARN)。 t.Setenv(ContextBudgetRatioEnv, "四分之三") got, err = LoadContextBudgetRatio(envFile) if err == nil { t.Fatal("非法比例必须返回错误") } if got != DefaultContextBudgetRatio { t.Fatalf("非法比例的回落 = %v, want %v", got, DefaultContextBudgetRatio) } // 未配置(.env 也不看):默认比例,不报错。 t.Setenv(ContextBudgetRatioEnv, "") got, err = LoadContextBudgetRatio(filepath.Join(dir, "missing.env")) if err != nil || got != DefaultContextBudgetRatio { t.Fatalf("未配置比例 = (%v, %v), want (%v, nil)", got, err, DefaultContextBudgetRatio) } } // unsetenvForTest 清掉一个环境变量并在测试结束时还原(t.Setenv 只能设值,而空串 // 会被解析成 0 / 空串,与「未设置」不是一回事 —— 本文件恰好要区分这两者)。 func unsetenvForTest(t *testing.T, key string) { t.Helper() prev, had := os.LookupEnv(key) if err := os.Unsetenv(key); err != nil { t.Fatalf("Unsetenv(%s): %v", key, err) } t.Cleanup(func() { if had { _ = os.Setenv(key, prev) return } _ = os.Unsetenv(key) }) } // TestContextMaxTokensEnvMatchesTag 钉住 ContextMaxTokensEnv 与字段的 env 标签 // 逐字一致:Load 用前者判定「未设置」,标签决定反射解码读哪个变量;两者一旦漂移, // 「未设置 ⇒ 按窗口推导」就会在真实进程里静默失效(而单测仍可能全绿)。 func TestContextMaxTokensEnvMatchesTag(t *testing.T) { t.Parallel() field, ok := reflect.TypeOf(ContextConfig{}).FieldByName("MaxTokens") if !ok { t.Fatal("ContextConfig 里没有 MaxTokens 字段") } if got := field.Tag.Get("env"); got != ContextMaxTokensEnv { t.Fatalf("MaxTokens 的 env 标签 = %q, want %q", got, ContextMaxTokensEnv) } } // TestLoad_ContextMaxTokensTriState 钉住三态判定的来源:只有真的配过这个键, // MaxTokensSet 才为真 —— 反射解码只能给出值,分不出「没配」与「配了 0」。 func TestLoad_ContextMaxTokensTriState(t *testing.T) { dir := t.TempDir() t.Setenv("JWT_SECRET", "0123456789012345678901234567890123456789") unsetenvForTest(t, ContextMaxTokensEnv) cfg, err := Load(filepath.Join(dir, "absent.env")) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Context.MaxTokens != 0 || cfg.Context.MaxTokensSet { t.Fatalf("未配置时 = (%d, set=%v), want (0, false)(未设置 ⇒ 按窗口推导)", cfg.Context.MaxTokens, cfg.Context.MaxTokensSet) } t.Setenv(ContextMaxTokensEnv, "0") cfg, err = Load(filepath.Join(dir, "absent.env")) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Context.MaxTokens != 0 || !cfg.Context.MaxTokensSet { t.Fatalf("显式 0 时 = (%d, set=%v), want (0, true)(显式 0 ⇒ 关闭压缩)", cfg.Context.MaxTokens, cfg.Context.MaxTokensSet) } // .env 里写了也算「配过」(与进程环境同一优先级链)。 envFile := filepath.Join(dir, "test.env") if err := os.WriteFile(envFile, []byte(ContextMaxTokensEnv+"=2048\n"), 0o600); err != nil { t.Fatalf("写 .env: %v", err) } unsetenvForTest(t, ContextMaxTokensEnv) cfg, err = Load(envFile) if err != nil { t.Fatalf("Load: %v", err) } if cfg.Context.MaxTokens != 2048 || !cfg.Context.MaxTokensSet { t.Fatalf(".env 配置时 = (%d, set=%v), want (2048, true)", cfg.Context.MaxTokens, cfg.Context.MaxTokensSet) } } // TestLoad_RejectsNegativeContextWindow 钉住窗口的负数校验:窗口没有负数语义, // 静默当成「未声明」会让一个配错的部署悄悄用回落窗口而非报错。 func TestLoad_RejectsNegativeContextWindow(t *testing.T) { t.Setenv("JWT_SECRET", "0123456789012345678901234567890123456789") t.Setenv("LLM_CONTEXT_WINDOW", "-1") if _, err := Load(filepath.Join(t.TempDir(), "absent.env")); err == nil { t.Fatal("负数窗口必须让 Load 报错") } t.Setenv("LLM_CONTEXT_WINDOW", "1048576") cfg, err := Load(filepath.Join(t.TempDir(), "absent.env")) if err != nil { t.Fatalf("Load: %v", err) } if cfg.LLM.ContextWindow != 1048576 { t.Fatalf("LLM_CONTEXT_WINDOW = %d, want 1048576", cfg.LLM.ContextWindow) } }