@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.
- package/CHANGELOG.md +9 -0
- package/README.md +6 -6
- package/package.json +8 -7
- package/src/migrations.ts +1 -34
- package/src/vendor/photon/index.js +4 -2
- package/src/vendor/photon/photon_rs_bg.wasm.b64.js +1 -0
- package/src/core/python-executor-display.test.ts +0 -42
- package/src/core/python-executor-lifecycle.test.ts +0 -99
- package/src/core/python-executor-mapping.test.ts +0 -41
- package/src/core/python-executor-per-call.test.ts +0 -49
- package/src/core/python-executor-session.test.ts +0 -103
- package/src/core/python-executor-streaming.test.ts +0 -77
- package/src/core/python-executor-timeout.test.ts +0 -35
- package/src/core/python-executor.lifecycle.test.ts +0 -139
- package/src/core/python-executor.result.test.ts +0 -49
- package/src/core/python-executor.test.ts +0 -180
- package/src/core/python-kernel-display.test.ts +0 -54
- package/src/core/python-kernel-env.test.ts +0 -138
- package/src/core/python-kernel-session.test.ts +0 -87
- package/src/core/python-kernel-ws.test.ts +0 -104
- package/src/core/python-kernel.lifecycle.test.ts +0 -249
- package/src/core/python-kernel.test.ts +0 -461
- package/src/core/python-modules.test.ts +0 -102
- package/src/core/python-prelude.test.ts +0 -140
- package/src/core/settings-manager-python.test.ts +0 -23
- package/src/core/streaming-output.test.ts +0 -26
- package/src/core/system-prompt.python.test.ts +0 -17
- package/src/core/tools/index.test.ts +0 -212
- package/src/core/tools/python-execution.test.ts +0 -68
- package/src/core/tools/python-fallback.test.ts +0 -72
- package/src/core/tools/python-renderer.test.ts +0 -36
- package/src/core/tools/python-tool-mode.test.ts +0 -43
- package/src/core/tools/python.test.ts +0 -121
- package/src/core/tools/schema-validation.test.ts +0 -530
- package/src/core/tools/web-scrapers/academic.test.ts +0 -239
- package/src/core/tools/web-scrapers/business.test.ts +0 -82
- package/src/core/tools/web-scrapers/dev-platforms.test.ts +0 -254
- package/src/core/tools/web-scrapers/documentation.test.ts +0 -85
- package/src/core/tools/web-scrapers/finance-media.test.ts +0 -144
- package/src/core/tools/web-scrapers/git-hosting.test.ts +0 -272
- package/src/core/tools/web-scrapers/media.test.ts +0 -138
- package/src/core/tools/web-scrapers/package-managers-2.test.ts +0 -199
- package/src/core/tools/web-scrapers/package-managers.test.ts +0 -171
- package/src/core/tools/web-scrapers/package-registries.test.ts +0 -259
- package/src/core/tools/web-scrapers/research.test.ts +0 -107
- package/src/core/tools/web-scrapers/security.test.ts +0 -103
- package/src/core/tools/web-scrapers/social-extended.test.ts +0 -192
- package/src/core/tools/web-scrapers/social.test.ts +0 -259
- package/src/core/tools/web-scrapers/stackexchange.test.ts +0 -120
- package/src/core/tools/web-scrapers/standards.test.ts +0 -122
- package/src/core/tools/web-scrapers/wikipedia.test.ts +0 -73
- package/src/core/tools/web-scrapers/youtube.test.ts +0 -198
- 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.
|