@oh-my-pi/pi-coding-agent 5.7.67 → 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 (52) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/package.json +8 -7
  3. package/src/migrations.ts +1 -34
  4. package/src/vendor/photon/index.js +4 -2
  5. package/src/vendor/photon/photon_rs_bg.wasm.b64.js +1 -0
  6. package/src/core/python-executor-display.test.ts +0 -42
  7. package/src/core/python-executor-lifecycle.test.ts +0 -99
  8. package/src/core/python-executor-mapping.test.ts +0 -41
  9. package/src/core/python-executor-per-call.test.ts +0 -49
  10. package/src/core/python-executor-session.test.ts +0 -103
  11. package/src/core/python-executor-streaming.test.ts +0 -77
  12. package/src/core/python-executor-timeout.test.ts +0 -35
  13. package/src/core/python-executor.lifecycle.test.ts +0 -139
  14. package/src/core/python-executor.result.test.ts +0 -49
  15. package/src/core/python-executor.test.ts +0 -180
  16. package/src/core/python-kernel-display.test.ts +0 -54
  17. package/src/core/python-kernel-env.test.ts +0 -138
  18. package/src/core/python-kernel-session.test.ts +0 -87
  19. package/src/core/python-kernel-ws.test.ts +0 -104
  20. package/src/core/python-kernel.lifecycle.test.ts +0 -249
  21. package/src/core/python-kernel.test.ts +0 -461
  22. package/src/core/python-modules.test.ts +0 -102
  23. package/src/core/python-prelude.test.ts +0 -140
  24. package/src/core/settings-manager-python.test.ts +0 -23
  25. package/src/core/streaming-output.test.ts +0 -26
  26. package/src/core/system-prompt.python.test.ts +0 -17
  27. package/src/core/tools/index.test.ts +0 -212
  28. package/src/core/tools/python-execution.test.ts +0 -68
  29. package/src/core/tools/python-fallback.test.ts +0 -72
  30. package/src/core/tools/python-renderer.test.ts +0 -36
  31. package/src/core/tools/python-tool-mode.test.ts +0 -43
  32. package/src/core/tools/python.test.ts +0 -121
  33. package/src/core/tools/schema-validation.test.ts +0 -530
  34. package/src/core/tools/web-scrapers/academic.test.ts +0 -239
  35. package/src/core/tools/web-scrapers/business.test.ts +0 -82
  36. package/src/core/tools/web-scrapers/dev-platforms.test.ts +0 -254
  37. package/src/core/tools/web-scrapers/documentation.test.ts +0 -85
  38. package/src/core/tools/web-scrapers/finance-media.test.ts +0 -144
  39. package/src/core/tools/web-scrapers/git-hosting.test.ts +0 -272
  40. package/src/core/tools/web-scrapers/media.test.ts +0 -138
  41. package/src/core/tools/web-scrapers/package-managers-2.test.ts +0 -199
  42. package/src/core/tools/web-scrapers/package-managers.test.ts +0 -171
  43. package/src/core/tools/web-scrapers/package-registries.test.ts +0 -259
  44. package/src/core/tools/web-scrapers/research.test.ts +0 -107
  45. package/src/core/tools/web-scrapers/security.test.ts +0 -103
  46. package/src/core/tools/web-scrapers/social-extended.test.ts +0 -192
  47. package/src/core/tools/web-scrapers/social.test.ts +0 -259
  48. package/src/core/tools/web-scrapers/stackexchange.test.ts +0 -120
  49. package/src/core/tools/web-scrapers/standards.test.ts +0 -122
  50. package/src/core/tools/web-scrapers/wikipedia.test.ts +0 -73
  51. package/src/core/tools/web-scrapers/youtube.test.ts +0 -198
  52. package/src/discovery/helpers.test.ts +0 -131
@@ -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
- });
@@ -1,23 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { SettingsManager } from "./settings-manager";
3
-
4
- describe("SettingsManager python settings", () => {
5
- it("defaults to both and session", () => {
6
- const settings = SettingsManager.inMemory();
7
-
8
- expect(settings.getPythonToolMode()).toBe("both");
9
- expect(settings.getPythonKernelMode()).toBe("session");
10
- });
11
-
12
- it("persists python tool and kernel modes", async () => {
13
- const settings = SettingsManager.inMemory();
14
-
15
- await settings.setPythonToolMode("bash-only");
16
- await settings.setPythonKernelMode("per-call");
17
-
18
- expect(settings.getPythonToolMode()).toBe("bash-only");
19
- expect(settings.getPythonKernelMode()).toBe("per-call");
20
- expect(settings.serialize().python?.toolMode).toBe("bash-only");
21
- expect(settings.serialize().python?.kernelMode).toBe("per-call");
22
- });
23
- });
@@ -1,26 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { createOutputSink } from "./streaming-output";
3
-
4
- function makeLargeOutput(size: number): string {
5
- return "x".repeat(size);
6
- }
7
-
8
- describe("createOutputSink", () => {
9
- it("spills to disk and truncates large output", async () => {
10
- const largeOutput = makeLargeOutput(60_000);
11
- const sink = createOutputSink(10, 70_000);
12
- const writer = sink.getWriter();
13
-
14
- await writer.write(largeOutput);
15
- await writer.close();
16
-
17
- const result = sink.dump();
18
-
19
- expect(result.truncated).toBe(true);
20
- expect(result.fullOutputPath).toBeDefined();
21
- expect(result.output.length).toBeLessThan(largeOutput.length);
22
-
23
- const fullOutput = await Bun.file(result.fullOutputPath!).text();
24
- expect(fullOutput).toBe(largeOutput);
25
- });
26
- });
@@ -1,17 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { buildSystemPrompt } from "./system-prompt";
3
-
4
- describe("buildSystemPrompt", () => {
5
- it("includes python tool details when enabled", async () => {
6
- const prompt = await buildSystemPrompt({
7
- cwd: "/tmp",
8
- toolNames: ["python"],
9
- contextFiles: [],
10
- skills: [],
11
- rules: [],
12
- });
13
-
14
- expect(prompt).toContain("python: Execute Python code via a session-backed IPython kernel");
15
- expect(prompt).toContain("What python IS for");
16
- });
17
- });
@@ -1,212 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { BUILTIN_TOOLS, createTools, HIDDEN_TOOLS, type ToolSession } from "./index";
3
-
4
- process.env.OMP_PYTHON_SKIP_CHECK = "1";
5
-
6
- function createTestSession(overrides: Partial<ToolSession> = {}): ToolSession {
7
- return {
8
- cwd: "/tmp/test",
9
- hasUI: false,
10
- getSessionFile: () => null,
11
- getSessionSpawns: () => "*",
12
- ...overrides,
13
- };
14
- }
15
-
16
- function createBaseSettings(overrides: Partial<NonNullable<ToolSession["settings"]>> = {}) {
17
- return {
18
- getImageAutoResize: () => true,
19
- getLspFormatOnWrite: () => true,
20
- getLspDiagnosticsOnWrite: () => true,
21
- getLspDiagnosticsOnEdit: () => false,
22
- getEditFuzzyMatch: () => true,
23
- getGitToolEnabled: () => true,
24
- getBashInterceptorEnabled: () => true,
25
- getBashInterceptorSimpleLsEnabled: () => true,
26
- getBashInterceptorRules: () => [],
27
- ...overrides,
28
- };
29
- }
30
-
31
- describe("createTools", () => {
32
- it("creates all builtin tools by default", async () => {
33
- const session = createTestSession();
34
- const tools = await createTools(session);
35
- const names = tools.map((t) => t.name);
36
-
37
- // Core tools should always be present
38
- expect(names).toContain("python");
39
- expect(names).not.toContain("bash");
40
- expect(names).toContain("calc");
41
- expect(names).toContain("read");
42
- expect(names).toContain("edit");
43
- expect(names).toContain("write");
44
- expect(names).toContain("grep");
45
- expect(names).toContain("find");
46
- expect(names).toContain("ls");
47
- expect(names).toContain("lsp");
48
- expect(names).toContain("notebook");
49
- expect(names).toContain("task");
50
- expect(names).toContain("todo_write");
51
- expect(names).toContain("output");
52
- expect(names).toContain("web_fetch");
53
- expect(names).toContain("web_search");
54
- });
55
-
56
- it("includes bash and python when python mode is both", async () => {
57
- const session = createTestSession({
58
- settings: createBaseSettings({
59
- getPythonToolMode: () => "both",
60
- getPythonKernelMode: () => "session",
61
- }),
62
- });
63
- const tools = await createTools(session);
64
- const names = tools.map((t) => t.name);
65
-
66
- expect(names).toContain("bash");
67
- expect(names).toContain("python");
68
- });
69
-
70
- it("includes bash only when python mode is bash-only", async () => {
71
- const session = createTestSession({
72
- settings: createBaseSettings({
73
- getPythonToolMode: () => "bash-only",
74
- getPythonKernelMode: () => "session",
75
- }),
76
- });
77
- const tools = await createTools(session);
78
- const names = tools.map((t) => t.name);
79
-
80
- expect(names).toContain("bash");
81
- expect(names).not.toContain("python");
82
- });
83
-
84
- it("excludes lsp tool when session disables LSP", async () => {
85
- const session = createTestSession({ enableLsp: false });
86
- const tools = await createTools(session, ["read", "lsp", "write"]);
87
- const names = tools.map((t) => t.name);
88
-
89
- expect(names).toEqual(["read", "write"]);
90
- });
91
-
92
- it("excludes lsp tool when disabled", async () => {
93
- const session = createTestSession({ enableLsp: false });
94
- const tools = await createTools(session);
95
- const names = tools.map((t) => t.name);
96
-
97
- expect(names).not.toContain("lsp");
98
- });
99
-
100
- it("respects requested tool subset", async () => {
101
- const session = createTestSession();
102
- const tools = await createTools(session, ["read", "write"]);
103
- const names = tools.map((t) => t.name);
104
-
105
- expect(names).toEqual(["read", "write"]);
106
- });
107
-
108
- it("includes hidden tools when explicitly requested", async () => {
109
- const session = createTestSession();
110
- const tools = await createTools(session, ["report_finding"]);
111
- const names = tools.map((t) => t.name);
112
-
113
- expect(names).toEqual(["report_finding"]);
114
- });
115
-
116
- it("includes complete tool when required", async () => {
117
- const session = createTestSession({ requireCompleteTool: true });
118
- const tools = await createTools(session);
119
- const names = tools.map((t) => t.name);
120
-
121
- expect(names).toContain("complete");
122
- });
123
-
124
- it("excludes ask tool when hasUI is false", async () => {
125
- const session = createTestSession({ hasUI: false });
126
- const tools = await createTools(session);
127
- const names = tools.map((t) => t.name);
128
-
129
- expect(names).not.toContain("ask");
130
- });
131
-
132
- it("includes ask tool when hasUI is true", async () => {
133
- const session = createTestSession({ hasUI: true });
134
- const tools = await createTools(session);
135
- const names = tools.map((t) => t.name);
136
-
137
- expect(names).toContain("ask");
138
- });
139
-
140
- it("excludes git tool when disabled in settings", async () => {
141
- const session = createTestSession({
142
- settings: createBaseSettings({ getGitToolEnabled: () => false }),
143
- });
144
- const tools = await createTools(session);
145
- const names = tools.map((t) => t.name);
146
-
147
- expect(names).not.toContain("git");
148
- });
149
-
150
- it("includes git tool when enabled in settings", async () => {
151
- const session = createTestSession({
152
- settings: createBaseSettings({ getGitToolEnabled: () => true }),
153
- });
154
- const tools = await createTools(session);
155
- const names = tools.map((t) => t.name);
156
-
157
- expect(names).toContain("git");
158
- });
159
-
160
- it("includes git tool when no settings provided (default enabled)", async () => {
161
- const session = createTestSession({ settings: undefined });
162
- const tools = await createTools(session);
163
- const names = tools.map((t) => t.name);
164
-
165
- expect(names).toContain("git");
166
- });
167
-
168
- it("always includes output tool when task tool is present", async () => {
169
- const session = createTestSession();
170
- const tools = await createTools(session);
171
- const names = tools.map((t) => t.name);
172
-
173
- // Both should be present together
174
- expect(names).toContain("task");
175
- expect(names).toContain("output");
176
- });
177
-
178
- it("BUILTIN_TOOLS contains all expected tools", () => {
179
- const expectedTools = [
180
- "ask",
181
- "bash",
182
- "python",
183
- "calc",
184
- "ssh",
185
- "edit",
186
- "find",
187
- "git",
188
- "grep",
189
- "ls",
190
- "lsp",
191
- "notebook",
192
- "output",
193
- "read",
194
- "task",
195
- "todo_write",
196
- "web_fetch",
197
- "web_search",
198
- "write",
199
- ];
200
-
201
- for (const tool of expectedTools) {
202
- expect(BUILTIN_TOOLS).toHaveProperty(tool);
203
- }
204
-
205
- // Ensure we haven't missed any
206
- expect(Object.keys(BUILTIN_TOOLS).sort()).toEqual(expectedTools.sort());
207
- });
208
-
209
- it("HIDDEN_TOOLS contains review tools", () => {
210
- expect(Object.keys(HIDDEN_TOOLS).sort()).toEqual(["complete", "report_finding"]);
211
- });
212
- });
@@ -1,68 +0,0 @@
1
- import { describe, expect, it, vi } from "bun:test";
2
- import { mkdtempSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import * as pythonExecutor from "../python-executor";
6
- import type { ToolSession } from "./index";
7
- import { createPythonTool } from "./python";
8
-
9
- function createSession(cwd: string): ToolSession {
10
- return {
11
- cwd,
12
- hasUI: false,
13
- getSessionFile: () => "session-file",
14
- getSessionSpawns: () => "*",
15
- settings: {
16
- getImageAutoResize: () => true,
17
- getLspFormatOnWrite: () => true,
18
- getLspDiagnosticsOnWrite: () => true,
19
- getLspDiagnosticsOnEdit: () => false,
20
- getEditFuzzyMatch: () => true,
21
- getGitToolEnabled: () => true,
22
- getBashInterceptorEnabled: () => true,
23
- getBashInterceptorSimpleLsEnabled: () => true,
24
- getBashInterceptorRules: () => [],
25
- getPythonToolMode: () => "ipy-only",
26
- getPythonKernelMode: () => "per-call",
27
- },
28
- };
29
- }
30
-
31
- describe("python tool execution", () => {
32
- it("passes kernel options from settings and args", async () => {
33
- const tempDir = mkdtempSync(join(tmpdir(), "python-tool-"));
34
- const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({
35
- output: "ok",
36
- exitCode: 0,
37
- cancelled: false,
38
- truncated: false,
39
- displayOutputs: [],
40
- stdinRequested: false,
41
- });
42
-
43
- const tool = createPythonTool(createSession(tempDir));
44
- const result = await tool.execute(
45
- "call-id",
46
- { code: "print('hi')", timeout: 5, workdir: tempDir, reset: true },
47
- undefined,
48
- undefined,
49
- undefined,
50
- );
51
-
52
- expect(executeSpy).toHaveBeenCalledWith(
53
- "print('hi')",
54
- expect.objectContaining({
55
- cwd: tempDir,
56
- timeout: 5000,
57
- sessionId: `session:session-file:workdir:${tempDir}`,
58
- kernelMode: "per-call",
59
- reset: true,
60
- }),
61
- );
62
- const text = result.content.find((item) => item.type === "text")?.text;
63
- expect(text).toBe("ok");
64
-
65
- executeSpy.mockRestore();
66
- rmSync(tempDir, { recursive: true, force: true });
67
- });
68
- });
@@ -1,72 +0,0 @@
1
- import { describe, expect, it, vi } from "bun:test";
2
- import * as pythonKernelModule from "../python-kernel";
3
- import type { ToolSession } from "./index";
4
- import { createTools } from "./index";
5
-
6
- function createTestSession(overrides: Partial<ToolSession> = {}): ToolSession {
7
- return {
8
- cwd: "/tmp/test",
9
- hasUI: false,
10
- getSessionFile: () => null,
11
- getSessionSpawns: () => "*",
12
- ...overrides,
13
- };
14
- }
15
-
16
- function createBaseSettings(overrides: Partial<NonNullable<ToolSession["settings"]>> = {}) {
17
- return {
18
- getImageAutoResize: () => true,
19
- getLspFormatOnWrite: () => true,
20
- getLspDiagnosticsOnWrite: () => true,
21
- getLspDiagnosticsOnEdit: () => false,
22
- getEditFuzzyMatch: () => true,
23
- getGitToolEnabled: () => true,
24
- getBashInterceptorEnabled: () => true,
25
- getBashInterceptorSimpleLsEnabled: () => true,
26
- getBashInterceptorRules: () => [],
27
- ...overrides,
28
- };
29
- }
30
-
31
- describe("createTools python fallback", () => {
32
- it("falls back to bash-only when kernel unavailable", async () => {
33
- const availabilitySpy = vi
34
- .spyOn(pythonKernelModule, "checkPythonKernelAvailability")
35
- .mockResolvedValue({ ok: false, reason: "unavailable" });
36
-
37
- const session = createTestSession({
38
- settings: createBaseSettings({
39
- getPythonToolMode: () => "ipy-only",
40
- getPythonKernelMode: () => "session",
41
- }),
42
- });
43
-
44
- const tools = await createTools(session, ["python"]);
45
- const names = tools.map((tool) => tool.name).sort();
46
-
47
- expect(names).toEqual(["bash"]);
48
-
49
- availabilitySpy.mockRestore();
50
- });
51
-
52
- it("keeps bash when python mode is both but unavailable", async () => {
53
- const availabilitySpy = vi
54
- .spyOn(pythonKernelModule, "checkPythonKernelAvailability")
55
- .mockResolvedValue({ ok: false, reason: "unavailable" });
56
-
57
- const session = createTestSession({
58
- settings: createBaseSettings({
59
- getPythonToolMode: () => "both",
60
- getPythonKernelMode: () => "session",
61
- }),
62
- });
63
-
64
- const tools = await createTools(session);
65
- const names = tools.map((tool) => tool.name);
66
-
67
- expect(names).toContain("bash");
68
- expect(names).not.toContain("python");
69
-
70
- availabilitySpy.mockRestore();
71
- });
72
- });