import test from "node:test"; import assert from "node:assert/strict"; import {ApiError, get, post, stream} from "../public/js/api.js"; test("JSON requests retain HTTP metadata, handle empty success, and distinguish bad bodies from network errors", async t => { let response; t.mock.method(globalThis, "fetch", async () => response); for (const status of [204, 205, 200]) { response = new Response(null, {status}); assert.deepEqual(await get("/empty"), {}); } response = new Response("Proxy unavailable", {status: 503, statusText: "Service Unavailable", headers: {"X-Request-ID": "proxy-123"}}); assert.deepEqual(await get("/proxy"), {error: "HTTP 503 Service Unavailable", status: 503, code: "HTTP_503", requestId: "proxy-123"}); response = Response.json({error: "Already exists", code: "USERNAME_TAKEN", status: 400, requestId: "body-123", retryAfter: 5}, {status: 409}); assert.deepEqual(await post("/register", {}), {error: "Already exists", code: "USERNAME_TAKEN", status: 409, requestId: "body-123", retryAfter: 5}); response = Response.json({error: {message: "Forbidden", code: "DENIED"}}, {status: 403}); assert.equal((await get("/private")).code, "DENIED"); response = new Response("not json", {headers: {"X-Request-ID": "invalid-body"}}); assert.deepEqual(await get("/invalid"), {error: "Invalid JSON response.", status: 200, code: "INVALID_RESPONSE", requestId: "invalid-body"}); t.mock.method(globalThis, "fetch", async () => { throw new TypeError("Failed to fetch"); }); assert.equal((await get("/offline")).code, "NETWORK_ERROR"); }); test("JSON requests support caller abort and deadlines across headers and body reads", async t => { let signal; t.mock.method(globalThis, "fetch", async (_url, options) => { signal = options.signal; return new Promise(() => {}); }); const controller = new AbortController(); const cancelled = get("/cancel", {signal: controller.signal}); controller.abort(); assert.equal((await cancelled).code, "ABORTED"); assert.equal((await get("/timeout", {timeoutMs: 10})).code, "TIMEOUT"); assert.equal(signal.aborted, true); t.mock.method(globalThis, "fetch", async () => new Response(new ReadableStream(), {headers: {"X-Request-ID": "slow-body"}})); const bodyTimeout = await get("/slow-body", {timeoutMs: 10}); assert.equal(bodyTimeout.code, "TIMEOUT"); assert.equal(bodyTimeout.status, 200); assert.equal(bodyTimeout.requestId, "slow-body"); }); test("SSE errors reject by default and deliver complete metadata to an explicit handler", async t => { const payload = {error: "Model unavailable", status: 503, code: "MODEL_UNAVAILABLE", requestId: "model-123"}; t.mock.method(globalThis, "fetch", async () => new Response(`event: error\ndata: ${JSON.stringify(payload)}\n\n`)); await assert.rejects(stream("/stream", {}), error => error instanceof ApiError && error.status === 503 && error.code === payload.code && error.requestId === payload.requestId); let handled; await stream("/stream", {}, {onError: error => { handled = error; }}); assert.deepEqual(handled, payload); t.mock.method(globalThis, "fetch", async () => new Response("upstream unavailable", {status: 503, headers: {"X-Request-ID": "proxy-123"}})); await assert.rejects(stream("/stream", {}), error => error.status === 503 && error.code === "HTTP_503" && error.requestId === "proxy-123"); }); test("SSE supports fragmented UTF-8, multiline data, replacements, cancellation and stalled reads", async t => { const received = []; const wire = 'event: chunk\r\ndata: {"chunk":"Hello 🌳"}\r\n\r\nevent: replace\ndata: {"target":"reply",\ndata: "text":"Corrected"}\n\nevent: done\ndata: {"reply":"Corrected"}\n\n'; t.mock.method(globalThis, "fetch", async () => new Response(new ReadableStream({start(controller) { for (const byte of new TextEncoder().encode(wire)) controller.enqueue(Uint8Array.of(byte)); controller.close(); }}))); await stream("/stream", {}, {onChunk: data => received.push(data.chunk), onReplace: data => received.push(data.text), onDone: data => received.push(data.reply)}); assert.deepEqual(received, ["Hello 🌳", "Corrected", "Corrected"]); let cancelled = 0; t.mock.method(globalThis, "fetch", async () => new Response(new ReadableStream({cancel() { cancelled++; }}), {headers: {"X-Request-ID": "stream-123"}})); await assert.rejects(stream("/stream", {}, {}, {timeoutMs: 10}), error => error.code === "TIMEOUT" && error.requestId === "stream-123"); assert.equal(cancelled, 1); const controller = new AbortController(); const pending = stream("/stream", {}, {}, controller.signal); controller.abort(); await assert.rejects(pending, error => error.code === "ABORTED"); });