Testing
novamem uses vitest across the workspace. ~200 unit + integration tests at the time of writing; CI gates every PR on green.
Run
# Everything
pnpm test
# A single package
pnpm --filter @azrtydxb/novamem-server test
# A single file (watch mode)
pnpm --filter @azrtydxb/novamem-server test -- --watch engine.test.ts
# Pattern
pnpm --filter @azrtydxb/novamem-server test -- "neighbors|search"Layout
Tests live next to the source they test:
src/
├── engine/index.ts
├── engine/engine.test.ts — engine logic
├── http.ts
├── http.test.ts — Fastify route tests via inject()
├── routes/mcp-sse.ts
├── routes/mcp-sse.test.ts — real SSE handshake via fetch
├── ...
└── test-fakes.ts — in-memory FakeWarmStore / FakeColdStore / FakeGraphStoreThe fakes implement the same interfaces as the real stores. Tests usually wire them into a MemoryEngine and exercise the full call path without a database.
Fakes vs real datastores
Almost every test uses fakes — fast, hermetic. A handful do real-network tests:
routes/mcp-sse.test.ts— Fastifylisten()+ realfetch()withAbortControllerper session, becauseapp.inject()buffers SSE indefinitelywarm-store/integration.test.ts(when present) — gated onRUN_DB_TESTS=1, requires a live Postgres
Patterns
Engine test
import { describe, expect, it } from "vitest";
import { bench } from "./test-bench";
describe("engine.search", () => {
it("fuses keyword + vector signals", async () => {
const b = bench();
const a = await b.engine.remember("public", { content: "Pascal likes coffee", force: true });
const r = await b.engine.search("public", { query: "coffee preference", k: 5 });
expect(r.results[0].id).toBe(a.id);
});
});bench() constructs an engine wired to in-memory fakes — see test-bench.ts.
Route test (Fastify inject)
import { describe, expect, it } from "vitest";
describe("POST /v1/search", () => {
it("rejects requests without a token", async () => {
const { app } = await makeApp();
const r = await app.inject({ method: "POST", url: "/v1/search", payload: {} });
expect(r.statusCode).toBe(401);
});
});makeApp() builds a Fastify instance against the fakes and skips the rate limit.
Real-network test (SSE)
For tests that need actual TCP (SSE concurrency caps, keepalive frames):
const port = await listen();
const ctrl = new AbortController();
const res = await fetch(`http://127.0.0.1:${port}/mcp/sse`, { signal: ctrl.signal });
// drain reader → assert frames → ctrl.abort() in afterAllUse sparingly. The fakes-backed inject path is faster and covers most logic.
Coverage
pnpm test -- --coverage (vitest-v8). Targets are not strictly enforced — focus on covering the failure modes instead of the percentage.
Adding a regression test
When a bug is fixed, add a test that fails on the bad version and passes on the fix. Keep the test commented to point at the bug:
// Regression for the FalkorDB driver-decode error path
// ("expected List or Null but was Path/Edge"). The engine catches the
// throw and surfaces it as a degraded result.
it("degrades when graph.neighbors throws", async () => {
// ...
});The comment plus the assertion together document why the test exists.
CI
.github/workflows/ci.yml runs:
pnpm testper packagepnpm typecheckpnpm lintpnpm audit --audit-level=moderate- Docker build + Trivy scan
- CodeQL static analysis
Branch protection requires all checks green + branch up-to-date with main before merge.