refactor: extracted fetch mocking into reusable hookFetch utility

- Extracted fetch mocking logic into reusable `hookFetch()` utility function with middleware-style handler pattern.
- Replaced manual `globalThis.fetch` assignment and restoration across 10 test files with `hookFetch()` calls using `using` statement for automatic cleanup.
- Implemented Disposable pattern with Symbol.dispose for fetch hook resource management, eliminating try-finally blocks.
- Exported `hookFetch` from utils public API to enable consistent fetch mocking across packages.
This commit is contained in:
can1357
2026-03-08 16:16:54 +01:00
parent e1d8ed2c10
commit 005401e82a
13 changed files with 169 additions and 270 deletions
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
import * as gatewayCoordinator from "@oh-my-pi/pi-coding-agent/ipy/gateway-coordinator";
import { PythonKernel } from "@oh-my-pi/pi-coding-agent/ipy/kernel";
import { TempDir } from "@oh-my-pi/pi-utils";
import { hookFetch, TempDir } from "@oh-my-pi/pi-utils";
import type { Subprocess } from "bun";
type SpawnOptions = Parameters<typeof Bun.spawn>[1];
@@ -74,7 +74,6 @@ const createFakeProcess = (): Subprocess => {
};
describe("PythonKernel gateway lifecycle", () => {
const originalFetch = globalThis.fetch;
const originalWebSocket = globalThis.WebSocket;
const originalSpawn = Bun.spawn;
const originalSleep = Bun.sleep;
@@ -140,7 +139,6 @@ describe("PythonKernel gateway lifecycle", () => {
Bun.env.PI_PYTHON_GATEWAY_TOKEN = originalGatewayToken;
}
globalThis.fetch = originalFetch;
globalThis.WebSocket = originalWebSocket;
Bun.spawn = originalSpawn;
@@ -155,7 +153,7 @@ describe("PythonKernel gateway lifecycle", () => {
isShared: true,
});
globalThis.fetch = (async (input: string | URL, init?: RequestInit) => {
using _hook = hookFetch((input, init) => {
const url = String(input);
env.fetchCalls.push({ url, init });
@@ -164,7 +162,7 @@ describe("PythonKernel gateway lifecycle", () => {
}
return createResponse({ ok: true }) as unknown as Response;
}) as typeof fetch;
});
const kernel = await PythonKernel.start({ cwd: tempDir.path() });
@@ -184,14 +182,14 @@ describe("PythonKernel gateway lifecycle", () => {
isShared: true,
});
globalThis.fetch = (async (input: string | URL, init?: RequestInit) => {
using _hook = hookFetch((input, init) => {
const url = String(input);
env.fetchCalls.push({ url, init });
if (url.endsWith("/api/kernels") && init?.method === "POST") {
return createResponse({ ok: false, status: 503, text: "oops" }) as unknown as Response;
}
return createResponse({ ok: true }) as unknown as Response;
}) as typeof fetch;
});
await expect(PythonKernel.start({ cwd: tempDir.path() })).rejects.toThrow(
"Failed to create kernel on shared gateway",
@@ -204,7 +202,7 @@ describe("PythonKernel gateway lifecycle", () => {
isShared: true,
});
globalThis.fetch = (async (input: string | URL, init?: RequestInit) => {
using _hook = hookFetch((input, init) => {
const url = String(input);
env.fetchCalls.push({ url, init });
if (url.endsWith("/api/kernels") && init?.method === "POST") {
@@ -214,7 +212,7 @@ describe("PythonKernel gateway lifecycle", () => {
throw new Error("delete failed");
}
return createResponse({ ok: true }) as unknown as Response;
}) as typeof fetch;
});
const kernel = await PythonKernel.start({ cwd: tempDir.path() });