diff --git a/packages/cli/src/__tests__/cmdrun-happy-path.test.ts b/packages/cli/src/__tests__/cmdrun-happy-path.test.ts index 867ee71ae..130b392b6 100644 --- a/packages/cli/src/__tests__/cmdrun-happy-path.test.ts +++ b/packages/cli/src/__tests__/cmdrun-happy-path.test.ts @@ -170,10 +170,15 @@ describe("cmdRun happy-path pipeline", () => { await cmdRun("claude", "sprite"); - // Should have fetched the manifest + the primary script URL - const scriptFetches = fetchCalls.filter((c) => !c.url.includes("manifest.json")); - expect(scriptFetches.length).toBe(1); - expect(scriptFetches[0].url).toContain("openrouter.ai"); + // Should have fetched the primary script URL, with no fallback to GitHub script + const primaryFetches = fetchCalls.filter( + (c) => c.url.includes("openrouter.ai") && !c.url.includes("manifest.json"), + ); + const fallbackScriptFetches = fetchCalls.filter( + (c) => c.url.includes("raw.githubusercontent.com") && !c.url.includes("manifest.json"), + ); + expect(primaryFetches.length).toBeGreaterThanOrEqual(1); + expect(fallbackScriptFetches.length).toBe(0); }); it("should log download start and completion messages for successful download", async () => { @@ -214,11 +219,15 @@ describe("cmdRun happy-path pipeline", () => { await cmdRun("claude", "sprite"); - // Should have fetched manifest + primary (failed) + fallback (success) - const scriptFetches = fetchCalls.filter((c) => !c.url.includes("manifest.json")); - expect(scriptFetches.length).toBe(2); - expect(scriptFetches[0].url).toContain("openrouter.ai"); - expect(scriptFetches[1].url).toContain("raw.githubusercontent.com"); + // Should have fetched primary (failed) + fallback script (success) + const primaryFetches = fetchCalls.filter( + (c) => c.url.includes("openrouter.ai") && !c.url.includes("manifest.json"), + ); + const fallbackScriptFetches = fetchCalls.filter( + (c) => c.url.includes("raw.githubusercontent.com") && !c.url.includes("manifest.json"), + ); + expect(primaryFetches.length).toBeGreaterThanOrEqual(1); + expect(fallbackScriptFetches.length).toBeGreaterThanOrEqual(1); }); it("should log fallback step message when primary fails", async () => { diff --git a/packages/cli/src/__tests__/digitalocean-token.test.ts b/packages/cli/src/__tests__/digitalocean-token.test.ts index e0d329129..90e9dcb87 100644 --- a/packages/cli/src/__tests__/digitalocean-token.test.ts +++ b/packages/cli/src/__tests__/digitalocean-token.test.ts @@ -114,8 +114,10 @@ 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: at least 1 API call + 1 connectivity check. + // Use >= because concurrent test files can inject extra fetch calls via the + // shared global.fetch, shifting the sequential callCount baseline. + expect(callCount).toBeGreaterThanOrEqual(2); }); 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..59c240021 100644 --- a/packages/cli/src/__tests__/hetzner-cov.test.ts +++ b/packages/cli/src/__tests__/hetzner-cov.test.ts @@ -585,10 +585,16 @@ describe("hetzner/createServer", () => { }, }, }; + // URL-aware mock: route responses by URL pattern so concurrent test files + // leaking extra fetch calls don't corrupt the sequential callCount routing. + let postServersCount = 0; let callCount = 0; - global.fetch = mock(() => { + global.fetch = mock((input: string | URL | Request) => { callCount++; - if (callCount <= 1) { + const url = String(input instanceof Request ? input.url : input); + const method = input instanceof Request ? input.method : "GET"; + + if (url.includes("/servers") && url.includes("per_page=1")) { // Token validation return Promise.resolve( new Response( @@ -598,8 +604,8 @@ describe("hetzner/createServer", () => { ), ); } - if (callCount <= 2) { - // SSH keys + if (url.includes("/ssh_keys")) { + // SSH key listing return Promise.resolve( new Response( JSON.stringify({ @@ -608,23 +614,15 @@ describe("hetzner/createServer", () => { ), ); } - if (callCount <= 3) { - // First create attempt — resource_limit_exceeded (HTTP 403) + if (url.includes("/primary_ips") && method === "DELETE") { + // Delete orphaned primary IP return Promise.resolve( - new Response( - JSON.stringify({ - error: { - code: "resource_limit_exceeded", - message: "primary_ip_limit", - }, - }), - { - status: 403, - }, - ), + new Response("", { + status: 204, + }), ); } - if (callCount <= 4) { + if (url.includes("/primary_ips")) { // List primary IPs for cleanup return Promise.resolve( new Response( @@ -645,22 +643,33 @@ describe("hetzner/createServer", () => { ), ); } - if (callCount <= 5) { - // Delete orphaned IP 100 - return Promise.resolve( - new Response("", { - status: 204, - }), - ); + if (url.includes("/servers")) { + // POST /servers — first attempt fails, retry succeeds + postServersCount++; + if (postServersCount === 1) { + return Promise.resolve( + new Response( + JSON.stringify({ + error: { + code: "resource_limit_exceeded", + message: "primary_ip_limit", + }, + }), + { + status: 403, + }, + ), + ); + } + return Promise.resolve(new Response(JSON.stringify(serverResp))); } - // Retry create — success - return Promise.resolve(new Response(JSON.stringify(serverResp))); + return Promise.resolve(new Response(JSON.stringify({}))); }); const { ensureHcloudToken, createServer } = await import("../hetzner/hetzner"); await ensureHcloudToken(); 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) + // Should have called: token, ssh_keys, create-fail, list-ips, delete-ip, create-ok expect(callCount).toBeGreaterThanOrEqual(6); });