@oh-my-pi/pi-coding-agent 5.7.67 → 5.7.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +6 -6
  3. package/package.json +8 -7
  4. package/src/migrations.ts +1 -34
  5. package/src/vendor/photon/index.js +4 -2
  6. package/src/vendor/photon/photon_rs_bg.wasm.b64.js +1 -0
  7. package/src/core/python-executor-display.test.ts +0 -42
  8. package/src/core/python-executor-lifecycle.test.ts +0 -99
  9. package/src/core/python-executor-mapping.test.ts +0 -41
  10. package/src/core/python-executor-per-call.test.ts +0 -49
  11. package/src/core/python-executor-session.test.ts +0 -103
  12. package/src/core/python-executor-streaming.test.ts +0 -77
  13. package/src/core/python-executor-timeout.test.ts +0 -35
  14. package/src/core/python-executor.lifecycle.test.ts +0 -139
  15. package/src/core/python-executor.result.test.ts +0 -49
  16. package/src/core/python-executor.test.ts +0 -180
  17. package/src/core/python-kernel-display.test.ts +0 -54
  18. package/src/core/python-kernel-env.test.ts +0 -138
  19. package/src/core/python-kernel-session.test.ts +0 -87
  20. package/src/core/python-kernel-ws.test.ts +0 -104
  21. package/src/core/python-kernel.lifecycle.test.ts +0 -249
  22. package/src/core/python-kernel.test.ts +0 -461
  23. package/src/core/python-modules.test.ts +0 -102
  24. package/src/core/python-prelude.test.ts +0 -140
  25. package/src/core/settings-manager-python.test.ts +0 -23
  26. package/src/core/streaming-output.test.ts +0 -26
  27. package/src/core/system-prompt.python.test.ts +0 -17
  28. package/src/core/tools/index.test.ts +0 -212
  29. package/src/core/tools/python-execution.test.ts +0 -68
  30. package/src/core/tools/python-fallback.test.ts +0 -72
  31. package/src/core/tools/python-renderer.test.ts +0 -36
  32. package/src/core/tools/python-tool-mode.test.ts +0 -43
  33. package/src/core/tools/python.test.ts +0 -121
  34. package/src/core/tools/schema-validation.test.ts +0 -530
  35. package/src/core/tools/web-scrapers/academic.test.ts +0 -239
  36. package/src/core/tools/web-scrapers/business.test.ts +0 -82
  37. package/src/core/tools/web-scrapers/dev-platforms.test.ts +0 -254
  38. package/src/core/tools/web-scrapers/documentation.test.ts +0 -85
  39. package/src/core/tools/web-scrapers/finance-media.test.ts +0 -144
  40. package/src/core/tools/web-scrapers/git-hosting.test.ts +0 -272
  41. package/src/core/tools/web-scrapers/media.test.ts +0 -138
  42. package/src/core/tools/web-scrapers/package-managers-2.test.ts +0 -199
  43. package/src/core/tools/web-scrapers/package-managers.test.ts +0 -171
  44. package/src/core/tools/web-scrapers/package-registries.test.ts +0 -259
  45. package/src/core/tools/web-scrapers/research.test.ts +0 -107
  46. package/src/core/tools/web-scrapers/security.test.ts +0 -103
  47. package/src/core/tools/web-scrapers/social-extended.test.ts +0 -192
  48. package/src/core/tools/web-scrapers/social.test.ts +0 -259
  49. package/src/core/tools/web-scrapers/stackexchange.test.ts +0 -120
  50. package/src/core/tools/web-scrapers/standards.test.ts +0 -122
  51. package/src/core/tools/web-scrapers/wikipedia.test.ts +0 -73
  52. package/src/core/tools/web-scrapers/youtube.test.ts +0 -198
  53. package/src/discovery/helpers.test.ts +0 -131
@@ -1,249 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
- import { mkdtempSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import type { Subprocess } from "bun";
6
- import { PythonKernel } from "./python-kernel";
7
-
8
- type SpawnOptions = Parameters<typeof Bun.spawn>[1];
9
-
10
- type FetchCall = { url: string; init?: RequestInit };
11
-
12
- type FetchResponse = {
13
- ok: boolean;
14
- status: number;
15
- json: () => Promise<unknown>;
16
- text: () => Promise<string>;
17
- };
18
-
19
- type MockEnvironment = {
20
- fetchCalls: FetchCall[];
21
- spawnCalls: { cmd: string[]; options: SpawnOptions }[];
22
- };
23
-
24
- type MessageEventPayload = { data: ArrayBuffer };
25
-
26
- type WebSocketHandler = (event: unknown) => void;
27
-
28
- type WebSocketMessageHandler = (event: MessageEventPayload) => void;
29
-
30
- class FakeWebSocket {
31
- static OPEN = 1;
32
- static CLOSED = 3;
33
- static instances: FakeWebSocket[] = [];
34
-
35
- readyState = FakeWebSocket.OPEN;
36
- binaryType = "arraybuffer";
37
- url: string;
38
- sent: ArrayBuffer[] = [];
39
-
40
- onopen: WebSocketHandler | null = null;
41
- onerror: WebSocketHandler | null = null;
42
- onclose: WebSocketHandler | null = null;
43
- onmessage: WebSocketMessageHandler | null = null;
44
-
45
- constructor(url: string) {
46
- this.url = url;
47
- FakeWebSocket.instances.push(this);
48
- queueMicrotask(() => {
49
- this.onopen?.(undefined);
50
- });
51
- }
52
-
53
- send(data: ArrayBuffer): void {
54
- this.sent.push(data);
55
- }
56
-
57
- close(): void {
58
- this.readyState = FakeWebSocket.CLOSED;
59
- this.onclose?.(undefined);
60
- }
61
- }
62
-
63
- const createResponse = (options: { ok: boolean; status?: number; json?: unknown; text?: string }): FetchResponse => {
64
- return {
65
- ok: options.ok,
66
- status: options.status ?? (options.ok ? 200 : 500),
67
- json: async () => options.json ?? {},
68
- text: async () => options.text ?? "",
69
- };
70
- };
71
-
72
- const createTempDir = () => mkdtempSync(join(tmpdir(), "omp-python-kernel-"));
73
-
74
- const createFakeProcess = (): Subprocess => {
75
- const exited = new Promise<number>(() => undefined);
76
- return { pid: 999999, exited } as Subprocess;
77
- };
78
-
79
- describe("PythonKernel gateway lifecycle", () => {
80
- const originalFetch = globalThis.fetch;
81
- const originalWebSocket = globalThis.WebSocket;
82
- const originalSpawn = Bun.spawn;
83
- const originalSleep = Bun.sleep;
84
- const originalWhich = Bun.which;
85
- const originalExecute = PythonKernel.prototype.execute;
86
- const originalGatewayUrl = process.env.OMP_PYTHON_GATEWAY_URL;
87
- const originalGatewayToken = process.env.OMP_PYTHON_GATEWAY_TOKEN;
88
- const originalBunEnv = process.env.BUN_ENV;
89
-
90
- let tempDir: string;
91
- let env: MockEnvironment;
92
-
93
- beforeEach(() => {
94
- tempDir = createTempDir();
95
- env = { fetchCalls: [], spawnCalls: [] };
96
-
97
- process.env.BUN_ENV = "test";
98
- delete process.env.OMP_PYTHON_GATEWAY_URL;
99
- delete process.env.OMP_PYTHON_GATEWAY_TOKEN;
100
-
101
- FakeWebSocket.instances = [];
102
- globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket;
103
-
104
- Bun.spawn = ((cmd: string[] | string, options?: SpawnOptions) => {
105
- const normalized = Array.isArray(cmd) ? cmd : [cmd];
106
- env.spawnCalls.push({ cmd: normalized, options: options ?? {} });
107
- return createFakeProcess();
108
- }) as typeof Bun.spawn;
109
-
110
- Bun.sleep = (async () => undefined) as typeof Bun.sleep;
111
-
112
- Bun.which = (() => "/usr/bin/python") as typeof Bun.which;
113
-
114
- Object.defineProperty(PythonKernel.prototype, "execute", {
115
- value: (async () => ({
116
- status: "ok",
117
- cancelled: false,
118
- timedOut: false,
119
- stdinRequested: false,
120
- })) as typeof PythonKernel.prototype.execute,
121
- configurable: true,
122
- });
123
- });
124
-
125
- afterEach(() => {
126
- if (tempDir) {
127
- rmSync(tempDir, { recursive: true, force: true });
128
- }
129
-
130
- if (originalBunEnv === undefined) {
131
- delete process.env.BUN_ENV;
132
- } else {
133
- process.env.BUN_ENV = originalBunEnv;
134
- }
135
- if (originalGatewayUrl === undefined) {
136
- delete process.env.OMP_PYTHON_GATEWAY_URL;
137
- } else {
138
- process.env.OMP_PYTHON_GATEWAY_URL = originalGatewayUrl;
139
- }
140
- if (originalGatewayToken === undefined) {
141
- delete process.env.OMP_PYTHON_GATEWAY_TOKEN;
142
- } else {
143
- process.env.OMP_PYTHON_GATEWAY_TOKEN = originalGatewayToken;
144
- }
145
-
146
- globalThis.fetch = originalFetch;
147
- globalThis.WebSocket = originalWebSocket;
148
-
149
- Bun.spawn = originalSpawn;
150
- Bun.sleep = originalSleep;
151
- Bun.which = originalWhich;
152
- Object.defineProperty(PythonKernel.prototype, "execute", { value: originalExecute, configurable: true });
153
- });
154
-
155
- it("starts local gateway, polls readiness, interrupts, and shuts down", async () => {
156
- let kernelspecAttempts = 0;
157
- globalThis.fetch = (async (input: string | URL, init?: RequestInit) => {
158
- const url = String(input);
159
- env.fetchCalls.push({ url, init });
160
-
161
- if (url.endsWith("/api/kernelspecs")) {
162
- kernelspecAttempts += 1;
163
- const ok = kernelspecAttempts >= 2;
164
- return createResponse({ ok }) as unknown as Response;
165
- }
166
-
167
- if (url.endsWith("/api/kernels") && init?.method === "POST") {
168
- return createResponse({ ok: true, json: { id: "kernel-123" } }) as unknown as Response;
169
- }
170
-
171
- return createResponse({ ok: true }) as unknown as Response;
172
- }) as typeof fetch;
173
-
174
- const kernel = await PythonKernel.start({ cwd: tempDir, useSharedGateway: false });
175
-
176
- expect(env.spawnCalls).toHaveLength(1);
177
- expect(env.spawnCalls[0].cmd).toEqual(
178
- expect.arrayContaining([
179
- "-m",
180
- "kernel_gateway",
181
- "--KernelGatewayApp.allow_origin=*",
182
- "--JupyterApp.answer_yes=true",
183
- ]),
184
- );
185
- expect(env.fetchCalls.filter((call) => call.url.endsWith("/api/kernelspecs"))).toHaveLength(2);
186
- expect(env.fetchCalls.some((call) => call.url.endsWith("/api/kernels") && call.init?.method === "POST")).toBe(
187
- true,
188
- );
189
-
190
- await kernel.interrupt();
191
- expect(env.fetchCalls.some((call) => call.url.includes("/interrupt") && call.init?.method === "POST")).toBe(true);
192
- expect(FakeWebSocket.instances[0]?.sent.length).toBe(1);
193
-
194
- await kernel.shutdown();
195
- expect(env.fetchCalls.some((call) => call.init?.method === "DELETE")).toBe(true);
196
- expect(kernel.isAlive()).toBe(false);
197
- });
198
-
199
- it("throws when gateway readiness never succeeds", async () => {
200
- const originalNow = Date.now;
201
- let now = 0;
202
- Date.now = () => {
203
- now += 1000;
204
- return now;
205
- };
206
-
207
- try {
208
- globalThis.fetch = (async (input: string | URL, init?: RequestInit) => {
209
- const url = String(input);
210
- env.fetchCalls.push({ url, init });
211
- if (url.endsWith("/api/kernelspecs")) {
212
- return createResponse({ ok: false, status: 503 }) as unknown as Response;
213
- }
214
- return createResponse({ ok: true }) as unknown as Response;
215
- }) as typeof fetch;
216
-
217
- await expect(PythonKernel.start({ cwd: tempDir, useSharedGateway: false })).rejects.toThrow(
218
- "Kernel gateway failed to start",
219
- );
220
- expect(env.spawnCalls).toHaveLength(3);
221
- } finally {
222
- Date.now = originalNow;
223
- }
224
- });
225
-
226
- it("does not throw when shutdown API fails", async () => {
227
- let kernelspecAttempts = 0;
228
- globalThis.fetch = (async (input: string | URL, init?: RequestInit) => {
229
- const url = String(input);
230
- env.fetchCalls.push({ url, init });
231
- if (url.endsWith("/api/kernelspecs")) {
232
- kernelspecAttempts += 1;
233
- const ok = kernelspecAttempts >= 1;
234
- return createResponse({ ok }) as unknown as Response;
235
- }
236
- if (url.endsWith("/api/kernels") && init?.method === "POST") {
237
- return createResponse({ ok: true, json: { id: "kernel-456" } }) as unknown as Response;
238
- }
239
- if (init?.method === "DELETE") {
240
- throw new Error("delete failed");
241
- }
242
- return createResponse({ ok: true }) as unknown as Response;
243
- }) as typeof fetch;
244
-
245
- const kernel = await PythonKernel.start({ cwd: tempDir });
246
-
247
- await expect(kernel.shutdown()).resolves.toBeUndefined();
248
- });
249
- });
@@ -1,461 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
2
- import { type KernelDisplayOutput, PythonKernel } from "./python-kernel";
3
- import { PYTHON_PRELUDE } from "./python-prelude";
4
-
5
- type JupyterMessage = {
6
- channel: string;
7
- header: {
8
- msg_id: string;
9
- session: string;
10
- username: string;
11
- date: string;
12
- msg_type: string;
13
- version: string;
14
- };
15
- parent_header: Record<string, unknown>;
16
- metadata: Record<string, unknown>;
17
- content: Record<string, unknown>;
18
- buffers?: Uint8Array[];
19
- };
20
-
21
- const textEncoder = new TextEncoder();
22
- const textDecoder = new TextDecoder();
23
-
24
- function encodeMessage(msg: JupyterMessage): ArrayBuffer {
25
- const msgText = JSON.stringify({
26
- channel: msg.channel,
27
- header: msg.header,
28
- parent_header: msg.parent_header,
29
- metadata: msg.metadata,
30
- content: msg.content,
31
- });
32
- const msgBytes = textEncoder.encode(msgText);
33
- const buffers = msg.buffers ?? [];
34
- const offsetCount = 1 + buffers.length;
35
- const headerSize = 4 + offsetCount * 4;
36
- let totalSize = headerSize + msgBytes.length;
37
- for (const buffer of buffers) {
38
- totalSize += buffer.length;
39
- }
40
- const result = new ArrayBuffer(totalSize);
41
- const view = new DataView(result);
42
- const bytes = new Uint8Array(result);
43
- view.setUint32(0, offsetCount, true);
44
- let offset = headerSize;
45
- view.setUint32(4, offset, true);
46
- bytes.set(msgBytes, offset);
47
- offset += msgBytes.length;
48
- buffers.forEach((buffer, index) => {
49
- view.setUint32(4 + (index + 1) * 4, offset, true);
50
- bytes.set(buffer, offset);
51
- offset += buffer.length;
52
- });
53
- return result;
54
- }
55
-
56
- function decodeMessage(data: ArrayBuffer): JupyterMessage {
57
- const view = new DataView(data);
58
- const offsetCount = view.getUint32(0, true);
59
- const offsets: number[] = [];
60
- for (let i = 0; i < offsetCount; i++) {
61
- offsets.push(view.getUint32(4 + i * 4, true));
62
- }
63
- const msgStart = offsets[0];
64
- const msgEnd = offsets.length > 1 ? offsets[1] : data.byteLength;
65
- const msgBytes = new Uint8Array(data, msgStart, msgEnd - msgStart);
66
- const msgText = textDecoder.decode(msgBytes);
67
- return JSON.parse(msgText) as JupyterMessage;
68
- }
69
-
70
- function sendOkExecution(ws: FakeWebSocket, msgId: string, executionCount = 1) {
71
- const reply: JupyterMessage = {
72
- channel: "shell",
73
- header: {
74
- msg_id: `reply-${msgId}`,
75
- session: "session",
76
- username: "omp",
77
- date: new Date().toISOString(),
78
- msg_type: "execute_reply",
79
- version: "5.5",
80
- },
81
- parent_header: { msg_id: msgId },
82
- metadata: {},
83
- content: { status: "ok", execution_count: executionCount },
84
- };
85
- const status: JupyterMessage = {
86
- channel: "iopub",
87
- header: {
88
- msg_id: `status-${msgId}`,
89
- session: "session",
90
- username: "omp",
91
- date: new Date().toISOString(),
92
- msg_type: "status",
93
- version: "5.5",
94
- },
95
- parent_header: { msg_id: msgId },
96
- metadata: {},
97
- content: { execution_state: "idle" },
98
- };
99
- ws.onmessage?.({ data: encodeMessage(reply) });
100
- ws.onmessage?.({ data: encodeMessage(status) });
101
- }
102
-
103
- class FakeWebSocket {
104
- static OPEN = 1;
105
- static CLOSED = 3;
106
- static lastInstance: FakeWebSocket | null = null;
107
- readyState = FakeWebSocket.OPEN;
108
- binaryType = "arraybuffer";
109
- onopen?: () => void;
110
- onmessage?: (event: { data: ArrayBuffer }) => void;
111
- onerror?: (event: unknown) => void;
112
- onclose?: () => void;
113
- readonly url: string;
114
- readonly sent: (ArrayBuffer | string)[] = [];
115
- private handleSend: ((data: ArrayBuffer | string) => void) | null = null;
116
- private pendingMessages: (ArrayBuffer | string)[] = [];
117
-
118
- constructor(url: string) {
119
- this.url = url;
120
- FakeWebSocket.lastInstance = this;
121
- queueMicrotask(() => this.onopen?.());
122
- }
123
-
124
- setSendHandler(handler: (data: ArrayBuffer | string) => void) {
125
- this.handleSend = handler;
126
- for (const msg of this.pendingMessages) {
127
- handler(msg);
128
- }
129
- this.pendingMessages = [];
130
- }
131
-
132
- send(data: ArrayBuffer | string) {
133
- this.sent.push(data);
134
- if (this.handleSend) {
135
- this.handleSend(data);
136
- } else {
137
- this.pendingMessages.push(data);
138
- }
139
- }
140
-
141
- close() {
142
- this.readyState = FakeWebSocket.CLOSED;
143
- this.onclose?.();
144
- }
145
- }
146
-
147
- describe("PythonKernel (external gateway)", () => {
148
- const originalEnv = { ...process.env };
149
- const originalFetch = globalThis.fetch;
150
- const originalWebSocket = globalThis.WebSocket;
151
-
152
- beforeEach(() => {
153
- process.env.OMP_PYTHON_GATEWAY_URL = "http://gateway.test";
154
- process.env.OMP_PYTHON_SKIP_CHECK = "1";
155
- globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket;
156
- });
157
-
158
- afterEach(() => {
159
- for (const key of Object.keys(process.env)) {
160
- if (!(key in originalEnv)) {
161
- delete process.env[key];
162
- }
163
- }
164
- for (const [key, value] of Object.entries(originalEnv)) {
165
- process.env[key] = value;
166
- }
167
- globalThis.fetch = originalFetch;
168
- globalThis.WebSocket = originalWebSocket;
169
- FakeWebSocket.lastInstance = null;
170
- vi.restoreAllMocks();
171
- });
172
-
173
- it("executes code via websocket stream and display data", async () => {
174
- const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
175
- if (url.endsWith("/api/kernels") && init?.method === "POST") {
176
- return new Response(JSON.stringify({ id: "kernel-1" }), { status: 201 });
177
- }
178
- if (url.includes("/api/kernels/") && init?.method === "DELETE") {
179
- return new Response("", { status: 204 });
180
- }
181
- return new Response("", { status: 200 });
182
- });
183
- globalThis.fetch = fetchMock as unknown as typeof fetch;
184
-
185
- let preludeSeen = false;
186
-
187
- const kernelPromise = PythonKernel.start({ cwd: "/" });
188
- await Bun.sleep(10);
189
- const ws = FakeWebSocket.lastInstance;
190
- if (!ws) throw new Error("WebSocket not initialized");
191
- ws.setSendHandler((data) => {
192
- const msg = typeof data === "string" ? (JSON.parse(data) as JupyterMessage) : decodeMessage(data);
193
- const code = String(msg.content.code ?? "");
194
- if (!preludeSeen) {
195
- expect(code).toBe(PYTHON_PRELUDE);
196
- preludeSeen = true;
197
- sendOkExecution(ws, msg.header.msg_id);
198
- return;
199
- }
200
-
201
- if (code === "print('hello')") {
202
- const stream: JupyterMessage = {
203
- channel: "iopub",
204
- header: {
205
- msg_id: "stream-1",
206
- session: "session",
207
- username: "omp",
208
- date: new Date().toISOString(),
209
- msg_type: "stream",
210
- version: "5.5",
211
- },
212
- parent_header: { msg_id: msg.header.msg_id },
213
- metadata: {},
214
- content: { text: "hello\n" },
215
- };
216
- const display: JupyterMessage = {
217
- channel: "iopub",
218
- header: {
219
- msg_id: "display-1",
220
- session: "session",
221
- username: "omp",
222
- date: new Date().toISOString(),
223
- msg_type: "execute_result",
224
- version: "5.5",
225
- },
226
- parent_header: { msg_id: msg.header.msg_id },
227
- metadata: {},
228
- content: {
229
- data: {
230
- "text/plain": "result",
231
- "application/json": { answer: 42 },
232
- },
233
- },
234
- };
235
- const reply: JupyterMessage = {
236
- channel: "shell",
237
- header: {
238
- msg_id: "reply-2",
239
- session: "session",
240
- username: "omp",
241
- date: new Date().toISOString(),
242
- msg_type: "execute_reply",
243
- version: "5.5",
244
- },
245
- parent_header: { msg_id: msg.header.msg_id },
246
- metadata: {},
247
- content: { status: "ok", execution_count: 2 },
248
- };
249
- const status: JupyterMessage = {
250
- channel: "iopub",
251
- header: {
252
- msg_id: "status-2",
253
- session: "session",
254
- username: "omp",
255
- date: new Date().toISOString(),
256
- msg_type: "status",
257
- version: "5.5",
258
- },
259
- parent_header: { msg_id: msg.header.msg_id },
260
- metadata: {},
261
- content: { execution_state: "idle" },
262
- };
263
- ws.onmessage?.({ data: encodeMessage(stream) });
264
- ws.onmessage?.({ data: encodeMessage(display) });
265
- ws.onmessage?.({ data: encodeMessage(reply) });
266
- ws.onmessage?.({ data: encodeMessage(status) });
267
- return;
268
- }
269
-
270
- sendOkExecution(ws, msg.header.msg_id);
271
- });
272
-
273
- const kernel = await kernelPromise;
274
- const chunks: string[] = [];
275
- const displays: KernelDisplayOutput[] = [];
276
-
277
- const result = await kernel.execute("print('hello')", {
278
- onChunk: (text) => {
279
- chunks.push(text);
280
- },
281
- onDisplay: (output) => {
282
- displays.push(output);
283
- },
284
- });
285
-
286
- expect(result.status).toBe("ok");
287
- expect(chunks.join("")).toContain("hello");
288
- expect(chunks.join("")).toContain("result");
289
- expect(displays).toEqual([{ type: "json", data: { answer: 42 } }]);
290
-
291
- await kernel.shutdown();
292
- expect(fetchMock).toHaveBeenCalledWith("http://gateway.test/api/kernels/kernel-1", {
293
- method: "DELETE",
294
- headers: {},
295
- });
296
- });
297
-
298
- it("marks kernel dead after repeated ping failures", async () => {
299
- const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
300
- if (url.endsWith("/api/kernels") && init?.method === "POST") {
301
- return new Response(JSON.stringify({ id: "kernel-2" }), { status: 201 });
302
- }
303
- if (url.includes("/api/kernels/kernel-2") && !init?.method) {
304
- throw new Error("ping failed");
305
- }
306
- return new Response("", { status: 200 });
307
- });
308
- globalThis.fetch = fetchMock as unknown as typeof fetch;
309
-
310
- let preludeSeen = false;
311
-
312
- const kernelPromise = PythonKernel.start({ cwd: "/" });
313
- await Bun.sleep(10);
314
- const ws = FakeWebSocket.lastInstance;
315
- if (!ws) throw new Error("WebSocket not initialized");
316
- ws.setSendHandler((data) => {
317
- const msg = typeof data === "string" ? (JSON.parse(data) as JupyterMessage) : decodeMessage(data);
318
- const code = String(msg.content.code ?? "");
319
- if (!preludeSeen) {
320
- expect(code).toBe(PYTHON_PRELUDE);
321
- preludeSeen = true;
322
- }
323
- sendOkExecution(ws, msg.header.msg_id);
324
- });
325
-
326
- const kernel = await kernelPromise;
327
- const firstPing = await kernel.ping(1);
328
- const secondPing = await kernel.ping(1);
329
-
330
- expect(firstPing).toBe(false);
331
- expect(secondPing).toBe(false);
332
- expect(kernel.isAlive()).toBe(false);
333
-
334
- await kernel.shutdown();
335
- });
336
-
337
- it("initializes the IPython prelude", async () => {
338
- const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
339
- if (url.endsWith("/api/kernels") && init?.method === "POST") {
340
- return new Response(JSON.stringify({ id: "kernel-3" }), { status: 201 });
341
- }
342
- return new Response("", { status: 200 });
343
- });
344
- globalThis.fetch = fetchMock as unknown as typeof fetch;
345
-
346
- let preludeSeen = false;
347
-
348
- const kernelPromise = PythonKernel.start({ cwd: "/" });
349
- await Bun.sleep(10);
350
- const ws = FakeWebSocket.lastInstance;
351
- if (!ws) throw new Error("WebSocket not initialized");
352
- ws.setSendHandler((data) => {
353
- const msg = typeof data === "string" ? (JSON.parse(data) as JupyterMessage) : decodeMessage(data);
354
- const code = String(msg.content.code ?? "");
355
- if (!preludeSeen) {
356
- expect(code).toBe(PYTHON_PRELUDE);
357
- preludeSeen = true;
358
- }
359
- sendOkExecution(ws, msg.header.msg_id);
360
- });
361
-
362
- const kernel = await kernelPromise;
363
- expect(kernel.isAlive()).toBe(true);
364
- await kernel.shutdown();
365
- });
366
-
367
- it("introspects prelude helpers", async () => {
368
- const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
369
- if (url.endsWith("/api/kernels") && init?.method === "POST") {
370
- return new Response(JSON.stringify({ id: "kernel-4" }), { status: 201 });
371
- }
372
- return new Response("", { status: 200 });
373
- });
374
- globalThis.fetch = fetchMock as unknown as typeof fetch;
375
-
376
- const docs = [
377
- {
378
- name: "read",
379
- signature: "(path, limit=None)",
380
- docstring: "Read file contents.",
381
- category: "File I/O",
382
- },
383
- ];
384
- const payload = JSON.stringify(docs);
385
-
386
- let preludeSeen = false;
387
-
388
- const kernelPromise = PythonKernel.start({ cwd: "/" });
389
- await Bun.sleep(10);
390
- const ws = FakeWebSocket.lastInstance;
391
- if (!ws) throw new Error("WebSocket not initialized");
392
- ws.setSendHandler((data) => {
393
- const msg = typeof data === "string" ? (JSON.parse(data) as JupyterMessage) : decodeMessage(data);
394
- const code = String(msg.content.code ?? "");
395
- if (!preludeSeen) {
396
- expect(code).toBe(PYTHON_PRELUDE);
397
- preludeSeen = true;
398
- sendOkExecution(ws, msg.header.msg_id);
399
- return;
400
- }
401
-
402
- if (code.includes("__omp_prelude_docs__")) {
403
- const stream: JupyterMessage = {
404
- channel: "iopub",
405
- header: {
406
- msg_id: "stream-docs",
407
- session: "session",
408
- username: "omp",
409
- date: new Date().toISOString(),
410
- msg_type: "stream",
411
- version: "5.5",
412
- },
413
- parent_header: { msg_id: msg.header.msg_id },
414
- metadata: {},
415
- content: { text: `${payload}\n` },
416
- };
417
- const reply: JupyterMessage = {
418
- channel: "shell",
419
- header: {
420
- msg_id: "reply-docs",
421
- session: "session",
422
- username: "omp",
423
- date: new Date().toISOString(),
424
- msg_type: "execute_reply",
425
- version: "5.5",
426
- },
427
- parent_header: { msg_id: msg.header.msg_id },
428
- metadata: {},
429
- content: { status: "ok", execution_count: 2 },
430
- };
431
- const status: JupyterMessage = {
432
- channel: "iopub",
433
- header: {
434
- msg_id: "status-docs",
435
- session: "session",
436
- username: "omp",
437
- date: new Date().toISOString(),
438
- msg_type: "status",
439
- version: "5.5",
440
- },
441
- parent_header: { msg_id: msg.header.msg_id },
442
- metadata: {},
443
- content: { execution_state: "idle" },
444
- };
445
- ws.onmessage?.({ data: encodeMessage(stream) });
446
- ws.onmessage?.({ data: encodeMessage(reply) });
447
- ws.onmessage?.({ data: encodeMessage(status) });
448
- return;
449
- }
450
-
451
- sendOkExecution(ws, msg.header.msg_id);
452
- });
453
-
454
- const kernel = await kernelPromise;
455
- const result = await kernel.introspectPrelude();
456
- expect(result).toEqual(docs);
457
- await kernel.shutdown();
458
- });
459
- });
460
-
461
- // TODO: add coverage for gateway process exit handling once PythonKernel exposes a test hook.