/** * TASK-0135 (Phase 15, pooling + replica) — LIVE replica-routing integration * suite. * * Proves, against the real NestJS API (Testcontainers Postgres) with a * DISTINCT read-replica wired via `DATABASE_REPLICA_URL`, the spec's * read/write split and staleness contract: * * • The block TREE read (`db.read()` — a spec-named replica read) is served * from the READER (replica). With a zero-copy replica the tree read is * STALE: it returns the replica's (empty) blocks, not the primary's. * * • The write-then-read-your-write invariant (spec: "a write-then-read- * your-write on the same request always hits primary") — the realtime * block FLUSH (`POST /internal/blocks/flush`) reads `pages.version` for * its `baseVersion` CAS and writes in one request. Against the empty * replica that read would 404 (no page) — it succeeds and bumps the * PRIMARY version, proving the version check read the PRIMARY, not the * replica. The optimistic-concurrency version check is never a replica * read. * * • The replica stays DIVERGED from the primary (replica.version stays 1 * while the primary bumps to 2) — the documented lag; the reader is a * distinct client, not the primary. * * The replica is a SEPARATE, schema-migrated but EMPTY database (zero-copy = * zero lag — every read the primary has is missing on the replica, so a * mis-route to the replica is always observable). PERF_CACHE is forced OFF so * the tree read is always a live reader read (no cache-hit masking). */ import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { PrismaClient } from "@prisma/client"; import { createTestApp, deploySchema, type TestAppHandle } from "./helpers/api"; import { getDatabaseUrl } from "./helpers/containers"; import { createUser, createWorkspace, authHeaders } from "./helpers/seed"; const REPLICA_DB = "factory_replica"; let app: TestAppHandle; let replicaUrl = ""; let replicaPrisma: PrismaClient | null = null; /** Build the replica URL = the primary URL with the database swapped. */ function buildReplicaUrl(primaryUrl: string): string { const u = new URL(primaryUrl); u.pathname = `/${REPLICA_DB}`; return u.toString(); } /** A standalone client to the primary (for schema + direct assertions). */ async function primaryClient(primaryUrl: string): Promise { const c = new PrismaClient({ datasources: { db: { url: primaryUrl } } }); await c.$connect(); return c; } beforeAll(async () => { const primaryUrl = await getDatabaseUrl(); const p = await primaryClient(primaryUrl); // 1) Create the replica DB (idempotent) + apply the schema (no data = lag). try { await p.$executeRawUnsafe(`CREATE DATABASE ${REPLICA_DB}`); } catch (e) { if (!String(e).includes("already exists")) throw e; } replicaUrl = buildReplicaUrl(primaryUrl); await deploySchema(replicaUrl); await p.$disconnect(); // 2) Wire the replica BEFORE the app boots (PrismaService reads it on // construction). PERF_CACHE off → the tree read is always a live reader // read (no cache-hit masking the replica route). process.env.DATABASE_REPLICA_URL = replicaUrl; process.env.PERF_CACHE = "off"; app = await createTestApp(); replicaPrisma = new PrismaClient({ datasources: { db: { url: replicaUrl } } }); await replicaPrisma.$connect(); }, 240_000); afterAll(async () => { await app?.close(); try { await replicaPrisma?.$disconnect(); } catch { /* ignore */ } // Drop the replica for cleanliness (after the app — its reader disconnects // in app.close(); an open reader would make the DROP wait for a session). try { const p = await primaryClient(await getDatabaseUrl()); await p.$executeRawUnsafe(`DROP DATABASE IF EXISTS ${REPLICA_DB}`); await p.$disconnect(); } catch { /* best-effort */ } }); describe("replica routing (live, DATABASE_REPLICA_URL set)", () => { let headers: Record; let workspaceId: string; let pageId: string; let primaryBlocks: { id: string; type: string; position: string; content: Record; properties: Record; parentId: string | null }[]; beforeAll(async () => { const owner = await createUser(app.request, { name: "Replica Owner" }); headers = authHeaders(owner.accessToken!); const ws = await createWorkspace(app.request, headers, `Replica WS ${Date.now()}`); workspaceId = ws.workspaceId; const page = await app.request("POST", `/api/v1/workspaces/${workspaceId}/pages`, { headers, body: { title: "Replica Page" } }); expect(page.status).toBeLessThan(300); pageId = ((await page.json()) as { id: string }).id; // Two blocks, written to the PRIMARY (the public create path — `db.write`). for (const i of [1, 2]) { const b = await app.request("POST", `/api/v1/pages/${pageId}/blocks`, { headers, body: { type: "paragraph", content: { text: `replica-${i}` }, properties: {} } }); expect(b.status).toBeLessThan(300); } // Sanity: the PRIMARY has the seeded title block + the 2 created // paragraphs (page create seeds a default title block). Capture ALL of // them so the flush below passes the full set back (a no-op diff). const rows = await app.prisma.block.findMany({ where: { pageId, deletedAt: null } }); expect(rows.length).toBeGreaterThanOrEqual(3); primaryBlocks = rows.map((b) => ({ id: b.id, type: b.type as string, position: b.position, content: b.content as Record, properties: b.properties as Record, parentId: b.parentId })); }); it("the block tree read (db.read) is served from the REPLICA → stale (empty)", async () => { // The tree read is the spec-named replica read. With the cache off and a // zero-copy replica, the page lookup + permission check run on the // primary (they succeed — the page exists there), but the BLOCK loader // (`this.prisma.read()`) runs on the replica → ZERO blocks. If the read // were mis-routed to the primary it would return the 2 blocks — a hard // failure of this assertion. const res = await app.request("GET", `/api/v1/pages/${pageId}/blocks?depth=all`, { headers }); expect(res.status).toBe(200); const body = (await res.json()) as { data: unknown[] }; expect(body.data).toHaveLength(0); // stale: the replica has no blocks }); it("write-then-read-your-write: the flush's version CAS hits the PRIMARY (not the replica)", async () => { // The flush reads pages.version for its baseVersion CAS and writes in the // SAME request. The primary page is at version 1; a replica read would // 404 (the page is absent on the empty replica) — it succeeds and bumps // the PRIMARY to 2, proving the version check read the primary, never the // replica (the optimistic-concurrency check is never a replica read). The // two blocks are passed back verbatim (a no-op diff — no soft-delete). const flush = await app.request("POST", "/internal/blocks/flush", { headers: { Authorization: `Bearer ${app.internalServiceToken}` }, body: { pageId, baseVersion: 1, blocks: primaryBlocks.map((b) => ({ id: b.id, parentId: b.parentId ?? null, type: b.type.toLowerCase(), position: b.position, content: b.content, properties: b.properties })) } }); expect(flush.status).toBe(200); const body = (await flush.json()) as { version?: number; conflict?: unknown }; expect(body.conflict).toBeUndefined(); // no CAS conflict expect(body.version).toBe(2); // the primary bumped 1 → 2 }); it("the replica stays DIVERGED from the primary (the documented lag)", async () => { // After the flush the primary page is at version 2; the empty replica // never received the page (no write path routes to it) — the strongest // possible divergence proof. A mis-routed write would have created one. const primaryPage = await app.prisma.page.findUnique({ where: { id: pageId }, select: { version: true } }); expect(primaryPage?.version).toBe(2); const replicaPage = await replicaPrisma!.page.findUnique({ where: { id: pageId }, select: { version: true } }); expect(replicaPage).toBeNull(); }); });