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>
265 lines
7.1 KiB
Go
265 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"git.agentview.dev/profit/golangLAKEHOUSE/internal/vectord"
|
|
)
|
|
|
|
// Closes R-005 for vectord: cmd-level tests for the 6 routes.
|
|
// Persistence-disabled mode (h.persist == nil) is the test config —
|
|
// keeps tests pure-in-memory; persistence is covered by g1p_smoke +
|
|
// proof GOLAKE-070.
|
|
|
|
func mountedRouter() chi.Router {
|
|
h := &handlers{reg: vectord.NewRegistry()}
|
|
r := chi.NewRouter()
|
|
h.register(r)
|
|
return r
|
|
}
|
|
|
|
func TestRoutesMounted(t *testing.T) {
|
|
r := mountedRouter()
|
|
want := map[string]bool{
|
|
"POST /vectors/index": false,
|
|
"GET /vectors/index": false,
|
|
"GET /vectors/index/{name}": false,
|
|
"DELETE /vectors/index/{name}": false,
|
|
"POST /vectors/index/{name}/add": false,
|
|
"POST /vectors/index/{name}/search": false,
|
|
}
|
|
chi.Walk(r, func(method, route string, _ http.Handler, _ ...func(http.Handler) http.Handler) error {
|
|
key := method + " " + route
|
|
if _, ok := want[key]; ok {
|
|
want[key] = true
|
|
}
|
|
return nil
|
|
})
|
|
for sig, found := range want {
|
|
if !found {
|
|
t.Errorf("expected route %q mounted", sig)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandleCreate_HappyPath_201(t *testing.T) {
|
|
r := mountedRouter()
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
resp, err := http.Post(srv.URL+"/vectors/index", "application/json",
|
|
strings.NewReader(`{"name":"test_idx","dimension":4}`))
|
|
if err != nil {
|
|
t.Fatalf("POST: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusCreated {
|
|
t.Errorf("expected 201 on create, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestHandleCreate_MissingDim_400(t *testing.T) {
|
|
r := mountedRouter()
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
resp, err := http.Post(srv.URL+"/vectors/index", "application/json",
|
|
strings.NewReader(`{"name":"missing_dim"}`))
|
|
if err != nil {
|
|
t.Fatalf("POST: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Errorf("expected 400 on missing dim, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestHandleCreate_MalformedJSON_400(t *testing.T) {
|
|
r := mountedRouter()
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
resp, err := http.Post(srv.URL+"/vectors/index", "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 TestHandleCreate_BodyTooLarge(t *testing.T) {
|
|
// 4xx range — see embedd's TestHandleEmbed_BodyTooLarge for the
|
|
// 413-vs-400 unwrap nuance. Contract is "client error, fails loud."
|
|
r := mountedRouter()
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
big := bytes.Repeat([]byte("x"), maxRequestBytes+(1<<20))
|
|
resp, err := http.Post(srv.URL+"/vectors/index", "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 TestHandleGetIndex_NotFound_404(t *testing.T) {
|
|
r := mountedRouter()
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
resp, err := http.Get(srv.URL + "/vectors/index/nonexistent")
|
|
if err != nil {
|
|
t.Fatalf("GET: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestHandleAdd_IndexNotFound_404(t *testing.T) {
|
|
r := mountedRouter()
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
resp, err := http.Post(srv.URL+"/vectors/index/missing/add", "application/json",
|
|
strings.NewReader(`{"items":[{"id":"v1","vector":[1,2,3,4]}]}`))
|
|
if err != nil {
|
|
t.Fatalf("POST: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Errorf("expected 404 on add to missing index, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestHandleAdd_EmptyItems_400(t *testing.T) {
|
|
r := mountedRouter()
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
// Create index first.
|
|
http.Post(srv.URL+"/vectors/index", "application/json",
|
|
strings.NewReader(`{"name":"empty_test","dimension":4}`))
|
|
|
|
resp, err := http.Post(srv.URL+"/vectors/index/empty_test/add", "application/json",
|
|
strings.NewReader(`{"items":[]}`))
|
|
if err != nil {
|
|
t.Fatalf("POST: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Errorf("expected 400 on empty items, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestHandleAdd_DimMismatch_400(t *testing.T) {
|
|
r := mountedRouter()
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
http.Post(srv.URL+"/vectors/index", "application/json",
|
|
strings.NewReader(`{"name":"dim_test","dimension":3}`))
|
|
|
|
resp, err := http.Post(srv.URL+"/vectors/index/dim_test/add", "application/json",
|
|
strings.NewReader(`{"items":[{"id":"x","vector":[1,2,3,4]}]}`))
|
|
if err != nil {
|
|
t.Fatalf("POST: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Errorf("expected 400 on dim mismatch, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestHandleAdd_EmptyID_400(t *testing.T) {
|
|
r := mountedRouter()
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
http.Post(srv.URL+"/vectors/index", "application/json",
|
|
strings.NewReader(`{"name":"id_test","dimension":4}`))
|
|
|
|
resp, err := http.Post(srv.URL+"/vectors/index/id_test/add", "application/json",
|
|
strings.NewReader(`{"items":[{"id":"","vector":[1,2,3,4]}]}`))
|
|
if err != nil {
|
|
t.Fatalf("POST: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Errorf("expected 400 on empty id, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestHandleSearch_IndexNotFound_404(t *testing.T) {
|
|
r := mountedRouter()
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
resp, err := http.Post(srv.URL+"/vectors/index/missing/search", "application/json",
|
|
strings.NewReader(`{"vector":[1,2,3,4],"k":5}`))
|
|
if err != nil {
|
|
t.Fatalf("POST: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Errorf("expected 404 on search of missing index, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestHandleDelete_NotFound_404(t *testing.T) {
|
|
r := mountedRouter()
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
req, _ := http.NewRequest(http.MethodDelete, srv.URL+"/vectors/index/missing", nil)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("DELETE: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusNotFound {
|
|
t.Errorf("expected 404 deleting missing index, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestHandleList_EmptyShape(t *testing.T) {
|
|
r := mountedRouter()
|
|
srv := httptest.NewServer(r)
|
|
defer srv.Close()
|
|
|
|
resp, err := http.Get(srv.URL + "/vectors/index")
|
|
if err != nil {
|
|
t.Fatalf("GET: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Errorf("expected 200, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestSearchK_DefaultsAndMax(t *testing.T) {
|
|
if defaultK <= 0 {
|
|
t.Errorf("defaultK = %d, must be > 0", defaultK)
|
|
}
|
|
if maxK < defaultK {
|
|
t.Errorf("maxK=%d < defaultK=%d", maxK, defaultK)
|
|
}
|
|
// Sanity bounds.
|
|
if maxK > 100_000 {
|
|
t.Errorf("maxK=%d unreasonably large", maxK)
|
|
}
|
|
}
|