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>
650 lines
20 KiB
TypeScript
650 lines
20 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);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* NS-01. Reading `patch.kind` and `patch.headers` off the raw answer put the
|
|
* credential decode outside the auth boundary: a throwing getter escaped into
|
|
* the transport catch and the outage was reported as `NETWORK_FAILURE`, so
|
|
* the operator saw a network incident instead of an auth integration one.
|
|
*/
|
|
const hostileOwners = [
|
|
{
|
|
label: "exposes a throwing kind getter",
|
|
attach: () =>
|
|
Object.defineProperty({}, "kind", {
|
|
enumerable: true,
|
|
get() {
|
|
throw new TypeError("hostile kind getter");
|
|
},
|
|
}) as never,
|
|
},
|
|
{
|
|
label: "exposes a throwing headers getter",
|
|
attach: () =>
|
|
Object.defineProperty({ kind: "READY" }, "headers", {
|
|
enumerable: true,
|
|
get() {
|
|
throw new TypeError("hostile headers getter");
|
|
},
|
|
}) as never,
|
|
},
|
|
{
|
|
label: "throws from an ownKeys trap",
|
|
attach: () =>
|
|
new Proxy(
|
|
{ kind: "READY", headers: { authorization: "Bearer ok" } },
|
|
{
|
|
ownKeys() {
|
|
throw new TypeError("hostile ownKeys trap");
|
|
},
|
|
},
|
|
) as never,
|
|
},
|
|
{
|
|
label: "throws from a getOwnPropertyDescriptor trap",
|
|
attach: () =>
|
|
new Proxy(
|
|
{ kind: "READY", headers: { authorization: "Bearer ok" } },
|
|
{
|
|
getOwnPropertyDescriptor() {
|
|
throw new TypeError("hostile descriptor trap");
|
|
},
|
|
},
|
|
) as never,
|
|
},
|
|
{
|
|
label: "carries the outcome only on its prototype",
|
|
attach: () =>
|
|
Object.create({
|
|
kind: "READY",
|
|
headers: { authorization: "Bearer ok" },
|
|
}) as never,
|
|
},
|
|
{
|
|
label: "carries an extra own field",
|
|
attach: () =>
|
|
Object.freeze({
|
|
kind: "READY",
|
|
headers: Object.freeze({ authorization: "Bearer ok" }),
|
|
injected: true,
|
|
}) as never,
|
|
},
|
|
{
|
|
label: "carries a symbol field",
|
|
attach: () =>
|
|
Object.freeze({
|
|
kind: "READY",
|
|
headers: Object.freeze({ authorization: "Bearer ok" }),
|
|
[Symbol.for("injected")]: true,
|
|
}) as never,
|
|
},
|
|
{
|
|
label: "hides the outcome behind a non-enumerable own field",
|
|
attach: () =>
|
|
Object.defineProperties(
|
|
{ kind: "READY" },
|
|
{
|
|
headers: {
|
|
enumerable: false,
|
|
value: { authorization: "Bearer ok" },
|
|
},
|
|
},
|
|
) as never,
|
|
},
|
|
];
|
|
|
|
for (const owner of hostileOwners) {
|
|
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("reads each field exactly once so a stateful answer cannot swap it", async () => {
|
|
const fetcher = vi.fn(async () => jsonResponse([]));
|
|
const kindReads: string[] = [];
|
|
const executor = createContractHttpExecutor({
|
|
baseUrl: "https://api.example/",
|
|
maxRetryAttempts: 0,
|
|
authProfiles: TEST_PROFILES,
|
|
attachCredentials: () =>
|
|
new Proxy(
|
|
{ kind: "READY", headers: { authorization: "Bearer first" } },
|
|
{
|
|
getOwnPropertyDescriptor(target, key) {
|
|
if (key === "kind") {
|
|
kindReads.push(key);
|
|
return {
|
|
configurable: true,
|
|
enumerable: true,
|
|
// A second read would answer with a different verdict.
|
|
value: kindReads.length > 1 ? "UNAUTHENTICATED" : "READY",
|
|
};
|
|
}
|
|
return Reflect.getOwnPropertyDescriptor(target, key);
|
|
},
|
|
},
|
|
) as never,
|
|
fetcher: fetcher as unknown as typeof fetch,
|
|
});
|
|
|
|
const outcome = await executor.execute(
|
|
bearerOperation(),
|
|
{ limit: 1 },
|
|
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
|
|
);
|
|
|
|
expect(outcome.kind).toBe("SUCCESS");
|
|
expect(kindReads).toHaveLength(1);
|
|
});
|
|
|
|
it("sends an owned header snapshot rather than the owner's live object", async () => {
|
|
const headers: Record<string, string> = { authorization: "Bearer first" };
|
|
let sentHeaders: Record<string, string> | undefined;
|
|
const fetcher = vi.fn(async (_url: unknown, init?: RequestInit) => {
|
|
sentHeaders = init?.headers as Record<string, string>;
|
|
return jsonResponse([]);
|
|
});
|
|
const executor = createContractHttpExecutor({
|
|
baseUrl: "https://api.example/",
|
|
maxRetryAttempts: 0,
|
|
authProfiles: TEST_PROFILES,
|
|
// The owner keeps a live reference to the object it handed over.
|
|
attachCredentials: () => ({ kind: "READY", headers }) as never,
|
|
fetcher: fetcher as unknown as typeof fetch,
|
|
});
|
|
|
|
const outcome = await executor.execute(
|
|
bearerOperation(),
|
|
{ limit: 1 },
|
|
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
|
|
);
|
|
|
|
expect(outcome.kind).toBe("SUCCESS");
|
|
headers.authorization = "Bearer swapped";
|
|
expect(sentHeaders?.["Authorization"] ?? sentHeaders?.["authorization"]).toBe(
|
|
"Bearer first",
|
|
);
|
|
});
|
|
|
|
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");
|
|
});
|
|
|
|
/**
|
|
* NS-03. The `NONE` probe used to await `reader.read()` with no signal, so a
|
|
* deadline produced a bounded public result while the raw reader kept its
|
|
* lease on the body: the connection and the buffer stayed held after the
|
|
* operation had already ended.
|
|
*/
|
|
it("cancels and releases the NONE probe reader when the deadline owns the execution", async () => {
|
|
let pulls = 0;
|
|
let cancels = 0;
|
|
const neverEndingBody = new ReadableStream<Uint8Array>({
|
|
pull() {
|
|
pulls += 1;
|
|
return new Promise<void>(() => {});
|
|
},
|
|
cancel() {
|
|
cancels += 1;
|
|
},
|
|
});
|
|
const response = new Response(neverEndingBody, { status: 200 });
|
|
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 () => response) as unknown as typeof fetch,
|
|
});
|
|
const noBodyOperation = {
|
|
...bearerOperation(20),
|
|
contract: {
|
|
...bearerOperation(20).contract,
|
|
responseBody: "NONE" as const,
|
|
},
|
|
};
|
|
|
|
const outcome = await executor.execute(
|
|
noBodyOperation as never,
|
|
{ limit: 1 },
|
|
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
|
|
);
|
|
|
|
expect(outcome.kind).toBe("TRANSPORT_FAILURE");
|
|
expect(
|
|
outcome.kind === "TRANSPORT_FAILURE" ? outcome.failure.kind : null,
|
|
).toBe("TIMEOUT");
|
|
expect(pulls).toBe(1);
|
|
await vi.waitFor(() => {
|
|
expect(cancels).toBe(1);
|
|
});
|
|
expect(response.body?.locked).toBe(false);
|
|
});
|
|
|
|
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"]);
|
|
});
|
|
});
|