/* test_vindex.c — build + RUN gate for the M8 HNSW vector index. * * Covers: recall@10 vs brute-force oracle, brute-force-vs-index speedup, * correctness edge cases (k>N, identical vectors, self-query, zero vector), * determinism (seeded PRNG → identical graphs), and vindex_build_from_store * over a real engram_store on-disk file. * * Pure C11; links engram_vindex.c + engram_store.c; -lm. ASan/UBSan clean. */ #include "engram_vindex.h" #include "engram_store.h" #include #include #include #include #include #include #include #define DIM 768 static int g_fail = 0; /* VINDEX_QUICK=1 shrinks the two large builds so the ASan/UBSan pass (which runs * ~5-10x slower) stays fast — memory-safety is size-independent. The perf numbers * (recall gate + speedup) come from the un-sanitized, full-size pass. */ static int g_quick = 0; static int envint(const char* k, int dflt){ const char* s=getenv(k); return s?atoi(s):dflt; } #define CHECK(cond, msg) do{ if(!(cond)){ printf(" FAIL: %s\n", msg); g_fail=1; } else { printf(" ok: %s\n", msg); } }while(0) /* deterministic test PRNG (splitmix64) */ static uint64_t rng_state = 0xABCDEF0123456789ULL; static uint64_t xrng(uint64_t* s){ uint64_t z=(*s+=0x9E3779B97F4A7C15ULL); z=(z^(z>>30))*0xBF58476D1CE4E5B9ULL; z=(z^(z>>27))*0x94D049BB133111EBULL; return z^(z>>31); } static float frand(uint64_t* s){ return (float)((xrng(s)>>11)*(1.0/9007199254740992.0)) - 0.5f; } static double now_s(void){ struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t); return t.tv_sec + t.tv_nsec*1e-9; } /* fill vec[N*DIM]: mostly random, some clustered groups (center + small noise). */ static void gen_vectors(float* v, int N, uint64_t seed){ uint64_t s = seed; int clustered = N/5; /* last fifth is clustered */ int ncenters = 20; float* centers = (float*)malloc((size_t)ncenters*DIM*sizeof(float)); for (int c=0;c0 && bd[p-1]>d){ bd[p]=bd[p-1]; ids[p]=ids[p-1]; p--; } bd[p]=d; ids[p]=i; } } free(bd); } /* ── Test 1: recall@10 vs brute force + latency/recall tradeoff ────────────── */ static void test_recall(void){ int N=envint("VINDEX_N_RECALL", g_quick?1500:5000), Q=200, K=10; printf("\n== Test 1: recall@10 vs brute force (N=%d, DIM=768) ==\n", N); float* v = (float*)malloc((size_t)N*DIM*sizeof(float)); gen_vectors(v, N, 111); double t0=now_s(); VIndex* ix = vindex_create(DIM, VINDEX_DEFAULT_M, VINDEX_DEFAULT_EF_CONSTRUCTION); for (int i=0;i= 0.90, "recall@10 >= 0.90 at default ef_search=128"); } free(oracle); free(qs); free(v); vindex_free(ix); } /* ── Test 2: speedup vs brute force ───────────────────────────────────────── */ static void speedup_at(int N){ int Q=100, K=10; float* v=(float*)malloc((size_t)N*DIM*sizeof(float)); gen_vectors(v,N,222); VIndex* ix=vindex_create(DIM,16,200); double bt0=now_s(); for(int i=0;i node count returns exactly node-count results"); vindex_free(ix); } /* duplicate / identical vectors */ { VIndex* ix=vindex_create(DIM,16,200); float a[DIM]; uint64_t s=2; for(int d=0;d=1 && ids[0]==(uint64_t)(1000+probe), "self-query returns itself as top-1"); CHECK(dd[0] < 1e-4f, "self-query top-1 distance ~0"); free(v); vindex_free(ix); } /* zero vector: no NaN, handled */ { VIndex* ix=vindex_create(DIM,16,200); float z[DIM]; memset(z,0,sizeof z); float a[DIM]; uint64_t s=3; for(int d=0;d=1 && !nan, "zero vector query produces no NaN/Inf"); n=vindex_search(ix, a, 2, 64, ids, dd); /* zero indexed */ nan=0; for(int i=0;i=1 && rids[0]<(uint64_t)nids && strcmp(ids[rids[0]], "node-42")==0); printf(" query for node-42's vector → top-1 id=%s dist=%.5f\n", (n>=1 && rids[0]<(uint64_t)nids)? ids[rids[0]] : "?", n?dd[0]:-1); CHECK(correct, "build_from_store query resolves to the right node id"); CHECK(n>=1 && dd[0]<1e-4f, "top-1 distance ~0 for exact stored vector"); for (int i=0;i