@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,139 +0,0 @@
1
- import { afterEach, describe, expect, it } from "bun:test";
2
- import { disposeAllKernelSessions, executePython } from "./python-executor";
3
- import { type KernelExecuteOptions, type KernelExecuteResult, PythonKernel } from "./python-kernel";
4
-
5
- process.env.OMP_PYTHON_SKIP_CHECK = "1";
6
-
7
- class FakeKernel {
8
- private result: KernelExecuteResult;
9
- private onExecute?: (options?: KernelExecuteOptions) => void;
10
- private alive: boolean;
11
- readonly executeCalls: string[] = [];
12
- shutdownCalls = 0;
13
-
14
- constructor(
15
- result: KernelExecuteResult,
16
- options: { alive?: boolean; onExecute?: (options?: KernelExecuteOptions) => void } = {},
17
- ) {
18
- this.result = result;
19
- this.onExecute = options.onExecute;
20
- this.alive = options.alive ?? true;
21
- }
22
-
23
- isAlive(): boolean {
24
- return this.alive;
25
- }
26
-
27
- async execute(code: string, options?: KernelExecuteOptions): Promise<KernelExecuteResult> {
28
- this.executeCalls.push(code);
29
- this.onExecute?.(options);
30
- return this.result;
31
- }
32
-
33
- async shutdown(): Promise<void> {
34
- this.shutdownCalls += 1;
35
- this.alive = false;
36
- }
37
-
38
- async ping(): Promise<boolean> {
39
- return this.alive;
40
- }
41
- }
42
-
43
- const okResult: KernelExecuteResult = {
44
- status: "ok",
45
- cancelled: false,
46
- timedOut: false,
47
- stdinRequested: false,
48
- };
49
-
50
- describe("executePython session lifecycle", () => {
51
- const originalStart = PythonKernel.start;
52
-
53
- afterEach(async () => {
54
- PythonKernel.start = originalStart;
55
- await disposeAllKernelSessions();
56
- });
57
-
58
- it("reuses a session kernel across calls", async () => {
59
- let startCount = 0;
60
- const kernel = new FakeKernel(okResult, { onExecute: (options) => options?.onChunk?.("ok\n") });
61
- PythonKernel.start = async () => {
62
- startCount += 1;
63
- return kernel as unknown as PythonKernel;
64
- };
65
-
66
- const first = await executePython("print('one')", { sessionId: "session-1" });
67
- const second = await executePython("print('two')", { sessionId: "session-1" });
68
-
69
- expect(startCount).toBe(1);
70
- expect(kernel.executeCalls).toEqual(["print('one')", "print('two')"]);
71
- expect(first.output).toContain("ok");
72
- expect(second.output).toContain("ok");
73
- });
74
-
75
- it("restarts the session kernel when not alive", async () => {
76
- const deadKernel = new FakeKernel(okResult, { alive: false });
77
- const liveKernel = new FakeKernel(okResult, { onExecute: (options) => options?.onChunk?.("live\n") });
78
- const kernels = [deadKernel, liveKernel];
79
- let startCount = 0;
80
-
81
- PythonKernel.start = async () => {
82
- startCount += 1;
83
- return kernels.shift() as unknown as PythonKernel;
84
- };
85
-
86
- const result = await executePython("print('restart')", { sessionId: "session-restart" });
87
-
88
- expect(startCount).toBe(2);
89
- expect(deadKernel.shutdownCalls).toBe(1);
90
- expect(deadKernel.executeCalls).toEqual([]);
91
- expect(liveKernel.executeCalls).toEqual(["print('restart')"]);
92
- expect(result.output).toContain("live");
93
- });
94
-
95
- it("resets the session kernel when requested", async () => {
96
- const firstKernel = new FakeKernel(okResult);
97
- const secondKernel = new FakeKernel(okResult);
98
- const kernels = [firstKernel, secondKernel];
99
- let startCount = 0;
100
-
101
- PythonKernel.start = async () => {
102
- startCount += 1;
103
- return kernels.shift() as unknown as PythonKernel;
104
- };
105
-
106
- await executePython("print('one')", { sessionId: "session-reset" });
107
- await executePython("print('two')", { sessionId: "session-reset", reset: true });
108
-
109
- expect(startCount).toBe(2);
110
- expect(firstKernel.shutdownCalls).toBe(1);
111
- expect(secondKernel.executeCalls).toEqual(["print('two')"]);
112
- });
113
-
114
- it("uses per-call kernels when configured", async () => {
115
- const kernelA = new FakeKernel(okResult);
116
- const kernelB = new FakeKernel(okResult);
117
- const kernels = [kernelA, kernelB];
118
- let startCount = 0;
119
- let shutdownCount = 0;
120
-
121
- PythonKernel.start = async () => {
122
- startCount += 1;
123
- return kernels.shift() as unknown as PythonKernel;
124
- };
125
-
126
- kernelA.shutdown = async () => {
127
- shutdownCount += 1;
128
- };
129
- kernelB.shutdown = async () => {
130
- shutdownCount += 1;
131
- };
132
-
133
- await executePython("print('one')", { kernelMode: "per-call" });
134
- await executePython("print('two')", { kernelMode: "per-call" });
135
-
136
- expect(startCount).toBe(2);
137
- expect(shutdownCount).toBe(2);
138
- });
139
- });
@@ -1,49 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { executePythonWithKernel, type PythonKernelExecutor } from "./python-executor";
3
- import type { KernelExecuteOptions, KernelExecuteResult } from "./python-kernel";
4
-
5
- class FakeKernel implements PythonKernelExecutor {
6
- private result: KernelExecuteResult;
7
- private onExecute?: (options?: KernelExecuteOptions) => void;
8
-
9
- constructor(result: KernelExecuteResult, onExecute?: (options?: KernelExecuteOptions) => void) {
10
- this.result = result;
11
- this.onExecute = onExecute;
12
- }
13
-
14
- async execute(_code: string, options?: KernelExecuteOptions): Promise<KernelExecuteResult> {
15
- this.onExecute?.(options);
16
- return this.result;
17
- }
18
- }
19
-
20
- describe("executePythonWithKernel result mapping", () => {
21
- it("adds timeout annotation when cancelled", async () => {
22
- const kernel = new FakeKernel({
23
- status: "ok",
24
- cancelled: true,
25
- timedOut: true,
26
- stdinRequested: false,
27
- });
28
-
29
- const result = await executePythonWithKernel(kernel, "sleep()", { timeout: 5000 });
30
-
31
- expect(result.exitCode).toBeUndefined();
32
- expect(result.cancelled).toBe(true);
33
- expect(result.output).toContain("Command timed out after 5 seconds");
34
- });
35
-
36
- it("maps kernel error status to exit code 1", async () => {
37
- const kernel = new FakeKernel(
38
- { status: "error", cancelled: false, timedOut: false, stdinRequested: false },
39
- (options) => {
40
- options?.onChunk?.("Traceback...\n");
41
- },
42
- );
43
-
44
- const result = await executePythonWithKernel(kernel, "raise ValueError('boom')");
45
-
46
- expect(result.exitCode).toBe(1);
47
- expect(result.output).toContain("Traceback");
48
- });
49
- });
@@ -1,180 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "bun:test";
2
- import { rmSync } from "node:fs";
3
- import {
4
- disposeAllKernelSessions,
5
- executePythonWithKernel,
6
- getPreludeDocs,
7
- type PythonKernelExecutor,
8
- resetPreludeDocsCache,
9
- warmPythonEnvironment,
10
- } from "./python-executor";
11
- import { type KernelExecuteOptions, type KernelExecuteResult, type PreludeHelper, PythonKernel } from "./python-kernel";
12
- import { DEFAULT_MAX_BYTES } from "./tools/truncate";
13
-
14
- class FakeKernel implements PythonKernelExecutor {
15
- private result: KernelExecuteResult;
16
- private onExecute: (options?: KernelExecuteOptions) => void;
17
-
18
- constructor(result: KernelExecuteResult, onExecute: (options?: KernelExecuteOptions) => void) {
19
- this.result = result;
20
- this.onExecute = onExecute;
21
- }
22
-
23
- async execute(_code: string, options?: KernelExecuteOptions): Promise<KernelExecuteResult> {
24
- this.onExecute(options);
25
- return this.result;
26
- }
27
- }
28
-
29
- describe("executePythonWithKernel", () => {
30
- it("captures text and display outputs", async () => {
31
- const kernel = new FakeKernel(
32
- { status: "ok", cancelled: false, timedOut: false, stdinRequested: false },
33
- (options) => {
34
- options?.onChunk?.("hello\n");
35
- options?.onDisplay?.({ type: "json", data: { foo: "bar" } });
36
- },
37
- );
38
-
39
- const result = await executePythonWithKernel(kernel, "print('hello')");
40
-
41
- expect(result.exitCode).toBe(0);
42
- expect(result.output).toContain("hello");
43
- expect(result.displayOutputs).toHaveLength(1);
44
- });
45
-
46
- it("marks stdin request as error", async () => {
47
- const kernel = new FakeKernel(
48
- { status: "ok", cancelled: false, timedOut: false, stdinRequested: true },
49
- () => {},
50
- );
51
-
52
- const result = await executePythonWithKernel(kernel, "input('prompt')");
53
-
54
- expect(result.exitCode).toBe(1);
55
- expect(result.stdinRequested).toBe(true);
56
- expect(result.output).toContain("Kernel requested stdin; interactive input is not supported.");
57
- });
58
-
59
- it("maps error status to exit code 1", async () => {
60
- const kernel = new FakeKernel(
61
- { status: "error", cancelled: false, timedOut: false, stdinRequested: false },
62
- (options) => {
63
- options?.onChunk?.("Traceback\n");
64
- },
65
- );
66
-
67
- const result = await executePythonWithKernel(kernel, "raise ValueError('nope')");
68
-
69
- expect(result.exitCode).toBe(1);
70
- expect(result.cancelled).toBe(false);
71
- expect(result.output).toContain("Traceback");
72
- });
73
-
74
- it("sanitizes streamed chunks", async () => {
75
- const kernel = new FakeKernel(
76
- { status: "ok", cancelled: false, timedOut: false, stdinRequested: false },
77
- (options) => {
78
- options?.onChunk?.("\u001b[31mred\r\n");
79
- },
80
- );
81
-
82
- const result = await executePythonWithKernel(kernel, "print('red')");
83
-
84
- expect(result.output).toBe("red\n");
85
- });
86
-
87
- it("returns cancelled result with timeout annotation", async () => {
88
- const kernel = new FakeKernel(
89
- { status: "ok", cancelled: true, timedOut: true, stdinRequested: false },
90
- (options) => {
91
- options?.onChunk?.("partial output\n");
92
- },
93
- );
94
-
95
- const result = await executePythonWithKernel(kernel, "while True: pass", { timeout: 4100 });
96
-
97
- expect(result.exitCode).toBeUndefined();
98
- expect(result.cancelled).toBe(true);
99
- expect(result.output).toContain("Command timed out after 4 seconds");
100
- });
101
-
102
- it("returns cancelled result without timeout annotation", async () => {
103
- const kernel = new FakeKernel(
104
- { status: "ok", cancelled: true, timedOut: false, stdinRequested: false },
105
- (options) => {
106
- options?.onChunk?.("cancelled output\n");
107
- },
108
- );
109
-
110
- const result = await executePythonWithKernel(kernel, "while True: pass");
111
-
112
- expect(result.exitCode).toBeUndefined();
113
- expect(result.cancelled).toBe(true);
114
- expect(result.output).toContain("cancelled output");
115
- expect(result.output).not.toContain("Command timed out");
116
- });
117
-
118
- it("truncates large output and stores full output file", async () => {
119
- const largeOutput = `${"x".repeat(DEFAULT_MAX_BYTES + 1024)}TAIL`;
120
- const kernel = new FakeKernel(
121
- { status: "ok", cancelled: false, timedOut: false, stdinRequested: false },
122
- (options) => {
123
- options?.onChunk?.(largeOutput);
124
- },
125
- );
126
-
127
- const result = await executePythonWithKernel(kernel, "print('big')");
128
-
129
- expect(result.truncated).toBe(true);
130
- expect(result.fullOutputPath).toBeDefined();
131
- expect(result.output).toContain("TAIL");
132
-
133
- const fullText = await Bun.file(result.fullOutputPath as string).text();
134
- expect(fullText).toBe(largeOutput);
135
-
136
- rmSync(result.fullOutputPath as string, { force: true });
137
- });
138
- });
139
-
140
- afterEach(async () => {
141
- await disposeAllKernelSessions();
142
- resetPreludeDocsCache();
143
- vi.restoreAllMocks();
144
- });
145
-
146
- describe("warmPythonEnvironment", () => {
147
- it("caches prelude docs on warmup", async () => {
148
- const previousSkip = process.env.OMP_PYTHON_SKIP_CHECK;
149
- process.env.OMP_PYTHON_SKIP_CHECK = "1";
150
- const docs: PreludeHelper[] = [
151
- {
152
- name: "read",
153
- signature: "(path)",
154
- docstring: "Read file contents.",
155
- category: "File I/O",
156
- },
157
- ];
158
- const kernel = {
159
- introspectPrelude: vi.fn().mockResolvedValue(docs),
160
- ping: vi.fn().mockResolvedValue(true),
161
- isAlive: () => true,
162
- shutdown: vi.fn().mockResolvedValue(undefined),
163
- };
164
- const startSpy = vi.spyOn(PythonKernel, "start").mockResolvedValue(kernel as unknown as PythonKernel);
165
-
166
- const result = await warmPythonEnvironment("/tmp/test", "session-1");
167
-
168
- expect(result.ok).toBe(true);
169
- expect(result.docs).toEqual(docs);
170
- expect(getPreludeDocs()).toEqual(docs);
171
- expect(kernel.introspectPrelude).toHaveBeenCalledTimes(1);
172
-
173
- startSpy.mockRestore();
174
- if (previousSkip === undefined) {
175
- delete process.env.OMP_PYTHON_SKIP_CHECK;
176
- } else {
177
- process.env.OMP_PYTHON_SKIP_CHECK = previousSkip;
178
- }
179
- });
180
- });
@@ -1,54 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { type KernelDisplayOutput, PythonKernel } from "./python-kernel";
3
-
4
- const renderDisplay = (
5
- PythonKernel as unknown as {
6
- prototype: {
7
- renderDisplay: (content: Record<string, unknown>) => {
8
- text: string;
9
- outputs: KernelDisplayOutput[];
10
- };
11
- };
12
- }
13
- ).prototype.renderDisplay;
14
-
15
- describe("PythonKernel display rendering", () => {
16
- it("normalizes text/plain output and returns no display outputs", () => {
17
- const { text, outputs } = renderDisplay.call({} as PythonKernel, {
18
- data: { "text/plain": "hello" },
19
- });
20
-
21
- expect(text).toBe("hello\n");
22
- expect(outputs).toHaveLength(0);
23
- });
24
-
25
- it("collects image and json display outputs without text", () => {
26
- const { text, outputs } = renderDisplay.call({} as PythonKernel, {
27
- data: { "image/png": "abc", "application/json": { foo: "bar" } },
28
- });
29
-
30
- expect(text).toBe("");
31
- expect(outputs).toEqual([
32
- { type: "image", data: "abc", mimeType: "image/png" },
33
- { type: "json", data: { foo: "bar" } },
34
- ]);
35
- });
36
-
37
- it("converts text/html to markdown", () => {
38
- const { text, outputs } = renderDisplay.call({} as PythonKernel, {
39
- data: { "text/html": "<p><strong>Hello</strong></p>" },
40
- });
41
-
42
- expect(outputs).toHaveLength(0);
43
- expect(text).toBe("**Hello**\n");
44
- });
45
-
46
- it("combines text/plain with json output", () => {
47
- const { text, outputs } = renderDisplay.call({} as PythonKernel, {
48
- data: { "text/plain": "value", "application/json": { ok: true } },
49
- });
50
-
51
- expect(text).toBe("value\n");
52
- expect(outputs).toEqual([{ type: "json", data: { ok: true } }]);
53
- });
54
- });
@@ -1,138 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
2
- import * as shell from "../utils/shell";
3
- import * as shellSnapshot from "../utils/shell-snapshot";
4
- import { PythonKernel } from "./python-kernel";
5
- import { PYTHON_PRELUDE } from "./python-prelude";
6
-
7
- class FakeWebSocket {
8
- static OPEN = 1;
9
- static CLOSED = 3;
10
- readyState = FakeWebSocket.OPEN;
11
- binaryType = "arraybuffer";
12
- url: string;
13
- onopen?: () => void;
14
- onerror?: (event: unknown) => void;
15
- onclose?: () => void;
16
- onmessage?: (event: { data: ArrayBuffer }) => void;
17
-
18
- constructor(url: string) {
19
- this.url = url;
20
- queueMicrotask(() => {
21
- this.onopen?.();
22
- });
23
- }
24
-
25
- send(_data: ArrayBuffer) {}
26
-
27
- close() {
28
- this.readyState = FakeWebSocket.CLOSED;
29
- this.onclose?.();
30
- }
31
- }
32
-
33
- describe("PythonKernel.start (local gateway)", () => {
34
- const originalEnv = { ...process.env };
35
- const originalFetch = globalThis.fetch;
36
- const originalWebSocket = globalThis.WebSocket;
37
-
38
- beforeEach(() => {
39
- process.env.BUN_ENV = "test";
40
- delete process.env.OMP_PYTHON_GATEWAY_URL;
41
- delete process.env.OMP_PYTHON_GATEWAY_TOKEN;
42
- globalThis.WebSocket = FakeWebSocket as unknown as typeof WebSocket;
43
- });
44
-
45
- afterEach(() => {
46
- for (const key of Object.keys(process.env)) {
47
- if (!(key in originalEnv)) {
48
- delete process.env[key];
49
- }
50
- }
51
- for (const [key, value] of Object.entries(originalEnv)) {
52
- process.env[key] = value;
53
- }
54
- globalThis.fetch = originalFetch;
55
- globalThis.WebSocket = originalWebSocket;
56
- vi.restoreAllMocks();
57
- });
58
-
59
- it("filters environment variables before spawning gateway", async () => {
60
- const fetchSpy = vi.fn(async (input: string | URL, init?: RequestInit) => {
61
- const url = typeof input === "string" ? input : input.toString();
62
- if (url.endsWith("/api/kernelspecs")) {
63
- return new Response(JSON.stringify({}), { status: 200 });
64
- }
65
- if (url.endsWith("/api/kernels") && init?.method === "POST") {
66
- return new Response(JSON.stringify({ id: "kernel-1" }), { status: 201 });
67
- }
68
- return new Response("", { status: 200 });
69
- });
70
- globalThis.fetch = fetchSpy as unknown as typeof fetch;
71
-
72
- const shellSpy = vi.spyOn(shell, "getShellConfig").mockResolvedValue({
73
- shell: "/bin/bash",
74
- args: ["-lc"],
75
- env: {
76
- PATH: "/bin",
77
- HOME: "/home/test",
78
- OPENAI_API_KEY: "secret",
79
- UNSAFE_TOKEN: "nope",
80
- OMP_CUSTOM: "1",
81
- LC_ALL: "en_US.UTF-8",
82
- },
83
- prefix: undefined,
84
- });
85
- const snapshotSpy = vi.spyOn(shellSnapshot, "getOrCreateSnapshot").mockResolvedValue(null);
86
- const whichSpy = vi.spyOn(Bun, "which").mockReturnValue("/usr/bin/python");
87
-
88
- let spawnEnv: Record<string, string | undefined> | undefined;
89
- let spawnArgs: string[] | undefined;
90
- const spawnSpy = vi.spyOn(Bun, "spawn").mockImplementation(((...args: unknown[]) => {
91
- const [cmd, options] = args as [string[] | { cmd: string[] }, { env?: Record<string, string | undefined> }?];
92
- spawnArgs = Array.isArray(cmd) ? cmd : cmd.cmd;
93
- spawnEnv = options?.env;
94
- return { pid: 1234, exited: Promise.resolve(0) } as unknown as Bun.Subprocess;
95
- }) as unknown as typeof Bun.spawn);
96
-
97
- const executeSpy = vi
98
- .spyOn(PythonKernel.prototype, "execute")
99
- .mockResolvedValue({ status: "ok", cancelled: false, timedOut: false, stdinRequested: false });
100
-
101
- const kernel = await PythonKernel.start({ cwd: "/tmp/project", env: { CUSTOM_VAR: "ok" } });
102
-
103
- const createCall = fetchSpy.mock.calls.find(([input, init]) => {
104
- const url = typeof input === "string" ? input : input.toString();
105
- return url.endsWith("/api/kernels") && init?.method === "POST";
106
- });
107
- expect(createCall).toBeDefined();
108
- if (createCall) {
109
- expect(JSON.parse(String(createCall[1]?.body ?? "{}"))).toEqual({ name: "python3" });
110
- }
111
-
112
- expect(spawnArgs).toContain("kernel_gateway");
113
- expect(spawnEnv?.PATH).toBe("/bin");
114
- expect(spawnEnv?.HOME).toBe("/home/test");
115
- expect(spawnEnv?.OMP_CUSTOM).toBe("1");
116
- expect(spawnEnv?.LC_ALL).toBe("en_US.UTF-8");
117
- expect(spawnEnv?.CUSTOM_VAR).toBe("ok");
118
- expect(spawnEnv?.OPENAI_API_KEY).toBeUndefined();
119
- expect(spawnEnv?.UNSAFE_TOKEN).toBeUndefined();
120
- expect(spawnEnv?.PYTHONPATH).toBe("/tmp/project");
121
-
122
- expect(executeSpy).toHaveBeenCalledWith(
123
- PYTHON_PRELUDE,
124
- expect.objectContaining({
125
- silent: true,
126
- storeHistory: false,
127
- }),
128
- );
129
-
130
- await kernel.shutdown();
131
-
132
- shellSpy.mockRestore();
133
- snapshotSpy.mockRestore();
134
- whichSpy.mockRestore();
135
- spawnSpy.mockRestore();
136
- executeSpy.mockRestore();
137
- });
138
- });
@@ -1,87 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
- import { disposeAllKernelSessions, executePython } from "./python-executor";
3
- import type { KernelExecuteOptions, KernelExecuteResult } from "./python-kernel";
4
- import { PythonKernel } from "./python-kernel";
5
-
6
- class FakeKernel {
7
- executeCalls = 0;
8
- shutdownCalls = 0;
9
- alive = true;
10
- readonly id: string;
11
-
12
- constructor(id: string) {
13
- this.id = id;
14
- }
15
-
16
- async execute(_code: string, options?: KernelExecuteOptions): Promise<KernelExecuteResult> {
17
- this.executeCalls += 1;
18
- options?.onChunk?.("ok\n");
19
- return { status: "ok", cancelled: false, timedOut: false, stdinRequested: false };
20
- }
21
-
22
- async shutdown(): Promise<void> {
23
- this.shutdownCalls += 1;
24
- this.alive = false;
25
- }
26
-
27
- isAlive(): boolean {
28
- return this.alive;
29
- }
30
-
31
- async ping(): Promise<boolean> {
32
- return this.alive;
33
- }
34
- }
35
-
36
- describe("executePython kernel reuse", () => {
37
- const originalStart = PythonKernel.start;
38
- let startCalls = 0;
39
- let kernels: FakeKernel[] = [];
40
-
41
- beforeEach(() => {
42
- process.env.OMP_PYTHON_SKIP_CHECK = "1";
43
- startCalls = 0;
44
- kernels = [];
45
- PythonKernel.start = (async () => {
46
- startCalls += 1;
47
- const kernel = new FakeKernel(`kernel-${startCalls}`);
48
- kernels.push(kernel);
49
- return kernel as unknown as PythonKernel;
50
- }) as typeof PythonKernel.start;
51
- });
52
-
53
- afterEach(async () => {
54
- PythonKernel.start = originalStart;
55
- await disposeAllKernelSessions();
56
- });
57
-
58
- it("reuses kernels for session mode", async () => {
59
- await executePython("print('one')", { cwd: "/tmp", sessionId: "session-a", kernelMode: "session" });
60
- await executePython("print('two')", { cwd: "/tmp", sessionId: "session-a", kernelMode: "session" });
61
-
62
- expect(startCalls).toBe(1);
63
- expect(kernels[0]?.executeCalls).toBe(2);
64
- });
65
-
66
- it("creates and disposes per-call kernels", async () => {
67
- await executePython("print('one')", { cwd: "/tmp", kernelMode: "per-call" });
68
- await executePython("print('two')", { cwd: "/tmp", kernelMode: "per-call" });
69
-
70
- expect(startCalls).toBe(2);
71
- expect(kernels[0]?.shutdownCalls).toBe(1);
72
- expect(kernels[1]?.shutdownCalls).toBe(1);
73
- });
74
-
75
- it("resets the session kernel when requested", async () => {
76
- await executePython("print('one')", { cwd: "/tmp", sessionId: "session-b", kernelMode: "session" });
77
- await executePython("print('two')", {
78
- cwd: "/tmp",
79
- sessionId: "session-b",
80
- kernelMode: "session",
81
- reset: true,
82
- });
83
-
84
- expect(startCalls).toBe(2);
85
- expect(kernels[0]?.shutdownCalls).toBe(1);
86
- });
87
- });