chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,627 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createContractHttpExecutor } from "../../src/adapters/http/http-execution-v3.ts";
|
||||
import type { InstalledHttpContract } from "../../src/contracts/external-contract-runtime.ts";
|
||||
import {
|
||||
TEST_CREATE_HTTP_CONTRACT,
|
||||
TEST_LIST_HTTP_CONTRACT,
|
||||
} from "../helpers/external-contract-fixture.ts";
|
||||
|
||||
const installed: InstalledHttpContract<unknown, unknown, unknown> =
|
||||
TEST_LIST_HTTP_CONTRACT;
|
||||
|
||||
const scope = Object.freeze({
|
||||
generation: 1,
|
||||
fingerprint: "scope-1",
|
||||
identities: Object.freeze({}) as never,
|
||||
signal: new AbortController().signal,
|
||||
isCurrent: () => true,
|
||||
});
|
||||
|
||||
const createInstalled: InstalledHttpContract<unknown, unknown, unknown> =
|
||||
TEST_CREATE_HTTP_CONTRACT;
|
||||
|
||||
function mutationIntent(
|
||||
overrides: Readonly<{
|
||||
intentId?: string;
|
||||
operationId?: string;
|
||||
idempotencyKey?: string | null;
|
||||
}> = {},
|
||||
) {
|
||||
return Object.freeze({
|
||||
intentId: overrides.intentId ?? "intent-1",
|
||||
operationId: overrides.operationId ?? "TEST_CREATE_ENTITY",
|
||||
canonicalInputIdentity: "opaque-input-identity",
|
||||
...(overrides.idempotencyKey === null
|
||||
? {}
|
||||
: { idempotencyKey: overrides.idempotencyKey ?? "key-1" }),
|
||||
createdAtMonotonicMs: 1,
|
||||
});
|
||||
}
|
||||
|
||||
function operation(
|
||||
overrides: Readonly<{
|
||||
deadlineMs?: number;
|
||||
responseBody?: "REQUIRED_JSON" | "OPTIONAL_JSON" | "NONE";
|
||||
}> = {},
|
||||
): InstalledHttpContract<unknown, unknown, unknown> {
|
||||
return {
|
||||
...installed,
|
||||
contract: {
|
||||
...installed.contract,
|
||||
responseBody: overrides.responseBody ?? installed.contract.responseBody,
|
||||
},
|
||||
frontend: {
|
||||
...installed.frontend,
|
||||
totalDeadlineMs: overrides.deadlineMs ?? installed.frontend.totalDeadlineMs,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function flushMicrotasks(): Promise<void> {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
describe("descriptor-driven HTTP execution lifetime", () => {
|
||||
it("normalizes a read-side 429 to the non-applicable effect vocabulary", async () => {
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY",
|
||||
headers: {},
|
||||
credentials: "omit",
|
||||
}),
|
||||
fetcher: async () =>
|
||||
Response.json(
|
||||
{ type: "about:blank", title: "limited", status: 429 },
|
||||
{ status: 429 },
|
||||
),
|
||||
});
|
||||
|
||||
await expect(
|
||||
executor.execute(installed, { limit: 20 }, { scope }),
|
||||
).resolves.toMatchObject({
|
||||
kind: "RATE_LIMITED",
|
||||
effect: "NOT_APPLICABLE",
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["absent intent", () => undefined],
|
||||
["empty key", () => mutationIntent({ idempotencyKey: "" })],
|
||||
["control-character key", () => mutationIntent({ idempotencyKey: "key\u0000private" })],
|
||||
["over-budget key", () => mutationIntent({ idempotencyKey: "k".repeat(257) })],
|
||||
[
|
||||
"wrong operation",
|
||||
() => mutationIntent({ operationId: "TEST_OTHER_COMMAND" }),
|
||||
],
|
||||
])(
|
||||
"rejects a KEYED command with %s before credentials or fetch",
|
||||
async (_label, intentFactory) => {
|
||||
const attachCredentials = vi.fn(() => ({
|
||||
kind: "READY" as const,
|
||||
headers: {},
|
||||
credentials: "omit" as const,
|
||||
}));
|
||||
const fetcher = vi.fn();
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials,
|
||||
fetcher,
|
||||
});
|
||||
const intent = intentFactory();
|
||||
|
||||
await expect(
|
||||
executor.execute(
|
||||
createInstalled,
|
||||
{ name: "created" },
|
||||
{ scope, ...(intent === undefined ? {} : { intent }) },
|
||||
),
|
||||
).resolves.toMatchObject({
|
||||
kind: "CONTRACT_VIOLATION",
|
||||
violation: {
|
||||
kind: "MISSING_IDEMPOTENCY_KEY",
|
||||
operation: "REQUEST",
|
||||
},
|
||||
effect: "NOT_STARTED",
|
||||
});
|
||||
expect(attachCredentials).not.toHaveBeenCalled();
|
||||
expect(fetcher).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["an intent without a key", mutationIntent({ idempotencyKey: null })],
|
||||
["an intent with a key", mutationIntent()],
|
||||
])(
|
||||
"rejects a query carrying %s before credentials or fetch",
|
||||
async (_label, intent) => {
|
||||
const attachCredentials = vi.fn(() => ({
|
||||
kind: "READY" as const,
|
||||
headers: {},
|
||||
credentials: "omit" as const,
|
||||
}));
|
||||
const fetcher = vi.fn();
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials,
|
||||
fetcher,
|
||||
});
|
||||
|
||||
await expect(
|
||||
executor.execute(installed, { limit: 20 }, { scope, intent }),
|
||||
).resolves.toMatchObject({
|
||||
kind: "CONTRACT_VIOLATION",
|
||||
violation: {
|
||||
kind: "UNEXPECTED_IDEMPOTENCY_KEY",
|
||||
operation: "REQUEST",
|
||||
},
|
||||
effect: "NOT_STARTED",
|
||||
});
|
||||
expect(attachCredentials).not.toHaveBeenCalled();
|
||||
expect(fetcher).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "query with the canonical reserved header",
|
||||
operation: installed,
|
||||
input: { limit: 20 },
|
||||
context: { scope },
|
||||
headerName: "Idempotency-Key",
|
||||
},
|
||||
{
|
||||
label: "valid KEYED command with a case-variant reserved header",
|
||||
operation: createInstalled,
|
||||
input: { name: "created" },
|
||||
context: { scope, intent: mutationIntent() },
|
||||
headerName: "iDeMpOtEnCy-KeY",
|
||||
},
|
||||
])(
|
||||
"rejects a credential patch for $label before fetch",
|
||||
async ({ operation, input, context, headerName }) => {
|
||||
const attachCredentials = vi.fn(() => ({
|
||||
kind: "READY" as const,
|
||||
headers: { [headerName]: "credential-owned-key" },
|
||||
credentials: "omit" as const,
|
||||
}));
|
||||
const fetcher = vi.fn();
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials,
|
||||
fetcher,
|
||||
});
|
||||
|
||||
await expect(
|
||||
executor.execute(operation, input, context),
|
||||
).resolves.toMatchObject({
|
||||
kind: "CONTRACT_VIOLATION",
|
||||
violation: {
|
||||
kind: "UNEXPECTED_IDEMPOTENCY_KEY",
|
||||
operation: "REQUEST",
|
||||
},
|
||||
effect: "NOT_STARTED",
|
||||
});
|
||||
expect(attachCredentials).toHaveBeenCalledOnce();
|
||||
expect(fetcher).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("reuses one supplied idempotency key across every physical retry", async () => {
|
||||
const observedKeys: Array<string | null> = [];
|
||||
let attempt = 0;
|
||||
const fetcher = vi.fn((_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
observedKeys.push(new Headers(init?.headers).get("Idempotency-Key"));
|
||||
attempt += 1;
|
||||
if (attempt === 1) throw new TypeError("synchronous pre-dispatch failure");
|
||||
return Promise.resolve(
|
||||
Response.json(
|
||||
{ id: "created", name: "Created" },
|
||||
{ status: 201 },
|
||||
),
|
||||
);
|
||||
});
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 1,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY",
|
||||
headers: {},
|
||||
credentials: "omit",
|
||||
}),
|
||||
fetcher,
|
||||
sleep: async () => {},
|
||||
random: () => 0,
|
||||
});
|
||||
const retryingCreate = {
|
||||
...createInstalled,
|
||||
frontend: {
|
||||
...createInstalled.frontend,
|
||||
retryBudget: 1 as const,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
executor.execute(
|
||||
retryingCreate,
|
||||
{ name: "created" },
|
||||
{ scope, intent: mutationIntent({ idempotencyKey: "logical-key" }) },
|
||||
),
|
||||
).resolves.toMatchObject({ kind: "SUCCESS" });
|
||||
expect(fetcher).toHaveBeenCalledTimes(2);
|
||||
expect(observedKeys).toEqual(["logical-key", "logical-key"]);
|
||||
});
|
||||
|
||||
it("never emits a mutation idempotency header for a query", async () => {
|
||||
const observedKeys: Array<string | null> = [];
|
||||
const fetcher = vi.fn(
|
||||
async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
observedKeys.push(
|
||||
new Headers(init?.headers).get("Idempotency-Key"),
|
||||
);
|
||||
return Response.json([]);
|
||||
},
|
||||
);
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY",
|
||||
headers: {},
|
||||
credentials: "omit",
|
||||
}),
|
||||
fetcher,
|
||||
});
|
||||
|
||||
await expect(
|
||||
executor.execute(
|
||||
installed,
|
||||
{ limit: 20 },
|
||||
{ scope },
|
||||
),
|
||||
).resolves.toMatchObject({ kind: "SUCCESS" });
|
||||
expect(fetcher).toHaveBeenCalledOnce();
|
||||
expect(observedKeys).toEqual([null]);
|
||||
});
|
||||
|
||||
it("keeps intent identities out of request URLs and safe observations", async () => {
|
||||
const urls: string[] = [];
|
||||
const observations: unknown[] = [];
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY",
|
||||
headers: {},
|
||||
credentials: "omit",
|
||||
}),
|
||||
fetcher: vi.fn(async (input) => {
|
||||
urls.push(String(input));
|
||||
return Response.json(
|
||||
{ id: "created", name: "Created" },
|
||||
{ status: 201 },
|
||||
);
|
||||
}),
|
||||
observe: (observation) => observations.push(observation),
|
||||
});
|
||||
|
||||
await expect(
|
||||
executor.execute(
|
||||
createInstalled,
|
||||
{ name: "created" },
|
||||
{
|
||||
scope,
|
||||
intent: Object.freeze({
|
||||
intentId: "private-intent-id",
|
||||
operationId: "TEST_CREATE_ENTITY",
|
||||
canonicalInputIdentity: "private-canonical-input",
|
||||
idempotencyKey: "private-idempotency-key",
|
||||
createdAtMonotonicMs: 1,
|
||||
}),
|
||||
},
|
||||
),
|
||||
).resolves.toMatchObject({ kind: "SUCCESS" });
|
||||
|
||||
const safeEvidence = JSON.stringify({ urls, observations });
|
||||
expect(urls).toEqual(["https://api.example/api/test-entities"]);
|
||||
expect(observations).toHaveLength(1);
|
||||
expect(safeEvidence).not.toContain("private-intent-id");
|
||||
expect(safeEvidence).not.toContain("private-canonical-input");
|
||||
expect(safeEvidence).not.toContain("private-idempotency-key");
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{}, "limit=20"],
|
||||
[{ limit: "7" }, "limit=7"],
|
||||
])("projects the canonical validated input %#", async (input, expectedQuery) => {
|
||||
const fetcher = vi.fn(async () => Response.json([]));
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY",
|
||||
headers: {},
|
||||
credentials: "omit",
|
||||
}),
|
||||
fetcher,
|
||||
});
|
||||
|
||||
await expect(executor.execute(installed, input, { scope })).resolves.toMatchObject({
|
||||
kind: "SUCCESS",
|
||||
});
|
||||
expect(String((fetcher.mock.calls as unknown[][])[0]?.[0])).toContain(
|
||||
expectedQuery,
|
||||
);
|
||||
});
|
||||
|
||||
it("contains throwing and malformed external request projections", async () => {
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY",
|
||||
headers: {},
|
||||
credentials: "omit",
|
||||
}),
|
||||
fetcher: vi.fn(),
|
||||
});
|
||||
const throwing = {
|
||||
...installed,
|
||||
contract: {
|
||||
...installed.contract,
|
||||
projectRequest: () => {
|
||||
throw new Error("external descriptor defect");
|
||||
},
|
||||
},
|
||||
};
|
||||
const malformed = {
|
||||
...installed,
|
||||
contract: {
|
||||
...installed.contract,
|
||||
projectRequest: () => ({ pathValues: {}, queryEntries: [["limit"]], body: null }),
|
||||
},
|
||||
} as unknown as typeof installed;
|
||||
|
||||
await expect(executor.execute(throwing, { limit: 20 }, { scope })).resolves.toMatchObject({
|
||||
kind: "CONTRACT_VIOLATION",
|
||||
effect: "NOT_APPLICABLE",
|
||||
});
|
||||
await expect(executor.execute(malformed, { limit: 20 }, { scope })).resolves.toMatchObject({
|
||||
kind: "CONTRACT_VIOLATION",
|
||||
effect: "NOT_APPLICABLE",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves MAYBE_APPLIED for a malformed command response after dispatch", async () => {
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY",
|
||||
headers: {},
|
||||
credentials: "omit",
|
||||
}),
|
||||
fetcher: vi.fn(async () => Response.json({ malformed: true }, { status: 201 })),
|
||||
});
|
||||
|
||||
await expect(
|
||||
executor.execute(
|
||||
createInstalled,
|
||||
{ name: "created" },
|
||||
{
|
||||
scope,
|
||||
intent: mutationIntent(),
|
||||
},
|
||||
),
|
||||
).resolves.toMatchObject({
|
||||
kind: "CONTRACT_VIOLATION",
|
||||
violation: { kind: "SUCCESS_SCHEMA_INVALID" },
|
||||
effect: "MAYBE_APPLIED",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves MAYBE_APPLIED when a command response arrives after its scope fence", async () => {
|
||||
let current = true;
|
||||
const lifetime = new AbortController();
|
||||
const fencedScope = Object.freeze({
|
||||
...scope,
|
||||
signal: lifetime.signal,
|
||||
isCurrent: () => current,
|
||||
});
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY",
|
||||
headers: {},
|
||||
credentials: "omit",
|
||||
}),
|
||||
fetcher: vi.fn(async () => {
|
||||
current = false;
|
||||
lifetime.abort();
|
||||
return Response.json(
|
||||
{ id: "created", name: "Created" },
|
||||
{ status: 201 },
|
||||
);
|
||||
}),
|
||||
});
|
||||
|
||||
await expect(
|
||||
executor.execute(
|
||||
createInstalled,
|
||||
{ name: "created" },
|
||||
{
|
||||
scope: fencedScope,
|
||||
intent: mutationIntent({ intentId: "intent-2", idempotencyKey: "key-2" }),
|
||||
},
|
||||
),
|
||||
).resolves.toMatchObject({
|
||||
kind: "CONTRACT_VIOLATION",
|
||||
violation: { kind: "SCOPE_FENCED" },
|
||||
effect: "MAYBE_APPLIED",
|
||||
});
|
||||
});
|
||||
|
||||
it("settles a credential hang at the total operation deadline", async () => {
|
||||
vi.useFakeTimers();
|
||||
let settled = false;
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => new Promise(() => {}),
|
||||
});
|
||||
|
||||
const result = executor
|
||||
.execute(operation({ deadlineMs: 5 }), { limit: 20 }, { scope })
|
||||
.then((outcome) => {
|
||||
settled = true;
|
||||
return outcome;
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(5);
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(settled).toBe(true);
|
||||
await expect(result).resolves.toMatchObject({
|
||||
kind: "TRANSPORT_FAILURE",
|
||||
failure: { kind: "TIMEOUT" },
|
||||
});
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("never retries a deadline-owned abort while the monotonic clock still has a sub-tick budget", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const iterationCount = 100;
|
||||
const sleep = vi.fn(async () => {
|
||||
throw new Error("a deadline-owned abort must not enter retry sleep");
|
||||
});
|
||||
const fetcher = vi.fn(
|
||||
(_input: RequestInfo | URL, init?: RequestInit) =>
|
||||
new Promise<Response>((_resolve, reject) => {
|
||||
init?.signal?.addEventListener(
|
||||
"abort",
|
||||
() => reject(new DOMException("Aborted", "AbortError")),
|
||||
{ once: true },
|
||||
);
|
||||
}),
|
||||
);
|
||||
const observe = vi.fn();
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 2,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY",
|
||||
headers: {},
|
||||
credentials: "omit",
|
||||
}),
|
||||
fetcher,
|
||||
monotonicNow: () => 0,
|
||||
random: () => 0,
|
||||
sleep,
|
||||
observe,
|
||||
});
|
||||
|
||||
for (let iteration = 0; iteration < iterationCount; iteration += 1) {
|
||||
const result = executor.execute(
|
||||
operation({ deadlineMs: 5 }),
|
||||
{ limit: 20 },
|
||||
{ scope },
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(5);
|
||||
await flushMicrotasks();
|
||||
|
||||
await expect(result).resolves.toMatchObject({
|
||||
kind: "TRANSPORT_FAILURE",
|
||||
failure: { kind: "TIMEOUT" },
|
||||
});
|
||||
}
|
||||
|
||||
expect(fetcher).toHaveBeenCalledTimes(iterationCount);
|
||||
expect(sleep).not.toHaveBeenCalled();
|
||||
expect(observe).toHaveBeenCalledTimes(iterationCount);
|
||||
for (const [observation] of observe.mock.calls) {
|
||||
expect(observation).toEqual(
|
||||
expect.objectContaining({ attempts: 1, certainty: "TIMEOUT" }),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("cancels a non-cooperative retry sleep when the caller aborts", async () => {
|
||||
const caller = new AbortController();
|
||||
let sleepSignal: AbortSignal | undefined;
|
||||
let settled = false;
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 2,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY",
|
||||
headers: {},
|
||||
credentials: "omit",
|
||||
}),
|
||||
fetcher: vi.fn(async () =>
|
||||
Response.json(
|
||||
{ type: "about:blank", title: "temporary", status: 503 },
|
||||
{ status: 503 },
|
||||
),
|
||||
),
|
||||
sleep: (_ms, signal) => {
|
||||
sleepSignal = signal;
|
||||
return new Promise(() => {});
|
||||
},
|
||||
random: () => 0,
|
||||
});
|
||||
|
||||
const result = executor
|
||||
.execute(installed, { limit: 20 }, { scope, signal: caller.signal })
|
||||
.then((outcome) => {
|
||||
settled = true;
|
||||
return outcome;
|
||||
});
|
||||
await vi.waitFor(() => expect(sleepSignal).toBeDefined());
|
||||
caller.abort();
|
||||
await flushMicrotasks();
|
||||
|
||||
expect(sleepSignal?.aborted).toBe(true);
|
||||
expect(settled).toBe(true);
|
||||
await expect(result).resolves.toMatchObject({ kind: "CANCELLED" });
|
||||
});
|
||||
|
||||
it("treats an unreadable forbidden-body probe as a transport failure", async () => {
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
pull(controller) {
|
||||
controller.error(new TypeError("stream failed"));
|
||||
},
|
||||
});
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY",
|
||||
headers: {},
|
||||
credentials: "omit",
|
||||
}),
|
||||
fetcher: vi.fn(async () => new Response(body, { status: 200 })),
|
||||
});
|
||||
|
||||
await expect(
|
||||
executor.execute(
|
||||
operation({ responseBody: "NONE" }),
|
||||
{ limit: 20 },
|
||||
{ scope },
|
||||
),
|
||||
).resolves.toMatchObject({
|
||||
kind: "TRANSPORT_FAILURE",
|
||||
failure: { kind: "RESPONSE_STREAM_FAILURE" },
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user