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 type { Subprocess } from "bun"; type SpawnOptions = Parameters[1]; type FetchCall = { url: string; init?: RequestInit }; type FetchResponse = { ok: boolean; status: number; json: () => Promise; text: () => Promise; }; type MockEnvironment = { fetchCalls: FetchCall[]; spawnCalls: { cmd: string[]; options: SpawnOptions }[]; }; type MessageEventPayload = { data: ArrayBuffer }; type WebSocketHandler = (event: unknown) => void; type WebSocketMessageHandler = (event: MessageEventPayload) => void; class FakeWebSocket { static OPEN = 1; static CLOSED = 3; static instances: FakeWebSocket[] = []; readyState = FakeWebSocket.OPEN; binaryType = "arraybuffer"; url: string; sent: ArrayBuffer[] = []; onopen: WebSocketHandler | null = null; onerror: WebSocketHandler | null = null; onclose: WebSocketHandler | null = null; onmessage: WebSocketMessageHandler | null = null; constructor(url: string) { this.url = url; FakeWebSocket.instances.push(this); queueMicrotask(() => { this.onopen?.(undefined); }); } send(data: ArrayBuffer): void { this.sent.push(data); } close(): void { this.readyState = FakeWebSocket.CLOSED; this.onclose?.(undefined); } } const createResponse = (options: { ok: boolean; status?: number; json?: unknown; text?: string }): FetchResponse => { return { ok: options.ok, status: options.status ?? (options.ok ? 200 : 500), json: async () => options.json ?? {}, text: async () => options.text ?? "", }; }; const createFakeProcess = (): Subprocess => { const exited = new Promise(() => undefined); return { pid: 999999, exited } as Subprocess; }; describe("PythonKernel gateway lifecycle", () => { const originalFetch = globalThis.fetch; const originalWebSocket = globalThis.WebSocket; const originalSpawn = Bun.spawn; const originalSleep = Bun.sleep; const originalWhich = Bun.which; const originalExecute = PythonKernel.prototype.execute; const originalGatewayUrl = process.env.PI_PYTHON_GATEWAY_URL; const originalGatewayToken = process.env.PI_PYTHON_GATEWAY_TOKEN; const originalBunEnv = process.env.BUN_ENV; let tempDir: TempDir; let env: MockEnvironment; beforeEach(() => { tempDir = TempDir.createSync("@omp-python-kernel-"); env = { fetchCalls: [], spawnCalls: [] }; process.env.BUN_ENV = "test"; delete process.env.PI_PYTHON_GATEWAY_URL; delete process.env.PI_PYTHON_GATEWAY_TOKEN; FakeWebSocket.instances = []; globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket; Bun.spawn = ((cmd: string[] | string, options?: SpawnOptions) => { const normalized = Array.isArray(cmd) ? cmd : [cmd]; env.spawnCalls.push({ cmd: normalized, options: options ?? {} }); return createFakeProcess(); }) as typeof Bun.spawn; Bun.sleep = (async () => undefined) as typeof Bun.sleep; Bun.which = (() => "/usr/bin/python") as typeof Bun.which; Object.defineProperty(PythonKernel.prototype, "execute", { value: (async () => ({ status: "ok", cancelled: false, timedOut: false, stdinRequested: false, })) as typeof PythonKernel.prototype.execute, configurable: true, }); }); afterEach(() => { if (tempDir) { tempDir.removeSync(); } if (originalBunEnv === undefined) { delete process.env.BUN_ENV; } else { process.env.BUN_ENV = originalBunEnv; } if (originalGatewayUrl === undefined) { delete process.env.PI_PYTHON_GATEWAY_URL; } else { process.env.PI_PYTHON_GATEWAY_URL = originalGatewayUrl; } if (originalGatewayToken === undefined) { delete process.env.PI_PYTHON_GATEWAY_TOKEN; } else { process.env.PI_PYTHON_GATEWAY_TOKEN = originalGatewayToken; } globalThis.fetch = originalFetch; globalThis.WebSocket = originalWebSocket; Bun.spawn = originalSpawn; Bun.sleep = originalSleep; Bun.which = originalWhich; Object.defineProperty(PythonKernel.prototype, "execute", { value: originalExecute, configurable: true }); }); it("starts shared gateway, interrupts, and shuts down", async () => { vi.spyOn(gatewayCoordinator, "acquireSharedGateway").mockResolvedValue({ url: "http://127.0.0.1:9999", isShared: true, }); globalThis.fetch = (async (input: string | URL, init?: RequestInit) => { const url = String(input); env.fetchCalls.push({ url, init }); if (url.endsWith("/api/kernels") && init?.method === "POST") { return createResponse({ ok: true, json: { id: "kernel-123" } }) as unknown as Response; } return createResponse({ ok: true }) as unknown as Response; }) as typeof fetch; const kernel = await PythonKernel.start({ cwd: tempDir.path() }); expect(env.fetchCalls.some(call => call.url.endsWith("/api/kernels") && call.init?.method === "POST")).toBe(true); await kernel.interrupt(); expect(env.fetchCalls.some(call => call.url.includes("/interrupt") && call.init?.method === "POST")).toBe(true); await kernel.shutdown(); expect(env.fetchCalls.some(call => call.init?.method === "DELETE")).toBe(true); expect(kernel.isAlive()).toBe(false); }); it("throws when shared gateway kernel creation never succeeds", async () => { vi.spyOn(gatewayCoordinator, "acquireSharedGateway").mockResolvedValue({ url: "http://127.0.0.1:9999", isShared: true, }); globalThis.fetch = (async (input: string | URL, init?: RequestInit) => { 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", ); }); it("does not throw when shutdown API fails", async () => { vi.spyOn(gatewayCoordinator, "acquireSharedGateway").mockResolvedValue({ url: "http://127.0.0.1:9999", isShared: true, }); globalThis.fetch = (async (input: string | URL, init?: RequestInit) => { const url = String(input); env.fetchCalls.push({ url, init }); if (url.endsWith("/api/kernels") && init?.method === "POST") { return createResponse({ ok: true, json: { id: "kernel-456" } }) as unknown as Response; } if (init?.method === "DELETE") { throw new Error("delete failed"); } return createResponse({ ok: true }) as unknown as Response; }) as typeof fetch; const kernel = await PythonKernel.start({ cwd: tempDir.path() }); await expect(kernel.shutdown()).resolves.toBeUndefined(); }); });