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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +6 -6
  3. package/package.json +8 -7
  4. package/src/migrations.ts +1 -34
  5. package/src/vendor/photon/index.js +4 -2
  6. package/src/vendor/photon/photon_rs_bg.wasm.b64.js +1 -0
  7. package/src/core/python-executor-display.test.ts +0 -42
  8. package/src/core/python-executor-lifecycle.test.ts +0 -99
  9. package/src/core/python-executor-mapping.test.ts +0 -41
  10. package/src/core/python-executor-per-call.test.ts +0 -49
  11. package/src/core/python-executor-session.test.ts +0 -103
  12. package/src/core/python-executor-streaming.test.ts +0 -77
  13. package/src/core/python-executor-timeout.test.ts +0 -35
  14. package/src/core/python-executor.lifecycle.test.ts +0 -139
  15. package/src/core/python-executor.result.test.ts +0 -49
  16. package/src/core/python-executor.test.ts +0 -180
  17. package/src/core/python-kernel-display.test.ts +0 -54
  18. package/src/core/python-kernel-env.test.ts +0 -138
  19. package/src/core/python-kernel-session.test.ts +0 -87
  20. package/src/core/python-kernel-ws.test.ts +0 -104
  21. package/src/core/python-kernel.lifecycle.test.ts +0 -249
  22. package/src/core/python-kernel.test.ts +0 -461
  23. package/src/core/python-modules.test.ts +0 -102
  24. package/src/core/python-prelude.test.ts +0 -140
  25. package/src/core/settings-manager-python.test.ts +0 -23
  26. package/src/core/streaming-output.test.ts +0 -26
  27. package/src/core/system-prompt.python.test.ts +0 -17
  28. package/src/core/tools/index.test.ts +0 -212
  29. package/src/core/tools/python-execution.test.ts +0 -68
  30. package/src/core/tools/python-fallback.test.ts +0 -72
  31. package/src/core/tools/python-renderer.test.ts +0 -36
  32. package/src/core/tools/python-tool-mode.test.ts +0 -43
  33. package/src/core/tools/python.test.ts +0 -121
  34. package/src/core/tools/schema-validation.test.ts +0 -530
  35. package/src/core/tools/web-scrapers/academic.test.ts +0 -239
  36. package/src/core/tools/web-scrapers/business.test.ts +0 -82
  37. package/src/core/tools/web-scrapers/dev-platforms.test.ts +0 -254
  38. package/src/core/tools/web-scrapers/documentation.test.ts +0 -85
  39. package/src/core/tools/web-scrapers/finance-media.test.ts +0 -144
  40. package/src/core/tools/web-scrapers/git-hosting.test.ts +0 -272
  41. package/src/core/tools/web-scrapers/media.test.ts +0 -138
  42. package/src/core/tools/web-scrapers/package-managers-2.test.ts +0 -199
  43. package/src/core/tools/web-scrapers/package-managers.test.ts +0 -171
  44. package/src/core/tools/web-scrapers/package-registries.test.ts +0 -259
  45. package/src/core/tools/web-scrapers/research.test.ts +0 -107
  46. package/src/core/tools/web-scrapers/security.test.ts +0 -103
  47. package/src/core/tools/web-scrapers/social-extended.test.ts +0 -192
  48. package/src/core/tools/web-scrapers/social.test.ts +0 -259
  49. package/src/core/tools/web-scrapers/stackexchange.test.ts +0 -120
  50. package/src/core/tools/web-scrapers/standards.test.ts +0 -122
  51. package/src/core/tools/web-scrapers/wikipedia.test.ts +0 -73
  52. package/src/core/tools/web-scrapers/youtube.test.ts +0 -198
  53. package/src/discovery/helpers.test.ts +0 -131
@@ -1,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
- });