@oh-my-pi/pi-coding-agent 5.6.77 → 5.7.68

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 (59) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +8 -8
  3. package/src/migrations.ts +1 -34
  4. package/src/utils/image-convert.ts +1 -1
  5. package/src/utils/image-resize.ts +2 -2
  6. package/src/vendor/photon/LICENSE.md +201 -0
  7. package/src/vendor/photon/README.md +158 -0
  8. package/src/vendor/photon/index.d.ts +3013 -0
  9. package/src/vendor/photon/index.js +4461 -0
  10. package/src/vendor/photon/photon_rs_bg.wasm +0 -0
  11. package/src/vendor/photon/photon_rs_bg.wasm.b64.js +1 -0
  12. package/src/vendor/photon/photon_rs_bg.wasm.d.ts +193 -0
  13. package/src/core/python-executor-display.test.ts +0 -42
  14. package/src/core/python-executor-lifecycle.test.ts +0 -99
  15. package/src/core/python-executor-mapping.test.ts +0 -41
  16. package/src/core/python-executor-per-call.test.ts +0 -49
  17. package/src/core/python-executor-session.test.ts +0 -103
  18. package/src/core/python-executor-streaming.test.ts +0 -77
  19. package/src/core/python-executor-timeout.test.ts +0 -35
  20. package/src/core/python-executor.lifecycle.test.ts +0 -139
  21. package/src/core/python-executor.result.test.ts +0 -49
  22. package/src/core/python-executor.test.ts +0 -180
  23. package/src/core/python-kernel-display.test.ts +0 -54
  24. package/src/core/python-kernel-env.test.ts +0 -138
  25. package/src/core/python-kernel-session.test.ts +0 -87
  26. package/src/core/python-kernel-ws.test.ts +0 -104
  27. package/src/core/python-kernel.lifecycle.test.ts +0 -249
  28. package/src/core/python-kernel.test.ts +0 -461
  29. package/src/core/python-modules.test.ts +0 -102
  30. package/src/core/python-prelude.test.ts +0 -140
  31. package/src/core/settings-manager-python.test.ts +0 -23
  32. package/src/core/streaming-output.test.ts +0 -26
  33. package/src/core/system-prompt.python.test.ts +0 -17
  34. package/src/core/tools/index.test.ts +0 -212
  35. package/src/core/tools/python-execution.test.ts +0 -68
  36. package/src/core/tools/python-fallback.test.ts +0 -72
  37. package/src/core/tools/python-renderer.test.ts +0 -36
  38. package/src/core/tools/python-tool-mode.test.ts +0 -43
  39. package/src/core/tools/python.test.ts +0 -121
  40. package/src/core/tools/schema-validation.test.ts +0 -530
  41. package/src/core/tools/web-scrapers/academic.test.ts +0 -239
  42. package/src/core/tools/web-scrapers/business.test.ts +0 -82
  43. package/src/core/tools/web-scrapers/dev-platforms.test.ts +0 -254
  44. package/src/core/tools/web-scrapers/documentation.test.ts +0 -85
  45. package/src/core/tools/web-scrapers/finance-media.test.ts +0 -144
  46. package/src/core/tools/web-scrapers/git-hosting.test.ts +0 -272
  47. package/src/core/tools/web-scrapers/media.test.ts +0 -138
  48. package/src/core/tools/web-scrapers/package-managers-2.test.ts +0 -199
  49. package/src/core/tools/web-scrapers/package-managers.test.ts +0 -171
  50. package/src/core/tools/web-scrapers/package-registries.test.ts +0 -259
  51. package/src/core/tools/web-scrapers/research.test.ts +0 -107
  52. package/src/core/tools/web-scrapers/security.test.ts +0 -103
  53. package/src/core/tools/web-scrapers/social-extended.test.ts +0 -192
  54. package/src/core/tools/web-scrapers/social.test.ts +0 -259
  55. package/src/core/tools/web-scrapers/stackexchange.test.ts +0 -120
  56. package/src/core/tools/web-scrapers/standards.test.ts +0 -122
  57. package/src/core/tools/web-scrapers/wikipedia.test.ts +0 -73
  58. package/src/core/tools/web-scrapers/youtube.test.ts +0 -198
  59. package/src/discovery/helpers.test.ts +0 -131
@@ -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.
@@ -1,102 +0,0 @@
1
- import { afterEach, describe, expect, it } from "bun:test";
2
- import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { basename, join, resolve } from "node:path";
5
- import { discoverPythonModules, loadPythonModules, type PythonModuleExecutor } from "./python-modules";
6
-
7
- const fixturesDir = resolve(__dirname, "../../test/fixtures/python-modules");
8
-
9
- const readFixture = (name: string): string => readFileSync(join(fixturesDir, name), "utf-8");
10
-
11
- const writeModule = (dir: string, name: string, tag: string) => {
12
- mkdirSync(dir, { recursive: true });
13
- const base = readFixture(name);
14
- writeFileSync(join(dir, name), `${base}\n# ${tag}`);
15
- };
16
-
17
- const createTempRoot = () => mkdtempSync(join(tmpdir(), "omp-python-modules-"));
18
-
19
- describe("python modules", () => {
20
- let tempRoot: string | null = null;
21
-
22
- afterEach(() => {
23
- if (tempRoot) {
24
- rmSync(tempRoot, { recursive: true, force: true });
25
- }
26
- tempRoot = null;
27
- });
28
-
29
- it("discovers modules with project override and sorted order", async () => {
30
- tempRoot = createTempRoot();
31
- const homeDir = join(tempRoot, "home");
32
- const cwd = join(tempRoot, "project");
33
-
34
- writeModule(join(homeDir, ".omp", "agent", "modules"), "alpha.py", "user-omp");
35
- writeModule(join(homeDir, ".pi", "agent", "modules"), "beta.py", "user-pi");
36
- writeModule(join(homeDir, ".pi", "agent", "modules"), "delta.py", "user-pi");
37
-
38
- writeModule(join(cwd, ".omp", "modules"), "alpha.py", "project-omp");
39
- writeModule(join(cwd, ".omp", "modules"), "beta.py", "project-omp");
40
- writeModule(join(cwd, ".pi", "modules"), "gamma.py", "project-pi");
41
-
42
- const modules = await discoverPythonModules({ cwd, homeDir });
43
- const names = modules.map((module) => basename(module.path));
44
- expect(names).toEqual(["alpha.py", "beta.py", "delta.py", "gamma.py"]);
45
- expect(modules.map((module) => ({ name: basename(module.path), source: module.source }))).toEqual([
46
- { name: "alpha.py", source: "project" },
47
- { name: "beta.py", source: "project" },
48
- { name: "delta.py", source: "user" },
49
- { name: "gamma.py", source: "project" },
50
- ]);
51
- expect(modules.find((module) => module.path.endsWith("alpha.py"))?.content).toContain("project-omp");
52
- expect(modules.find((module) => module.path.endsWith("delta.py"))?.content).toContain("user-pi");
53
- });
54
-
55
- it("loads modules in sorted order with silent execution", async () => {
56
- tempRoot = createTempRoot();
57
- const homeDir = join(tempRoot, "home");
58
- const cwd = join(tempRoot, "project");
59
-
60
- writeModule(join(homeDir, ".omp", "agent", "modules"), "beta.py", "user-omp");
61
- writeModule(join(homeDir, ".omp", "agent", "modules"), "alpha.py", "user-omp");
62
-
63
- const calls: Array<{ name: string; options?: { silent?: boolean; storeHistory?: boolean } }> = [];
64
- const executor: PythonModuleExecutor = {
65
- execute: async (code: string, options?: { silent?: boolean; storeHistory?: boolean }) => {
66
- const name = code.includes("def alpha") ? "alpha" : "beta";
67
- calls.push({ name, options });
68
- return { status: "ok", cancelled: false };
69
- },
70
- };
71
-
72
- await loadPythonModules(executor, { cwd, homeDir });
73
- expect(calls.map((call) => call.name)).toEqual(["alpha", "beta"]);
74
- for (const call of calls) {
75
- expect(call.options).toEqual({ silent: true, storeHistory: false });
76
- }
77
- });
78
-
79
- it("fails fast when a module fails to execute", async () => {
80
- tempRoot = createTempRoot();
81
- const homeDir = join(tempRoot, "home");
82
- const cwd = join(tempRoot, "project");
83
-
84
- writeModule(join(homeDir, ".omp", "agent", "modules"), "alpha.py", "user-omp");
85
- writeModule(join(cwd, ".omp", "modules"), "beta.py", "project-omp");
86
-
87
- const executor: PythonModuleExecutor = {
88
- execute: async (code: string) => {
89
- if (code.includes("def beta")) {
90
- return {
91
- status: "error",
92
- cancelled: false,
93
- error: { name: "Error", value: "boom", traceback: [] },
94
- };
95
- }
96
- return { status: "ok", cancelled: false };
97
- },
98
- };
99
-
100
- await expect(loadPythonModules(executor, { cwd, homeDir })).rejects.toThrow("Failed to load Python module");
101
- });
102
- });
@@ -1,140 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { existsSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { resetPreludeDocsCache, warmPythonEnvironment } from "./python-executor";
5
- import { createPythonTool, getPythonToolDescription } from "./tools/python";
6
-
7
- const resolvePythonPath = (): string | null => {
8
- const venvPath = process.env.VIRTUAL_ENV;
9
- const candidates = [venvPath, join(process.cwd(), ".venv"), join(process.cwd(), "venv")].filter(Boolean) as string[];
10
- for (const candidate of candidates) {
11
- const binDir = process.platform === "win32" ? "Scripts" : "bin";
12
- const exeName = process.platform === "win32" ? "python.exe" : "python";
13
- const pythonCandidate = join(candidate, binDir, exeName);
14
- if (existsSync(pythonCandidate)) {
15
- return pythonCandidate;
16
- }
17
- }
18
- return Bun.which("python") ?? Bun.which("python3");
19
- };
20
-
21
- const pythonPath = resolvePythonPath();
22
- const hasKernelDeps = (() => {
23
- if (!pythonPath) return false;
24
- const result = Bun.spawnSync(
25
- [
26
- pythonPath,
27
- "-c",
28
- "import importlib.util,sys;sys.exit(0 if importlib.util.find_spec('kernel_gateway') and importlib.util.find_spec('ipykernel') else 1)",
29
- ],
30
- { stdin: "ignore", stdout: "pipe", stderr: "pipe" },
31
- );
32
- return result.exitCode === 0;
33
- })();
34
-
35
- const shouldRun = Boolean(pythonPath) && hasKernelDeps;
36
-
37
- describe.skipIf(!shouldRun)("PYTHON_PRELUDE integration", () => {
38
- it("exposes prelude helpers via python tool", async () => {
39
- const helpers = [
40
- "pwd",
41
- "cd",
42
- "env",
43
- "read",
44
- "write",
45
- "append",
46
- "mkdir",
47
- "rm",
48
- "mv",
49
- "cp",
50
- "ls",
51
- "cat",
52
- "touch",
53
- "find",
54
- "grep",
55
- "rgrep",
56
- "head",
57
- "tail",
58
- "replace",
59
- "sed",
60
- "rsed",
61
- "wc",
62
- "sort_lines",
63
- "uniq",
64
- "cols",
65
- "tree",
66
- "stat",
67
- "diff",
68
- "glob_files",
69
- "batch",
70
- "lines",
71
- "delete_lines",
72
- "delete_matching",
73
- "insert_at",
74
- "git_status",
75
- "git_diff",
76
- "git_log",
77
- "git_show",
78
- "git_file_at",
79
- "git_branch",
80
- "git_has_changes",
81
- "run",
82
- "sh",
83
- ];
84
-
85
- const session = {
86
- cwd: process.cwd(),
87
- hasUI: false,
88
- getSessionFile: () => null,
89
- getSessionSpawns: () => null,
90
- settings: {
91
- getImageAutoResize: () => true,
92
- getLspFormatOnWrite: () => false,
93
- getLspDiagnosticsOnWrite: () => false,
94
- getLspDiagnosticsOnEdit: () => false,
95
- getEditFuzzyMatch: () => true,
96
- getGitToolEnabled: () => true,
97
- getBashInterceptorEnabled: () => true,
98
- getBashInterceptorSimpleLsEnabled: () => true,
99
- getBashInterceptorRules: () => [],
100
- getPythonToolMode: () => "ipy-only" as const,
101
- getPythonKernelMode: () => "per-call" as const,
102
- },
103
- };
104
-
105
- const tool = createPythonTool(session);
106
- const code = `
107
- helpers = ${JSON.stringify(helpers)}
108
- missing = [name for name in helpers if name not in globals() or not callable(globals()[name])]
109
- docs = __omp_prelude_docs__()
110
- doc_names = [d.get("name") for d in docs]
111
- doc_categories = [d.get("category") for d in docs]
112
- print("HELPERS_OK=" + ("1" if not missing else "0"))
113
- print("DOCS_OK=" + ("1" if "pwd" in doc_names and "Navigation" in doc_categories else "0"))
114
- if missing:
115
- print("MISSING=" + ",".join(missing))
116
- `;
117
-
118
- const result = await tool.execute("tool-call-1", { code });
119
- const output = result.content.find((item) => item.type === "text")?.text ?? "";
120
- expect(output).toContain("HELPERS_OK=1");
121
- expect(output).toContain("DOCS_OK=1");
122
- });
123
-
124
- it("exposes prelude docs via warmup", async () => {
125
- resetPreludeDocsCache();
126
- const result = await warmPythonEnvironment(process.cwd(), undefined, false);
127
- expect(result.ok).toBe(true);
128
- const names = result.docs.map((doc) => doc.name);
129
- expect(names).toContain("pwd");
130
- });
131
-
132
- it("renders prelude docs in python tool description", async () => {
133
- resetPreludeDocsCache();
134
- const result = await warmPythonEnvironment(process.cwd(), undefined, false);
135
- expect(result.ok).toBe(true);
136
- const description = getPythonToolDescription();
137
- expect(description).toContain("pwd");
138
- expect(description).not.toContain("Documentation unavailable");
139
- });
140
- });