package main import ( "bytes" "context" "errors" "net/http" "net/http/httptest" "strings" "testing" "github.com/go-chi/chi/v5" "git.agentview.dev/profit/golangLAKEHOUSE/internal/embed" ) // Closes R-005 for embedd: cmd-level tests for the /embed handler's // decode + validation paths (empty texts → 400, body cap → 413, // upstream error → 502). Provider semantics live in // internal/embed/ollama_test.go. // stubProvider implements embed.Provider with deterministic stubs. type stubProvider struct { result embed.Result err error } func (s *stubProvider) Embed(_ context.Context, _ []string, _ string) (embed.Result, error) { return s.result, s.err } func mountWithProvider(p embed.Provider) chi.Router { h := &handlers{provider: p} r := chi.NewRouter() h.register(r) return r } func TestRoutesMounted(t *testing.T) { r := mountWithProvider(&stubProvider{}) found := false chi.Walk(r, func(method, route string, _ http.Handler, _ ...func(http.Handler) http.Handler) error { if method == "POST" && route == "/embed" { found = true } return nil }) if !found { t.Error("POST /embed not mounted") } } func TestHandleEmbed_BodyTooLarge(t *testing.T) { // MaxBytesReader trips during JSON decode. Depending on whether // the decoder unwrapping surfaces MaxBytesError or wraps it as a // generic decode error, the response is either 413 or 400. Both // are valid "client error, fails loud" contracts; the harness's // proof_assert_status_4xx covers either at the integration level. r := mountWithProvider(&stubProvider{}) srv := httptest.NewServer(r) defer srv.Close() big := bytes.Repeat([]byte("x"), maxRequestBytes+(1<<20)) resp, err := http.Post(srv.URL+"/embed", "application/json", bytes.NewReader(big)) if err != nil { t.Fatalf("POST: %v", err) } defer resp.Body.Close() if resp.StatusCode < 400 || resp.StatusCode >= 500 { t.Errorf("expected 4xx on oversize, got %d", resp.StatusCode) } } func TestHandleEmbed_MalformedJSON_400(t *testing.T) { r := mountWithProvider(&stubProvider{}) srv := httptest.NewServer(r) defer srv.Close() resp, err := http.Post(srv.URL+"/embed", "application/json", strings.NewReader("not json")) if err != nil { t.Fatalf("POST: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected 400 on malformed, got %d", resp.StatusCode) } } func TestHandleEmbed_EmptyTextRejected_400(t *testing.T) { // Per scrum O-W3 (Opus): reject empty strings up front. r := mountWithProvider(&stubProvider{}) srv := httptest.NewServer(r) defer srv.Close() resp, err := http.Post(srv.URL+"/embed", "application/json", strings.NewReader(`{"texts":["valid",""]}`)) if err != nil { t.Fatalf("POST: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected 400 on empty text in batch, got %d", resp.StatusCode) } } func TestHandleEmbed_UpstreamError_502(t *testing.T) { // Provider returns a generic error → handler maps to 502 (the // "embedding backend was wrong" case, distinct from 400 = your // input was wrong). r := mountWithProvider(&stubProvider{err: errors.New("ollama is down")}) srv := httptest.NewServer(r) defer srv.Close() resp, err := http.Post(srv.URL+"/embed", "application/json", strings.NewReader(`{"texts":["hello"]}`)) if err != nil { t.Fatalf("POST: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadGateway { t.Errorf("expected 502 on provider error, got %d", resp.StatusCode) } } func TestHandleEmbed_HappyPath_ProviderEcho(t *testing.T) { stub := &stubProvider{result: embed.Result{ Model: "test-model", Dimension: 3, Vectors: [][]float32{{0.1, 0.2, 0.3}}, }} r := mountWithProvider(stub) srv := httptest.NewServer(r) defer srv.Close() resp, err := http.Post(srv.URL+"/embed", "application/json", strings.NewReader(`{"texts":["hello"],"model":"test-model"}`)) if err != nil { t.Fatalf("POST: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Errorf("expected 200 happy path, got %d", resp.StatusCode) } } func TestItoa(t *testing.T) { cases := []struct { in int out string }{ {0, "0"}, {1, "1"}, {42, "42"}, {1000, "1000"}, {99, "99"}, } for _, tc := range cases { if got := itoa(tc.in); got != tc.out { t.Errorf("itoa(%d) = %q, want %q", tc.in, got, tc.out) } } }