Three trust boundaries checked a caller's object and then read it again to use it. Between those two reads an accessor or a Proxy can answer differently, so the value that passed validation and the value that was installed were not the same value. A credential owner's answer was read field by field outside the auth boundary: a throwing `kind` getter escaped into the transport catch and an auth outage reached operators as `NETWORK_FAILURE`. Contract composition validated a contribution and then copied it, so a policy that answered 10,000 to the ceiling check and 999,999 to the copy installed the second value. The cursor runtime validated its profile once and re-read it on every page, so raising `maxPages` after construction widened a cap that had already been checked. `src/contracts/exact-snapshot.ts` is the one descriptor-based decoder they now share: every property is read exactly once, an accessor, a symbol, an inherited or non-enumerable field and a throwing trap all resolve to a typed failure, and validation runs on the owned copy. Separately, the `responseBody: NONE` probe awaited a bare `read()`. The deadline produced a bounded public result while the raw reader kept its lease, so the body stayed locked and the outer compensator could not cancel it. The probe now takes the operation lifetime and owns the cancel and the lock release itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
279 lines
7.2 KiB
TypeScript
279 lines
7.2 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" },
|
|
});
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 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);
|
|
});
|
|
}
|
|
});
|