Files
clean-architecture-frontend…/tests/unit/http-execution-v3.test.ts
T
DongHyeonkaandClaude Opus 5 dfb7734674 fix: run the provider sandbox and admit a release to a named environment
The provider sandbox never ran. bubblewrap 0.9.0 stops parsing an `--args`
file at the first non-option and never hands the remainder back, so the
command written into that file was silently dropped: bwrap printed its usage
text, exited 1, and the provider produced no evidence at all. The options
still travel in the args file — that is what keeps host paths and credentials
out of `/proc/<pid>/cmdline` — but the command now rides on real argv, and
`encodeProviderBwrapInput` refuses a `--` so the drop cannot come back.

The scope wrapper then could not exit. It read the supervisor's liveness pipe
through `fs`, which runs a blocking `read(2)` on a threadpool thread; the
supervisor holds that pipe open for the scope's whole life, so the read never
returned and closing the descriptor did not interrupt it. Once bubblewrap
finished the wrapper deadlocked in `process.exit`, the scope outlived the
provider, and a completed run was reported as a timeout kill. The channel is
now read through the event loop, so teardown is observable and terminal.

Creation modes were left to the ambient umask. `mkdir(mode)` and `open(mode)`
are requests the kernel subtracts the umask from, so a runner exporting a
restrictive umask produced directories it could not enter and handed `tar` a
file it could not re-open. Private modes are pinned instead of inherited.

Promotion cleanup deleted before it checked. Removals run through a pinned
descriptor, so a leaf substituted after validation had this promotion's exact
five destroyed first and the substitution reported afterwards, leaving a
half-emptied directory a retry could not tell from a completed one. The name
is re-bound to the inode before anything is removed, so the failure is total.

Separately, release coherence proved the artifacts agreed with each other but
never that they belonged where they were going: a build whose runtime document
said `APP_ENV: local`, `AUTH_MODE: demo` and a loopback API is coherent with
itself and passed every gate. `public/` is copied verbatim into `dist/`, so
that local document shipped with every build regardless of what the build was
for. Runtime configuration now comes from a declared profile, and FE-GATE-027
refuses to admit an artifact to an environment it does not match — including
refusing an undeclared destination, so nothing is admitted by omission.

`REQUEST_TIMEOUT_MS` and `VITE_ROUTER_BASE_PATH` were validated and then
dropped: the V3 executor ran every operation on its contract's own deadline,
and Vite emitted root-absolute assets for a sub-path deployment. The timeout is
now a ceiling that may tighten a contract but never loosen one, and one base
path feeds the router, the Service Worker scope and the asset base together.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 16:38:19 +09:00

754 lines
23 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { createContractHttpExecutor } from "../../src/adapters/http/http-execution-v3.ts";
import type { InstalledHttpContract } from "../../src/contracts/external-contract-runtime.ts";
import {
TEST_CREATE_HTTP_CONTRACT,
TEST_LIST_HTTP_CONTRACT,
} from "../helpers/external-contract-fixture.ts";
const installed: InstalledHttpContract<unknown, unknown, unknown> =
TEST_LIST_HTTP_CONTRACT;
const ROUTE_ID = "TEST_ROUTE";
const scope = Object.freeze({
generation: 1,
fingerprint: "scope-1",
identities: Object.freeze({}) as never,
signal: new AbortController().signal,
isCurrent: () => true,
});
const createInstalled: InstalledHttpContract<unknown, unknown, unknown> =
TEST_CREATE_HTTP_CONTRACT;
function mutationIntent(
overrides: Readonly<{
intentId?: string;
operationId?: string;
idempotencyKey?: string | null;
}> = {},
) {
return Object.freeze({
intentId: overrides.intentId ?? "intent-1",
operationId: overrides.operationId ?? "TEST_CREATE_ENTITY",
canonicalInputIdentity: "opaque-input-identity",
...(overrides.idempotencyKey === null
? {}
: { idempotencyKey: overrides.idempotencyKey ?? "key-1" }),
createdAtMonotonicMs: 1,
});
}
function operation(
overrides: Readonly<{
deadlineMs?: number;
responseBody?: "REQUIRED_JSON" | "OPTIONAL_JSON" | "NONE";
}> = {},
): InstalledHttpContract<unknown, unknown, unknown> {
return {
...installed,
contract: {
...installed.contract,
responseBody: overrides.responseBody ?? installed.contract.responseBody,
},
frontend: {
...installed.frontend,
totalDeadlineMs: overrides.deadlineMs ?? installed.frontend.totalDeadlineMs,
},
};
}
async function flushMicrotasks(): Promise<void> {
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
}
describe("runtime request deadline ceiling", () => {
/**
* `REQUEST_TIMEOUT_MS` was validated by the runtime config schema and then
* never handed to the V3 executor, so the deployment dial did nothing and
* every operation ran on its contract's own deadline. It is a ceiling: it may
* tighten an operation, never loosen one.
*/
async function settlesWithin(
contractDeadlineMs: number,
ceilingMs: number | undefined,
advanceMs: number,
): Promise<boolean> {
vi.useFakeTimers();
try {
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
...(ceilingMs === undefined ? {} : { requestDeadlineCeilingMs: ceilingMs }),
attachCredentials: () => ({ kind: "READY", headers: {} }),
// A request that only ever ends by being cut off, so what settles it is
// exactly the deadline under test.
fetcher: (_input, init) =>
new Promise((_resolve, reject) => {
const signal = (init as RequestInit | undefined)?.signal;
signal?.addEventListener(
"abort",
() => reject(new DOMException("aborted", "AbortError")),
{ once: true },
);
}),
});
let settled = false;
const pending = executor
.execute(operation({ deadlineMs: contractDeadlineMs }), {}, {
routeId: ROUTE_ID,
scope,
})
.then(
() => { settled = true; },
() => { settled = true; },
);
await vi.advanceTimersByTimeAsync(advanceMs);
await flushMicrotasks();
const observed = settled;
if (!observed) await vi.advanceTimersByTimeAsync(contractDeadlineMs + 1_000);
await pending;
return observed;
} finally {
vi.useRealTimers();
}
}
it("applies the tighter of the contract and deployment bounds", async () => {
await expect(settlesWithin(5_000, 500, 800)).resolves.toBe(true);
await expect(settlesWithin(5_000, undefined, 800)).resolves.toBe(false);
});
it("never extends a contract deadline", async () => {
await expect(settlesWithin(500, 60_000, 800)).resolves.toBe(true);
});
it("ignores a ceiling that is not a usable duration", async () => {
for (const ceiling of [0, -1, Number.NaN]) {
await expect(settlesWithin(5_000, ceiling, 800), String(ceiling)).resolves.toBe(
false,
);
}
});
});
describe("descriptor-driven HTTP execution lifetime", () => {
it("normalizes a read-side 429 to the non-applicable effect vocabulary", async () => {
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
attachCredentials: () => ({
kind: "READY",
headers: {},
}),
fetcher: async () =>
Response.json(
{ type: "about:blank", title: "limited", status: 429 },
{ status: 429 },
),
});
await expect(
executor.execute(installed, { limit: 20 }, { routeId: ROUTE_ID, scope }),
).resolves.toMatchObject({
kind: "RATE_LIMITED",
effect: "NOT_APPLICABLE",
});
});
it.each([
["absent intent", () => undefined],
["empty key", () => mutationIntent({ idempotencyKey: "" })],
["control-character key", () => mutationIntent({ idempotencyKey: "key\u0000private" })],
["over-budget key", () => mutationIntent({ idempotencyKey: "k".repeat(257) })],
[
"wrong operation",
() => mutationIntent({ operationId: "TEST_OTHER_COMMAND" }),
],
])(
"rejects a KEYED command with %s before credentials or fetch",
async (_label, intentFactory) => {
const attachCredentials = vi.fn(() => ({
kind: "READY" as const,
headers: {},
}));
const fetcher = vi.fn();
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
attachCredentials,
fetcher,
});
const intent = intentFactory();
await expect(
executor.execute(
createInstalled,
{ name: "created" },
{ routeId: ROUTE_ID, scope, ...(intent === undefined ? {} : { intent }) },
),
).resolves.toMatchObject({
kind: "CONTRACT_VIOLATION",
violation: {
kind: "MISSING_IDEMPOTENCY_KEY",
operation: "REQUEST",
},
effect: "NOT_STARTED",
});
expect(attachCredentials).not.toHaveBeenCalled();
expect(fetcher).not.toHaveBeenCalled();
},
);
it.each([
["an intent without a key", mutationIntent({ idempotencyKey: null })],
["an intent with a key", mutationIntent()],
])(
"rejects a query carrying %s before credentials or fetch",
async (_label, intent) => {
const attachCredentials = vi.fn(() => ({
kind: "READY" as const,
headers: {},
}));
const fetcher = vi.fn();
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
attachCredentials,
fetcher,
});
await expect(
executor.execute(installed, { limit: 20 }, { routeId: ROUTE_ID, scope, intent }),
).resolves.toMatchObject({
kind: "CONTRACT_VIOLATION",
violation: {
kind: "UNEXPECTED_IDEMPOTENCY_KEY",
operation: "REQUEST",
},
effect: "NOT_STARTED",
});
expect(attachCredentials).not.toHaveBeenCalled();
expect(fetcher).not.toHaveBeenCalled();
},
);
it.each([
{
label: "query with the canonical reserved header",
operation: installed,
input: { limit: 20 },
context: { routeId: ROUTE_ID, scope },
headerName: "Idempotency-Key",
},
{
label: "valid KEYED command with a case-variant reserved header",
operation: createInstalled,
input: { name: "created" },
context: { routeId: ROUTE_ID, scope, intent: mutationIntent() },
headerName: "iDeMpOtEnCy-KeY",
},
])(
"rejects a credential patch for $label before fetch",
async ({ operation, input, context, headerName }) => {
const attachCredentials = vi.fn(() => ({
kind: "READY" as const,
headers: { [headerName]: "credential-owned-key" },
}));
const fetcher = vi.fn();
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
attachCredentials,
fetcher,
});
await expect(
executor.execute(operation, input, context),
).resolves.toMatchObject({
kind: "CONTRACT_VIOLATION",
violation: {
kind: "UNEXPECTED_IDEMPOTENCY_KEY",
operation: "REQUEST",
},
effect: "NOT_STARTED",
});
expect(attachCredentials).toHaveBeenCalledOnce();
expect(fetcher).not.toHaveBeenCalled();
},
);
it("reuses one supplied idempotency key across every physical retry", async () => {
const observedKeys: Array<string | null> = [];
let attempt = 0;
const fetcher = vi.fn((_input: RequestInfo | URL, init?: RequestInit) => {
observedKeys.push(new Headers(init?.headers).get("Idempotency-Key"));
attempt += 1;
if (attempt === 1) throw new TypeError("synchronous pre-dispatch failure");
return Promise.resolve(
Response.json(
{ id: "created", name: "Created" },
{ status: 201 },
),
);
});
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 1,
attachCredentials: () => ({
kind: "READY",
headers: {},
}),
fetcher,
sleep: async () => {},
random: () => 0,
});
const retryingCreate = {
...createInstalled,
frontend: {
...createInstalled.frontend,
retryBudget: 1 as const,
},
};
await expect(
executor.execute(
retryingCreate,
{ name: "created" },
{ routeId: ROUTE_ID, scope, intent: mutationIntent({ idempotencyKey: "logical-key" }) },
),
).resolves.toMatchObject({ kind: "SUCCESS" });
expect(fetcher).toHaveBeenCalledTimes(2);
expect(observedKeys).toEqual(["logical-key", "logical-key"]);
});
it("never emits a mutation idempotency header for a query", async () => {
const observedKeys: Array<string | null> = [];
const fetcher = vi.fn(
async (_input: RequestInfo | URL, init?: RequestInit) => {
observedKeys.push(
new Headers(init?.headers).get("Idempotency-Key"),
);
return Response.json([]);
},
);
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
attachCredentials: () => ({
kind: "READY",
headers: {},
}),
fetcher,
});
await expect(
executor.execute(
installed,
{ limit: 20 },
{ routeId: ROUTE_ID, scope },
),
).resolves.toMatchObject({ kind: "SUCCESS" });
expect(fetcher).toHaveBeenCalledOnce();
expect(observedKeys).toEqual([null]);
});
it("keeps intent identities out of request URLs and safe observations", async () => {
const urls: string[] = [];
const observations: unknown[] = [];
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
attachCredentials: () => ({
kind: "READY",
headers: {},
}),
fetcher: vi.fn(async (input) => {
urls.push(String(input));
return Response.json(
{ id: "created", name: "Created" },
{ status: 201 },
);
}),
observe: (observation) => observations.push(observation),
});
await expect(
executor.execute(
createInstalled,
{ name: "created" },
{
routeId: ROUTE_ID,
scope,
intent: Object.freeze({
intentId: "private-intent-id",
operationId: "TEST_CREATE_ENTITY",
canonicalInputIdentity: "private-canonical-input",
idempotencyKey: "private-idempotency-key",
createdAtMonotonicMs: 1,
}),
},
),
).resolves.toMatchObject({ kind: "SUCCESS" });
const safeEvidence = JSON.stringify({ urls, observations });
expect(urls).toEqual(["https://api.example/api/test-entities"]);
expect(observations).toHaveLength(1);
expect(safeEvidence).not.toContain("private-intent-id");
expect(safeEvidence).not.toContain("private-canonical-input");
expect(safeEvidence).not.toContain("private-idempotency-key");
});
it.each([
[{}, "limit=20"],
[{ limit: "7" }, "limit=7"],
])("projects the canonical validated input %#", async (input, expectedQuery) => {
const fetcher = vi.fn(async () => Response.json([]));
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
attachCredentials: () => ({
kind: "READY",
headers: {},
}),
fetcher,
});
await expect(executor.execute(installed, input, { routeId: ROUTE_ID, scope })).resolves.toMatchObject({
kind: "SUCCESS",
});
expect(String((fetcher.mock.calls as unknown[][])[0]?.[0])).toContain(
expectedQuery,
);
});
it("contains throwing and malformed external request projections", async () => {
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
attachCredentials: () => ({
kind: "READY",
headers: {},
}),
fetcher: vi.fn(),
});
const throwing = {
...installed,
contract: {
...installed.contract,
projectRequest: () => {
throw new Error("external descriptor defect");
},
},
};
const malformed = {
...installed,
contract: {
...installed.contract,
projectRequest: () => ({ pathValues: {}, queryEntries: [["limit"]], body: null }),
},
} as unknown as typeof installed;
await expect(executor.execute(throwing, { limit: 20 }, { routeId: ROUTE_ID, scope })).resolves.toMatchObject({
kind: "CONTRACT_VIOLATION",
effect: "NOT_APPLICABLE",
});
await expect(executor.execute(malformed, { limit: 20 }, { routeId: ROUTE_ID, scope })).resolves.toMatchObject({
kind: "CONTRACT_VIOLATION",
effect: "NOT_APPLICABLE",
});
});
it("preserves MAYBE_APPLIED for a malformed command response after dispatch", async () => {
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
attachCredentials: () => ({
kind: "READY",
headers: {},
}),
fetcher: vi.fn(async () => Response.json({ malformed: true }, { status: 201 })),
});
await expect(
executor.execute(
createInstalled,
{ name: "created" },
{
routeId: ROUTE_ID,
scope,
intent: mutationIntent(),
},
),
).resolves.toMatchObject({
kind: "CONTRACT_VIOLATION",
violation: { kind: "SUCCESS_SCHEMA_INVALID" },
effect: "MAYBE_APPLIED",
});
});
it("preserves MAYBE_APPLIED when a command response arrives after its scope fence", async () => {
let current = true;
const lifetime = new AbortController();
const fencedScope = Object.freeze({
...scope,
signal: lifetime.signal,
isCurrent: () => current,
});
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
attachCredentials: () => ({
kind: "READY",
headers: {},
}),
fetcher: vi.fn(async () => {
current = false;
lifetime.abort();
return Response.json(
{ id: "created", name: "Created" },
{ status: 201 },
);
}),
});
await expect(
executor.execute(
createInstalled,
{ name: "created" },
{
routeId: ROUTE_ID,
scope: fencedScope,
intent: mutationIntent({ intentId: "intent-2", idempotencyKey: "key-2" }),
},
),
).resolves.toMatchObject({
kind: "CONTRACT_VIOLATION",
violation: { kind: "SCOPE_FENCED" },
effect: "MAYBE_APPLIED",
});
});
it("keeps a dispatched command MAYBE_APPLIED when a retry-time fence lands between scope checks", async () => {
// Attempt 1 dispatches an idempotent command and receives 429. The retry
// sleep resolves, the loop-entry scope check is still current, and only the
// pre-dispatch final invariant observes the fence.
let armed = false;
let checksAfterArming = 0;
const idempotentCommand: InstalledHttpContract<unknown, unknown, unknown> = {
...createInstalled,
contract: {
...createInstalled.contract,
retrySemantics: "IDEMPOTENT" as const,
},
frontend: { ...createInstalled.frontend, retryBudget: 1 as const },
};
const racingScope = Object.freeze({
...scope,
isCurrent: () => {
if (!armed) return true;
checksAfterArming += 1;
// The retry loop entry still observes a current scope; the pre-dispatch
// final invariant is the first observation of the fence.
return checksAfterArming <= 1;
},
});
const fetcher = vi.fn(async () =>
Response.json({ type: "about:blank", title: "slow down", status: 429 }, {
status: 429,
}),
);
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 1,
attachCredentials: () => ({ kind: "READY", headers: {} }),
fetcher,
sleep: async () => {
armed = true;
},
random: () => 0,
});
const outcome = await executor.execute(
idempotentCommand,
{ name: "created" },
{
routeId: ROUTE_ID,
scope: racingScope,
intent: mutationIntent({ idempotencyKey: null }),
},
);
expect(fetcher).toHaveBeenCalledTimes(1);
expect(outcome).toMatchObject({
kind: "CONTRACT_VIOLATION",
violation: { kind: "SCOPE_FENCED" },
effect: "MAYBE_APPLIED",
});
});
it("settles a credential hang at the total operation deadline", async () => {
vi.useFakeTimers();
let settled = false;
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
attachCredentials: () => new Promise(() => {}),
});
const result = executor
.execute(operation({ deadlineMs: 5 }), { limit: 20 }, { routeId: ROUTE_ID, scope })
.then((outcome) => {
settled = true;
return outcome;
});
await vi.advanceTimersByTimeAsync(5);
await flushMicrotasks();
expect(settled).toBe(true);
await expect(result).resolves.toMatchObject({
kind: "TRANSPORT_FAILURE",
failure: { kind: "TIMEOUT" },
});
vi.useRealTimers();
});
it("never retries a deadline-owned abort while the monotonic clock still has a sub-tick budget", async () => {
vi.useFakeTimers();
try {
const iterationCount = 100;
const sleep = vi.fn(async () => {
throw new Error("a deadline-owned abort must not enter retry sleep");
});
const fetcher = vi.fn(
(_input: RequestInfo | URL, init?: RequestInit) =>
new Promise<Response>((_resolve, reject) => {
init?.signal?.addEventListener(
"abort",
() => reject(new DOMException("Aborted", "AbortError")),
{ once: true },
);
}),
);
const observe = vi.fn();
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 2,
attachCredentials: () => ({
kind: "READY",
headers: {},
}),
fetcher,
monotonicNow: () => 0,
random: () => 0,
sleep,
observe,
});
for (let iteration = 0; iteration < iterationCount; iteration += 1) {
const result = executor.execute(
operation({ deadlineMs: 5 }),
{ limit: 20 },
{ routeId: ROUTE_ID, scope },
);
await vi.advanceTimersByTimeAsync(5);
await flushMicrotasks();
await expect(result).resolves.toMatchObject({
kind: "TRANSPORT_FAILURE",
failure: { kind: "TIMEOUT" },
});
}
expect(fetcher).toHaveBeenCalledTimes(iterationCount);
expect(sleep).not.toHaveBeenCalled();
expect(observe).toHaveBeenCalledTimes(iterationCount);
for (const [observation] of observe.mock.calls) {
expect(observation).toEqual(
expect.objectContaining({
attemptCount: 1,
errorKind: "TIMEOUT",
terminalReason: "TIMEOUT",
cancellationOwner: "DEADLINE",
routeId: ROUTE_ID,
operationId: "TEST_LIST_ENTITIES",
}),
);
}
} finally {
vi.useRealTimers();
}
});
it("cancels a non-cooperative retry sleep when the caller aborts", async () => {
const caller = new AbortController();
let sleepSignal: AbortSignal | undefined;
let settled = false;
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 2,
attachCredentials: () => ({
kind: "READY",
headers: {},
}),
fetcher: vi.fn(async () =>
Response.json(
{ type: "about:blank", title: "temporary", status: 503 },
{ status: 503 },
),
),
sleep: (_ms, signal) => {
sleepSignal = signal;
return new Promise(() => {});
},
random: () => 0,
});
const result = executor
.execute(installed, { limit: 20 }, { routeId: ROUTE_ID, scope, signal: caller.signal })
.then((outcome) => {
settled = true;
return outcome;
});
await vi.waitFor(() => expect(sleepSignal).toBeDefined());
caller.abort();
await flushMicrotasks();
expect(sleepSignal?.aborted).toBe(true);
expect(settled).toBe(true);
await expect(result).resolves.toMatchObject({ kind: "CANCELLED" });
});
it("treats an unreadable forbidden-body probe as a transport failure", async () => {
const body = new ReadableStream<Uint8Array>({
pull(controller) {
controller.error(new TypeError("stream failed"));
},
});
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
attachCredentials: () => ({
kind: "READY",
headers: {},
}),
fetcher: vi.fn(async () => new Response(body, { status: 200 })),
});
await expect(
executor.execute(
operation({ responseBody: "NONE" }),
{ limit: 20 },
{ routeId: ROUTE_ID, scope },
),
).resolves.toMatchObject({
kind: "TRANSPORT_FAILURE",
failure: { kind: "RESPONSE_STREAM_FAILURE" },
});
});
});