package classify import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "time" ) func TestClientClassifyRequestAndResponse(t *testing.T) { var ( gotPath string gotAuth string gotBody map[string]any callCount int32 ) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&callCount, 1) gotPath = r.URL.Path gotAuth = r.Header.Get("Authorization") if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil { t.Errorf("decode request: %v", err) } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{ "model": "jev-1.13.0", "answers": { "category": {"type":"choice","choice":"Action","confidence":0.81,"probabilities":{"Action":0.81,"Info":0.19}}, "importance": {"type":"choice","choice":"high","confidence":0.9,"probabilities":{"high":0.9}}, "action": {"type":"choice","choice":"reply","confidence":0.7,"probabilities":{"reply":0.7}} }, "usage": {"input_tokens": 100, "output_tokens": 20} }`)) })) defer srv.Close() client := NewClient(srv.URL, "test-key", "jev-latest", nil) res, err := client.Classify(context.Background(), MessageInput{ From: "favrolfe@gmail.com", To: "hello@where-woof.com", Subject: "Quick question", Source: "hello@where-woof.com", Body: "Can you send me the invoice?", Received: "2026-10-11T09:00:00Z", }, []CategoryOption{ {Name: "Action", Description: "Needs Sam to do something"}, {Name: "Info", Description: "Informational"}, }) if err != nil { t.Fatalf("Classify() error = %v", err) } if gotPath != "/v1/systemone" { t.Errorf("path = %q, want /v1/systemone", gotPath) } if gotAuth != "Bearer test-key" { t.Errorf("Authorization = %q", gotAuth) } if gotBody["model"] != "jev-latest" { t.Errorf("model = %v, want jev-latest", gotBody["model"]) } questions, _ := gotBody["questions"].(map[string]any) if len(questions) != 3 { t.Fatalf("questions = %d, want 3", len(questions)) } categoryQ, _ := questions["category"].(map[string]any) criteria, _ := categoryQ["criteria"].(map[string]any) if len(criteria) != 2 { t.Errorf("category criteria = %v, want the live category list", criteria) } state, _ := gotBody["state"].(map[string]any) if _, ok := state["email"]; !ok { t.Errorf("state = %v, want an email object", state) } if res.Category != "Action" || res.Importance != "high" || res.Action != "reply" { t.Errorf("result = %+v", res) } if res.CategoryConfidence != 0.81 { t.Errorf("CategoryConfidence = %v, want 0.81", res.CategoryConfidence) } if res.Model != "jev-1.13.0" { t.Errorf("Model = %q, want jev-1.13.0", res.Model) } if atomic.LoadInt32(&callCount) != 1 { t.Errorf("call count = %d, want 1", callCount) } } func TestClientRetriesRateLimit(t *testing.T) { var attempts int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if atomic.AddInt32(&attempts, 1) == 1 { w.WriteHeader(http.StatusTooManyRequests) _, _ = w.Write([]byte(`{"error":"rate limited"}`)) return } _, _ = w.Write([]byte(`{"model":"jev-1.13.0","answers":{ "category":{"type":"choice","choice":"Info","confidence":1}, "importance":{"type":"choice","choice":"low","confidence":1}, "action":{"type":"choice","choice":"keep","confidence":1}}}`)) })) defer srv.Close() client := NewClient(srv.URL, "k", "", nil) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() res, err := client.Classify(ctx, MessageInput{Subject: "hi"}, []CategoryOption{{Name: "Info"}}) if err != nil { t.Fatalf("Classify() error = %v", err) } if res.Category != "Info" { t.Errorf("Category = %q, want Info", res.Category) } if got := atomic.LoadInt32(&attempts); got != 2 { t.Errorf("attempts = %d, want 2", got) } } func TestClientRequiresAPIKey(t *testing.T) { client := NewClient("http://example.invalid", "", "", nil) if client.Configured() { t.Fatal("Configured() = true without an API key") } if _, err := client.Classify(context.Background(), MessageInput{}, []CategoryOption{{Name: "Info"}}); err != ErrNotConfigured { t.Fatalf("err = %v, want ErrNotConfigured", err) } } func TestClientDefaultsBaseURL(t *testing.T) { client := NewClient("", "k", "", nil) if client.baseURL != DefaultBaseURL { t.Errorf("baseURL = %q, want %q", client.baseURL, DefaultBaseURL) } if client.Model() != DefaultModel { t.Errorf("model = %q, want %q", client.Model(), DefaultModel) } } func TestClientHTTPErrorSurfacesBody(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"invalid api key"}`)) })) defer srv.Close() client := NewClient(srv.URL, "bad", "", nil) _, err := client.Classify(context.Background(), MessageInput{}, []CategoryOption{{Name: "Info"}}) if err == nil || !strings.Contains(err.Error(), "invalid api key") { t.Fatalf("err = %v, want the 401 body", err) } }