Project one typed HttpExecutionObservation per logical V3 execution through a closed composition-root projector: only registered diagnostic context keys and bucketed values reach the sinks, and terminal non-abort failures now emit exactly one api.request.failed telemetry event. Caller cancellation and scope fencing record a diagnostic but never a failure event. routeId becomes a required input at the installed operation-executor boundary so the feature gateway's low-cardinality route identity survives to the sink. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
640 lines
19 KiB
TypeScript
640 lines
19 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: {},
|
|
credentials: "omit",
|
|
}),
|
|
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: {},
|
|
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" },
|
|
{ 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: {},
|
|
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 }, { 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" },
|
|
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" },
|
|
{ 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: {},
|
|
credentials: "omit",
|
|
}),
|
|
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: {},
|
|
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" },
|
|
{
|
|
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: {},
|
|
credentials: "omit",
|
|
}),
|
|
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: {},
|
|
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 }, { 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: {},
|
|
credentials: "omit",
|
|
}),
|
|
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: {},
|
|
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" },
|
|
{
|
|
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("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: {},
|
|
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 },
|
|
{ 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: {},
|
|
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 }, { 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: {},
|
|
credentials: "omit",
|
|
}),
|
|
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" },
|
|
});
|
|
});
|
|
});
|