From a724766b1f8007e1c869d580c379e711b14f9941 Mon Sep 17 00:00:00 2001 From: B <6723574+louisgv@users.noreply.github.com> Date: Thu, 14 May 2026 15:01:55 +0000 Subject: [PATCH] fix(tests): stabilize hetzner and DO tests against PostHog fetch contamination Background telemetry (PostHog) calls to globalThis.fetch were incrementing callCount in mocked fetch handlers, causing deterministic failures when running the full test suite. Also adds _testHelpers.resetState() to the hetzner module to prevent token state leaking between tests. Agent: code-health Co-Authored-By: Claude Sonnet 4.6 --- packages/cli/package.json | 2 +- .../src/__tests__/digitalocean-token.test.ts | 26 +++++++++---------- .../cli/src/__tests__/hetzner-cov.test.ts | 26 ++++++++++++------- packages/cli/src/hetzner/hetzner.ts | 12 +++++++++ 4 files changed, 43 insertions(+), 23 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 7ddfc6674..2d64862a0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "1.0.44", + "version": "1.0.45", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/__tests__/digitalocean-token.test.ts b/packages/cli/src/__tests__/digitalocean-token.test.ts index e0d329129..51e2e8eac 100644 --- a/packages/cli/src/__tests__/digitalocean-token.test.ts +++ b/packages/cli/src/__tests__/digitalocean-token.test.ts @@ -88,23 +88,22 @@ describe("doApi 401 OAuth recovery", () => { it("attempts OAuth recovery on 401 before throwing", async () => { state.token = "expired-token"; - let callCount = 0; + let apiCallCount = 0; + let oauthCheckCount = 0; globalThis.fetch = mock((url: string | URL | Request) => { - callCount++; const urlStr = String(url); - // First call: the actual API call returning 401 - if (callCount === 1) { - return Promise.resolve( - new Response("Unauthorized", { - status: 401, - }), - ); - } - // Second call: OAuth connectivity check — fail it so tryDoOAuth returns null quickly + // OAuth connectivity check — fail it so tryDoOAuth returns null quickly // (avoids starting a real Bun.serve OAuth server) if (urlStr.includes("cloud.digitalocean.com")) { + oauthCheckCount++; return Promise.reject(new Error("network unavailable")); } + // Ignore background telemetry calls (PostHog) that may fire during the suite + if (!urlStr.includes("api.digitalocean.com")) { + return Promise.resolve(new Response("OK")); + } + apiCallCount++; + // API call returning 401 return Promise.resolve( new Response("Unauthorized", { status: 401, @@ -114,8 +113,9 @@ describe("doApi 401 OAuth recovery", () => { // OAuth recovery fails (connectivity check fails), so doApi throws the 401 await expect(doApi("GET", "/account", undefined, 1)).rejects.toThrow("DigitalOcean API error 401"); - // Verify recovery was attempted: 1 API call + 1 connectivity check = 2 - expect(callCount).toBe(2); + // Verify recovery was attempted: 1 API call + 1 connectivity check + expect(apiCallCount).toBe(1); + expect(oauthCheckCount).toBe(1); }); it("succeeds after OAuth recovery provides a new token", async () => { diff --git a/packages/cli/src/__tests__/hetzner-cov.test.ts b/packages/cli/src/__tests__/hetzner-cov.test.ts index ed1c050f1..77df8bb0e 100644 --- a/packages/cli/src/__tests__/hetzner-cov.test.ts +++ b/packages/cli/src/__tests__/hetzner-cov.test.ts @@ -4,6 +4,7 @@ import { mockBunSpawn, mockClackPrompts } from "./test-helpers"; mockClackPrompts(); import { + _testHelpers, cleanupOrphanedPrimaryIps, DEFAULT_LOCATION, DEFAULT_SERVER_TYPE, @@ -21,6 +22,7 @@ beforeEach(() => { ...process.env, }; stderrSpy = spyOn(process.stderr, "write").mockReturnValue(true); + _testHelpers.resetState(); }); afterEach(() => { @@ -28,6 +30,7 @@ afterEach(() => { process.env = origEnv; stderrSpy.mockRestore(); mock.restore(); + _testHelpers.resetState(); }); // ─── getConnectionInfo ─────────────────────────────────────────────────────── @@ -585,10 +588,15 @@ describe("hetzner/createServer", () => { }, }, }; - let callCount = 0; - global.fetch = mock(() => { - callCount++; - if (callCount <= 1) { + let hetznerCallCount = 0; + global.fetch = mock((url: string | URL | Request) => { + const urlStr = String(url); + // Ignore background telemetry calls (PostHog) that may fire during the suite + if (!urlStr.includes("api.hetzner.cloud")) { + return Promise.resolve(new Response("OK")); + } + hetznerCallCount++; + if (hetznerCallCount <= 1) { // Token validation return Promise.resolve( new Response( @@ -598,7 +606,7 @@ describe("hetzner/createServer", () => { ), ); } - if (callCount <= 2) { + if (hetznerCallCount <= 2) { // SSH keys return Promise.resolve( new Response( @@ -608,7 +616,7 @@ describe("hetzner/createServer", () => { ), ); } - if (callCount <= 3) { + if (hetznerCallCount <= 3) { // First create attempt — resource_limit_exceeded (HTTP 403) return Promise.resolve( new Response( @@ -624,7 +632,7 @@ describe("hetzner/createServer", () => { ), ); } - if (callCount <= 4) { + if (hetznerCallCount <= 4) { // List primary IPs for cleanup return Promise.resolve( new Response( @@ -645,7 +653,7 @@ describe("hetzner/createServer", () => { ), ); } - if (callCount <= 5) { + if (hetznerCallCount <= 5) { // Delete orphaned IP 100 return Promise.resolve( new Response("", { @@ -661,7 +669,7 @@ describe("hetzner/createServer", () => { const conn = await createServer("test-retry", "cx23", "fsn1"); expect(conn.ip).toBe("10.0.0.5"); // Should have called: token(1), ssh_keys(2), create-fail(3), list-ips(4), delete-ip(5), create-ok(6) - expect(callCount).toBeGreaterThanOrEqual(6); + expect(hetznerCallCount).toBeGreaterThanOrEqual(6); }); it("throws with guidance when resource limit hit and no orphaned IPs to clean", async () => { diff --git a/packages/cli/src/hetzner/hetzner.ts b/packages/cli/src/hetzner/hetzner.ts index 7c83e5278..2b9a00cbe 100644 --- a/packages/cli/src/hetzner/hetzner.ts +++ b/packages/cli/src/hetzner/hetzner.ts @@ -1010,3 +1010,15 @@ export async function destroyServer(serverId?: string): Promise { } logInfo(`Server ${id} destroyed`); } + +/** @internal Reset module-level state between tests. */ +export const _testHelpers = { + get state() { + return _state; + }, + resetState() { + _state.hcloudToken = ""; + _state.serverId = ""; + _state.serverIp = ""; + }, +};