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 { entityQueryKeys, TEST_HTTP_CONTRACT, } from "../helpers/http-contract-fixture.ts"; import { TEST_CREATE_HTTP_CONTRACT } from "../helpers/external-contract-fixture.ts"; function successResponse(data: unknown) { return Response.json({ success: true, data, meta: { requestId: "request-1", traceId: "trace-1" }, }); } function failureResponse(status: number) { return Response.json( { success: false, error: { code: "TEMPORARY" }, meta: { requestId: "request-1", traceId: "trace-1" }, }, { status }, ); } function immediateClock() { return { now: () => 0, sleep: async () => {} }; } function recordingScheduler() { const callbacks: Array<() => void> = []; return { callbacks, setTimeout: vi.fn((callback: () => void) => { callbacks.push(callback); return callbacks.length - 1; }), clearTimeout: vi.fn(), }; } type HttpDependencies = Parameters[0]; function testClient(options: HttpDependencies) { return createHttpClient({ ...TEST_HTTP_CONTRACT, ...options }); } 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 = []; const fetcher = vi.fn( async (_input: RequestInfo | URL, init?: RequestInit) => { observedKeys.push(new Headers(init?.headers).get("Idempotency-Key")); return Response.json( { id: "created", name: "Created" }, { status: 201 }, ); }, ); const executor = createContractHttpExecutor({ baseUrl: "https://api.example/", maxRetryAttempts: 0, attachCredentials, fetcher, }); const nonKeyedCommand = { ...TEST_CREATE_HTTP_CONTRACT, contract: { ...TEST_CREATE_HTTP_CONTRACT.contract, retrySemantics: "NEVER" as const, commandRecovery: null, }, }; const scope = Object.freeze({ generation: 1, fingerprint: "scope-1", identities: Object.freeze({}) as never, signal: new AbortController().signal, isCurrent: () => true, }); const intent = Object.freeze({ intentId: "intent-1", operationId: "TEST_CREATE_ENTITY", canonicalInputIdentity: "opaque-input-identity", createdAtMonotonicMs: 1, }); await expect( executor.execute( nonKeyedCommand, { name: "created" }, { scope, intent }, ), ).resolves.toMatchObject({ kind: "SUCCESS" }); expect(observedKeys).toEqual([null]); attachCredentials.mockClear(); fetcher.mockClear(); await expect( executor.execute( nonKeyedCommand, { name: "created" }, { scope, intent: { ...intent, idempotencyKey: "unexpected-key" } }, ), ).resolves.toMatchObject({ kind: "CONTRACT_VIOLATION", violation: { kind: "UNEXPECTED_IDEMPOTENCY_KEY", operation: "REQUEST", }, effect: "NOT_STARTED", }); expect(attachCredentials).not.toHaveBeenCalled(); expect(fetcher).not.toHaveBeenCalled(); }); it("fails auth-required execution before fetch when the session integration is unavailable", async () => { const fetcher = vi.fn(); const client = testClient({ baseUrl: "https://api.test", fetcher, authSession: undefined, }); await expect( client.execute({ operationId: "LIST_ENTITIES", routeId: "TEST_ROUTE" }), ).resolves.toMatchObject({ ok: false, error: { kind: "AUTH_INTEGRATION_FAILURE" }, }); expect(fetcher).not.toHaveBeenCalled(); }); it("rejects an oversized response without materializing its JSON", async () => { const largeOperation = { ...TEST_HTTP_CONTRACT.getOperation("LIST_ENTITIES"), maxResponseBytes: 32, responseMediaTypes: ["application/json"], successStatuses: [200], }; const client = testClient({ baseUrl: "https://api.test", getOperation: () => largeOperation, fetcher: vi.fn(async () => Response.json({ success: true, data: [{ id: "one", name: "a".repeat(100) }], meta: { requestId: "request-1", traceId: "trace-1" }, }), ), }); await expect(client.execute("LIST_ENTITIES")).resolves.toMatchObject({ ok: false, error: { kind: "RESPONSE_BODY_LIMIT" }, }); }); it("sends parsed search/body values and aligns canonical query identity", async () => { const requests: Request[] = []; const scheduler = recordingScheduler(); const fetcher = vi.fn(async (input: RequestInfo | URL) => { const request = input as Request; requests.push(request); if (request.method === "POST") { return successResponse({ id: "created", name: "Trimmed" }); } return successResponse([]); }); const client = testClient({ baseUrl: "https://api.test", fetcher, clock: immediateClock(), scheduler, }); const filters = { tags: ["open", "new"], cursor: "a/b", limit: 5 }; await client.execute({ operationId: "LIST_ENTITIES", routeId: "TEST_ROUTE", searchParams: filters, }); await client.execute({ operationId: "CREATE_ENTITY", routeId: "TEST_ROUTE", body: { name: " Trimmed " }, idempotencyKey: "logical-command", }); expect(requests[0].url).toBe( "https://api.test/api/entities?cursor=a%2Fb&limit=5&tags=open&tags=new", ); expect(requests[0].headers.get("Idempotency-Key")).toBeNull(); expect(entityQueryKeys.list(filters).at(-1)).toEqual(filters); await expect(requests[1].json()).resolves.toEqual({ name: "Trimmed" }); expect(requests[1].headers.get("Idempotency-Key")).toBe("logical-command"); expect(requests[1].url).not.toContain("logical-command"); expect(requests[0].headers.get("X-Correlation-ID")).toBeTruthy(); expect(requests[0].credentials).toBe("same-origin"); expect(requests[0].cache).toBe("no-store"); expect(requests[0].redirect).toBe("error"); expect(scheduler.setTimeout).toHaveBeenCalledTimes(2); expect(scheduler.clearTimeout).toHaveBeenCalledTimes(2); }); it("performs no fetch or timer work for invalid request input", async () => { const fetcher = vi.fn(); const scheduler = recordingScheduler(); const client = testClient({ baseUrl: "https://api.test", fetcher, scheduler, }); await expect( client.execute({ operationId: "CREATE_ENTITY", routeId: "TEST_ROUTE", body: { name: " " }, }), ).resolves.toMatchObject({ ok: false, error: { kind: "VALIDATION_REJECTED" }, }); expect(fetcher).not.toHaveBeenCalled(); expect(scheduler.setTimeout).not.toHaveBeenCalled(); expect(scheduler.clearTimeout).not.toHaveBeenCalled(); }); it.each([ [0, 1], [1, 2], [2, 3], ])( "applies runtime max retry count %i as %i total attempts", async (maxRetryAttempts, totalAttempts) => { const fetcher = vi.fn(async () => failureResponse(503)); const scheduler = recordingScheduler(); const client = testClient({ baseUrl: "https://api.test", fetcher, clock: immediateClock(), scheduler, maxRetryAttempts, }); await expect( client.execute({ operationId: "LIST_ENTITIES", routeId: "TEST_ROUTE", }), ).resolves.toMatchObject({ ok: false, error: { kind: "SERVER_FAILURE" }, }); expect(fetcher).toHaveBeenCalledTimes(totalAttempts); expect(scheduler.setTimeout).toHaveBeenCalledTimes(totalAttempts); expect(scheduler.clearTimeout).toHaveBeenCalledTimes(totalAttempts); }, ); it("distinguishes a runtime timeout from caller navigation abort and cleans listeners", async () => { const scheduler = recordingScheduler(); const fetcher = vi.fn( (input: RequestInfo | URL) => new Promise((_resolve, reject) => { (input as Request).signal.addEventListener( "abort", () => reject(new DOMException("aborted", "AbortError")), { once: true }, ); }), ); const client = testClient({ baseUrl: "https://api.test", fetcher, scheduler, maxRetryAttempts: 0, }); const timeoutResult = client.execute({ operationId: "LIST_ENTITIES", routeId: "TEST_ROUTE", }); await vi.waitFor(() => expect(scheduler.callbacks).toHaveLength(1)); scheduler.callbacks[0](); await expect(timeoutResult).resolves.toMatchObject({ ok: false, error: { kind: "REQUEST_TIMEOUT" }, }); const caller = new AbortController(); const add = vi.spyOn(caller.signal, "addEventListener"); const remove = vi.spyOn(caller.signal, "removeEventListener"); const abortResult = client.execute({ operationId: "LIST_ENTITIES", routeId: "TEST_ROUTE", signal: caller.signal, }); await vi.waitFor(() => expect(fetcher).toHaveBeenCalledTimes(2)); caller.abort("navigation"); await expect(abortResult).resolves.toMatchObject({ ok: false, error: { kind: "REQUEST_ABORTED" }, }); expect(add).toHaveBeenCalledOnce(); expect(remove).toHaveBeenCalledOnce(); expect(scheduler.clearTimeout).toHaveBeenCalledTimes(2); }); it("never retries unsafe, non-retryable status, or schema failures", async () => { const unsafeOperation = { method: "POST", path: "/api/unsafe", operationId: "UNSAFE", auth: "none", timeoutMs: null, idempotency: "none", retry: "never", requestSource: "none", requestSchema: "unused", responseSchema: "EntityPayload", owner: "test", } as const; const unsafeFetch = vi.fn(async () => failureResponse(503)); const unsafeClient = testClient({ baseUrl: "https://api.test", fetcher: unsafeFetch, clock: immediateClock(), getOperation: () => unsafeOperation, }); await unsafeClient.execute({ operationId: "UNSAFE", routeId: "TEST", }); expect(unsafeFetch).toHaveBeenCalledOnce(); const statusFetch = vi.fn(async () => failureResponse(500)); const statusClient = testClient({ baseUrl: "https://api.test", fetcher: statusFetch, clock: immediateClock(), }); await statusClient.execute({ operationId: "LIST_ENTITIES", routeId: "TEST_ROUTE", }); expect(statusFetch).toHaveBeenCalledOnce(); const schemaFetch = vi.fn(async () => successResponse([{ id: "one", name: 42 }]), ); const schemaScheduler = recordingScheduler(); const schemaClient = testClient({ baseUrl: "https://api.test", fetcher: schemaFetch, clock: immediateClock(), scheduler: schemaScheduler, }); await schemaClient.execute({ operationId: "LIST_ENTITIES", routeId: "TEST_ROUTE", }); expect(schemaFetch).toHaveBeenCalledOnce(); expect(schemaScheduler.clearTimeout).toHaveBeenCalledOnce(); }); });