golangLAKEHOUSE/cmd/embedd/main_test.go
root 0f79bce948 Batch 3: cmd/<bin>/main_test.go × 6 — closes R-005
Adds main_test.go for each of the 6 cmd binaries that lacked them
(storaged already had main_test.go; that's where the pattern came
from). Each test file focuses on the cmd-specific surface — route
mounts, body caps, decode/validation paths — without re-testing
internal package logic that's covered elsewhere.

cmd/catalogd/main_test.go — 6 funcs
  TestRoutesMounted: chi.Walk asserts /catalog/{register,manifest/*,list}
  TestHandleRegister_BodyTooLarge: 5 MiB body → 4xx
  TestHandleRegister_MalformedJSON: 400
  TestHandleRegister_EmptyName_400: ErrEmptyName surfaces as 400
  TestHandleGetManifest_404 + TestHandleList_EmptyShape

cmd/embedd/main_test.go — 8 funcs
  stubProvider implements embed.Provider deterministically
  TestRoutesMounted, MalformedJSON_400, EmptyTextRejected_400 (per
    scrum O-W3), UpstreamError_502 (provider error → 502, not 500),
    HappyPath_ProviderEcho, BodyTooLarge (4xx range), TestItoa
    (covers the no-strconv helper)

cmd/gateway/main_test.go — 4 funcs
  TestMustParseUpstream_HappyPaths: 3 valid URLs
  TestMustParseUpstream_FailureExits: re-execs the test binary in a
    subprocess with env flag (standard pattern for testing os.Exit
    callers); subprocess invokes mustParseUpstream("127.0.0.1:3211")
    [missing scheme]; expects exit non-zero. Same pattern for garbage.
  TestUpstreamConfigKeys_DocumentedShape: locks the 6 _url keys

cmd/ingestd/main_test.go — 7 funcs
  Stubs both storaged and catalogd via httptest.Server so the cmd
  layer can be exercised without bringing the full chain up.
  TestHandleIngest_MissingNameQueryParam: 400 with "name" in body
  TestHandleIngest_MalformedMultipart: 400
  TestHandleIngest_MissingFormFile: 400 (valid multipart, wrong field)
  TestHandleIngest_BodyTooLarge: 4xx
  TestEscapeKeyPath: 6-case URL-escape table (apostrophe, space, etc.)
  TestParquetKeyPath_Format: locks the datasets/<n>/<fp>.parquet shape
    per scrum C-DRIFT (any rename breaks idempotent re-ingest)

cmd/queryd/main_test.go — 6 funcs
  Tests pre-DB paths (decode, body cap, empty SQL); db.QueryContext
  itself needs DuckDB so it's covered by GOLAKE-040 in the proof
  harness, not unit tests. handlers.db = nil here is intentional.
  TestHandleSQL_EmptySQL_400: 3 cases (empty, whitespace, mixed-WS)
  TestMaxSQLBodyBytes_Reasonable: locks the 64 KiB constant in a
    sane range so a refactor can't blow it open
  TestPrimaryBucket_Constant: locks "primary" — secrets lookup uses
    this; rename = silent secret-resolution failure at boot

cmd/vectord/main_test.go — 14 funcs
  All 6 routes verified mounted. handlers.persist = nil = pure
  in-memory mode; persistence is GOLAKE-070 in the proof harness.
  Coverage of every error branch in handleCreate/Add/Search/Delete:
    missing index → 404, dim mismatch → 400, empty items → 400,
    empty id → 400, malformed JSON → 400, body too large → 4xx,
    happy create → 201, happy list → 200.

One real finding caught during writing:
  Body-cap rejection is sometimes 413 (typed MaxBytesError survives
  unwrap) and sometimes 400 (decoder wraps it as a generic decode
  error). Both are valid client-error contracts; the contract isn't
  "exactly 413" but "fails loud as 4xx, never silent 200 or 5xx."
  Tests assert 4xx range. The proof harness's
  proof_assert_status_4xx already had this shape — just bringing
  the unit tests in line with it.

Verified:
  go test -count=1 -short ./cmd/...  — all 7 packages green
  just verify                         — vet + test + 9 smokes 35s

Closes audit risk R-005 (6/7 cmd/main.go untested). Combined with
the proof harness's wiring coverage, every cmd-level handler now
has both unit-test and integration-test coverage of the wiring
layer. R-005 → CLOSED.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 06:18:46 -05:00

163 lines
4.3 KiB
Go

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)
}
}
}