LIVE-01. A credential collaborator that returns UNAVAILABLE, throws, rejects or answers off-contract is an outage of the auth integration, not evidence about the user's session. Each of those now closes as AUTH_INTEGRATION_FAILURE with zero fetches, so the composition root's logout path stays reserved for a genuinely absent session. The synchronous and asynchronous failure sites share one classifier. LIVE-02 / LIVE-03. Object.freeze(new Map(...)) freezes the wrapper, not the backing store, so an exported registry could still be cleared or replaced after composition. Both the installed REST auth profile registry and the composed HTTP/event lookups are now read facades over private stores, and every composed row is an exact own-data snapshot that rejects accessors, inherited and symbol-keyed fields. LIVE-04. The total deadline now bounds the physical waits rather than being checked between them: dispatch and response admission race the attempt signal, the bounded reader takes that signal, and an abandoned operation is still observed once so a late native rejection cannot surface unhandled. A body that completes after the deadline or the caller owns the execution is no longer admitted; a stale generation keeps its more specific SCOPE_FENCED verdict. LIVE-05. DEADLINE is no longer treated as a caller-owned cancellation, so a timeout reaches api.request.failed exactly once while caller, route, scope and shutdown aborts stay excluded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
413 lines
13 KiB
TypeScript
413 lines
13 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
createContractHttpExecutor,
|
|
type HttpExecutionObservation,
|
|
} from "../../src/adapters/http/http-execution-v3.ts";
|
|
import { createHttpObservationProjector } from "../../src/bootstrap/runtime-adapters.ts";
|
|
import { installRestAuthProfileRegistry } from "../../src/contracts/rest-profiles.ts";
|
|
import { TEST_LIST_HTTP_CONTRACT } from "../helpers/external-contract-fixture.ts";
|
|
|
|
const ROUTE_ID = "TEST_ROUTE";
|
|
|
|
const TEST_PROFILES = installRestAuthProfileRegistry({
|
|
TEST_AUTH: {
|
|
authProfileId: "TEST_AUTH",
|
|
transport: "BEARER_HEADER",
|
|
credentials: "omit",
|
|
allowedCredentialHeaders: ["authorization"],
|
|
requiredCredentialHeaders: ["authorization"],
|
|
},
|
|
});
|
|
|
|
function scopeSnapshot(signal: AbortSignal = new AbortController().signal) {
|
|
return Object.freeze({
|
|
generation: 1,
|
|
fingerprint: "scope-1",
|
|
identities: Object.freeze({}) as never,
|
|
signal,
|
|
isCurrent: () => true,
|
|
});
|
|
}
|
|
|
|
function bearerOperation(deadlineMs = 10_000) {
|
|
return {
|
|
...TEST_LIST_HTTP_CONTRACT,
|
|
frontend: {
|
|
...TEST_LIST_HTTP_CONTRACT.frontend,
|
|
authProfileId: "TEST_AUTH",
|
|
totalDeadlineMs: deadlineMs,
|
|
},
|
|
};
|
|
}
|
|
|
|
function jsonResponse(body: unknown): Response {
|
|
return new Response(JSON.stringify(body), {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* LIVE-01. A credential collaborator that is broken, unavailable or throwing is
|
|
* an integration failure of the auth system. Reporting it as `UNAUTHENTICATED`
|
|
* makes the composition root run its logout path, so an auth outage would sign
|
|
* every user out.
|
|
*/
|
|
describe("LIVE-01 credential integration failures are not user session failures", () => {
|
|
const brokenOwners = [
|
|
{
|
|
label: "returns UNAVAILABLE",
|
|
attach: () => Object.freeze({ kind: "UNAVAILABLE" as const }),
|
|
},
|
|
{
|
|
label: "throws synchronously",
|
|
attach: () => {
|
|
throw new Error("credential owner exploded");
|
|
},
|
|
},
|
|
{
|
|
label: "rejects asynchronously",
|
|
attach: () => Promise.reject(new Error("credential owner exploded")),
|
|
},
|
|
{
|
|
label: "returns a malformed outcome",
|
|
attach: () => ({ kind: "TOTALLY_UNKNOWN" }) as never,
|
|
},
|
|
];
|
|
|
|
for (const owner of brokenOwners) {
|
|
it(`closes as AUTH_INTEGRATION_FAILURE when the owner ${owner.label}`, async () => {
|
|
const fetcher = vi.fn(async () => jsonResponse([]));
|
|
const executor = createContractHttpExecutor({
|
|
baseUrl: "https://api.example/",
|
|
maxRetryAttempts: 0,
|
|
authProfiles: TEST_PROFILES,
|
|
attachCredentials: owner.attach,
|
|
fetcher: fetcher as unknown as typeof fetch,
|
|
});
|
|
|
|
const outcome = await executor.execute(
|
|
bearerOperation(),
|
|
{ limit: 1 },
|
|
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
|
|
);
|
|
|
|
expect(outcome.kind).toBe("AUTH_INTEGRATION_FAILURE");
|
|
expect(outcome.effect).toBe("NOT_APPLICABLE");
|
|
expect(fetcher).toHaveBeenCalledTimes(0);
|
|
});
|
|
}
|
|
|
|
it("still reports a real absent session as UNAUTHENTICATED", async () => {
|
|
const fetcher = vi.fn(async () => jsonResponse([]));
|
|
const executor = createContractHttpExecutor({
|
|
baseUrl: "https://api.example/",
|
|
maxRetryAttempts: 0,
|
|
authProfiles: TEST_PROFILES,
|
|
attachCredentials: () =>
|
|
Object.freeze({ kind: "UNAUTHENTICATED" as const }),
|
|
fetcher: fetcher as unknown as typeof fetch,
|
|
});
|
|
|
|
const outcome = await executor.execute(
|
|
bearerOperation(),
|
|
{ limit: 1 },
|
|
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
|
|
);
|
|
|
|
expect(outcome.kind).toBe("UNAUTHENTICATED");
|
|
expect(fetcher).toHaveBeenCalledTimes(0);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* LIVE-04. The total deadline must bound the physical wait, not merely be
|
|
* checked between awaits. A non-cooperative `fetch` or body reader that ignores
|
|
* the abort signal cannot hold the port result open, and a value that arrives
|
|
* after the deadline already owns the execution must not be admitted.
|
|
*/
|
|
describe("LIVE-04 the total deadline owns every physical wait", () => {
|
|
it("does not wait for a non-cooperative fetch past the deadline", async () => {
|
|
const executor = createContractHttpExecutor({
|
|
baseUrl: "https://api.example/",
|
|
maxRetryAttempts: 0,
|
|
authProfiles: TEST_PROFILES,
|
|
attachCredentials: () =>
|
|
Object.freeze({
|
|
kind: "READY" as const,
|
|
headers: { authorization: "Bearer t" },
|
|
}),
|
|
fetcher: (() => new Promise<Response>(() => {})) as unknown as typeof fetch,
|
|
});
|
|
|
|
const outcome = await executor.execute(
|
|
bearerOperation(5),
|
|
{ limit: 1 },
|
|
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
|
|
);
|
|
|
|
expect(outcome.kind).toBe("TRANSPORT_FAILURE");
|
|
expect(
|
|
outcome.kind === "TRANSPORT_FAILURE" ? outcome.failure.kind : null,
|
|
).toBe("TIMEOUT");
|
|
});
|
|
|
|
it("does not wait for a non-cooperative body reader past the deadline", async () => {
|
|
const neverEndingBody = new ReadableStream<Uint8Array>({
|
|
pull() {
|
|
return new Promise<void>(() => {});
|
|
},
|
|
});
|
|
const executor = createContractHttpExecutor({
|
|
baseUrl: "https://api.example/",
|
|
maxRetryAttempts: 0,
|
|
authProfiles: TEST_PROFILES,
|
|
attachCredentials: () =>
|
|
Object.freeze({
|
|
kind: "READY" as const,
|
|
headers: { authorization: "Bearer t" },
|
|
}),
|
|
fetcher: (async () =>
|
|
new Response(neverEndingBody, {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
})) as unknown as typeof fetch,
|
|
});
|
|
|
|
const outcome = await executor.execute(
|
|
bearerOperation(20),
|
|
{ limit: 1 },
|
|
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
|
|
);
|
|
|
|
expect(outcome.kind).toBe("TRANSPORT_FAILURE");
|
|
expect(
|
|
outcome.kind === "TRANSPORT_FAILURE" ? outcome.failure.kind : null,
|
|
).toBe("TIMEOUT");
|
|
});
|
|
|
|
it("does not admit a body that completes after the deadline owns the execution", async () => {
|
|
let releaseBody: (() => void) | undefined;
|
|
const lateBody = new ReadableStream<Uint8Array>({
|
|
pull(controller) {
|
|
return new Promise<void>((resolve) => {
|
|
releaseBody = () => {
|
|
// The executor is expected to have cancelled this reader already;
|
|
// enqueueing into the closed controller then throws, which is the
|
|
// late producer this scenario is about.
|
|
try {
|
|
controller.enqueue(new TextEncoder().encode("[]"));
|
|
controller.close();
|
|
} catch {
|
|
// The stream was already cancelled by the deadline owner.
|
|
}
|
|
resolve();
|
|
};
|
|
});
|
|
},
|
|
});
|
|
const executor = createContractHttpExecutor({
|
|
baseUrl: "https://api.example/",
|
|
maxRetryAttempts: 0,
|
|
authProfiles: TEST_PROFILES,
|
|
attachCredentials: () =>
|
|
Object.freeze({
|
|
kind: "READY" as const,
|
|
headers: { authorization: "Bearer t" },
|
|
}),
|
|
fetcher: (async () =>
|
|
new Response(lateBody, {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
})) as unknown as typeof fetch,
|
|
});
|
|
|
|
const pending = executor.execute(
|
|
bearerOperation(10),
|
|
{ limit: 1 },
|
|
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
|
|
);
|
|
setTimeout(() => releaseBody?.(), 40);
|
|
const outcome = await pending;
|
|
|
|
expect(outcome.kind).toBe("TRANSPORT_FAILURE");
|
|
expect(
|
|
outcome.kind === "TRANSPORT_FAILURE" ? outcome.failure.kind : null,
|
|
).toBe("TIMEOUT");
|
|
});
|
|
|
|
it("preserves the caller and the scope as distinct cancellation owners", async () => {
|
|
const observations: HttpExecutionObservation[] = [];
|
|
const makeExecutor = () =>
|
|
createContractHttpExecutor({
|
|
baseUrl: "https://api.example/",
|
|
maxRetryAttempts: 0,
|
|
authProfiles: TEST_PROFILES,
|
|
attachCredentials: () =>
|
|
Object.freeze({
|
|
kind: "READY" as const,
|
|
headers: { authorization: "Bearer t" },
|
|
}),
|
|
fetcher: (() =>
|
|
new Promise<Response>(() => {})) as unknown as typeof fetch,
|
|
observe: (observation) => observations.push(observation),
|
|
});
|
|
|
|
const callerController = new AbortController();
|
|
const callerPending = makeExecutor().execute(
|
|
bearerOperation(10_000),
|
|
{ limit: 1 },
|
|
{
|
|
routeId: ROUTE_ID,
|
|
scope: scopeSnapshot(),
|
|
signal: callerController.signal,
|
|
},
|
|
);
|
|
callerController.abort();
|
|
expect((await callerPending).kind).toBe("CANCELLED");
|
|
expect(observations.at(-1)?.cancellationOwner).toBe("CALLER");
|
|
|
|
const scopeController = new AbortController();
|
|
const scopePending = makeExecutor().execute(
|
|
bearerOperation(10_000),
|
|
{ limit: 1 },
|
|
{ routeId: ROUTE_ID, scope: scopeSnapshot(scopeController.signal) },
|
|
);
|
|
scopeController.abort();
|
|
expect((await scopePending).kind).toBe("TRANSPORT_FAILURE");
|
|
expect(observations.at(-1)?.cancellationOwner).toBe("SCOPE_FENCE");
|
|
});
|
|
|
|
it("observes a late native rejection without an unhandled rejection", async () => {
|
|
const rejections: unknown[] = [];
|
|
const onUnhandled = (event: PromiseRejectionEvent) => {
|
|
rejections.push(event.reason);
|
|
event.preventDefault();
|
|
};
|
|
globalThis.addEventListener?.(
|
|
"unhandledrejection",
|
|
onUnhandled as EventListener,
|
|
);
|
|
try {
|
|
const executor = createContractHttpExecutor({
|
|
baseUrl: "https://api.example/",
|
|
maxRetryAttempts: 0,
|
|
authProfiles: TEST_PROFILES,
|
|
attachCredentials: () =>
|
|
Object.freeze({
|
|
kind: "READY" as const,
|
|
headers: { authorization: "Bearer t" },
|
|
}),
|
|
fetcher: (() =>
|
|
new Promise<Response>((_resolve, reject) => {
|
|
setTimeout(() => reject(new Error("late native failure")), 30);
|
|
})) as unknown as typeof fetch,
|
|
});
|
|
|
|
const outcome = await executor.execute(
|
|
bearerOperation(5),
|
|
{ limit: 1 },
|
|
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
|
|
);
|
|
expect(outcome.kind).toBe("TRANSPORT_FAILURE");
|
|
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
expect(rejections).toEqual([]);
|
|
} finally {
|
|
globalThis.removeEventListener?.(
|
|
"unhandledrejection",
|
|
onUnhandled as EventListener,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
/**
|
|
* LIVE-05. A deadline TIMEOUT is an operational failure of the API call, not a
|
|
* caller decision. Excluding it from `api.request.failed` hides exactly the
|
|
* class of outage the telemetry exists to surface.
|
|
*/
|
|
describe("LIVE-05 deadline timeouts reach failure telemetry", () => {
|
|
function projectorHarness() {
|
|
const emitted: string[] = [];
|
|
const recorded: string[] = [];
|
|
const project = createHttpObservationProjector({
|
|
diagnostics: {
|
|
record: (input) => recorded.push(input.eventId),
|
|
},
|
|
telemetry: {
|
|
emit: (eventName) => emitted.push(eventName),
|
|
},
|
|
});
|
|
return { emitted, recorded, project };
|
|
}
|
|
|
|
const base = Object.freeze({
|
|
routeId: ROUTE_ID,
|
|
operationId: "TEST_LIST_ENTITIES",
|
|
diagnosticsOperation: "test.read",
|
|
errorKind: "TIMEOUT",
|
|
attemptCount: 1,
|
|
durationMs: 10,
|
|
effect: "NOT_APPLICABLE" as const,
|
|
terminalReason: "TIMEOUT",
|
|
});
|
|
|
|
it("emits exactly one api.request.failed for a deadline timeout", () => {
|
|
const harness = projectorHarness();
|
|
harness.project(
|
|
Object.freeze({
|
|
...base,
|
|
outcome: "TRANSPORT_FAILURE" as const,
|
|
cancellationOwner: "DEADLINE" as const,
|
|
}),
|
|
);
|
|
expect(harness.emitted).toEqual(["api.request.failed"]);
|
|
expect(harness.recorded).toEqual(["http.request.completed"]);
|
|
});
|
|
|
|
it("emits nothing for caller, route and shutdown cancellation", () => {
|
|
for (const owner of [
|
|
"CALLER",
|
|
"ROUTE_TRANSITION",
|
|
"SCOPE_FENCE",
|
|
"APPLICATION_SHUTDOWN",
|
|
] as const) {
|
|
const harness = projectorHarness();
|
|
harness.project(
|
|
Object.freeze({
|
|
...base,
|
|
outcome: "CANCELLED" as const,
|
|
errorKind: "REQUEST_ABORTED",
|
|
cancellationOwner: owner,
|
|
}),
|
|
);
|
|
expect(harness.emitted).toEqual([]);
|
|
}
|
|
});
|
|
|
|
it("still emits for an ordinary network and auth integration failure", () => {
|
|
const network = projectorHarness();
|
|
network.project(
|
|
Object.freeze({
|
|
...base,
|
|
outcome: "TRANSPORT_FAILURE" as const,
|
|
errorKind: "NETWORK_FAILURE",
|
|
terminalReason: "NETWORK_FAILURE",
|
|
}),
|
|
);
|
|
expect(network.emitted).toEqual(["api.request.failed"]);
|
|
|
|
const auth = projectorHarness();
|
|
auth.project(
|
|
Object.freeze({
|
|
...base,
|
|
outcome: "AUTH_INTEGRATION_FAILURE" as const,
|
|
errorKind: "CREDENTIAL_OWNER_FAILED",
|
|
terminalReason: "AUTH_INTEGRATION_FAILURE",
|
|
}),
|
|
);
|
|
expect(auth.emitted).toEqual(["api.request.failed"]);
|
|
});
|
|
});
|