Files
clean-architecture-frontend…/tests/unit/cursor-pagination-runtime.test.ts
T
DongHyeonkaandClaude Opus 5 4fe924ee0f fix: harden bounded state sidecars
N-05: the conditional-validator key was a colon join over components that may
themselves contain colons, so two distinct valid bindings could collide and one
definition's ETag could be prepared for another. The key is now a validated,
byte-bounded fixed tuple encoded with JSON.stringify.

N-09: capture localStorage exactly once and compare StorageEvent.storageArea
against that object identity, so a pulse from sessionStorage or any other area
is rejected instead of matching on key and value alone. The pulse key is
registered in the storage registry as CACHE_INVALIDATION_PULSE.

N-10: race loadPage against the caller signal and re-check before observing a
page, so a non-cooperative loader can neither hold loadAll forever nor have a
post-abort completion accumulated into a successful result.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 23:35:48 +09:00

161 lines
4.1 KiB
TypeScript

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" },
});
});
});