The product was materialized from the template at `4dc033c` and has stayed on it through 43 template commits, so it was missing all three rounds of adapter remediation — including files it never had, such as the shared `abortable-operation` primitive and the `exact-snapshot` decoder that later fixes are written against. Taking only the newest round was not possible for that reason: the delta is coherent only as a whole. The product had not touched `src/adapters` at all since materialization, so the 140-file delta applied with a three-way merge and no conflicts. `package.json` was the single overlap and merged cleanly: the product owns `name`, the template contributed `check:adapter-inventory`, `check:remediation-ledger` and the image-resolve-signal type fixture. All 24 product-owned files — README, index.html, CI workflow, i18n catalog, home page, generated schemas, evidence scripts, component and visual snapshots — are byte-identical to `main`. `template.lock.json` now pins the synced revision and tree. Verified in this repository, not inherited from the template: six type projects, lint, nine gates (adapter inventory, remediation ledger, registries, diagnostics, realtime boundaries, architecture, browser file/storage boundaries, optional recipes, documentation), the production build, and 2,054 of 2,073 tests. The 19 failures are all in `tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template records; four suites that failed once under parallel load pass in isolation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
684 lines
20 KiB
TypeScript
684 lines
20 KiB
TypeScript
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 ROUTE_ID = "TEST_ROUTE";
|
|
|
|
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: {},
|
|
}),
|
|
fetcher: async () =>
|
|
Response.json(
|
|
{ type: "about:blank", title: "limited", status: 429 },
|
|
{ status: 429 },
|
|
),
|
|
});
|
|
|
|
await expect(
|
|
executor.execute(installed, { limit: 20 }, { routeId: ROUTE_ID, 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: {},
|
|
}));
|
|
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" },
|
|
{ routeId: ROUTE_ID, 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: {},
|
|
}));
|
|
const fetcher = vi.fn();
|
|
const executor = createContractHttpExecutor({
|
|
baseUrl: "https://api.example/",
|
|
maxRetryAttempts: 0,
|
|
attachCredentials,
|
|
fetcher,
|
|
});
|
|
|
|
await expect(
|
|
executor.execute(installed, { limit: 20 }, { routeId: ROUTE_ID, 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: { routeId: ROUTE_ID, scope },
|
|
headerName: "Idempotency-Key",
|
|
},
|
|
{
|
|
label: "valid KEYED command with a case-variant reserved header",
|
|
operation: createInstalled,
|
|
input: { name: "created" },
|
|
context: { routeId: ROUTE_ID, 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" },
|
|
}));
|
|
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: {},
|
|
}),
|
|
fetcher,
|
|
sleep: async () => {},
|
|
random: () => 0,
|
|
});
|
|
const retryingCreate = {
|
|
...createInstalled,
|
|
frontend: {
|
|
...createInstalled.frontend,
|
|
retryBudget: 1 as const,
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
executor.execute(
|
|
retryingCreate,
|
|
{ name: "created" },
|
|
{ routeId: ROUTE_ID, 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: {},
|
|
}),
|
|
fetcher,
|
|
});
|
|
|
|
await expect(
|
|
executor.execute(
|
|
installed,
|
|
{ limit: 20 },
|
|
{ routeId: ROUTE_ID, 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: {},
|
|
}),
|
|
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" },
|
|
{
|
|
routeId: ROUTE_ID,
|
|
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: {},
|
|
}),
|
|
fetcher,
|
|
});
|
|
|
|
await expect(executor.execute(installed, input, { routeId: ROUTE_ID, 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: {},
|
|
}),
|
|
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 }, { routeId: ROUTE_ID, scope })).resolves.toMatchObject({
|
|
kind: "CONTRACT_VIOLATION",
|
|
effect: "NOT_APPLICABLE",
|
|
});
|
|
await expect(executor.execute(malformed, { limit: 20 }, { routeId: ROUTE_ID, 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: {},
|
|
}),
|
|
fetcher: vi.fn(async () => Response.json({ malformed: true }, { status: 201 })),
|
|
});
|
|
|
|
await expect(
|
|
executor.execute(
|
|
createInstalled,
|
|
{ name: "created" },
|
|
{
|
|
routeId: ROUTE_ID,
|
|
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: {},
|
|
}),
|
|
fetcher: vi.fn(async () => {
|
|
current = false;
|
|
lifetime.abort();
|
|
return Response.json(
|
|
{ id: "created", name: "Created" },
|
|
{ status: 201 },
|
|
);
|
|
}),
|
|
});
|
|
|
|
await expect(
|
|
executor.execute(
|
|
createInstalled,
|
|
{ name: "created" },
|
|
{
|
|
routeId: ROUTE_ID,
|
|
scope: fencedScope,
|
|
intent: mutationIntent({ intentId: "intent-2", idempotencyKey: "key-2" }),
|
|
},
|
|
),
|
|
).resolves.toMatchObject({
|
|
kind: "CONTRACT_VIOLATION",
|
|
violation: { kind: "SCOPE_FENCED" },
|
|
effect: "MAYBE_APPLIED",
|
|
});
|
|
});
|
|
|
|
it("keeps a dispatched command MAYBE_APPLIED when a retry-time fence lands between scope checks", async () => {
|
|
// Attempt 1 dispatches an idempotent command and receives 429. The retry
|
|
// sleep resolves, the loop-entry scope check is still current, and only the
|
|
// pre-dispatch final invariant observes the fence.
|
|
let armed = false;
|
|
let checksAfterArming = 0;
|
|
const idempotentCommand: InstalledHttpContract<unknown, unknown, unknown> = {
|
|
...createInstalled,
|
|
contract: {
|
|
...createInstalled.contract,
|
|
retrySemantics: "IDEMPOTENT" as const,
|
|
},
|
|
frontend: { ...createInstalled.frontend, retryBudget: 1 as const },
|
|
};
|
|
const racingScope = Object.freeze({
|
|
...scope,
|
|
isCurrent: () => {
|
|
if (!armed) return true;
|
|
checksAfterArming += 1;
|
|
// The retry loop entry still observes a current scope; the pre-dispatch
|
|
// final invariant is the first observation of the fence.
|
|
return checksAfterArming <= 1;
|
|
},
|
|
});
|
|
const fetcher = vi.fn(async () =>
|
|
Response.json({ type: "about:blank", title: "slow down", status: 429 }, {
|
|
status: 429,
|
|
}),
|
|
);
|
|
const executor = createContractHttpExecutor({
|
|
baseUrl: "https://api.example/",
|
|
maxRetryAttempts: 1,
|
|
attachCredentials: () => ({ kind: "READY", headers: {} }),
|
|
fetcher,
|
|
sleep: async () => {
|
|
armed = true;
|
|
},
|
|
random: () => 0,
|
|
});
|
|
|
|
const outcome = await executor.execute(
|
|
idempotentCommand,
|
|
{ name: "created" },
|
|
{
|
|
routeId: ROUTE_ID,
|
|
scope: racingScope,
|
|
intent: mutationIntent({ idempotencyKey: null }),
|
|
},
|
|
);
|
|
|
|
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
expect(outcome).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 }, { routeId: ROUTE_ID, 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: {},
|
|
}),
|
|
fetcher,
|
|
monotonicNow: () => 0,
|
|
random: () => 0,
|
|
sleep,
|
|
observe,
|
|
});
|
|
|
|
for (let iteration = 0; iteration < iterationCount; iteration += 1) {
|
|
const result = executor.execute(
|
|
operation({ deadlineMs: 5 }),
|
|
{ limit: 20 },
|
|
{ routeId: ROUTE_ID, 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({
|
|
attemptCount: 1,
|
|
errorKind: "TIMEOUT",
|
|
terminalReason: "TIMEOUT",
|
|
cancellationOwner: "DEADLINE",
|
|
routeId: ROUTE_ID,
|
|
operationId: "TEST_LIST_ENTITIES",
|
|
}),
|
|
);
|
|
}
|
|
} 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: {},
|
|
}),
|
|
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 }, { routeId: ROUTE_ID, 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: {},
|
|
}),
|
|
fetcher: vi.fn(async () => new Response(body, { status: 200 })),
|
|
});
|
|
|
|
await expect(
|
|
executor.execute(
|
|
operation({ responseBody: "NONE" }),
|
|
{ limit: 20 },
|
|
{ routeId: ROUTE_ID, scope },
|
|
),
|
|
).resolves.toMatchObject({
|
|
kind: "TRANSPORT_FAILURE",
|
|
failure: { kind: "RESPONSE_STREAM_FAILURE" },
|
|
});
|
|
});
|
|
});
|