import { describe, expect, it, vi } from "vitest"; import { createCursorPaginationRuntime } from "../../src/adapters/query-cache/cursor-pagination-runtime.ts"; const profile = { profileId: "bounded-cursor-v1", maxPages: 3, maxTotalItems: 4, maxEstimatedBytes: 1_024, maxCursorBytes: 64, allowSparsePage: false, } as const; describe("bounded cursor pagination runtime", () => { it("returns PAGINATION_ABORTED when a non-cooperative page resolves after abort", async () => { const controller = new AbortController(); let releasePage: ((page: unknown) => void) | undefined; const loadPage = vi.fn( () => new Promise((resolve) => { releasePage = resolve as (page: unknown) => void; }), ); const runtime = createCursorPaginationRuntime({ definitionId: "bounded", profile, loadPage: loadPage as never, }); const loading = runtime.loadAll({ signal: controller.signal }); await Promise.resolve(); controller.abort(); const result = await loading; expect(result).toMatchObject({ ok: false, error: { kind: "REQUEST_ABORTED", code: "PAGINATION_ABORTED" }, }); // The late page completion must be ignored, not accumulated. releasePage?.({ ok: true, value: { items: ["late"], nextCursor: null, hasMore: false, snapshotToken: "snapshot-1", }, }); await Promise.resolve(); expect(loadPage).toHaveBeenCalledOnce(); }); it("loads a stable finite chain without exposing cursors in its value", async () => { const loadPage = vi .fn() .mockResolvedValueOnce({ ok: true, value: { items: ["one"], nextCursor: "cursor-2", hasMore: true, snapshotToken: "snapshot-a", }, }) .mockResolvedValueOnce({ ok: true, value: { items: ["two"], nextCursor: null, hasMore: false, snapshotToken: "snapshot-a", }, }); const runtime = createCursorPaginationRuntime({ definitionId: "LIST_ALL", profile, loadPage, }); await expect(runtime.loadAll({})).resolves.toEqual({ ok: true, value: ["one", "two"], }); expect(loadPage.mock.calls.map(([cursor]) => cursor)).toEqual([ null, "cursor-2", ]); }); it.each([ { page: { items: [], nextCursor: "next", hasMore: true, snapshotToken: null, }, code: "PAGINATION_PAGE_INVALID", }, { page: { items: ["one"], nextCursor: null, hasMore: true, snapshotToken: null, }, code: "PAGINATION_PAGE_INVALID", }, ])("rejects invalid page invariants", async ({ page, code }) => { const runtime = createCursorPaginationRuntime({ definitionId: "LIST_ALL", profile, loadPage: async () => ({ ok: true, value: page }), }); await expect(runtime.loadAll({})).resolves.toMatchObject({ ok: false, error: { kind: "PAGINATION_CONTRACT_VIOLATION", code }, }); }); it("rejects cursor loops and snapshot drift", async () => { const loop = createCursorPaginationRuntime({ definitionId: "LIST_ALL", profile, loadPage: async () => ({ ok: true, value: { items: ["one"], nextCursor: "same", hasMore: true, snapshotToken: null, }, }), }); await expect(loop.loadAll({})).resolves.toMatchObject({ ok: false, error: { code: "PAGINATION_CURSOR_LOOP" }, }); let page = 0; const drift = createCursorPaginationRuntime({ definitionId: "LIST_ALL", profile, loadPage: async () => ({ ok: true, value: { items: [String(page)], nextCursor: page++ === 0 ? "next" : null, hasMore: page === 1, snapshotToken: page === 1 ? "snapshot-a" : "snapshot-b", }, }), }); await expect(drift.loadAll({})).resolves.toMatchObject({ ok: false, error: { code: "PAGINATION_SNAPSHOT_CHANGED" }, }); }); }); /** * NS-07. The profile was validated once and then re-read on every page, so * raising `maxPages` after construction widened a cap that had already been * checked — the runtime issued more requests and returned more items than the * validated profile allowed. */ describe("NS-07 the caps are the ones that were validated", () => { it("keeps the page cap captured at construction", async () => { const mutable: { profileId: string; maxPages: number; maxTotalItems: number; maxEstimatedBytes: number; maxCursorBytes: number; allowSparsePage: boolean; } = { ...profile, maxPages: 1 }; const loadPage = vi.fn(async () => ({ ok: true as const, value: { items: [1], nextCursor: `cursor-${loadPage.mock.calls.length}`, hasMore: true, snapshotToken: "snapshot-a", }, })); const runtime = createCursorPaginationRuntime({ definitionId: "LIST_ALL", profile: mutable, loadPage, }); mutable.maxPages = 3; mutable.maxTotalItems = 99; await expect(runtime.loadAll({})).resolves.toMatchObject({ ok: false, error: { code: "PAGINATION_PAGE_LIMIT" }, }); expect(loadPage).toHaveBeenCalledTimes(1); }); it("keeps the loader captured at construction", async () => { const original = vi.fn(async () => ({ ok: true as const, value: { items: [1], nextCursor: null, hasMore: false, snapshotToken: null, }, })); const replacement = vi.fn(); const dependencies = { definitionId: "LIST_ALL", profile, loadPage: original, }; const runtime = createCursorPaginationRuntime(dependencies); dependencies.loadPage = replacement as never; await expect(runtime.loadAll({})).resolves.toMatchObject({ ok: true }); expect(original).toHaveBeenCalledTimes(1); expect(replacement).not.toHaveBeenCalled(); }); const hostileProfiles: readonly (readonly [string, () => unknown])[] = [ [ "an accessor cap", () => Object.defineProperty({ ...profile }, "maxPages", { enumerable: true, get: () => 3, }), ], [ "an inherited cap", () => Object.create({ ...profile }) as unknown, ], ["an extra own field", () => ({ ...profile, injected: true })], [ "a symbol field", () => ({ ...profile, [Symbol.for("injected")]: true }), ], [ "a non-enumerable own field", () => Object.defineProperty({ ...profile }, "injected", { enumerable: false, value: true, }), ], [ "a throwing ownKeys trap", () => new Proxy( { ...profile }, { ownKeys() { throw new TypeError("hostile ownKeys trap"); }, }, ), ], ]; for (const [label, build] of hostileProfiles) { it(`refuses to build a runtime from ${label}`, () => { expect(() => createCursorPaginationRuntime({ definitionId: "LIST_ALL", profile: build() as never, loadPage: vi.fn(), }), ).toThrow(TypeError); }); } });