package engine import ( "os" "path/filepath" "testing" "time" ) func TestContextWindowCalibrator_DefaultBeforeCalibration(t *testing.T) { c := NewContextWindowCalibrator("") // 无校准记录时返回静态默认值 got := c.EffectiveWindow("MiniMax-M2.7", 200_000) if got != 200_000 { t.Errorf("expected 200000, got %d", got) } } func TestContextWindowCalibrator_RecordFailureUpdatesWindow(t *testing.T) { c := NewContextWindowCalibrator("") // 记录一次失败:actual=210000, max=200000 c.RecordFailure("MiniMax-M2.7", 210_000, 200_000) // 有效窗口 = max * (1 - 0.10) = 200000 * 0.90 = 180000 got := c.EffectiveWindow("MiniMax-M2.7", 200_000) want := int(200_000 * (1.0 - contextCalibrationSafetyMargin)) if got != want { t.Errorf("expected %d, got %d", want, got) } } func TestContextWindowCalibrator_ConservativeMinPolicy(t *testing.T) { c := NewContextWindowCalibrator("") // 第一次失败:max=200000 → effective=180000 c.RecordFailure("model-x", 205_000, 200_000) // 第二次失败:max=190000 → effective=171000(更小,应该被采用) c.RecordFailure("model-x", 195_000, 190_000) got := c.EffectiveWindow("model-x", 200_000) want := int(190_000 * (1.0 - contextCalibrationSafetyMargin)) if got != want { t.Errorf("conservative min: expected %d, got %d", want, got) } // 第三次失败:max=210000 → effective=189000(更大,不应覆盖历史最小值) c.RecordFailure("model-x", 215_000, 210_000) got2 := c.EffectiveWindow("model-x", 200_000) if got2 != want { t.Errorf("conservative min not preserved: expected %d, got %d", want, got2) } } func TestContextWindowCalibrator_FallbackWhenNoMax(t *testing.T) { c := NewContextWindowCalibrator("") // max=0:只有 actual,用 actual 估算 c.RecordFailure("model-y", 130_000, 0) got := c.EffectiveWindow("model-y", 128_000) want := int(130_000 * (1.0 - contextCalibrationSafetyMargin)) if got != want { t.Errorf("expected %d, got %d", want, got) } } func TestContextWindowCalibrator_IgnoreEmptyModel(t *testing.T) { c := NewContextWindowCalibrator("") c.RecordFailure("", 100_000, 90_000) // 空 model,应忽略 recs := c.Records() if len(recs) != 0 { t.Errorf("expected no records for empty model, got %d", len(recs)) } } func TestContextWindowCalibrator_PersistAndReload(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "context_calibration.json") c1 := NewContextWindowCalibrator(path) c1.RecordFailure("claude-sonnet-4-6", 205_000, 200_000) // 新实例从磁盘加载 c2 := NewContextWindowCalibrator(path) got := c2.EffectiveWindow("claude-sonnet-4-6", 200_000) want := int(200_000 * (1.0 - contextCalibrationSafetyMargin)) if got != want { t.Errorf("after reload: expected %d, got %d", want, got) } } func TestContextWindowCalibrator_CorruptFileFailOpen(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "context_calibration.json") os.WriteFile(path, []byte("NOT VALID JSON{{{"), 0600) // 损坏文件不应 panic,应静默降级 c := NewContextWindowCalibrator(path) got := c.EffectiveWindow("any-model", 100_000) if got != 100_000 { t.Errorf("corrupt file: expected static default 100000, got %d", got) } } func TestContextWindowCalibrator_Records(t *testing.T) { c := NewContextWindowCalibrator("") c.RecordFailure("m1", 100_000, 90_000) c.RecordFailure("m2", 200_000, 180_000) recs := c.Records() if len(recs) != 2 { t.Fatalf("expected 2 records, got %d", len(recs)) } if recs["m1"].FailureCount != 1 { t.Errorf("m1 FailureCount: expected 1, got %d", recs["m1"].FailureCount) } if recs["m1"].ObservedAt.IsZero() { t.Error("m1 ObservedAt should not be zero") } _ = recs["m1"].ObservedAt.Before(time.Now()) // 只检查不是零值 } func TestParseContextError_Anthropic(t *testing.T) { msg := "prompt is too long: 210000 tokens > 200000 maximum" actual, max := ParseContextError(msg) if actual != 210_000 { t.Errorf("actual: expected 210000, got %d", actual) } if max != 200_000 { t.Errorf("max: expected 200000, got %d", max) } } func TestParseContextError_OpenAI(t *testing.T) { msg := "This model's maximum context length is 128000 tokens. However, your messages resulted in 130000 tokens. Please reduce the length of the messages." actual, max := ParseContextError(msg) if max != 128_000 { t.Errorf("max: expected 128000, got %d", max) } if actual != 130_000 { t.Errorf("actual: expected 130000, got %d", actual) } } func TestParseContextError_Unknown(t *testing.T) { actual, max := ParseContextError("some random error message") if actual != 0 || max != 0 { t.Errorf("unknown format: expected (0,0), got (%d,%d)", actual, max) } } func TestClassifyAPIError_ContextTooLong(t *testing.T) { cases := []string{ "prompt is too long: 210000 tokens > 200000 maximum", "prompt too long", "too many tokens in context", "context length exceeded", "This model's maximum context length is 128000", "HTTP 413", "context_length_exceeded", } for _, msg := range cases { code := ClassifyAPIError(msg) if code != ErrContextTooLong { t.Errorf("ClassifyAPIError(%q) = %q, want ErrContextTooLong", msg, code) } } } func TestClassifyAPIError_NotContextTooLong(t *testing.T) { // 确保 HTTP 400 格式不被误判(ErrContextTooLong 检测在 400 之前) code := ClassifyAPIError("HTTP 400: invalid request body") if code == ErrContextTooLong { t.Error("HTTP 400 generic should not be ErrContextTooLong") } }