package jevdecide import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "time" ) func TestClientDecideSuccess(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/decide" { t.Fatalf("path=%s", r.URL.Path) } if got := r.Header.Get("Authorization"); got != "Bearer test-key" { t.Fatalf("auth=%q", got) } _ = json.NewEncoder(w).Encode(Response{ Model: "test", Answers: map[string]Answer{ "realtime": {Type: "noul", Choice: "yes", Yes: true, Confidence: 0.95, Probabilities: map[string]float64{"yes": 0.95, "no": 0.05}}, }, }) })) defer srv.Close() c := NewClient(srv.URL, "test-key") c.Metrics = &RolloutMetrics{} yes, ans, err := c.DecideRealtime(context.Background(), "今天北京天气怎么样") if err != nil { t.Fatal(err) } if !yes || ans.Confidence < 0.9 { t.Fatalf("got yes=%v ans=%+v", yes, ans) } if c.Metrics.Calls.Load() != 1 { t.Fatalf("calls=%d", c.Metrics.Calls.Load()) } } func TestClientUnauthorized(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"unauthorized"}`)) })) defer srv.Close() c := NewClient(srv.URL, "bad") c.Metrics = &RolloutMetrics{} _, _, err := c.DecideRealtime(context.Background(), "x") if err == nil || !strings.Contains(err.Error(), "401") { t.Fatalf("want 401, got %v", err) } if c.Metrics.JevFail.Load() != 1 { t.Fatalf("fail=%d", c.Metrics.JevFail.Load()) } } func TestClientServerError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadGateway) _, _ = w.Write([]byte("upstream down")) })) defer srv.Close() c := NewClient(srv.URL, "k") _, err := c.Decide(context.Background(), "x", map[string]Question{"a": {Type: "noul", Question: "?"}}) if err == nil || !strings.Contains(err.Error(), "502") { t.Fatalf("want 502, got %v", err) } } func TestClientBadJSON(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("not-json")) })) defer srv.Close() c := NewClient(srv.URL, "k") _, err := c.Decide(context.Background(), "x", map[string]Question{"a": {Type: "noul", Question: "?"}}) if err == nil || !strings.Contains(err.Error(), "decode") { t.Fatalf("want decode error, got %v", err) } } func TestClientTimeout(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { time.Sleep(200 * time.Millisecond) _, _ = w.Write([]byte(`{"answers":{}}`)) })) defer srv.Close() c := NewClient(srv.URL, "k") c.HTTP = &http.Client{Timeout: 50 * time.Millisecond} _, err := c.Decide(context.Background(), "x", map[string]Question{"a": {Type: "noul", Question: "?"}}) if err == nil { t.Fatal("expected timeout") } } func TestCircuitOpensAndRecovers(t *testing.T) { var hits atomic.Int64 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { hits.Add(1) w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte("fail")) })) defer srv.Close() c := NewClient(srv.URL, "k") c.Breaker = NewCircuitBreaker(3, 50*time.Millisecond) c.Metrics = &RolloutMetrics{} for i := 0; i < 3; i++ { _, _ = c.Decide(context.Background(), "x", map[string]Question{"a": {Type: "noul", Question: "?"}}) } if !c.Breaker.Open() { t.Fatal("circuit should be open") } _, err := c.Decide(context.Background(), "x", map[string]Question{"a": {Type: "noul", Question: "?"}}) if err == nil || !strings.Contains(err.Error(), "circuit open") { t.Fatalf("want circuit open, got %v", err) } before := hits.Load() time.Sleep(60 * time.Millisecond) _, _ = c.Decide(context.Background(), "x", map[string]Question{"a": {Type: "noul", Question: "?"}}) if hits.Load() <= before { t.Fatal("expected half-open probe") } } func TestDecideProfileBucket(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(Response{ Answers: map[string]Answer{ "bucket": {Type: "choice", Choice: "high", Confidence: 0.8, Probabilities: map[string]float64{"high": 0.8, "mid": 0.15, "low": 0.05}}, }, }) })) defer srv.Close() c := NewClient(srv.URL, "k") bucket, ans, err := c.DecideProfileBucket(context.Background(), "写一份详尽报告") if err != nil || bucket != "high" { t.Fatalf("bucket=%q err=%v ans=%+v", bucket, err, ans) } if ans.Margin() < 0.5 { t.Fatalf("margin=%v", ans.Margin()) } } func TestAnswerReliable(t *testing.T) { ok := Answer{Choice: "yes", Confidence: 0.95, Probabilities: map[string]float64{"yes": 0.95, "no": 0.05}} if !ok.Reliable(MinAssistConf, MinAssistMargin) { t.Fatal("expected reliable") } low := Answer{Choice: "yes", Confidence: 0.7, Probabilities: map[string]float64{"yes": 0.7, "no": 0.3}} if low.Reliable(MinAssistConf, MinAssistMargin) { t.Fatal("expected unreliable") } } func TestParseMode(t *testing.T) { if ParseMode("") != ModeOff || ParseMode("SHADOW") != ModeShadow || ParseMode("assist") != ModeAssist { t.Fatal("parse mismatch") } if ParseMode("weird") != ModeOff { t.Fatal("unknown must fail closed to off") } } func TestProfileBucket(t *testing.T) { if ProfileBucket(10) != "low" || ProfileBucket(45) != "mid" || ProfileBucket(60) != "high" { t.Fatal("bucket mapping wrong") } }