@oh-my-pi/pi-coding-agent 15.9.67 → 15.10.0

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 (128) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/gallery-cli.d.ts +43 -0
  4. package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
  5. package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
  6. package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
  7. package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
  8. package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
  9. package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
  10. package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
  11. package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
  12. package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
  13. package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
  14. package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
  15. package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
  16. package/dist/types/cli/gallery-screenshot.d.ts +35 -0
  17. package/dist/types/commands/gallery.d.ts +47 -0
  18. package/dist/types/config/keybindings.d.ts +6 -1
  19. package/dist/types/config/model-id-affixes.d.ts +2 -0
  20. package/dist/types/config/model-registry.d.ts +8 -1
  21. package/dist/types/config/settings-schema.d.ts +32 -6
  22. package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
  23. package/dist/types/lsp/types.d.ts +10 -0
  24. package/dist/types/main.d.ts +3 -2
  25. package/dist/types/memory-backend/index.d.ts +2 -1
  26. package/dist/types/memory-backend/resolve.d.ts +1 -1
  27. package/dist/types/memory-backend/types.d.ts +1 -1
  28. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  29. package/dist/types/modes/components/tool-execution.d.ts +18 -0
  30. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  31. package/dist/types/modes/index.d.ts +5 -4
  32. package/dist/types/modes/interactive-mode.d.ts +1 -1
  33. package/dist/types/modes/setup-version.d.ts +11 -0
  34. package/dist/types/modes/setup-wizard/index.d.ts +2 -1
  35. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
  36. package/dist/types/modes/types.d.ts +1 -1
  37. package/dist/types/sdk.d.ts +1 -1
  38. package/dist/types/task/executor.d.ts +7 -0
  39. package/dist/types/telemetry-export.d.ts +1 -1
  40. package/dist/types/tools/eval-render.d.ts +1 -8
  41. package/dist/types/tools/fetch.d.ts +15 -7
  42. package/dist/types/tools/render-utils.d.ts +8 -0
  43. package/dist/types/tools/renderers.d.ts +16 -2
  44. package/dist/types/tools/search.d.ts +1 -1
  45. package/dist/types/tools/write.d.ts +2 -0
  46. package/dist/types/web/scrapers/github.d.ts +22 -0
  47. package/dist/types/web/search/providers/perplexity.d.ts +8 -1
  48. package/dist/types/web/search/types.d.ts +1 -1
  49. package/package.json +9 -9
  50. package/scripts/dev-launch +42 -0
  51. package/scripts/dev-launch-preload.ts +19 -0
  52. package/src/cli/args.ts +2 -2
  53. package/src/cli/gallery-cli.ts +223 -0
  54. package/src/cli/gallery-fixtures/agentic.ts +292 -0
  55. package/src/cli/gallery-fixtures/codeintel.ts +188 -0
  56. package/src/cli/gallery-fixtures/edit.ts +194 -0
  57. package/src/cli/gallery-fixtures/fs.ts +153 -0
  58. package/src/cli/gallery-fixtures/index.ts +40 -0
  59. package/src/cli/gallery-fixtures/interaction.ts +49 -0
  60. package/src/cli/gallery-fixtures/memory.ts +81 -0
  61. package/src/cli/gallery-fixtures/misc.ts +221 -0
  62. package/src/cli/gallery-fixtures/search.ts +213 -0
  63. package/src/cli/gallery-fixtures/shell.ts +167 -0
  64. package/src/cli/gallery-fixtures/types.ts +41 -0
  65. package/src/cli/gallery-fixtures/web.ts +158 -0
  66. package/src/cli/gallery-screenshot.ts +279 -0
  67. package/src/cli-commands.ts +1 -0
  68. package/src/commands/gallery.ts +52 -0
  69. package/src/commands/launch.ts +1 -1
  70. package/src/config/keybindings.ts +15 -6
  71. package/src/config/model-equivalence.ts +35 -12
  72. package/src/config/model-id-affixes.ts +39 -22
  73. package/src/config/model-registry.ts +16 -16
  74. package/src/config/settings-schema.ts +18 -5
  75. package/src/config/settings.ts +11 -0
  76. package/src/dap/client.ts +14 -16
  77. package/src/edit/renderer.ts +36 -48
  78. package/src/eval/__tests__/agent-bridge.test.ts +75 -32
  79. package/src/eval/agent-bridge.ts +34 -7
  80. package/src/extensibility/extensions/runner.ts +1 -0
  81. package/src/extensibility/plugins/doctor.ts +0 -1
  82. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  83. package/src/goals/tools/goal-tool.ts +2 -2
  84. package/src/internal-urls/docs-index.generated.ts +5 -5
  85. package/src/lsp/client.ts +104 -55
  86. package/src/lsp/types.ts +10 -0
  87. package/src/main.ts +44 -49
  88. package/src/memory-backend/index.ts +13 -1
  89. package/src/memory-backend/resolve.ts +3 -5
  90. package/src/memory-backend/types.ts +1 -1
  91. package/src/modes/components/custom-editor.ts +10 -1
  92. package/src/modes/components/status-line.ts +3 -5
  93. package/src/modes/components/tool-execution.ts +61 -16
  94. package/src/modes/controllers/command-controller.ts +13 -2
  95. package/src/modes/controllers/input-controller.ts +11 -3
  96. package/src/modes/controllers/selector-controller.ts +2 -2
  97. package/src/modes/index.ts +5 -4
  98. package/src/modes/interactive-mode.ts +17 -3
  99. package/src/modes/setup-version.ts +11 -0
  100. package/src/modes/setup-wizard/index.ts +3 -2
  101. package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
  102. package/src/modes/types.ts +1 -1
  103. package/src/modes/utils/context-usage.ts +10 -6
  104. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  105. package/src/sdk.ts +21 -23
  106. package/src/session/agent-session.ts +7 -7
  107. package/src/slash-commands/builtin-registry.ts +1 -1
  108. package/src/slash-commands/helpers/usage-report.ts +2 -0
  109. package/src/task/executor.ts +20 -2
  110. package/src/task/render.ts +1 -2
  111. package/src/telemetry-export.ts +25 -7
  112. package/src/tools/eval-backends.ts +6 -17
  113. package/src/tools/eval-render.ts +21 -18
  114. package/src/tools/eval.ts +5 -4
  115. package/src/tools/fetch.ts +94 -84
  116. package/src/tools/render-utils.ts +17 -3
  117. package/src/tools/renderers.ts +16 -1
  118. package/src/tools/report-tool-issue.ts +1 -1
  119. package/src/tools/search.ts +173 -81
  120. package/src/tools/todo.ts +20 -7
  121. package/src/tools/write.ts +22 -1
  122. package/src/web/scrapers/github.ts +255 -3
  123. package/src/web/scrapers/youtube.ts +3 -2
  124. package/src/web/search/providers/perplexity.ts +199 -51
  125. package/src/web/search/render.ts +39 -54
  126. package/src/web/search/types.ts +5 -1
  127. package/dist/types/eval/__tests__/shared-executors.test.d.ts +0 -1
  128. package/src/eval/__tests__/shared-executors.test.ts +0 -609
@@ -1,609 +0,0 @@
1
- import { afterAll, afterEach, describe, expect, it, vi } from "bun:test";
2
- import * as fs from "node:fs/promises";
3
- import * as path from "node:path";
4
- import type { AssistantMessage } from "@oh-my-pi/pi-ai";
5
- import { TempDir } from "@oh-my-pi/pi-utils";
6
- import type { ModelRegistry } from "../../config/model-registry";
7
- import { Settings } from "../../config/settings";
8
- import type { LoadExtensionsResult } from "../../extensibility/extensions/types";
9
- import type { CreateAgentSessionOptions, CreateAgentSessionResult } from "../../sdk";
10
- import * as sdkModule from "../../sdk";
11
- import type { AgentSession, AgentSessionEvent, PromptOptions } from "../../session/agent-session";
12
- import { TaskTool } from "../../task";
13
- import * as discoveryModule from "../../task/discovery";
14
- import type { AgentDefinition, TaskParams } from "../../task/types";
15
- import type { ToolSession } from "../../tools";
16
- import { EventBus } from "../../utils/event-bus";
17
- import { disposeAllVmContexts } from "../js/context-manager";
18
- import { executeJs } from "../js/executor";
19
- import { disposeAllKernelSessions, executePython } from "../py/executor";
20
-
21
- function createToolSession(cwd: string, sessionFile: string | null, evalSessionId?: string): ToolSession {
22
- const modelRegistry = {
23
- authStorage: undefined,
24
- refresh: async () => {},
25
- getAvailable: () => [],
26
- getApiKey: async () => null,
27
- } as unknown as ModelRegistry;
28
- return {
29
- cwd,
30
- hasUI: false,
31
- settings: Settings.isolated({
32
- "async.enabled": false,
33
- "task.isolation.mode": "none",
34
- }),
35
- getSessionFile: () => sessionFile,
36
- getSessionSpawns: () => "*",
37
- getEvalSessionId: evalSessionId ? () => evalSessionId : undefined,
38
- modelRegistry,
39
- } as unknown as ToolSession;
40
- }
41
-
42
- function createBridgeToolSession(resultText: string, calls: unknown[]): ToolSession {
43
- const readTool = {
44
- name: "read",
45
- label: "read",
46
- description: "read",
47
- parameters: { type: "object" },
48
- async execute(_id: string, args: unknown) {
49
- calls.push(args);
50
- return { content: [{ type: "text" as const, text: resultText }] };
51
- },
52
- };
53
- const tools = new Map<string, unknown>([["read", readTool]]);
54
- return { getToolByName: (name: string) => tools.get(name) } as unknown as ToolSession;
55
- }
56
-
57
- function assistantStopMessage(text: string): AssistantMessage {
58
- return {
59
- role: "assistant",
60
- content: text ? [{ type: "text", text }] : [],
61
- api: "openai-responses",
62
- provider: "openai",
63
- model: "mock",
64
- usage: {
65
- input: 0,
66
- output: 0,
67
- cacheRead: 0,
68
- cacheWrite: 0,
69
- totalTokens: 0,
70
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
71
- },
72
- stopReason: "stop",
73
- timestamp: Date.now(),
74
- };
75
- }
76
-
77
- function createYieldingSubagentSession(onPrompt: () => Promise<void>): AgentSession {
78
- const listeners: Array<(event: AgentSessionEvent) => void> = [];
79
- const state = { messages: [] as AssistantMessage[] };
80
- const emit = (event: AgentSessionEvent) => {
81
- for (const listener of listeners) listener(event);
82
- };
83
- return {
84
- state,
85
- agent: { state: { systemPrompt: ["test"] } },
86
- model: undefined,
87
- extensionRunner: undefined,
88
- sessionManager: {
89
- appendSessionInit: () => {},
90
- },
91
- getActiveToolNames: () => ["eval", "yield"],
92
- setActiveToolsByName: async () => {},
93
- subscribe: (listener: (event: AgentSessionEvent) => void) => {
94
- listeners.push(listener);
95
- return () => {
96
- const index = listeners.indexOf(listener);
97
- if (index >= 0) listeners.splice(index, 1);
98
- };
99
- },
100
- prompt: async (_text: string, _options?: PromptOptions) => {
101
- await onPrompt();
102
- state.messages.push(assistantStopMessage("done"));
103
- emit({
104
- type: "tool_execution_end",
105
- toolCallId: "yield-call",
106
- toolName: "yield",
107
- result: {
108
- content: [{ type: "text", text: "Result submitted." }],
109
- details: { status: "success", data: { ok: true } },
110
- },
111
- isError: false,
112
- });
113
- },
114
- waitForIdle: async () => {},
115
- getLastAssistantMessage: () => state.messages[state.messages.length - 1],
116
- abort: async () => {},
117
- dispose: async () => {},
118
- } as unknown as AgentSession;
119
- }
120
-
121
- const taskAgent: AgentDefinition = {
122
- name: "task",
123
- description: "Task agent",
124
- systemPrompt: "Read eval state and yield.",
125
- source: "bundled",
126
- tools: ["eval", "yield"],
127
- };
128
-
129
- const taskParams: TaskParams = {
130
- agent: "task",
131
- tasks: [{ id: "ReadEval", description: "Read eval state", assignment: "Read parent eval state." }],
132
- };
133
-
134
- describe("shared eval executors", () => {
135
- afterEach(() => {
136
- vi.restoreAllMocks();
137
- });
138
-
139
- afterAll(async () => {
140
- await disposeAllVmContexts();
141
- await disposeAllKernelSessions();
142
- });
143
-
144
- it("shares JavaScript state across executeJs calls with one session id", async () => {
145
- using tempDir = TempDir.createSync("@omp-eval-js-shared-");
146
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
147
- const sessionId = `js-shared:${crypto.randomUUID()}`;
148
- const session = createToolSession(tempDir.path(), sessionFile);
149
-
150
- await executeJs("globalThis.x = 41;", { sessionId, session, sessionFile });
151
- const result = await executeJs("return globalThis.x + 1;", { sessionId, session, sessionFile });
152
-
153
- expect(result.exitCode).toBe(0);
154
- expect(result.output.trim()).toBe("42");
155
- });
156
-
157
- it("treats idleTimeoutMs as caller-owned watchdog metadata, not a fixed timer", async () => {
158
- using tempDir = TempDir.createSync("@omp-eval-js-idle-budget-");
159
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
160
- const sessionId = `js-idle-budget:${crypto.randomUUID()}`;
161
- const session = createToolSession(tempDir.path(), sessionFile);
162
-
163
- // With no wall-clock deadlineMs/timeoutMs and no aborting signal, a cell that
164
- // runs well past idleTimeoutMs must still complete: the backend must never
165
- // derive a competing fixed timer from the caller-owned watchdog budget.
166
- const result = await executeJs("await Bun.sleep(120); return 'done';", {
167
- sessionId,
168
- session,
169
- sessionFile,
170
- idleTimeoutMs: 30,
171
- });
172
-
173
- expect(result.cancelled).toBe(false);
174
- expect(result.exitCode).toBe(0);
175
- expect(result.output.trim()).toBe("done");
176
- });
177
-
178
- it("shares Python state across executePython calls with one session id", async () => {
179
- using tempDir = TempDir.createSync("@omp-eval-py-shared-");
180
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
181
- const sessionId = `py-shared:${crypto.randomUUID()}`;
182
-
183
- await executePython("x = 41", { cwd: tempDir.path(), sessionId, sessionFile });
184
- const result = await executePython("print(x + 1)", { cwd: tempDir.path(), sessionId, sessionFile });
185
-
186
- expect(result.exitCode).toBe(0);
187
- expect(result.output.trim()).toBe("42");
188
- });
189
-
190
- it("deduplicates concurrent first JavaScript session acquisition", async () => {
191
- using tempDir = TempDir.createSync("@omp-eval-js-cold-start-");
192
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
193
- const sessionId = `js-cold-start:${crypto.randomUUID()}`;
194
- const session = createToolSession(tempDir.path(), sessionFile);
195
-
196
- const [first, second] = await Promise.all([
197
- executeJs(
198
- "globalThis.sharedMarker ??= crypto.randomUUID(); await Bun.sleep(50); return globalThis.sharedMarker;",
199
- {
200
- sessionId,
201
- session,
202
- sessionFile,
203
- },
204
- ),
205
- executeJs("globalThis.sharedMarker ??= crypto.randomUUID(); return globalThis.sharedMarker;", {
206
- sessionId,
207
- session,
208
- sessionFile,
209
- }),
210
- ]);
211
- const third = await executeJs("return globalThis.sharedMarker;", { sessionId, session, sessionFile });
212
-
213
- expect(first.exitCode).toBe(0);
214
- expect(second.exitCode).toBe(0);
215
- expect(third.exitCode).toBe(0);
216
- expect(first.output.trim()).toBe(second.output.trim());
217
- expect(third.output.trim()).toBe(first.output.trim());
218
- });
219
-
220
- it("deduplicates concurrent first Python session acquisition", async () => {
221
- using tempDir = TempDir.createSync("@omp-eval-py-cold-start-");
222
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
223
- const sessionId = `py-cold-start:${crypto.randomUUID()}`;
224
-
225
- const [first, second] = await Promise.all([
226
- executePython(
227
- `import asyncio, uuid
228
- shared_marker = globals().get("shared_marker") or str(uuid.uuid4())
229
- globals()["shared_marker"] = shared_marker
230
- await asyncio.sleep(0.05)
231
- print(shared_marker)`,
232
- { cwd: tempDir.path(), sessionId, sessionFile },
233
- ),
234
- executePython(
235
- `import uuid
236
- shared_marker = globals().get("shared_marker") or str(uuid.uuid4())
237
- globals()["shared_marker"] = shared_marker
238
- print(shared_marker)`,
239
- { cwd: tempDir.path(), sessionId, sessionFile },
240
- ),
241
- ]);
242
- const third = await executePython("print(shared_marker)", { cwd: tempDir.path(), sessionId, sessionFile });
243
-
244
- expect(first.exitCode).toBe(0);
245
- expect(second.exitCode).toBe(0);
246
- expect(third.exitCode).toBe(0);
247
- expect(first.output.trim()).toBe(second.output.trim());
248
- expect(third.output.trim()).toBe(first.output.trim());
249
- });
250
-
251
- it("splits retained Python kernels by cwd for one shared session id", async () => {
252
- using tempDir = TempDir.createSync("@omp-eval-py-cwd-");
253
- const dirA = path.join(tempDir.path(), "a");
254
- const dirB = path.join(tempDir.path(), "b");
255
- await fs.mkdir(dirA);
256
- await fs.mkdir(dirB);
257
- const realDirA = await fs.realpath(dirA);
258
- const realDirB = await fs.realpath(dirB);
259
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
260
- const sessionId = `py-cwd:${crypto.randomUUID()}`;
261
-
262
- const first = await executePython(
263
- `import os
264
- token = "from-a"
265
- print(os.getcwd())`,
266
- {
267
- cwd: dirA,
268
- sessionId,
269
- sessionFile,
270
- },
271
- );
272
- const second = await executePython(
273
- `import os
274
- print(os.getcwd())
275
- print("token" in globals())`,
276
- {
277
- cwd: dirB,
278
- sessionId,
279
- sessionFile,
280
- },
281
- );
282
- const third = await executePython("print(token)", { cwd: dirA, sessionId, sessionFile });
283
-
284
- expect(first.exitCode).toBe(0);
285
- expect(first.output.trim()).toBe(realDirA);
286
- expect(second.exitCode).toBe(0);
287
- expect(second.output.trim().split("\n")).toEqual([realDirB, "False"]);
288
- expect(third.exitCode).toBe(0);
289
- expect(third.output.trim()).toBe("from-a");
290
- });
291
-
292
- it("interrupts timed out synchronous Python cells before they mutate shared state", async () => {
293
- using tempDir = TempDir.createSync("@omp-eval-py-sync-timeout-");
294
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
295
- const sessionId = `py-sync-timeout:${crypto.randomUUID()}`;
296
-
297
- const timedOut = await executePython("import time\ntime.sleep(0.2)\nleaked_after_timeout = True", {
298
- cwd: tempDir.path(),
299
- sessionId,
300
- sessionFile,
301
- timeoutMs: 20,
302
- });
303
- await Bun.sleep(250);
304
- const probe = await executePython('print("leaked_after_timeout" in globals())', {
305
- cwd: tempDir.path(),
306
- sessionId,
307
- sessionFile,
308
- });
309
-
310
- expect(timedOut.cancelled).toBe(true);
311
- expect(probe.exitCode).toBe(0);
312
- expect(probe.output.trim()).toBe("False");
313
- });
314
-
315
- it("settles Python cells that raise SystemExit", async () => {
316
- using tempDir = TempDir.createSync("@omp-eval-py-system-exit-");
317
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
318
- const sessionId = `py-system-exit:${crypto.randomUUID()}`;
319
-
320
- const result = await executePython('raise SystemExit("bye")', {
321
- cwd: tempDir.path(),
322
- sessionId,
323
- sessionFile,
324
- timeoutMs: 500,
325
- });
326
-
327
- expect(result.exitCode).toBe(1);
328
- expect(result.output).toContain("SystemExit");
329
- expect(result.output).toContain("bye");
330
- });
331
-
332
- it("lets a subagent inherit parent JavaScript and Python eval state", async () => {
333
- using tempDir = TempDir.createSync("@omp-eval-subagent-");
334
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
335
- const evalSessionId = `session:${sessionFile}:cwd:${tempDir.path()}`;
336
- const parentSession = createToolSession(tempDir.path(), sessionFile, evalSessionId);
337
- let seenJs = "";
338
- let seenPy = "";
339
- let capturedOptions: CreateAgentSessionOptions | undefined;
340
-
341
- await executeJs('globalThis.parentSecret = "hello-js";', {
342
- sessionId: `js:${evalSessionId}`,
343
- session: parentSession,
344
- sessionFile,
345
- });
346
- await executePython('parent_secret = "hello-py"', {
347
- cwd: tempDir.path(),
348
- sessionId: `python:${evalSessionId}`,
349
- sessionFile,
350
- });
351
-
352
- vi.spyOn(discoveryModule, "discoverAgents").mockResolvedValue({ agents: [taskAgent], projectAgentsDir: null });
353
- vi.spyOn(sdkModule, "createAgentSession").mockImplementation(async (options = {}) => {
354
- capturedOptions = options;
355
- const inherited = options.parentEvalSessionId;
356
- if (!inherited) throw new Error("Missing parent eval session id");
357
- return {
358
- session: createYieldingSubagentSession(async () => {
359
- const jsResult = await executeJs("return globalThis.parentSecret;", {
360
- sessionId: `js:${inherited}`,
361
- session: parentSession,
362
- sessionFile,
363
- });
364
- const pyResult = await executePython("print(parent_secret)", {
365
- cwd: tempDir.path(),
366
- sessionId: `python:${inherited}`,
367
- sessionFile,
368
- });
369
- seenJs = jsResult.output.trim();
370
- seenPy = pyResult.output.trim();
371
- }),
372
- extensionsResult: {} as unknown as LoadExtensionsResult,
373
- setToolUIContext: () => {},
374
- eventBus: new EventBus(),
375
- } satisfies CreateAgentSessionResult;
376
- });
377
-
378
- const tool = await TaskTool.create(parentSession);
379
- await tool.execute("tool-call", taskParams);
380
-
381
- expect(capturedOptions?.parentEvalSessionId).toBe(evalSessionId);
382
- expect(seenJs).toBe("hello-js");
383
- expect(seenPy).toBe("hello-py");
384
- });
385
-
386
- it("routes interleaved JavaScript display output to the matching run", async () => {
387
- using tempDir = TempDir.createSync("@omp-eval-js-interleave-");
388
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
389
- const sessionId = `js-interleave:${crypto.randomUUID()}`;
390
- const session = createToolSession(tempDir.path(), sessionFile);
391
-
392
- const first = executeJs('await Bun.sleep(80); display({ label: "A" });', {
393
- sessionId,
394
- session,
395
- sessionFile,
396
- });
397
- await Bun.sleep(10);
398
- const second = executeJs('display({ label: "B" });', {
399
- sessionId,
400
- session,
401
- sessionFile,
402
- });
403
-
404
- const [firstResult, secondResult] = await Promise.all([first, second]);
405
- expect(firstResult.exitCode).toBe(0);
406
- expect(secondResult.exitCode).toBe(0);
407
- expect(firstResult.displayOutputs).toEqual([{ type: "json", data: { label: "A" } }]);
408
- expect(secondResult.displayOutputs).toEqual([{ type: "json", data: { label: "B" } }]);
409
- });
410
-
411
- it("routes interleaved Python display output to the matching run", async () => {
412
- using tempDir = TempDir.createSync("@omp-eval-py-interleave-");
413
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
414
- const sessionId = `py-interleave:${crypto.randomUUID()}`;
415
-
416
- const first = executePython(
417
- `import asyncio
418
- await asyncio.sleep(0.08)
419
- display({"label": "A"})`,
420
- {
421
- cwd: tempDir.path(),
422
- sessionId,
423
- sessionFile,
424
- },
425
- );
426
- await Bun.sleep(10);
427
- const second = executePython('display({"label": "B"})', {
428
- cwd: tempDir.path(),
429
- sessionId,
430
- sessionFile,
431
- });
432
-
433
- const [firstResult, secondResult] = await Promise.all([first, second]);
434
- expect(firstResult.exitCode).toBe(0);
435
- expect(secondResult.exitCode).toBe(0);
436
- expect(firstResult.displayOutputs).toEqual([{ type: "json", data: { label: "A" } }]);
437
- expect(secondResult.displayOutputs).toEqual([{ type: "json", data: { label: "B" } }]);
438
- });
439
- it("preserves module-level singleton state across re-imports of an unchanged file", async () => {
440
- using tempDir = TempDir.createSync("@omp-eval-js-mtime-");
441
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
442
- const sessionId = `js-mtime:${crypto.randomUUID()}`;
443
- const session = createToolSession(tempDir.path(), sessionFile);
444
- const modulePath = path.join(tempDir.path(), "singleton.ts");
445
- const moduleSpec = JSON.stringify(modulePath);
446
- await Bun.write(
447
- modulePath,
448
- "let value = 0;\nexport function set(v) { value = v; }\nexport function get() { return value; }\n",
449
- );
450
-
451
- const initResult = await executeJs(`const mod = await import(${moduleSpec}); mod.set(42); return mod.get();`, {
452
- sessionId,
453
- session,
454
- sessionFile,
455
- });
456
- expect(initResult.exitCode).toBe(0);
457
- expect(initResult.output.trim()).toBe("42");
458
-
459
- // Unchanged file: re-import must reuse the existing module namespace so the
460
- // counter is still 42. This is the regression — the previous unconditional
461
- // `delete require.cache[target]` reset singletons on every dynamic import.
462
- const reuseResult = await executeJs(`const mod = await import(${moduleSpec}); return mod.get();`, {
463
- sessionId,
464
- session,
465
- sessionFile,
466
- });
467
- expect(reuseResult.exitCode).toBe(0);
468
- expect(reuseResult.output.trim()).toBe("42");
469
-
470
- // Bump mtime by 5s to simulate an edit; the next import must evict the cache
471
- // and re-evaluate the file, dropping the counter back to its initializer.
472
- const future = new Date(Date.now() + 5_000);
473
- await fs.utimes(modulePath, future, future);
474
-
475
- const reloadResult = await executeJs(`const mod = await import(${moduleSpec}); return mod.get();`, {
476
- sessionId,
477
- session,
478
- sessionFile,
479
- });
480
- expect(reloadResult.exitCode).toBe(0);
481
- expect(reloadResult.output.trim()).toBe("0");
482
- });
483
-
484
- it("reloads a local re-export when a transitive dependency changes", async () => {
485
- using tempDir = TempDir.createSync("@omp-eval-js-transitive-");
486
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
487
- const sessionId = `js-transitive:${crypto.randomUUID()}`;
488
- const session = createToolSession(tempDir.path(), sessionFile);
489
- const leafPath = path.join(tempDir.path(), "leaf.ts");
490
- const entryPath = path.join(tempDir.path(), "entry.ts");
491
- const entrySpec = JSON.stringify(entryPath);
492
- await Bun.write(leafPath, "export const value = 1;\n");
493
- await Bun.write(entryPath, 'export { value } from "./leaf.ts";\n');
494
-
495
- const initial = await executeJs(`const mod = await import(${entrySpec}); return mod.value;`, {
496
- sessionId,
497
- session,
498
- sessionFile,
499
- });
500
- expect(initial.exitCode).toBe(0);
501
- expect(initial.output.trim()).toBe("1");
502
-
503
- await Bun.write(leafPath, "export const value = 2;\n");
504
- const future = new Date(Date.now() + 5_000);
505
- await fs.utimes(leafPath, future, future);
506
-
507
- const reloaded = await executeJs(`const mod = await import(${entrySpec}); return mod.value;`, {
508
- sessionId,
509
- session,
510
- sessionFile,
511
- });
512
- expect(reloaded.exitCode).toBe(0);
513
- expect(reloaded.output.trim()).toBe("2");
514
- });
515
-
516
- it("links a cyclic local module graph without crashing", async () => {
517
- // Regression: the loader used to link()+evaluate() each local module individually
518
- // inside the recursive linker callback. On any import cycle that re-entered Bun's
519
- // node:vm linker mid-instantiation and segfaulted the process (SIGTRAP,
520
- // getImportedModule on a null record) — e.g. `await import("…/edit/streaming.ts")`,
521
- // whose relative-import subtree is cyclic. The graph must now link in a single pass.
522
- using tempDir = TempDir.createSync("@omp-eval-js-cycle-");
523
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
524
- const sessionId = `js-cycle:${crypto.randomUUID()}`;
525
- const session = createToolSession(tempDir.path(), sessionFile);
526
- const alphaPath = path.join(tempDir.path(), "alpha.ts");
527
- const betaPath = path.join(tempDir.path(), "beta.ts");
528
- const alphaSpec = JSON.stringify(alphaPath);
529
- const betaSpec = JSON.stringify(betaPath);
530
- await Bun.write(
531
- alphaPath,
532
- 'import { betaName } from "./beta.ts";\nexport const alphaName = "alpha";\nexport function combined() { return alphaName + ":" + betaName; }\n',
533
- );
534
- await Bun.write(
535
- betaPath,
536
- 'import { alphaName } from "./alpha.ts";\nexport const betaName = "beta";\nexport function viaAlpha() { return alphaName; }\n',
537
- );
538
-
539
- const result = await executeJs(
540
- `const a = await import(${alphaSpec});\nconst b = await import(${betaSpec});\nreturn [a.combined(), b.viaAlpha()].join("|");`,
541
- { sessionId, session, sessionFile },
542
- );
543
-
544
- expect(result.exitCode).toBe(0);
545
- expect(result.output.trim()).toBe("alpha:beta|alpha");
546
- });
547
-
548
- it("loads TypeScript type-only imports in cells and local modules", async () => {
549
- using tempDir = TempDir.createSync("@omp-eval-js-type-imports-");
550
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
551
- const sessionId = `js-type-imports:${crypto.randomUUID()}`;
552
- const session = createToolSession(tempDir.path(), sessionFile);
553
- const typesPath = path.join(tempDir.path(), "types.ts");
554
- const valuesPath = path.join(tempDir.path(), "values.ts");
555
- const entryPath = path.join(tempDir.path(), "entry.ts");
556
- const typesSpec = JSON.stringify(typesPath);
557
- const entrySpec = JSON.stringify(entryPath);
558
- await Bun.write(typesPath, "export interface TypeOnly { value: number }\n");
559
- await Bun.write(valuesPath, "export interface InlineOnly { value: number }\nexport const imported = 41;\n");
560
- await Bun.write(
561
- entryPath,
562
- [
563
- 'import type { TypeOnly } from "./types.ts";',
564
- 'import { type InlineOnly, imported } from "./values.ts";',
565
- "export const typeOnly = 1;",
566
- "export const inlineType = imported;",
567
- "",
568
- ].join("\n"),
569
- );
570
-
571
- const result = await executeJs(
572
- `import type { TypeOnly } from ${typesSpec};\nconst mod = await import(${entrySpec});\nreturn mod.typeOnly + mod.inlineType;`,
573
- {
574
- sessionId,
575
- session,
576
- sessionFile,
577
- },
578
- );
579
-
580
- expect(result.exitCode).toBe(0);
581
- expect(result.output.trim()).toBe("42");
582
- });
583
-
584
- it("refreshes the Python tool proxy when bridge env appears after kernel warm-up", async () => {
585
- using tempDir = TempDir.createSync("@omp-eval-py-tool-proxy-");
586
- const sessionFile = path.join(tempDir.path(), "session.jsonl");
587
- const sessionId = `py-tool-proxy:${crypto.randomUUID()}`;
588
- const bridgeCalls: unknown[] = [];
589
- const bridgeSession = createBridgeToolSession("bridge-ok", bridgeCalls);
590
-
591
- const withoutBridge = await executePython(
592
- 'try:\n print(tool.read({"path": "foo.txt"}))\nexcept Exception as exc:\n print(type(exc).__name__)\n print(str(exc))',
593
- { cwd: tempDir.path(), sessionId, sessionFile },
594
- );
595
- const withBridge = await executePython('print(tool.read({"path": "foo.txt"}))', {
596
- cwd: tempDir.path(),
597
- sessionId,
598
- sessionFile,
599
- toolSession: bridgeSession,
600
- });
601
-
602
- expect(withoutBridge.exitCode).toBe(0);
603
- expect(withoutBridge.output).toContain("RuntimeError");
604
- expect(withoutBridge.output).toContain("tool bridge is unavailable");
605
- expect(withBridge.exitCode).toBe(0);
606
- expect(withBridge.output.trim()).toBe("bridge-ok");
607
- expect(bridgeCalls).toEqual([{ path: "foo.txt", _i: "py prelude" }]);
608
- });
609
- });