// Unit tests for the relay provider's shared response handling and the // operator-call helpers introduced by the dedup refactor // (handleRelayResponse / operatorPost / operatorGet). The relay is // network-dependent, so we stub global.fetch. These nail: // - the tts error-path control flow, which must THROW, not fall through // to the binary arrayBuffer read (regression for the missing-return fix); // - handleRelayResponse's envelope parsing + the recordRelayError-only- // when-no-envelope rule; // - the deliberate throw-vs-null split between operatorPost (writes) and // operatorGet (reads), including the missing-config branch. import { test, describe, afterEach } from "node:test"; import assert from "node:assert/strict"; import { createRelayProvider, setRelayUserTier, getRelayUserTier, createRelayTierInvoice, getRelayTierPlans, getRelayExpiringSubscriptions, } from "../providers/relay.js"; import { getRelayState, resetRelayState } from "../relay-state.js"; import { getRelayOperatorKey } from "../relay-default.js"; const BASE = "https://relay.recaps.cc"; // Pin an operator key so the operator-helper tests exercise the configured // path. node --test isolates each test file in its own process, so this // doesn't leak into other suites. process.env.RECAP_RELAY_OPERATOR_KEY = "test-op-key"; const realFetch = global.fetch; let fetchCalls = []; // Install a stub that records calls and returns (or runs) the given response. function stubFetch(response) { fetchCalls = []; global.fetch = async (url, opts) => { fetchCalls.push({ url: String(url), opts }); return typeof response === "function" ? response(url, opts) : response; }; } afterEach(() => { global.fetch = realFetch; resetRelayState(); }); // Minimal Response-like stubs covering the surfaces the provider touches. function jsonRes({ ok = true, status = 200, body = {} }) { return { ok, status, text: async () => JSON.stringify(body), json: async () => body }; } function textRes({ ok = false, status = 502, body = "Bad Gateway" }) { return { ok, status, text: async () => body, json: async () => { throw new SyntaxError("not json"); }, }; } function binRes({ headers = {}, bytes = [1, 2, 3] }) { return { ok: true, status: 200, headers: new Headers(headers), arrayBuffer: async () => new Uint8Array(bytes).buffer, }; } const provider = () => createRelayProvider({ baseURL: BASE, installId: "test-install" }); // A never-aborting signal so tts skips its 90s AbortSignal.timeout fallback. const noTimeout = () => new AbortController().signal; describe("relay tts: error path throws (regression for the missing-return fix)", () => { test("throws on a non-OK response instead of falling through to the binary read", async () => { stubFetch(jsonRes({ ok: false, status: 500, body: { error: "kokoro busy" } })); await assert.rejects( () => provider().tts({ text: "hi", voice: "bm_george", signal: noTimeout() }), (err) => { assert.equal(err.status, 500); assert.equal(err.message, "Relay /relay/tts 500: kokoro busy"); assert.deepEqual(err.envelope, { error: "kokoro busy" }); return true; } ); }); test("harvests the credit envelope on a parsed error, records NO relay error", async () => { stubFetch(jsonRes({ ok: false, status: 402, body: { error: "out of credits", credits_remaining: 0 } })); await assert.rejects(() => provider().tts({ text: "x", voice: "v", signal: noTimeout() })); const state = getRelayState("inst:test-install"); assert.equal(state.creditsRemaining, 0); // updateRelayState ran off the envelope assert.equal(state.lastError, null); // recordRelayError did NOT (envelope present) }); test("records a relay error when the error body is not JSON", async () => { stubFetch(textRes({ ok: false, status: 502, body: "Bad Gateway" })); await assert.rejects( () => provider().tts({ text: "x", voice: "v", signal: noTimeout() }), (err) => { assert.equal(err.status, 502); assert.equal(err.message, "Relay /relay/tts 502: Bad Gateway"); return true; } ); assert.equal(getRelayState("inst:test-install").lastError, "Bad Gateway"); }); test("success path returns binary audio and never enters the error branch", async () => { stubFetch( binRes({ headers: { "Content-Type": "audio/mpeg", "X-Recap-Credits-Remaining": "7", "X-Recap-Tier": "pro", "X-Recap-Credit-Charged": "1", "X-Recap-Audio-Duration": "12.5", }, bytes: [10, 20, 30], }) ); const out = await provider().tts({ text: "hi", voice: "bm_george", signal: noTimeout() }); assert.ok(Buffer.isBuffer(out.audio)); assert.equal(out.audio.length, 3); assert.equal(out.contentType, "audio/mpeg"); assert.equal(out.creditCharged, 1); assert.equal(out.durationSeconds, 12.5); const state = getRelayState("inst:test-install"); assert.equal(state.creditsRemaining, 7); assert.equal(state.tier, "pro"); }); }); describe("operatorPost (writes — throw on failure)", () => { test("setRelayUserTier POSTs the right URL/headers/body and returns data on OK", async () => { stubFetch(jsonRes({ ok: true, body: { ok: true, tier: "max" } })); const data = await setRelayUserTier({ userId: "u1", tier: "max", expiresAt: "2026-09-01T00:00:00Z" }); assert.deepEqual(data, { ok: true, tier: "max" }); assert.equal(fetchCalls.length, 1); const { url, opts } = fetchCalls[0]; assert.equal(url, `${BASE}/relay/user-tier`); assert.equal(opts.method, "POST"); assert.equal(opts.headers["Content-Type"], "application/json"); assert.equal(opts.headers["X-Recap-Operator-Key"], "test-op-key"); assert.deepEqual(JSON.parse(opts.body), { user_id: "u1", tier: "max", expires_at: "2026-09-01T00:00:00Z", }); }); test("omits undefined body keys (expires_at) via JSON.stringify", async () => { stubFetch(jsonRes({ ok: true, body: {} })); await setRelayUserTier({ userId: "u1", tier: "pro" }); // expiresAt defaults null → undefined assert.deepEqual(JSON.parse(fetchCalls[0].opts.body), { user_id: "u1", tier: "pro" }); }); test("throws with .status and the relay's error message on a non-OK response", async () => { stubFetch(jsonRes({ ok: false, status: 409, body: { error: "already granted" } })); await assert.rejects( () => setRelayUserTier({ userId: "u1", tier: "max" }), (err) => { assert.equal(err.status, 409); assert.equal(err.message, "already granted"); return true; } ); }); test("falls back to '