Files
tech-log-frontend/tests/integration/http-scenario-catalog.test.ts
DongHyeonkaandClaude Opus 5 4bff9ca151 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>
2026-08-15 12:04:58 +09:00

420 lines
14 KiB
TypeScript

import { mkdir, rm } from "node:fs/promises";
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,
type HttpExecutionObservation,
} from "../../src/adapters/http/http-execution-v3.ts";
import {
type InstalledHttpContract,
} from "../../src/contracts/external-contract-runtime.ts";
import type { CacheScopeSnapshot } from "../../src/contracts/server-state-scope.ts";
import { REFERENCE_FEATURE_TEMPLATE_CONTRIBUTION } from "../../src/features/reference-feature/contracts/reference-feature-contract-contribution.ts";
import {
computeHttpScenarioCatalogDigest,
httpScenarioReceiptSchema,
type AttemptStatus,
type BodyDisposition,
type HttpScenarioAssertionGroups,
type RetryReason,
} from "../../scripts/lib/http-scenario-evidence.ts";
import { writeValidatedJsonArtifact } from "../../scripts/lib/validated-json-artifact.ts";
import { createReferenceScenarioHandlers } from "../mocks/handlers/reference-resources.ts";
import { createStrictMockServer } from "../mocks/server.ts";
import {
HTTP_SCENARIO_EXECUTION_IDS,
HTTP_SCENARIO_EXPECTATIONS,
HTTP_SCENARIO_RECEIPT_SCHEMA_VERSION,
OPERATION_SCENARIO_CATALOG,
type HttpScenarioExpectation,
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",
);
const mockApi = createStrictMockServer();
beforeAll(mockApi.listen);
afterAll(mockApi.close);
type AttemptTrace = {
status: AttemptStatus;
media: string | null;
pulledBytes: number;
appliedCeiling: number | null;
completed: boolean;
cancelled: boolean;
};
function operationFor(operationId: HttpScenarioOperationId) {
const operation = REFERENCE_FEATURE_TEMPLATE_CONTRIBUTION.http.find(
(candidate) => candidate.contract.operationId === operationId,
);
if (!operation) throw new Error(`Missing operation fixture: ${operationId}`);
return operation as InstalledHttpContract<unknown, unknown, unknown>;
}
function inputFor(operationId: HttpScenarioOperationId): unknown {
if (operationId === "LIST_REFERENCE_RESOURCES") return { limit: 20 };
if (operationId === "GET_REFERENCE_RESOURCE") {
return { resourceId: "reference-1" };
}
return { name: "Created" };
}
function normalizeMedia(response: Response): string | null {
const value = response.headers.get("content-type");
return value?.split(";", 1)[0]?.trim().toLowerCase() || null;
}
function tracingFetcher(attempts: AttemptTrace[]): typeof fetch {
return async (input, init) => {
const trace: AttemptTrace = {
status: "PENDING_ABORT",
media: null,
pulledBytes: 0,
appliedCeiling: null,
completed: false,
cancelled: false,
};
attempts.push(trace);
let response: Response;
try {
response = await fetch(input, init);
} catch (error) {
trace.status = init?.signal?.aborted
? "PENDING_ABORT"
: "NETWORK_REJECTION";
throw error;
}
trace.status = response.status;
trace.media = normalizeMedia(response);
if (!response.body) return response;
const reader = response.body.getReader();
const proxy = new ReadableStream<Uint8Array>(
{
async pull(controller) {
try {
const chunk = await reader.read();
if (chunk.done) {
trace.completed = true;
controller.close();
return;
}
trace.pulledBytes += chunk.value.byteLength;
controller.enqueue(chunk.value);
} catch (error) {
controller.error(error);
}
},
cancel(reason) {
trace.cancelled = true;
// MSW-transferred oversized bodies leave the original cancel promise
// permanently pending. Record and request cancellation without making
// the proxy response less responsive than the real executor contract.
void reader.cancel(reason).catch(() => {});
},
},
{ highWaterMark: 0 },
);
return new Response(proxy, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
};
}
function outcomeDetail(outcome: Readonly<Record<string, unknown>>): string | null {
if (outcome.kind === "CONTRACT_VIOLATION") {
return String((outcome.violation as Readonly<{ kind: string }>).kind);
}
if (outcome.kind === "TRANSPORT_FAILURE") {
return String((outcome.failure as Readonly<{ kind: string }>).kind);
}
if (outcome.kind === "PROBLEM") {
return String((outcome.metadata as Readonly<{ status: number }>).status);
}
return null;
}
function retryReason(status: AttemptStatus): RetryReason {
if (status === "NETWORK_REJECTION") return "NETWORK_FAILURE";
if (status === 429) return "HTTP_429";
if (status === 503) return "HTTP_503";
throw new Error(`Sleep followed a non-retryable attempt: ${String(status)}`);
}
function bodyDisposition(
trace: AttemptTrace,
outcome: Readonly<Record<string, unknown>>,
): BodyDisposition {
if (trace.status === "NETWORK_REJECTION" || trace.status === "PENDING_ABORT") {
return "NO_RESPONSE";
}
if (
outcome.kind === "CONTRACT_VIOLATION" &&
outcomeDetail(outcome) === "RESPONSE_TOO_LARGE" &&
trace.cancelled
) {
return "REJECTED_LIMIT";
}
if (trace.completed) return "FULLY_READ_WITHIN_BOUND";
if (trace.cancelled) return "CANCELLED_WITHOUT_READ";
throw new Error(`Response body was neither consumed nor cancelled: ${trace.status}`);
}
async function waitForHandler(
ready: Promise<void>,
executionId: string,
): Promise<void> {
let watchdog: ReturnType<typeof setTimeout> | undefined;
try {
await Promise.race([
ready,
new Promise<never>((_resolve, reject) => {
watchdog = setTimeout(
() => reject(new Error(`Handler readiness timed out: ${executionId}`)),
2_000,
);
}),
]);
} finally {
if (watchdog !== undefined) clearTimeout(watchdog);
}
}
async function executeScenario(
entry: HttpScenarioExpectation,
): Promise<HttpScenarioAssertionGroups> {
const physicalAttempts: AttemptTrace[] = [];
const sleeps: RetryReason[] = [];
const observations: HttpExecutionObservation[] = [];
const caller = new AbortController();
const scopeLifetime = new AbortController();
let scopeCurrent = true;
let handlerReady!: () => void;
const ready = new Promise<void>((resolve) => {
handlerReady = resolve;
});
const scope: CacheScopeSnapshot = Object.freeze({
generation: 1,
fingerprint: "scenario-scope-1",
identities: Object.freeze({}) as CacheScopeSnapshot["identities"],
signal: scopeLifetime.signal,
isCurrent: () => scopeCurrent,
});
const baseOperation = operationFor(entry.operationId);
const operation =
entry.testDeadlineOverrideMs === null
? baseOperation
: Object.freeze({
...baseOperation,
frontend: Object.freeze({
...baseOperation.frontend,
totalDeadlineMs: entry.testDeadlineOverrideMs,
}),
});
const fetcher = tracingFetcher(physicalAttempts);
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: { authorization: "Bearer scenario-token" },
}),
fetcher,
random: () => 0,
readBoundedResponseBytes: async (response, maximumBytes) => {
const trace = physicalAttempts.at(-1);
if (!trace) throw new Error("Body reader ran before a physical attempt");
if (trace.appliedCeiling !== null) {
throw new Error("Body reader ran more than once for one attempt");
}
trace.appliedCeiling = maximumBytes;
return readBoundedBytes(response, maximumBytes);
},
sleep: async () => {
const prior = physicalAttempts.at(-1);
if (!prior) throw new Error("Retry sleep occurred before an attempt");
sleeps.push(retryReason(prior.status));
},
observe: (observation) => observations.push(observation),
});
mockApi.server.use(
...createReferenceScenarioHandlers({
scenarios: { [entry.operationId]: entry.scenarioId },
onAttempt(attempt) {
if (
attempt.operationId === entry.operationId &&
attempt.scenarioId === entry.scenarioId
) {
handlerReady();
}
},
}),
);
try {
const execution = executor.execute(operation, inputFor(entry.operationId), {
routeId: routeIdFor(entry.operationId),
scope,
signal: caller.signal,
...(entry.operationId === "CREATE_REFERENCE_RESOURCE"
? {
intent: Object.freeze({
intentId: `intent-${entry.scenarioId}`,
operationId: entry.operationId,
canonicalInputIdentity: "scenario-input",
idempotencyKey: `scenario-${entry.scenarioId}`,
createdAtMonotonicMs: 1,
}),
}
: {}),
});
if (entry.scenarioId === "timeout" || entry.scenarioId === "aborted") {
await waitForHandler(ready, entry.executionId);
if (entry.scenarioId === "aborted") {
if (entry.operationId === "LIST_REFERENCE_RESOURCES") {
caller.abort();
} else {
scopeCurrent = false;
scopeLifetime.abort();
}
}
}
const outcome = (await execution) as unknown as Readonly<Record<string, unknown>>;
expect(observations, `${entry.executionId} observer count`).toHaveLength(1);
const observation = observations[0]!;
const observedSignal = scopeLifetime.signal.aborted ? "ABORTED" : "ACTIVE";
const cancellationOwner =
observation.terminalReason === "TIMEOUT"
? "DEADLINE"
: caller.signal.aborted
? "CALLER"
: scopeLifetime.signal.aborted
? "SCOPE_FENCE"
: "NONE";
const observed: HttpScenarioAssertionGroups = Object.freeze({
status: Object.freeze({
attempts: Object.freeze(physicalAttempts.map((attempt) => attempt.status)),
final: physicalAttempts.at(-1)!.status,
}),
outcome: Object.freeze({
kind: String(outcome.kind),
detail: outcomeDetail(outcome),
}),
effect: Object.freeze({
outcome: String(outcome.effect),
observer: observation.terminalReason,
}),
retry: Object.freeze({ count: sleeps.length, reasons: Object.freeze(sleeps) }),
fetch: Object.freeze({
count: physicalAttempts.length,
observerAttempts: observation.attemptCount,
agrees: physicalAttempts.length === observation.attemptCount,
}),
media: Object.freeze({
attempts: Object.freeze(physicalAttempts.map((attempt) => attempt.media)),
final: physicalAttempts.at(-1)?.media ?? null,
}),
body: Object.freeze({
attempts: Object.freeze(
physicalAttempts.map((attempt) =>
Object.freeze({
disposition: bodyDisposition(attempt, outcome),
pulledBytes: attempt.pulledBytes,
ceiling: attempt.appliedCeiling ?? 0,
}),
),
),
}),
scope: Object.freeze({
start: "CURRENT",
end: scopeCurrent ? "CURRENT" : "STALE",
signal: observedSignal,
cancellationOwner,
}),
});
return observed;
} finally {
caller.abort();
scopeLifetime.abort();
mockApi.reset();
}
}
describe("HTTP scenario catalog execution evidence", () => {
it("declares exactly the 54 behaviorally distinct operation scenarios", () => {
const executionIds = Object.entries(OPERATION_SCENARIO_CATALOG).flatMap(
([operationId, scenarioIds]) =>
scenarioIds.map((scenarioId) => `${operationId}::${scenarioId}`),
);
expect(OPERATION_SCENARIO_CATALOG.LIST_REFERENCE_RESOURCES).toHaveLength(19);
expect(OPERATION_SCENARIO_CATALOG.GET_REFERENCE_RESOURCE).toHaveLength(18);
expect(OPERATION_SCENARIO_CATALOG.CREATE_REFERENCE_RESOURCE).toHaveLength(17);
expect(executionIds).toHaveLength(54);
expect(new Set(executionIds)).toHaveLength(54);
expect(executionIds).toEqual(HTTP_SCENARIO_EXECUTION_IDS);
});
it("executes every declared scenario and publishes one complete receipt", async () => {
await rm(RECEIPT_PATH, { force: true });
const rows: Array<Readonly<{
executionId: string;
expected: HttpScenarioAssertionGroups;
observed: HttpScenarioAssertionGroups;
testDeadlineOverrideMs: number | null;
}>> = [];
for (const entry of HTTP_SCENARIO_EXPECTATIONS) {
const observed = await executeScenario(entry);
expect(observed, entry.executionId).toEqual(entry.expected);
rows.push(
Object.freeze({
executionId: entry.executionId,
expected: entry.expected,
observed,
testDeadlineOverrideMs: entry.testDeadlineOverrideMs,
}),
);
}
const sortedRows = [...rows].sort((left, right) =>
left.executionId.localeCompare(right.executionId),
);
await mkdir(path.dirname(RECEIPT_PATH), { recursive: true });
await writeValidatedJsonArtifact({
path: RECEIPT_PATH,
schema: httpScenarioReceiptSchema,
value: {
schemaVersion: HTTP_SCENARIO_RECEIPT_SCHEMA_VERSION,
catalogDigest: computeHttpScenarioCatalogDigest(
HTTP_SCENARIO_RECEIPT_SCHEMA_VERSION,
HTTP_SCENARIO_EXPECTATIONS,
),
catalogTotal: 54,
executedIds: sortedRows.map((row) => row.executionId),
rows: sortedRows,
},
});
}, 30_000);
});