fix(rpc): isolate integration test from cross-suite fetch pollution

packages/csr's actions.test.ts and reactive.test.ts both leave
globalThis.fetch mutated across bun test files (reactive.test.ts's
'cache invalidation refetches...' test replaces it and never restores
it). Since bun test runs files sequentially rather than importing all
of them up front, a module-level capture of fetch in this file would
already observe csr's leftover mock (csr sorts before rpc).

Route the real-socket assertion through a small node:http-backed fetch
implementation instead of relying on globalThis.fetch at all, keeping
the test's actual target - httpTransport()'s default resolveOrigin -
unaffected by any other suite's global mutation.
This commit is contained in:
2026-08-05 20:57:08 +05:30
parent 3eec9fd8c6
commit 98205daef6
+48 -1
View File
@@ -1,4 +1,5 @@
import { afterEach, describe, expect, test } from "bun:test";
import * as http from "node:http";
import type { Context } from "@wrnexus/core";
import { v } from "@wrnexus/validation";
import { serviceClient } from "../src/client.ts";
@@ -70,6 +71,50 @@ function httpWire(allowed: boolean) {
});
}
// packages/csr's actions.test.ts deletes globalThis.fetch (and restores its
// own captured copy) around each of its tests, and packages/csr's
// reactive.test.ts's "cache invalidation refetches matching client Async
// boundaries" test replaces globalThis.fetch with a mock and never restores
// it at all. bun test runs test files sequentially — importing a file and
// running its tests before moving to the next — rather than importing every
// file up front, so a module-level `const realFetch = fetch` captured here
// would already observe whatever packages/csr left behind by the time this
// file (which sorts after csr) is loaded. The variable under test below is
// `resolveOrigin`, not `fetch`, so instead of depending on the shared
// `globalThis.fetch` at all, this makes a real request over a real socket
// using node:http directly. That keeps the assertion about
// httpTransport()'s default origin resolution intact regardless of what any
// other suite does to the global `fetch` binding.
function realFetch(url: string, init: RequestInit): Promise<Response> {
return new Promise((resolve, reject) => {
const target = new URL(url);
const req = http.request(
{
hostname: target.hostname,
port: target.port,
path: `${target.pathname}${target.search}`,
method: init.method ?? "GET",
headers: init.headers as Record<string, string>,
},
(res) => {
const chunks: Buffer[] = [];
res.on("data", (chunk: Buffer) => chunks.push(chunk));
res.on("error", reject);
res.on("end", () => {
resolve(
new Response(Buffer.concat(chunks), {
status: res.statusCode ?? 500,
}),
);
});
},
);
req.on("error", reject);
if (typeof init.body === "string") req.write(init.body);
req.end();
});
}
describe("RPC integration", () => {
test("propagates identity and validates the callee permission", async () => {
process.env.WRNEXUS_RPC_SECRET = "test-rpc-secret-at-least-32-chars-long";
@@ -151,7 +196,9 @@ describe("RPC integration", () => {
});
const client = serviceClient(billing, {
app: "billing",
transport: httpTransport(), // no resolveOrigin override — uses the real default
// no resolveOrigin override — uses the real default; fetch is pinned
// to the node:http-backed implementation above (see comment there).
transport: httpTransport({ fetch: realFetch as unknown as typeof fetch }),
});
expect(await client.createInvoice({ amountCents: 42 })).toEqual({
invoiceId: "inv_42",