- 真 bug:文档注入预算(1M 窗口 ⇒ 349525 字符)> 输入护栏硬编码上限(262144)⇒ 带 744KB 附件的 POST /chat 直接 input_length_exceeded/guardrail_input,整个回合作废(主会话活服务实测)。 - 修法:guardrailMaxInputChars → guardrailMinInputChars(256Ki 下限) + 纯函数 deriveGuardrailInputLimit(log, override, budgetTokens, docInjectChars):派生值 = max(下限, 推导预算 tokens)(1M ⇒ 699050);新增 env GUARDRAIL_MAX_INPUT_CHARS(未设/0=派生、>0=覆盖、<0=WARN 回落);预算只在 wire_context 推导一次并经 buildGuardrailPipeline(ctxBudget) 传入(避免「窗口未声明」WARN 记两遍);输出上限 65536 刻意不参与派生(有显式回归钉住)。 - 关系不变量:文档注入预算 ≤ 输入上限,人为破坏 ⇒ 一条 WARN(不静默)。 - 第 5 条 flake(exec/TestTerminal_Timeout_Killed):仅改测试、terminal.go 保持 pristine;根因不是「杀进程失效」而是旧断言的 ~200ms 精度把宿主负载下的进程创建耗时算了进去(重压下 cmd.Start 会直接返回 deadline exceeded,红行却报 process not killed)⇒ 改为「控制组证明同命令不设短超时正常退出」的确定性证明 + 把墙钟断言降级为 10s 语义判别界(子进程自然存活期拉到 30s)。 - 主会话独立验收:隔离 worktree(G2 的 10 个文件,排除 M1 在途的 wire_channels.go)build/vet 绿、全树 59 包 0 FAIL、双平台交叉编译绿、Guardrail* 全 PASS、exec -count=20 绿;变异独立复现(把派生值退回硬编码 ⇒ 4 条红,含 budget=699050 → 262144)。 - 残留缺口(已排 G2b):护栏量的是整段装配请求、注入预算是单份 ⇒ ≥2 份各撞注入预算的大附件仍可能被拦。
69 lines
2.6 KiB
Go
69 lines
2.6 KiB
Go
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)
|
||
}
|
||
}
|