Project one typed HttpExecutionObservation per logical V3 execution through a closed composition-root projector: only registered diagnostic context keys and bucketed values reach the sinks, and terminal non-abort failures now emit exactly one api.request.failed telemetry event. Caller cancellation and scope fencing record a diagnostic but never a failure event. routeId becomes a required input at the installed operation-executor boundary so the feature gateway's low-cardinality route identity survives to the sink. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
320 lines
9.7 KiB
TypeScript
320 lines
9.7 KiB
TypeScript
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: {},
|
|
credentials: "omit" as const,
|
|
}),
|
|
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: {},
|
|
credentials: "omit" as const,
|
|
}),
|
|
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: {},
|
|
credentials: "omit" as const,
|
|
}),
|
|
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: {},
|
|
credentials: "omit" as const,
|
|
}),
|
|
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: {},
|
|
credentials: "omit" as const,
|
|
}),
|
|
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);
|
|
});
|
|
});
|