chore: sync the frontend template from 4dc033c to 8157ad4
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
002ba3624e
commit
4bff9ca151
@@ -59,6 +59,88 @@ describe("shared HTTP client", () => {
|
||||
expect(attempts).toBe(3);
|
||||
});
|
||||
|
||||
it.each(["", " ", "bad\u0000key", "x".repeat(513)])(
|
||||
"rejects invalid keyed command key %j before credentials and fetch",
|
||||
async (idempotencyKey) => {
|
||||
let fetched = 0;
|
||||
let credentialAttempts = 0;
|
||||
server.use(
|
||||
http.post("https://api.test/api/entities", () => {
|
||||
fetched += 1;
|
||||
return HttpResponse.json({ success: true, data: {} });
|
||||
}),
|
||||
);
|
||||
const client = testClient({
|
||||
baseUrl: "https://api.test",
|
||||
clock,
|
||||
authSession: {
|
||||
getState: () => "authenticated",
|
||||
subscribe: () => () => {},
|
||||
beginSignIn: async () => {},
|
||||
signOut: async () => {},
|
||||
async credentialPatch() {
|
||||
credentialAttempts += 1;
|
||||
return { headers: {} };
|
||||
},
|
||||
recover: async () => "no-session" as const,
|
||||
onUnauthenticated: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
client.execute("CREATE_ENTITY", {
|
||||
body: { name: "n" },
|
||||
idempotencyKey,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
kind: "VALIDATION_REJECTED",
|
||||
code: "IDEMPOTENCY_KEY_INVALID",
|
||||
// The repository reports attempt counts as 1-based; the invariant
|
||||
// proved below is that no physical attempt happened at all.
|
||||
attemptCount: 1,
|
||||
},
|
||||
});
|
||||
expect(fetched).toBe(0);
|
||||
expect(credentialAttempts).toBe(0);
|
||||
},
|
||||
);
|
||||
|
||||
it("bounds a non-cooperative legacy credential owner by total deadline", async () => {
|
||||
let fetched = 0;
|
||||
let observedSignal: AbortSignal | undefined;
|
||||
server.use(
|
||||
http.get("https://api.test/api/entities", () => {
|
||||
fetched += 1;
|
||||
return HttpResponse.json({ success: true, data: [] });
|
||||
}),
|
||||
);
|
||||
const client = testClient({
|
||||
baseUrl: "https://api.test",
|
||||
timeoutMs: 5,
|
||||
clock: { now: () => 0, sleep: async () => {} },
|
||||
authSession: {
|
||||
getState: () => "authenticated",
|
||||
subscribe: () => () => {},
|
||||
beginSignIn: async () => {},
|
||||
signOut: async () => {},
|
||||
credentialPatch: (_binding, context) => {
|
||||
observedSignal = context?.signal;
|
||||
// Never settles on its own.
|
||||
return new Promise<never>(() => {});
|
||||
},
|
||||
recover: async () => "no-session" as const,
|
||||
onUnauthenticated: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await client.execute("LIST_ENTITIES");
|
||||
expect(result).toMatchObject({ ok: false });
|
||||
expect(fetched).toBe(0);
|
||||
expect(observedSignal).toBeDefined();
|
||||
});
|
||||
|
||||
it("rejects a non-JSON response without exposing its body", async () => {
|
||||
server.use(
|
||||
http.get(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createHttpClient } from "../../src/adapters/http/client.ts";
|
||||
import { createContractHttpExecutor } from "../../src/adapters/http/http-execution-v3.ts";
|
||||
import { joinMutationEffectCertainty } from "../../src/adapters/http/http-effect-certainty.ts";
|
||||
import {
|
||||
entityQueryKeys,
|
||||
TEST_HTTP_CONTRACT,
|
||||
@@ -49,12 +50,27 @@ function testClient(options: HttpDependencies) {
|
||||
return createHttpClient({ ...TEST_HTTP_CONTRACT, ...options });
|
||||
}
|
||||
|
||||
describe("mutation effect certainty lattice", () => {
|
||||
it.each([
|
||||
["NOT_STARTED", "NOT_APPLIED", "NOT_APPLIED"],
|
||||
["NOT_APPLIED", "NOT_STARTED", "NOT_APPLIED"],
|
||||
["NOT_STARTED", "MAYBE_APPLIED", "MAYBE_APPLIED"],
|
||||
["MAYBE_APPLIED", "NOT_STARTED", "MAYBE_APPLIED"],
|
||||
["MAYBE_APPLIED", "NOT_APPLIED", "MAYBE_APPLIED"],
|
||||
["NOT_APPLIED", "MAYBE_APPLIED", "MAYBE_APPLIED"],
|
||||
["MAYBE_APPLIED", "APPLIED_CONFIRMED", "APPLIED_CONFIRMED"],
|
||||
["APPLIED_CONFIRMED", "NOT_STARTED", "APPLIED_CONFIRMED"],
|
||||
["APPLIED_CONFIRMED", "MAYBE_APPLIED", "APPLIED_CONFIRMED"],
|
||||
] as const)("joins %s with %s as %s", (current, observed, expected) => {
|
||||
expect(joinMutationEffectCertainty(current, observed)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTTP operation execution contract", () => {
|
||||
it("permits a keyless command intent but rejects an unexpected key before dispatch", async () => {
|
||||
const attachCredentials = vi.fn(() => ({
|
||||
kind: "READY" as const,
|
||||
headers: {},
|
||||
credentials: "omit" as const,
|
||||
}));
|
||||
const observedKeys: Array<string | null> = [];
|
||||
const fetcher = vi.fn(
|
||||
@@ -98,7 +114,7 @@ describe("HTTP operation execution contract", () => {
|
||||
executor.execute(
|
||||
nonKeyedCommand,
|
||||
{ name: "created" },
|
||||
{ scope, intent },
|
||||
{ routeId: "TEST_ROUTE", scope, intent },
|
||||
),
|
||||
).resolves.toMatchObject({ kind: "SUCCESS" });
|
||||
expect(observedKeys).toEqual([null]);
|
||||
@@ -109,7 +125,7 @@ describe("HTTP operation execution contract", () => {
|
||||
executor.execute(
|
||||
nonKeyedCommand,
|
||||
{ name: "created" },
|
||||
{ scope, intent: { ...intent, idempotencyKey: "unexpected-key" } },
|
||||
{ routeId: "TEST_ROUTE", scope, intent: { ...intent, idempotencyKey: "unexpected-key" } },
|
||||
),
|
||||
).resolves.toMatchObject({
|
||||
kind: "CONTRACT_VIOLATION",
|
||||
@@ -290,6 +306,9 @@ describe("HTTP operation execution contract", () => {
|
||||
operationId: "LIST_ENTITIES",
|
||||
routeId: "TEST_ROUTE",
|
||||
});
|
||||
// The credential wait is bounded by the same attempt controller, so wait
|
||||
// until the request is actually in flight before firing the deadline.
|
||||
await vi.waitFor(() => expect(fetcher).toHaveBeenCalledTimes(1));
|
||||
await vi.waitFor(() => expect(scheduler.callbacks).toHaveLength(1));
|
||||
scheduler.callbacks[0]();
|
||||
await expect(timeoutResult).resolves.toMatchObject({
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createContractHttpExecutor } from "../../src/adapters/http/http-execution-v3.ts";
|
||||
import {
|
||||
composeContractContributions,
|
||||
ContractContributionError,
|
||||
} from "../../src/contracts/external-contract-runtime.ts";
|
||||
import {
|
||||
installRestAuthProfileRegistry,
|
||||
REST_AUTH_PROFILES,
|
||||
} from "../../src/contracts/rest-profiles.ts";
|
||||
import {
|
||||
TEST_CONTRACT_CONTRIBUTION,
|
||||
TEST_LIST_HTTP_CONTRACT,
|
||||
} from "../helpers/external-contract-fixture.ts";
|
||||
|
||||
const ROUTE_ID = "TEST_ROUTE";
|
||||
|
||||
/**
|
||||
* The shipped fixture policy declares `TEST_AUTH`; the installed registry owns
|
||||
* the concrete transport rules, so this suite installs an equivalent bearer
|
||||
* profile under that identity.
|
||||
*/
|
||||
const TEST_PROFILES = installRestAuthProfileRegistry({
|
||||
TEST_AUTH: {
|
||||
authProfileId: "TEST_AUTH",
|
||||
transport: "BEARER_HEADER",
|
||||
credentials: "omit",
|
||||
allowedCredentialHeaders: ["authorization"],
|
||||
requiredCredentialHeaders: ["authorization"],
|
||||
},
|
||||
TEST_ANONYMOUS: {
|
||||
authProfileId: "TEST_ANONYMOUS",
|
||||
transport: "ANONYMOUS",
|
||||
credentials: "omit",
|
||||
allowedCredentialHeaders: [],
|
||||
requiredCredentialHeaders: [],
|
||||
},
|
||||
});
|
||||
|
||||
function scopeSnapshot(isCurrent: () => boolean = () => true) {
|
||||
return Object.freeze({
|
||||
generation: 1,
|
||||
fingerprint: "scope-1",
|
||||
identities: Object.freeze({}) as never,
|
||||
signal: new AbortController().signal,
|
||||
isCurrent,
|
||||
});
|
||||
}
|
||||
|
||||
function operationWithProfile(authProfileId: string, deadlineMs?: number) {
|
||||
return {
|
||||
...TEST_LIST_HTTP_CONTRACT,
|
||||
frontend: {
|
||||
...TEST_LIST_HTTP_CONTRACT.frontend,
|
||||
authProfileId,
|
||||
...(deadlineMs === undefined ? {} : { totalDeadlineMs: deadlineMs }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const bearerOperation = () => operationWithProfile("TEST_AUTH");
|
||||
const anonymousOperation = () => operationWithProfile("TEST_ANONYMOUS");
|
||||
|
||||
describe("V3 installed auth profile authority", () => {
|
||||
it("rejects an unknown auth profile during composition", () => {
|
||||
const contribution = {
|
||||
...TEST_CONTRACT_CONTRIBUTION,
|
||||
http: [
|
||||
{
|
||||
...TEST_LIST_HTTP_CONTRACT,
|
||||
frontend: {
|
||||
...TEST_LIST_HTTP_CONTRACT.frontend,
|
||||
authProfileId: "NO_SUCH_PROFILE",
|
||||
},
|
||||
},
|
||||
] as unknown as readonly never[],
|
||||
};
|
||||
|
||||
let thrown: unknown;
|
||||
try {
|
||||
composeContractContributions([contribution] as never);
|
||||
} catch (error) {
|
||||
thrown = error;
|
||||
}
|
||||
expect(thrown).toBeInstanceOf(ContractContributionError);
|
||||
expect((thrown as ContractContributionError).reason).toContain(
|
||||
"unknown authProfileId NO_SUCH_PROFILE",
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
installRestAuthProfileRegistry({
|
||||
BROKEN: {
|
||||
authProfileId: "BROKEN",
|
||||
transport: "BEARER_HEADER",
|
||||
credentials: "include",
|
||||
allowedCredentialHeaders: [],
|
||||
requiredCredentialHeaders: ["authorization"],
|
||||
},
|
||||
}),
|
||||
).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("rejects credential attempts to replace Accept Content-Type or credentials", async () => {
|
||||
for (const hostileHeaders of [
|
||||
{ accept: "text/plain" },
|
||||
{ "content-type": "text/plain" },
|
||||
{ cookie: "session=1" },
|
||||
]) {
|
||||
const fetcher = vi.fn();
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
authProfiles: TEST_PROFILES,
|
||||
attachCredentials: () =>
|
||||
({
|
||||
kind: "READY" as const,
|
||||
headers: {
|
||||
authorization: "Bearer token",
|
||||
...hostileHeaders,
|
||||
},
|
||||
}) as never,
|
||||
fetcher: fetcher as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
const outcome = await executor.execute(
|
||||
bearerOperation(),
|
||||
{ limit: 5 },
|
||||
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
|
||||
);
|
||||
|
||||
expect(outcome).toMatchObject({
|
||||
kind: "AUTH_INTEGRATION_FAILURE",
|
||||
effect: "NOT_APPLICABLE",
|
||||
});
|
||||
expect(fetcher).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it("requires authorization for a bearer profile before fetch", async () => {
|
||||
const fetcher = vi.fn();
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
authProfiles: TEST_PROFILES,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY" as const,
|
||||
headers: {},
|
||||
}),
|
||||
fetcher: fetcher as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
const outcome = await executor.execute(
|
||||
bearerOperation(),
|
||||
{ limit: 5 },
|
||||
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
|
||||
);
|
||||
|
||||
expect(outcome).toMatchObject({
|
||||
kind: "AUTH_INTEGRATION_FAILURE",
|
||||
reason: "MISSING_REQUIRED_CREDENTIAL_HEADER",
|
||||
});
|
||||
expect(fetcher).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("forbids credential headers for an anonymous profile", async () => {
|
||||
const withCredential = vi.fn();
|
||||
const rejected = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
authProfiles: TEST_PROFILES,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY" as const,
|
||||
headers: { authorization: "Bearer token" },
|
||||
}),
|
||||
fetcher: withCredential as unknown as typeof fetch,
|
||||
});
|
||||
const rejectedOutcome = await rejected.execute(
|
||||
anonymousOperation(),
|
||||
{ limit: 5 },
|
||||
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
|
||||
);
|
||||
expect(rejectedOutcome).toMatchObject({
|
||||
kind: "AUTH_INTEGRATION_FAILURE",
|
||||
reason: "CREDENTIAL_HEADER_NOT_ALLOWED",
|
||||
});
|
||||
expect(withCredential).not.toHaveBeenCalled();
|
||||
|
||||
const observed: Array<Readonly<Record<string, string>>> = [];
|
||||
const accepted = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
authProfiles: TEST_PROFILES,
|
||||
attachCredentials: () => ({ kind: "READY" as const, headers: {} }),
|
||||
fetcher: (async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const headers = new Headers(init?.headers);
|
||||
observed.push(
|
||||
Object.freeze(Object.fromEntries(headers.entries())),
|
||||
);
|
||||
expect(init?.credentials).toBe("omit");
|
||||
return Response.json([]);
|
||||
}) as unknown as typeof fetch,
|
||||
});
|
||||
const acceptedOutcome = await accepted.execute(
|
||||
anonymousOperation(),
|
||||
{ limit: 5 },
|
||||
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
|
||||
);
|
||||
expect(acceptedOutcome.kind).toBe("SUCCESS");
|
||||
expect(observed).toHaveLength(1);
|
||||
expect(observed[0]?.authorization).toBeUndefined();
|
||||
expect(observed[0]?.accept).toBe("application/json");
|
||||
});
|
||||
|
||||
it("bounds a non-cooperative credential owner by the operation lifetime", async () => {
|
||||
const fetcher = vi.fn();
|
||||
let observedSignal: AbortSignal | undefined;
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
authProfiles: TEST_PROFILES,
|
||||
attachCredentials: (_operation, context) => {
|
||||
observedSignal = context.signal;
|
||||
// Non-cooperative: it never settles on its own.
|
||||
return new Promise<never>(() => {});
|
||||
},
|
||||
fetcher: fetcher as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
const outcome = await executor.execute(
|
||||
operationWithProfile("TEST_AUTH", 10),
|
||||
{ limit: 5 },
|
||||
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
|
||||
);
|
||||
|
||||
expect(outcome).toMatchObject({
|
||||
kind: "TRANSPORT_FAILURE",
|
||||
failure: { kind: "TIMEOUT" },
|
||||
});
|
||||
expect(fetcher).not.toHaveBeenCalled();
|
||||
expect(observedSignal?.aborted).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores a late credential completion after caller abort or scope fence", async () => {
|
||||
const fetcher = vi.fn();
|
||||
let release: ((patch: unknown) => void) | undefined;
|
||||
const caller = new AbortController();
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
authProfiles: TEST_PROFILES,
|
||||
attachCredentials: () =>
|
||||
new Promise((resolve) => {
|
||||
release = resolve as (patch: unknown) => void;
|
||||
}) as never,
|
||||
fetcher: fetcher as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
const execution = executor.execute(
|
||||
bearerOperation(),
|
||||
{ limit: 5 },
|
||||
{
|
||||
routeId: ROUTE_ID,
|
||||
scope: scopeSnapshot(),
|
||||
signal: caller.signal,
|
||||
},
|
||||
);
|
||||
await Promise.resolve();
|
||||
caller.abort();
|
||||
const outcome = await execution;
|
||||
expect(outcome.kind).toBe("CANCELLED");
|
||||
|
||||
release?.({
|
||||
kind: "READY",
|
||||
headers: { authorization: "Bearer late" },
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(fetcher).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the shipped bearer profile strict", () => {
|
||||
expect(REST_AUTH_PROFILES.REFERENCE_EXTERNAL_BEARER).toMatchObject({
|
||||
transport: "BEARER_HEADER",
|
||||
credentials: "omit",
|
||||
requiredCredentialHeaders: ["authorization"],
|
||||
});
|
||||
expect(REST_AUTH_PROFILES.ANONYMOUS.requiredCredentialHeaders).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,649 @@
|
||||
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"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,314 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createContractHttpExecutor } from "../../src/adapters/http/http-execution-v3.ts";
|
||||
import type {
|
||||
HttpExecutionObservation,
|
||||
} from "../../src/adapters/http/http-execution-v3.ts";
|
||||
import { createHttpObservationProjector } from "../../src/bootstrap/runtime-adapters.ts";
|
||||
import { createReferenceFeatureInstalledInput } from "../../src/features/reference-feature/adapters/create-reference-feature-input.ts";
|
||||
import {
|
||||
DIAGNOSTIC_CONTEXT_ALLOWLIST,
|
||||
projectDiagnosticRecord,
|
||||
type DiagnosticRecordInput,
|
||||
} from "../../src/contracts/diagnostics.ts";
|
||||
import {
|
||||
projectTelemetryEvent,
|
||||
type TelemetryEventName,
|
||||
} from "../../src/contracts/telemetry.ts";
|
||||
import {
|
||||
TEST_CREATE_HTTP_CONTRACT,
|
||||
TEST_LIST_HTTP_CONTRACT,
|
||||
} from "../helpers/external-contract-fixture.ts";
|
||||
|
||||
const ROUTE_ID = "TEST_ROUTE";
|
||||
|
||||
function scopeSnapshot(isCurrent: () => boolean = () => true) {
|
||||
return Object.freeze({
|
||||
generation: 1,
|
||||
fingerprint: "scope-1",
|
||||
identities: Object.freeze({}) as never,
|
||||
signal: new AbortController().signal,
|
||||
isCurrent,
|
||||
});
|
||||
}
|
||||
|
||||
type RecordedDiagnostic = DiagnosticRecordInput;
|
||||
type RecordedTelemetry = Readonly<{
|
||||
eventName: TelemetryEventName;
|
||||
attributes: Readonly<Record<string, unknown>>;
|
||||
}>;
|
||||
|
||||
function recordingSinks() {
|
||||
const diagnostics: RecordedDiagnostic[] = [];
|
||||
const telemetry: RecordedTelemetry[] = [];
|
||||
return {
|
||||
diagnostics,
|
||||
telemetry,
|
||||
projector: createHttpObservationProjector({
|
||||
diagnostics: {
|
||||
record(input: DiagnosticRecordInput) {
|
||||
diagnostics.push(input);
|
||||
},
|
||||
},
|
||||
telemetry: {
|
||||
emit(
|
||||
eventName: TelemetryEventName,
|
||||
attributes: Record<string, unknown>,
|
||||
) {
|
||||
telemetry.push(Object.freeze({ eventName, attributes }));
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("V3 HTTP observability projection", () => {
|
||||
it("projects every V3 terminal outcome through the closed diagnostics allowlist", async () => {
|
||||
const sinks = recordingSinks();
|
||||
|
||||
const cases: readonly Readonly<{
|
||||
label: string;
|
||||
fetcher: typeof fetch;
|
||||
}>[] = [
|
||||
{
|
||||
label: "SUCCESS",
|
||||
fetcher: (async () =>
|
||||
Response.json([{ id: "a", name: "A" }])) as unknown as typeof fetch,
|
||||
},
|
||||
{
|
||||
label: "PROBLEM",
|
||||
fetcher: (async () =>
|
||||
Response.json(
|
||||
{ type: "about:blank", title: "nope", status: 400 },
|
||||
{ status: 400 },
|
||||
)) as unknown as typeof fetch,
|
||||
},
|
||||
{
|
||||
label: "TRANSPORT_FAILURE",
|
||||
fetcher: (async () => {
|
||||
throw new TypeError("network down");
|
||||
}) as unknown as typeof fetch,
|
||||
},
|
||||
{
|
||||
label: "CONTRACT_VIOLATION",
|
||||
fetcher: (async () =>
|
||||
new Response("<html/>", {
|
||||
status: 200,
|
||||
headers: { "content-type": "text/html" },
|
||||
})) as unknown as typeof fetch,
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of cases) {
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY" as const,
|
||||
headers: {},
|
||||
}),
|
||||
fetcher: testCase.fetcher,
|
||||
observe: sinks.projector,
|
||||
});
|
||||
await executor.execute(
|
||||
TEST_LIST_HTTP_CONTRACT,
|
||||
{ limit: 5 },
|
||||
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
|
||||
);
|
||||
}
|
||||
|
||||
expect(sinks.diagnostics).toHaveLength(cases.length);
|
||||
for (const recorded of sinks.diagnostics) {
|
||||
expect(recorded.eventId).toBe("http.request.completed");
|
||||
const contextKeys = Object.keys(recorded.context ?? {});
|
||||
expect(contextKeys).toEqual(
|
||||
expect.arrayContaining([
|
||||
"route_id",
|
||||
"operation_id",
|
||||
"operation",
|
||||
"outcome",
|
||||
"error_kind",
|
||||
"http_status_group",
|
||||
"attempt_count_bucket",
|
||||
"duration_bucket",
|
||||
]),
|
||||
);
|
||||
for (const key of contextKeys) {
|
||||
expect(DIAGNOSTIC_CONTEXT_ALLOWLIST).toContain(key);
|
||||
}
|
||||
expect(contextKeys).not.toContain("attempts");
|
||||
expect(contextKeys).not.toContain("certainty");
|
||||
const projection = projectDiagnosticRecord(recorded);
|
||||
expect(projection.success).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("emits one failure telemetry event for a non-abort terminal failure", async () => {
|
||||
const sinks = recordingSinks();
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY" as const,
|
||||
headers: {},
|
||||
}),
|
||||
fetcher: (async () => {
|
||||
throw new TypeError("network down");
|
||||
}) as unknown as typeof fetch,
|
||||
observe: sinks.projector,
|
||||
});
|
||||
|
||||
const outcome = await executor.execute(
|
||||
TEST_LIST_HTTP_CONTRACT,
|
||||
{ limit: 5 },
|
||||
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
|
||||
);
|
||||
|
||||
expect(outcome.kind).toBe("TRANSPORT_FAILURE");
|
||||
expect(sinks.telemetry).toHaveLength(1);
|
||||
const emitted = sinks.telemetry[0];
|
||||
expect(emitted?.eventName).toBe("api.request.failed");
|
||||
expect(emitted?.attributes).toMatchObject({
|
||||
route_id: ROUTE_ID,
|
||||
operation_id: "TEST_LIST_ENTITIES",
|
||||
error_kind: "NETWORK_FAILURE",
|
||||
http_status_group: "none",
|
||||
attempt_count_bucket: "1",
|
||||
});
|
||||
const projected = projectTelemetryEvent(
|
||||
emitted?.eventName ?? "api.request.failed",
|
||||
emitted?.attributes ?? {},
|
||||
);
|
||||
expect(projected.success).toBe(true);
|
||||
expect(sinks.diagnostics).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("does not emit failure telemetry for caller cancellation or scope fencing", async () => {
|
||||
const cancelled = recordingSinks();
|
||||
const callerController = new AbortController();
|
||||
callerController.abort();
|
||||
const cancelledExecutor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY" as const,
|
||||
headers: {},
|
||||
}),
|
||||
fetcher: (async () =>
|
||||
Response.json([{ id: "a", name: "A" }])) as unknown as typeof fetch,
|
||||
observe: cancelled.projector,
|
||||
});
|
||||
const cancelledOutcome = await cancelledExecutor.execute(
|
||||
TEST_LIST_HTTP_CONTRACT,
|
||||
{ limit: 5 },
|
||||
{
|
||||
routeId: ROUTE_ID,
|
||||
scope: scopeSnapshot(),
|
||||
signal: callerController.signal,
|
||||
},
|
||||
);
|
||||
expect(cancelledOutcome.kind).toBe("CANCELLED");
|
||||
expect(cancelled.diagnostics).toHaveLength(1);
|
||||
expect(cancelled.telemetry).toHaveLength(0);
|
||||
|
||||
const fenced = recordingSinks();
|
||||
const fencedExecutor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY" as const,
|
||||
headers: {},
|
||||
}),
|
||||
fetcher: (async () =>
|
||||
Response.json([{ id: "a", name: "A" }])) as unknown as typeof fetch,
|
||||
observe: fenced.projector,
|
||||
});
|
||||
const fencedOutcome = await fencedExecutor.execute(
|
||||
TEST_LIST_HTTP_CONTRACT,
|
||||
{ limit: 5 },
|
||||
{ routeId: ROUTE_ID, scope: scopeSnapshot(() => false) },
|
||||
);
|
||||
expect(fencedOutcome.kind).toBe("CONTRACT_VIOLATION");
|
||||
expect(fenced.diagnostics).toHaveLength(1);
|
||||
expect(fenced.telemetry).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("preserves the feature route id through the installed operation executor", async () => {
|
||||
const seen: Array<Readonly<Record<string, unknown>>> = [];
|
||||
const installed = createReferenceFeatureInstalledInput({
|
||||
contractOperations: Object.freeze({
|
||||
async execute(
|
||||
_operationId: string,
|
||||
_input: unknown,
|
||||
context?: Readonly<Record<string, unknown>>,
|
||||
) {
|
||||
seen.push(Object.freeze({ ...(context ?? {}) }));
|
||||
return Object.freeze({
|
||||
kind: "SUCCESS" as const,
|
||||
value: [],
|
||||
metadata: Object.freeze({ status: 200 }),
|
||||
effect: "NOT_APPLICABLE" as const,
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await installed.input.listResources({ limit: 20 });
|
||||
await installed.input.getResource("resource-1");
|
||||
|
||||
expect(seen.map((context) => context.routeId)).toEqual([
|
||||
"REFERENCE_RESOURCE_LIST",
|
||||
"REFERENCE_RESOURCE_DETAIL",
|
||||
]);
|
||||
});
|
||||
|
||||
it("cannot change the HTTP result when diagnostics or telemetry throws", async () => {
|
||||
const projector = createHttpObservationProjector({
|
||||
diagnostics: {
|
||||
record() {
|
||||
throw new Error("diagnostics sink exploded");
|
||||
},
|
||||
},
|
||||
telemetry: {
|
||||
emit() {
|
||||
throw new Error("telemetry sink exploded");
|
||||
},
|
||||
},
|
||||
});
|
||||
const observe = vi.fn((observation: HttpExecutionObservation) => {
|
||||
projector(observation);
|
||||
});
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.example/",
|
||||
maxRetryAttempts: 0,
|
||||
attachCredentials: () => ({
|
||||
kind: "READY" as const,
|
||||
headers: {},
|
||||
}),
|
||||
fetcher: (async () =>
|
||||
Response.json({ id: "created", name: "Created" }, {
|
||||
status: 201,
|
||||
})) as unknown as typeof fetch,
|
||||
observe,
|
||||
});
|
||||
|
||||
const outcome = await executor.execute(
|
||||
TEST_CREATE_HTTP_CONTRACT,
|
||||
{ name: "Created" },
|
||||
{
|
||||
routeId: ROUTE_ID,
|
||||
scope: scopeSnapshot(),
|
||||
intent: Object.freeze({
|
||||
intentId: "intent-1",
|
||||
operationId: "TEST_CREATE_ENTITY",
|
||||
canonicalInputIdentity: "opaque-input-identity",
|
||||
idempotencyKey: "key-1",
|
||||
createdAtMonotonicMs: 1,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
expect(outcome.kind).toBe("SUCCESS");
|
||||
expect(observe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,10 @@ import path from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
|
||||
import { readBoundedBytes } from "../../src/adapters/http/bounded-body-reader.ts";
|
||||
import { createContractHttpExecutor } from "../../src/adapters/http/http-execution-v3.ts";
|
||||
import {
|
||||
createContractHttpExecutor,
|
||||
type HttpExecutionObservation,
|
||||
} from "../../src/adapters/http/http-execution-v3.ts";
|
||||
import {
|
||||
type InstalledHttpContract,
|
||||
} from "../../src/contracts/external-contract-runtime.ts";
|
||||
@@ -30,6 +33,13 @@ import {
|
||||
type HttpScenarioOperationId,
|
||||
} from "../mocks/scenarios/catalog.ts";
|
||||
|
||||
/** The reference gateway owns these low-cardinality route identities. */
|
||||
function routeIdFor(operationId: HttpScenarioOperationId): string {
|
||||
return operationId === "GET_REFERENCE_RESOURCE"
|
||||
? "REFERENCE_RESOURCE_DETAIL"
|
||||
: "REFERENCE_RESOURCE_LIST";
|
||||
}
|
||||
|
||||
const RECEIPT_PATH = path.resolve(
|
||||
"artifacts/tests/http-scenario-executions.json",
|
||||
);
|
||||
@@ -191,11 +201,7 @@ async function executeScenario(
|
||||
): Promise<HttpScenarioAssertionGroups> {
|
||||
const physicalAttempts: AttemptTrace[] = [];
|
||||
const sleeps: RetryReason[] = [];
|
||||
const observations: Array<Readonly<{
|
||||
outcome: string;
|
||||
attempts: number;
|
||||
certainty: string;
|
||||
}>> = [];
|
||||
const observations: HttpExecutionObservation[] = [];
|
||||
const caller = new AbortController();
|
||||
const scopeLifetime = new AbortController();
|
||||
let scopeCurrent = true;
|
||||
@@ -225,10 +231,11 @@ async function executeScenario(
|
||||
const executor = createContractHttpExecutor({
|
||||
baseUrl: "https://api.test",
|
||||
maxRetryAttempts: 2,
|
||||
// The reference contribution declares REFERENCE_EXTERNAL_BEARER, so the
|
||||
// credential owner must supply the required proof header before dispatch.
|
||||
attachCredentials: () => ({
|
||||
kind: "READY",
|
||||
headers: {},
|
||||
credentials: "omit",
|
||||
headers: { authorization: "Bearer scenario-token" },
|
||||
}),
|
||||
fetcher,
|
||||
random: () => 0,
|
||||
@@ -265,6 +272,7 @@ async function executeScenario(
|
||||
|
||||
try {
|
||||
const execution = executor.execute(operation, inputFor(entry.operationId), {
|
||||
routeId: routeIdFor(entry.operationId),
|
||||
scope,
|
||||
signal: caller.signal,
|
||||
...(entry.operationId === "CREATE_REFERENCE_RESOURCE"
|
||||
@@ -296,7 +304,7 @@ async function executeScenario(
|
||||
const observation = observations[0]!;
|
||||
const observedSignal = scopeLifetime.signal.aborted ? "ABORTED" : "ACTIVE";
|
||||
const cancellationOwner =
|
||||
observation.certainty === "TIMEOUT"
|
||||
observation.terminalReason === "TIMEOUT"
|
||||
? "DEADLINE"
|
||||
: caller.signal.aborted
|
||||
? "CALLER"
|
||||
@@ -314,13 +322,13 @@ async function executeScenario(
|
||||
}),
|
||||
effect: Object.freeze({
|
||||
outcome: String(outcome.effect),
|
||||
observer: observation.certainty,
|
||||
observer: observation.terminalReason,
|
||||
}),
|
||||
retry: Object.freeze({ count: sleeps.length, reasons: Object.freeze(sleeps) }),
|
||||
fetch: Object.freeze({
|
||||
count: physicalAttempts.length,
|
||||
observerAttempts: observation.attempts,
|
||||
agrees: physicalAttempts.length === observation.attempts,
|
||||
observerAttempts: observation.attemptCount,
|
||||
agrees: physicalAttempts.length === observation.attemptCount,
|
||||
}),
|
||||
media: Object.freeze({
|
||||
attempts: Object.freeze(physicalAttempts.map((attempt) => attempt.media)),
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
import { HttpResponse, http } from "msw";
|
||||
import { setupServer } from "msw/node";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createHttpClient } from "../../src/adapters/http/client.ts";
|
||||
import { defineRestOperation } from "../../src/contracts/api-operations.ts";
|
||||
import { createRestProviderProfile } from "../../src/contracts/rest-profiles.ts";
|
||||
import { TEST_HTTP_CONTRACT } from "../helpers/http-contract-fixture.ts";
|
||||
|
||||
/**
|
||||
* LEG-01 / LEG-02. The V2 client is not the default path any more, but a
|
||||
* rollback re-activates it, so its credential and recovery authority must match
|
||||
* V3 rather than diverge quietly.
|
||||
*/
|
||||
|
||||
const BEARER_LIST = defineRestOperation({
|
||||
method: "GET",
|
||||
path: "/api/entities",
|
||||
operationId: "LIST_ENTITIES",
|
||||
auth: "external-session",
|
||||
timeoutMs: null,
|
||||
idempotency: "safe",
|
||||
retry: "runtime",
|
||||
requestSource: "search",
|
||||
requestSchema: "EntityListQuery",
|
||||
responseSchema: "EntityListPayload",
|
||||
owner: "test-fixture",
|
||||
contractVersion: 2,
|
||||
protocol: "REST",
|
||||
semantics: "QUERY",
|
||||
replayPolicy: "SAFE",
|
||||
idempotencyKeyPolicy: "NONE",
|
||||
mapperId: "EntityListMapper",
|
||||
successStatuses: [200],
|
||||
responseMediaTypes: ["application/json"],
|
||||
maxResponseBytes: 32_768,
|
||||
providerId: "PRIMARY_API",
|
||||
authProfileId: "REFERENCE_EXTERNAL_BEARER",
|
||||
csrfProfileId: "NO_CSRF_BEARER",
|
||||
pathSchema: "NoRequest",
|
||||
pathParameterNames: [],
|
||||
maxEncodedSearchBytes: 1_024,
|
||||
});
|
||||
|
||||
let observedAuthorization: string | null = null;
|
||||
let requestCount = 0;
|
||||
let nextStatus = 200;
|
||||
|
||||
const server = setupServer(
|
||||
http.get("https://api.test/api/entities", ({ request }) => {
|
||||
requestCount += 1;
|
||||
observedAuthorization = request.headers.get("authorization");
|
||||
if (nextStatus === 401) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: { code: "UNAUTHENTICATED" },
|
||||
meta: { requestId: "request-1", traceId: "trace-1" },
|
||||
},
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
return HttpResponse.json({
|
||||
success: true,
|
||||
data: [{ id: "resource-1", name: "Example" }],
|
||||
meta: { requestId: "request-2", traceId: "trace-1" },
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
|
||||
afterEach(() => {
|
||||
observedAuthorization = null;
|
||||
requestCount = 0;
|
||||
nextStatus = 200;
|
||||
server.resetHandlers();
|
||||
});
|
||||
afterAll(() => server.close());
|
||||
|
||||
const clock = { now: () => 0, sleep: async () => {} };
|
||||
|
||||
type HttpDependencies = Parameters<typeof createHttpClient>[0];
|
||||
|
||||
function bearerClient(options: Partial<HttpDependencies>) {
|
||||
return createHttpClient({
|
||||
...TEST_HTTP_CONTRACT,
|
||||
getOperation: (operationId: string) => {
|
||||
if (operationId !== "LIST_ENTITIES") {
|
||||
throw new Error(`Unknown test operation: ${operationId}`);
|
||||
}
|
||||
return BEARER_LIST;
|
||||
},
|
||||
baseUrl: "https://api.test",
|
||||
providerProfile: createRestProviderProfile("PRIMARY_API", "https://api.test", [
|
||||
"omit",
|
||||
]),
|
||||
// The V2 fixture declares `NoRequest` for its path codec, which the shared
|
||||
// schema registry does not carry.
|
||||
validatePath: () => ({ success: true as const, data: {} }),
|
||||
clock,
|
||||
...options,
|
||||
} as HttpDependencies);
|
||||
}
|
||||
|
||||
function sessionStub(
|
||||
overrides: Partial<{
|
||||
getState: () => "authenticated" | "unauthenticated" | "recovery-pending" | "integration-failed";
|
||||
credentialPatch: (...args: never[]) => Promise<{ headers: Record<string, string> }>;
|
||||
recover: (...args: never[]) => Promise<"restored" | "no-session">;
|
||||
onUnauthenticated: () => void;
|
||||
}> = {},
|
||||
) {
|
||||
return {
|
||||
getState: () => "authenticated" as const,
|
||||
subscribe: () => () => {},
|
||||
beginSignIn: async () => {},
|
||||
signOut: async () => {},
|
||||
credentialPatch: async () => ({ headers: { authorization: "Bearer t" } }),
|
||||
recover: async () => "restored" as const,
|
||||
onUnauthenticated: () => {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("LEG-02 the bearer profile's required header is enforced", () => {
|
||||
it("refuses to dispatch when a READY patch omits authorization", async () => {
|
||||
const client = bearerClient({
|
||||
authSession: sessionStub({
|
||||
credentialPatch: async () => ({ headers: {} }),
|
||||
}) as never,
|
||||
});
|
||||
|
||||
await expect(client.execute("LIST_ENTITIES")).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { kind: "AUTH_INTEGRATION_FAILURE" },
|
||||
});
|
||||
expect(requestCount).toBe(0);
|
||||
});
|
||||
|
||||
it("rejects a malformed or duplicated authorization value", async () => {
|
||||
const hostilePatches: readonly Record<string, string>[] = [
|
||||
{ authorization: "" },
|
||||
{ authorization: "Bearer bad\nvalue" },
|
||||
{ Authorization: "Bearer a", authorization: "Bearer b" },
|
||||
];
|
||||
for (const headers of hostilePatches) {
|
||||
const client = bearerClient({
|
||||
authSession: sessionStub({
|
||||
credentialPatch: async () => ({ headers }),
|
||||
}) as never,
|
||||
});
|
||||
await expect(client.execute("LIST_ENTITIES")).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { kind: "AUTH_INTEGRATION_FAILURE" },
|
||||
});
|
||||
}
|
||||
expect(requestCount).toBe(0);
|
||||
});
|
||||
|
||||
it("dispatches with the admitted authorization header", async () => {
|
||||
const client = bearerClient({
|
||||
authSession: sessionStub() as never,
|
||||
});
|
||||
|
||||
await expect(client.execute("LIST_ENTITIES")).resolves.toMatchObject({
|
||||
ok: true,
|
||||
});
|
||||
expect(observedAuthorization).toBe("Bearer t");
|
||||
});
|
||||
});
|
||||
|
||||
describe("LEG-01 recovery notification follows the adopted result", () => {
|
||||
it("does not sign the user out when recovery answers after the deadline", async () => {
|
||||
nextStatus = 401;
|
||||
const onUnauthenticated = vi.fn();
|
||||
let elapsed = 0;
|
||||
const client = bearerClient({
|
||||
clock: {
|
||||
now: () => elapsed,
|
||||
sleep: async () => {},
|
||||
},
|
||||
timeoutMs: 20,
|
||||
authSession: sessionStub({
|
||||
onUnauthenticated,
|
||||
recover: async () => {
|
||||
// The request's own deadline expires while recovery is still out.
|
||||
elapsed = 10_000;
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
return "no-session" as const;
|
||||
},
|
||||
}) as never,
|
||||
});
|
||||
|
||||
const outcome = await client.execute("LIST_ENTITIES");
|
||||
expect(outcome.ok).toBe(false);
|
||||
await new Promise((resolve) => setTimeout(resolve, 60));
|
||||
expect(onUnauthenticated).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("signs the user out exactly once for an adopted no-session result", async () => {
|
||||
nextStatus = 401;
|
||||
const onUnauthenticated = vi.fn();
|
||||
const client = bearerClient({
|
||||
authSession: sessionStub({
|
||||
onUnauthenticated,
|
||||
recover: async () => "no-session" as const,
|
||||
}) as never,
|
||||
});
|
||||
|
||||
await expect(client.execute("LIST_ENTITIES")).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: { kind: "AUTH_REQUIRED" },
|
||||
});
|
||||
expect(onUnauthenticated).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("stays bounded when recovery never settles", async () => {
|
||||
nextStatus = 401;
|
||||
const onUnauthenticated = vi.fn();
|
||||
let elapsed = 0;
|
||||
const client = bearerClient({
|
||||
clock: {
|
||||
now: () => {
|
||||
elapsed += 5;
|
||||
return elapsed;
|
||||
},
|
||||
sleep: async () => {},
|
||||
},
|
||||
timeoutMs: 20,
|
||||
authSession: sessionStub({
|
||||
onUnauthenticated,
|
||||
recover: () => new Promise<"restored">(() => {}),
|
||||
}) as never,
|
||||
});
|
||||
|
||||
const outcome = await client.execute("LIST_ENTITIES");
|
||||
expect(outcome.ok).toBe(false);
|
||||
expect(onUnauthenticated).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { cp, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
RELEASE_CANDIDATE_MANIFEST_PATH,
|
||||
releaseCandidateManifestSchema,
|
||||
} from "../../scripts/lib/release-candidate.ts";
|
||||
import { linkFixtureNodeModules } from "../../scripts/lib/fixture-node-modules.ts";
|
||||
|
||||
it(
|
||||
"builds a real candidate assessment and passes the default archived verifier from the captured archive",
|
||||
@@ -43,7 +44,7 @@ it(
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
await symlink(path.join(sourceRoot, "node_modules"), path.join(fixtureRoot, "node_modules"), "dir");
|
||||
await linkFixtureNodeModules(fixtureRoot, sourceRoot);
|
||||
const git = spawnSync("git", ["show", "-s", "--format=%H%n%ct", "HEAD"], {
|
||||
cwd: sourceRoot,
|
||||
encoding: "utf8",
|
||||
|
||||
Reference in New Issue
Block a user