golangLAKEHOUSE/tests/proof/cases/06_vector_add_search.sh
root 6d18394416 proof harness Phase B: 4 contract cases · 53/0/1 green
Added the contract tier above 00_health canary. All 5 contract cases
now cover GOLAKE-001-003, 050, 060-061, 080-085 — 53 assertions pass,
1 informational skip, 0 fail. Wall: 4s end-to-end (cached binaries).

Cases:
  05_embedding_contract.sh
    GOLAKE-050. POST /v1/embed with one short text → asserts dim=768,
    one vector returned, vector length matches dimension, sum of
    squared elements > 0 (proxy for non-zero), response.model echoed.
    Skips with explicit reason if Ollama is unreachable (502 from
    embedd) — per spec hard rule "skipped tests do not appear as
    passed."

  06_vector_add_search.sh
    GOLAKE-060 + GOLAKE-061. Synthetic dim=4 unit basis vectors.
    Create index → add 3 vectors → get-index returns length=3 →
    search([1,0,0,0],k=3) returns v1 at rank 1 with distance < 0.001.
    Cleanup with DELETE. No embedd dependency — pure contract layer.

  08_gateway_contracts.sh
    GOLAKE-003. For each /v1/* route, asserts gateway and direct
    upstream return identical status AND identical response body
    (sha256 match). Confirms gateway is a proxy not a transformer.
    Status passthrough verified on both 200 path (storage/list,
    catalog/list) and 4xx path (sql empty body → 400 from queryd).

  09_failure_modes.sh
    GOLAKE-080..085. Six failure-mode contracts:
      080 malformed JSON → 4xx on catalog/ingest/sql/embed
      081 missing required field → 4xx on catalog/vectors/embed
      082 bad SQL → 4xx with non-empty error body
      083 vector dim mismatch → 4xx
      084 missing storage object → 404
      085 duplicate vector ID → INFORMATIONAL (spec says required:false)
          first/second statuses recorded as evidence; contract decided
          later from the recorded record.

Two new lib helpers in lib/assert.sh:
  proof_assert_status_in <id> <claim> "200 201 204" <probe>
    pass if status is in the space-separated list. Used for
    delete-returns-200-or-204 case where vectord returns 204.

  proof_assert_status_4xx <id> <claim> <probe>
    pass if status in [400, 500). Used for failure modes where the
    specific 4xx code may vary (400 vs 422 vs 409). Records actual
    code as evidence.

Two real contract findings recorded by the harness during build:
  - vectord add expects {"items": [...]}, not {"vectors": [...]}.
    My initial test sent the wrong field; would have masked the bug
    forever in CI. The harness caught it via the assertion failure.
  - vectord create returns 201 Created, delete returns 204 No Content.
    Documented in the test fixtures as canonical.

Regression: just verify wall 33s, vet + test + 9 smokes still green.

Phase C (integration) lands next: 01_storage_roundtrip, 02_catalog_manifest,
03_ingest_csv_to_parquet, 04_query_correctness, 05/06 integration extends,
07_vector_persistence_restart.

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

78 lines
3.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# 06_vector_add_search.sh — GOLAKE-060 + GOLAKE-061.
# Vector add + search round-trip. Synthetic dim=4 unit basis vectors,
# no embedd dependency — this is the contract layer.
#
# GOLAKE-060: add succeeds + lookup-by-id returns the inserted IDs
# GOLAKE-061: nearest-neighbor search — inserted vector ranks #1 vs itself
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../lib/env.sh"
source "${SCRIPT_DIR}/../lib/http.sh"
source "${SCRIPT_DIR}/../lib/assert.sh"
CASE_ID="GOLAKE-060-061"
CASE_NAME="Vector add + lookup + nearest-neighbor"
CASE_TYPE="contract"
if [ "${1:-}" = "--metadata-only" ]; then return 0 2>/dev/null || exit 0; fi
INDEX_NAME="proof_contract_idx"
# Idempotent prelude — clean any prior run state. 404 is fine.
proof_delete "$CASE_ID" "pre_clean" \
"${PROOF_GATEWAY_URL}/v1/vectors/index/${INDEX_NAME}" >/dev/null
# Create the index. vectord returns 201.
proof_post "$CASE_ID" "create_index" "${PROOF_GATEWAY_URL}/v1/vectors/index" \
"application/json" \
"{\"name\":\"${INDEX_NAME}\",\"dimension\":4}" >/dev/null
proof_assert_eq "$CASE_ID" "create index → 201" "201" \
"$(proof_status_of "$CASE_ID" "create_index")"
# Add three deterministic vectors. Unit basis vectors so search recall
# is unambiguous: searching for [1,0,0,0] must return v1 first.
# vectord wants {"items": [...]}, NOT {"vectors": [...]}.
add_body='{"items":[
{"id":"v1","vector":[1,0,0,0]},
{"id":"v2","vector":[0,1,0,0]},
{"id":"v3","vector":[0,0,1,0]}
]}'
proof_post "$CASE_ID" "add_vectors" \
"${PROOF_GATEWAY_URL}/v1/vectors/index/${INDEX_NAME}/add" \
"application/json" "$add_body" >/dev/null
proof_assert_eq "$CASE_ID" "add vectors → 200" "200" \
"$(proof_status_of "$CASE_ID" "add_vectors")"
# Lookup-by-id (GOLAKE-060 evidence). The /index/{name} GET returns
# {"params": {...}, "length": N}.
proof_get "$CASE_ID" "get_index" \
"${PROOF_GATEWAY_URL}/v1/vectors/index/${INDEX_NAME}" >/dev/null
proof_assert_eq "$CASE_ID" "get index → 200" "200" \
"$(proof_status_of "$CASE_ID" "get_index")"
length=$(jq -r '.length' \
"${PROOF_REPORT_DIR}/raw/http/${CASE_ID}/get_index.body")
proof_assert_eq "$CASE_ID" "index length = 3 after add" "3" "$length"
# Search — query is identical to v1; expect v1 at rank 1 with distance ≈ 0.
search_body='{"vector":[1,0,0,0],"k":3}'
proof_post "$CASE_ID" "search" \
"${PROOF_GATEWAY_URL}/v1/vectors/index/${INDEX_NAME}/search" \
"application/json" "$search_body" >/dev/null
proof_assert_eq "$CASE_ID" "search → 200" "200" \
"$(proof_status_of "$CASE_ID" "search")"
# Search response shape: {"results": [{"id","distance","metadata?"}]}.
search_body_path="${PROOF_REPORT_DIR}/raw/http/${CASE_ID}/search.body"
top1_id=$(jq -r '.results[0].id' "$search_body_path")
proof_assert_eq "$CASE_ID" "top-1 id = v1 (self-recall)" "v1" "$top1_id"
top1_dist=$(jq -r '.results[0].distance' "$search_body_path")
proof_assert_lt "$CASE_ID" "top-1 distance < 0.001 (cosine self ≈ 0)" \
"$top1_dist" "0.001"
# Cleanup — vectord returns 204 No Content on delete success.
proof_delete "$CASE_ID" "post_clean" \
"${PROOF_GATEWAY_URL}/v1/vectors/index/${INDEX_NAME}" >/dev/null
proof_assert_status_in "$CASE_ID" "delete index → 200 or 204" "200 204" "post_clean"