@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.7
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.
- package/CHANGELOG.md +54 -0
- package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-5k4dq4g6.md} +54 -0
- package/dist/cli.js +3342 -3274
- package/dist/types/capability/index.d.ts +1 -1
- package/dist/types/capability/types.d.ts +23 -1
- package/dist/types/config/settings-schema.d.ts +39 -2
- package/dist/types/cursor.d.ts +2 -1
- package/dist/types/extensibility/extensions/runner.d.ts +4 -0
- package/dist/types/extensibility/shared-events.d.ts +18 -1
- package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
- package/dist/types/internal-urls/parse.d.ts +12 -0
- package/dist/types/internal-urls/router.d.ts +6 -0
- package/dist/types/internal-urls/types.d.ts +6 -0
- package/dist/types/lsp/config.d.ts +1 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +5 -0
- package/dist/types/mcp/tool-bridge.d.ts +13 -0
- package/dist/types/modes/components/custom-editor.d.ts +5 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/session/agent-session-types.d.ts +8 -6
- package/dist/types/session/agent-session.d.ts +28 -1
- package/dist/types/session/model-controls.d.ts +4 -1
- package/dist/types/session/session-advisors.d.ts +7 -1
- package/dist/types/session/session-tools.d.ts +44 -6
- package/dist/types/session/streaming-output.d.ts +7 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -4
- package/dist/types/session/turn-recovery.d.ts +5 -3
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/types.d.ts +3 -11
- package/dist/types/thinking.d.ts +21 -2
- package/dist/types/tools/index.d.ts +8 -3
- package/dist/types/tools/output-meta.d.ts +5 -0
- package/dist/types/tools/read.d.ts +9 -1
- package/dist/types/tools/xdev.d.ts +53 -67
- package/dist/types/utils/cpuprofile.d.ts +51 -0
- package/dist/types/utils/inspect-image-mode.d.ts +29 -0
- package/dist/types/utils/profile-tree.d.ts +47 -0
- package/dist/types/utils/sample-profile.d.ts +67 -0
- package/dist/types/utils/title-generator.d.ts +17 -16
- package/package.json +12 -12
- package/src/capability/index.ts +43 -12
- package/src/capability/mcp.ts +21 -0
- package/src/capability/types.ts +20 -1
- package/src/cli/read-cli.ts +44 -2
- package/src/config/settings-schema.ts +42 -2
- package/src/config/settings.ts +35 -0
- package/src/cursor.ts +4 -3
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
- package/src/eval/py/runner.py +16 -2
- package/src/extensibility/extensions/runner.ts +117 -5
- package/src/extensibility/extensions/types.ts +0 -1
- package/src/extensibility/extensions/wrapper.ts +74 -42
- package/src/extensibility/hooks/tool-wrapper.ts +11 -4
- package/src/extensibility/hooks/types.ts +0 -1
- package/src/extensibility/shared-events.ts +18 -1
- package/src/internal-urls/mcp-protocol.ts +17 -3
- package/src/internal-urls/parse.ts +31 -0
- package/src/internal-urls/router.ts +24 -4
- package/src/internal-urls/types.ts +6 -0
- package/src/live/transport.ts +2 -2
- package/src/lsp/client.ts +2 -2
- package/src/lsp/config.ts +4 -0
- package/src/lsp/types.ts +2 -0
- package/src/mcp/config.ts +26 -14
- package/src/mcp/manager.ts +26 -9
- package/src/mcp/tool-bridge.ts +52 -1
- package/src/memories/index.ts +25 -6
- package/src/modes/components/custom-editor.ts +39 -16
- package/src/modes/components/status-line/segments.ts +3 -1
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +8 -7
- package/src/modes/controllers/command-controller.ts +10 -10
- package/src/modes/controllers/extension-ui-controller.ts +7 -7
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/rpc/rpc-mode.ts +60 -47
- package/src/prompts/steering/user-interjection.md +2 -5
- package/src/prompts/tools/task.md +4 -2
- package/src/sdk.ts +64 -58
- package/src/session/agent-session-types.ts +8 -5
- package/src/session/agent-session.ts +144 -11
- package/src/session/model-controls.ts +48 -12
- package/src/session/session-advisors.ts +30 -14
- package/src/session/session-listing.ts +66 -4
- package/src/session/session-tools.ts +162 -54
- package/src/session/streaming-output.ts +18 -6
- package/src/session/tool-choice-queue.ts +19 -4
- package/src/session/turn-recovery.ts +25 -6
- package/src/slash-commands/builtin-registry.ts +69 -0
- package/src/task/executor.ts +11 -3
- package/src/task/index.ts +43 -32
- package/src/task/types.ts +12 -17
- package/src/thinking.ts +68 -5
- package/src/tools/bash.ts +16 -9
- package/src/tools/index.ts +36 -16
- package/src/tools/output-meta.ts +20 -0
- package/src/tools/read.ts +89 -15
- package/src/tools/write.ts +16 -7
- package/src/tools/xdev.ts +198 -210
- package/src/utils/cpuprofile.ts +235 -0
- package/src/utils/inspect-image-mode.ts +39 -0
- package/src/utils/profile-tree.ts +111 -0
- package/src/utils/sample-profile.ts +437 -0
- package/src/utils/title-generator.ts +88 -34
- package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
- package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
- package/dist/types/cli/update-cli.test.d.ts +0 -1
- package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
- package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
- package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
- package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
- package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
- package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
- package/dist/types/hindsight/client.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
- package/dist/types/launch/broker-list-order.test.d.ts +0 -1
- package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
- package/dist/types/launch/protocol.test.d.ts +0 -1
- package/dist/types/launch/spawn-options.test.d.ts +0 -1
- package/dist/types/launch/terminal-output.test.d.ts +0 -1
- package/dist/types/live/protocol.test.d.ts +0 -1
- package/dist/types/mcp/config-writer.test.d.ts +0 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
- package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
- package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
- package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
- package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
- package/dist/types/modes/print-mode.test.d.ts +0 -1
- package/dist/types/modes/session-teardown.test.d.ts +0 -1
- package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
- package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
- package/dist/types/modes/warp-events.test.d.ts +0 -1
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
- package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
- package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
- package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
- package/dist/types/session/blob-store.test.d.ts +0 -1
- package/dist/types/session/messages.test.d.ts +0 -1
- package/dist/types/session/session-context.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
- package/dist/types/system-prompt.test.d.ts +0 -1
- package/dist/types/task/render.test.d.ts +0 -1
- package/dist/types/task/spawn-policy.test.d.ts +0 -1
- package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
- package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
- package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
- package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
- package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
- package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
- package/src/advisor/__tests__/advisor.test.ts +0 -4889
- package/src/advisor/__tests__/config.test.ts +0 -349
- package/src/advisor/__tests__/emission-guard.test.ts +0 -147
- package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
- package/src/cli/update-cli.test.ts +0 -28
- package/src/config/__tests__/model-registry.test.ts +0 -182
- package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
- package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
- package/src/eval/__tests__/budget-bridge.test.ts +0 -80
- package/src/eval/__tests__/completion-bridge.test.ts +0 -412
- package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
- package/src/eval/__tests__/idle-timeout.test.ts +0 -80
- package/src/eval/__tests__/js-context-manager.test.ts +0 -456
- package/src/eval/__tests__/julia-prelude.test.ts +0 -66
- package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
- package/src/eval/__tests__/prelude-agent.test.ts +0 -156
- package/src/eval/__tests__/process-entry-import.test.ts +0 -137
- package/src/eval/py/__tests__/prelude.test.ts +0 -104
- package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
- package/src/hindsight/client.test.ts +0 -75
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
- package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
- package/src/launch/broker-list-order.test.ts +0 -89
- package/src/launch/broker-output-snapshot.test.ts +0 -126
- package/src/launch/protocol.test.ts +0 -59
- package/src/launch/spawn-options.test.ts +0 -31
- package/src/launch/terminal-output.test.ts +0 -107
- package/src/live/protocol.test.ts +0 -140
- package/src/mcp/config-writer.test.ts +0 -43
- package/src/mcp/smithery-auth.test.ts +0 -29
- package/src/mcp/smithery-registry.test.ts +0 -51
- package/src/mcp/transports/stdio.test.ts +0 -427
- package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
- package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
- package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
- package/src/modes/components/__tests__/skill-message.test.ts +0 -94
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
- package/src/modes/components/custom-editor.test.ts +0 -510
- package/src/modes/components/login-dialog.test.ts +0 -56
- package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
- package/src/modes/components/status-line/component.test.ts +0 -84
- package/src/modes/components/tool-execution.test.ts +0 -162
- package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
- package/src/modes/noninteractive-dispose.test.ts +0 -73
- package/src/modes/print-mode.test.ts +0 -71
- package/src/modes/session-teardown.test.ts +0 -219
- package/src/modes/theme/mermaid-rendering.test.ts +0 -53
- package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
- package/src/modes/warp-events.test.ts +0 -794
- package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
- package/src/plan-mode/model-transition.test.ts +0 -60
- package/src/plan-mode/reentry-prompt.test.ts +0 -41
- package/src/session/agent-session-error-log.test.ts +0 -59
- package/src/session/blob-store.test.ts +0 -56
- package/src/session/messages.test.ts +0 -282
- package/src/session/session-context.test.ts +0 -384
- package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
- package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
- package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
- package/src/system-prompt.test.ts +0 -236
- package/src/task/render.test.ts +0 -290
- package/src/task/spawn-policy.test.ts +0 -62
- package/src/tools/__tests__/eval-description.test.ts +0 -18
- package/src/tools/__tests__/glob.test.ts +0 -37
- package/src/tools/__tests__/json-tree.test.ts +0 -35
- package/src/tools/__tests__/vibe-render.test.ts +0 -210
- package/src/tools/hub/launch-compat.test.ts +0 -40
- package/src/vibe/__tests__/token-rate.test.ts +0 -96
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { TempDir } from "@oh-my-pi/pi-utils/temp";
|
|
4
|
-
import { createHelpers, type HelperContext } from "../js/shared/helpers";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The eval helpers (`read`/`write`) must substitute injected on-disk
|
|
8
|
-
* roots for internal-URL schemes. Without it, `write("local://x.md")` hits a
|
|
9
|
-
* stdlib `path.resolve` that collapses `local://` to `local:/`, creating a junk
|
|
10
|
-
* `local:` directory under the cwd instead of landing where `read local://x.md`
|
|
11
|
-
* resolves. These lock the substitution contract and its guards.
|
|
12
|
-
*/
|
|
13
|
-
function makeCtx(cwd: string, roots: Record<string, string>): HelperContext {
|
|
14
|
-
return {
|
|
15
|
-
cwd: () => cwd,
|
|
16
|
-
env: new Map(),
|
|
17
|
-
localRoots: () => roots,
|
|
18
|
-
emitStatus: () => {},
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe("eval js helpers internal-url resolution", () => {
|
|
23
|
-
it("writes and reads local:// under the injected root", async () => {
|
|
24
|
-
using tmp = TempDir.createSync("@eval-helpers-local-");
|
|
25
|
-
const root = path.join(tmp.path(), "local");
|
|
26
|
-
const helpers = createHelpers(makeCtx(tmp.path(), { local: root }));
|
|
27
|
-
|
|
28
|
-
const written = await helpers.writeFile("local://notes/merge-map.md", "hello");
|
|
29
|
-
expect(written).toBe(path.join(root, "notes", "merge-map.md"));
|
|
30
|
-
expect(await Bun.file(written).text()).toBe("hello");
|
|
31
|
-
expect(await helpers.read("local://notes/merge-map.md")).toBe("hello");
|
|
32
|
-
|
|
33
|
-
// Regression: no literal `local:` directory created under the cwd.
|
|
34
|
-
expect(await Bun.file(path.join(tmp.path(), "local:")).exists()).toBe(false);
|
|
35
|
-
expect(await Bun.file(path.join(tmp.path(), "local:", "notes", "merge-map.md")).exists()).toBe(false);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("rejects traversal and schemes without an injected root", async () => {
|
|
39
|
-
using tmp = TempDir.createSync("@eval-helpers-guard-");
|
|
40
|
-
const helpers = createHelpers(makeCtx(tmp.path(), { local: path.join(tmp.path(), "local") }));
|
|
41
|
-
|
|
42
|
-
await expect(helpers.writeFile("local://../escape.md", "x")).rejects.toThrow(/traversal|escapes/i);
|
|
43
|
-
await expect(helpers.writeFile("memory://x.md", "x")).rejects.toThrow(/not supported/i);
|
|
44
|
-
await expect(helpers.read("https://example.com/page")).rejects.toThrow(/not supported/i);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("leaves plain relative and absolute paths resolving against the cwd", async () => {
|
|
48
|
-
using tmp = TempDir.createSync("@eval-helpers-plain-");
|
|
49
|
-
const helpers = createHelpers(makeCtx(tmp.path(), {}));
|
|
50
|
-
|
|
51
|
-
const rel = await helpers.writeFile("foo/bar.txt", "bar");
|
|
52
|
-
expect(rel).toBe(path.join(tmp.path(), "foo", "bar.txt"));
|
|
53
|
-
expect(await helpers.read("foo/bar.txt")).toBe("bar");
|
|
54
|
-
});
|
|
55
|
-
});
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { IdleTimeout } from "../idle-timeout";
|
|
3
|
-
|
|
4
|
-
/** Resolve true if `signal` aborts within `ms`, false if the window elapses first. */
|
|
5
|
-
function abortedWithin(signal: AbortSignal, ms: number): Promise<boolean> {
|
|
6
|
-
if (signal.aborted) return Promise.resolve(true);
|
|
7
|
-
const { promise, resolve } = Promise.withResolvers<boolean>();
|
|
8
|
-
const timer = setTimeout(() => resolve(false), ms);
|
|
9
|
-
signal.addEventListener(
|
|
10
|
-
"abort",
|
|
11
|
-
() => {
|
|
12
|
-
clearTimeout(timer);
|
|
13
|
-
resolve(true);
|
|
14
|
-
},
|
|
15
|
-
{ once: true },
|
|
16
|
-
);
|
|
17
|
-
return promise;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe("IdleTimeout", () => {
|
|
21
|
-
it("aborts with a TimeoutError reason once the idle window elapses with no activity", async () => {
|
|
22
|
-
using idle = new IdleTimeout(40);
|
|
23
|
-
expect(idle.signal.aborted).toBe(false);
|
|
24
|
-
|
|
25
|
-
const fired = await abortedWithin(idle.signal, 500);
|
|
26
|
-
expect(fired).toBe(true);
|
|
27
|
-
expect(idle.signal.aborted).toBe(true);
|
|
28
|
-
// The reason must be a TimeoutError so downstream timeout detection
|
|
29
|
-
// (kernel `isTimeoutReason`, executor `isTimedOutCancellation`) classifies
|
|
30
|
-
// the cancellation as a timeout rather than a plain abort.
|
|
31
|
-
expect(idle.signal.reason).toBeInstanceOf(DOMException);
|
|
32
|
-
expect((idle.signal.reason as DOMException).name).toBe("TimeoutError");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("ignores elapsed time while paused and resumes with a fresh window", async () => {
|
|
36
|
-
using idle = new IdleTimeout(80);
|
|
37
|
-
idle.pause();
|
|
38
|
-
await Bun.sleep(160);
|
|
39
|
-
expect(idle.signal.aborted).toBe(false);
|
|
40
|
-
|
|
41
|
-
idle.resume();
|
|
42
|
-
const firedEarly = await abortedWithin(idle.signal, 30);
|
|
43
|
-
expect(firedEarly).toBe(false);
|
|
44
|
-
const fired = await abortedWithin(idle.signal, 500);
|
|
45
|
-
expect(fired).toBe(true);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("reference-counts overlapping pauses", async () => {
|
|
49
|
-
using idle = new IdleTimeout(60);
|
|
50
|
-
idle.pause();
|
|
51
|
-
idle.pause();
|
|
52
|
-
await Bun.sleep(120);
|
|
53
|
-
expect(idle.signal.aborted).toBe(false);
|
|
54
|
-
|
|
55
|
-
idle.resume();
|
|
56
|
-
await Bun.sleep(90);
|
|
57
|
-
expect(idle.signal.aborted).toBe(false);
|
|
58
|
-
|
|
59
|
-
idle.resume();
|
|
60
|
-
const fired = await abortedWithin(idle.signal, 500);
|
|
61
|
-
expect(fired).toBe(true);
|
|
62
|
-
});
|
|
63
|
-
it("never fires after dispose()", async () => {
|
|
64
|
-
const idle = new IdleTimeout(30);
|
|
65
|
-
idle.dispose();
|
|
66
|
-
const fired = await abortedWithin(idle.signal, 150);
|
|
67
|
-
expect(fired).toBe(false);
|
|
68
|
-
expect(idle.signal.aborted).toBe(false);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("ignores pause/resume after the watchdog has already fired", async () => {
|
|
72
|
-
using idle = new IdleTimeout(30);
|
|
73
|
-
await abortedWithin(idle.signal, 500);
|
|
74
|
-
expect(idle.signal.aborted).toBe(true);
|
|
75
|
-
// Late activity must not un-abort or rearm a settled watchdog.
|
|
76
|
-
idle.pause();
|
|
77
|
-
idle.resume();
|
|
78
|
-
expect(idle.signal.aborted).toBe(true);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
@@ -1,456 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { TempDir } from "@oh-my-pi/pi-utils";
|
|
5
|
-
import { Settings } from "../../config/settings";
|
|
6
|
-
import type { ToolSession } from "../../tools";
|
|
7
|
-
import {
|
|
8
|
-
disposeAllVmContexts,
|
|
9
|
-
setJsEvalWorkerThreadForTests,
|
|
10
|
-
setWorkerCloseTimeoutMsForTests,
|
|
11
|
-
} from "../js/context-manager";
|
|
12
|
-
import { executeJs } from "../js/executor";
|
|
13
|
-
|
|
14
|
-
const originalWorker = globalThis.Worker;
|
|
15
|
-
|
|
16
|
-
interface FakeWorkerStats {
|
|
17
|
-
closeRequests: number;
|
|
18
|
-
terminateCalls: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface FakeWorkerBehavior {
|
|
22
|
-
exitOnClose: boolean;
|
|
23
|
-
settleRuns: boolean;
|
|
24
|
-
errorOnStart?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function makeSession(cwd: string): ToolSession {
|
|
28
|
-
return {
|
|
29
|
-
cwd,
|
|
30
|
-
hasUI: false,
|
|
31
|
-
settings: Settings.isolated({
|
|
32
|
-
"async.enabled": false,
|
|
33
|
-
"task.isolation.mode": "none",
|
|
34
|
-
"task.enableLsp": true,
|
|
35
|
-
}),
|
|
36
|
-
taskDepth: 0,
|
|
37
|
-
enableLsp: true,
|
|
38
|
-
getSessionFile: () => null,
|
|
39
|
-
getSessionSpawns: () => "*",
|
|
40
|
-
getActiveModelString: () => "p/active",
|
|
41
|
-
getModelString: () => "p/fallback",
|
|
42
|
-
getArtifactsDir: () => null,
|
|
43
|
-
getSessionId: () => "test-session",
|
|
44
|
-
getEvalSessionId: () => "test-eval-session",
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
|
|
49
|
-
let timeout: NodeJS.Timeout | undefined;
|
|
50
|
-
try {
|
|
51
|
-
return await Promise.race([
|
|
52
|
-
promise,
|
|
53
|
-
new Promise<never>((_, reject) => {
|
|
54
|
-
timeout = setTimeout(() => reject(new Error(`${label} timed out`)), ms);
|
|
55
|
-
}),
|
|
56
|
-
]);
|
|
57
|
-
} finally {
|
|
58
|
-
if (timeout) clearTimeout(timeout);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function waitForRealWorkerExitAfterClose(cwd: string): Promise<void> {
|
|
63
|
-
const worker = new originalWorker(new URL("../js/worker-entry.ts", import.meta.url).href, { type: "module" });
|
|
64
|
-
const ready = Promise.withResolvers<void>();
|
|
65
|
-
const runComplete = Promise.withResolvers<void>();
|
|
66
|
-
const closedAck = Promise.withResolvers<void>();
|
|
67
|
-
const workerClosed = Promise.withResolvers<void>();
|
|
68
|
-
const runId = `keep-alive:${crypto.randomUUID()}`;
|
|
69
|
-
const snapshot = { cwd, sessionId: `worker-exit:${crypto.randomUUID()}` };
|
|
70
|
-
|
|
71
|
-
worker.addEventListener("message", event => {
|
|
72
|
-
const msg = event.data as { type?: string; runId?: string; ok?: boolean };
|
|
73
|
-
if (msg.type === "ready") ready.resolve();
|
|
74
|
-
else if (msg.type === "result" && msg.runId === runId && msg.ok) runComplete.resolve();
|
|
75
|
-
else if (msg.type === "closed") closedAck.resolve();
|
|
76
|
-
});
|
|
77
|
-
worker.addEventListener("close", () => workerClosed.resolve());
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
worker.postMessage({ type: "init", snapshot });
|
|
81
|
-
await withTimeout(ready.promise, 1_000, "worker ready");
|
|
82
|
-
worker.postMessage({
|
|
83
|
-
type: "run",
|
|
84
|
-
runId,
|
|
85
|
-
code: "globalThis.__keepAlive = setInterval(() => {}, 1000);\nundefined;",
|
|
86
|
-
filename: "keep-alive.js",
|
|
87
|
-
snapshot,
|
|
88
|
-
});
|
|
89
|
-
await withTimeout(runComplete.promise, 1_000, "worker run");
|
|
90
|
-
worker.postMessage({ type: "close" });
|
|
91
|
-
await withTimeout(closedAck.promise, 1_000, "worker closed ack");
|
|
92
|
-
await withTimeout(workerClosed.promise, 1_000, "worker close event");
|
|
93
|
-
} finally {
|
|
94
|
-
worker.terminate();
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function installFakeWorker(stats: FakeWorkerStats, behavior: FakeWorkerBehavior): void {
|
|
99
|
-
class FakeWorker {
|
|
100
|
-
#messageListeners = new Set<(event: MessageEvent) => void>();
|
|
101
|
-
#closeListeners = new Set<(event: Event) => void>();
|
|
102
|
-
#errorListeners = new Set<(event: Event) => void>();
|
|
103
|
-
#readyQueued = false;
|
|
104
|
-
#exited = false;
|
|
105
|
-
|
|
106
|
-
postMessage(message: unknown): void {
|
|
107
|
-
if (!message || typeof message !== "object") return;
|
|
108
|
-
const typed = message as { type?: string; runId?: string };
|
|
109
|
-
if (typed.type === "run" && typed.runId && behavior.settleRuns) {
|
|
110
|
-
queueMicrotask(() => this.#emitMessage({ type: "result", runId: typed.runId, ok: true }));
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (typed.type === "close") {
|
|
114
|
-
stats.closeRequests++;
|
|
115
|
-
queueMicrotask(() => {
|
|
116
|
-
this.#emitMessage({ type: "closed" });
|
|
117
|
-
if (behavior.exitOnClose) this.#emitClose();
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
addEventListener(type: string, listener: (event: MessageEvent | Event) => void): void {
|
|
123
|
-
if (type === "close") {
|
|
124
|
-
this.#closeListeners.add(listener as (event: Event) => void);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
if (type === "error") {
|
|
128
|
-
this.#errorListeners.add(listener as (event: Event) => void);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (type !== "message") return;
|
|
132
|
-
this.#messageListeners.add(listener as (event: MessageEvent) => void);
|
|
133
|
-
if (!this.#readyQueued) {
|
|
134
|
-
this.#readyQueued = true;
|
|
135
|
-
queueMicrotask(() => {
|
|
136
|
-
if (behavior.errorOnStart) this.#emitError();
|
|
137
|
-
else this.#emitMessage({ type: "ready" });
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
removeEventListener(type: string, listener: (event: MessageEvent | Event) => void): void {
|
|
143
|
-
if (type === "close") {
|
|
144
|
-
this.#closeListeners.delete(listener as (event: Event) => void);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (type === "error") {
|
|
148
|
-
this.#errorListeners.delete(listener as (event: Event) => void);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
if (type !== "message") return;
|
|
152
|
-
this.#messageListeners.delete(listener as (event: MessageEvent) => void);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
terminate(): void {
|
|
156
|
-
stats.terminateCalls++;
|
|
157
|
-
this.#emitClose();
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
#emitMessage(data: unknown): void {
|
|
161
|
-
const event = new MessageEvent("message", { data });
|
|
162
|
-
for (const listener of this.#messageListeners) listener(event);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
#emitClose(): void {
|
|
166
|
-
if (this.#exited) return;
|
|
167
|
-
this.#exited = true;
|
|
168
|
-
const event = new Event("close");
|
|
169
|
-
for (const listener of this.#closeListeners) listener(event);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
#emitError(): void {
|
|
173
|
-
const event = new ErrorEvent("error", {
|
|
174
|
-
message: "fake worker failed to start",
|
|
175
|
-
error: new Error("fake worker failed to start"),
|
|
176
|
-
});
|
|
177
|
-
for (const listener of this.#errorListeners) listener(event);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
Object.defineProperty(globalThis, "Worker", {
|
|
182
|
-
configurable: true,
|
|
183
|
-
writable: true,
|
|
184
|
-
value: FakeWorker as unknown as typeof Worker,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
describe("JavaScript eval worker lifecycle", () => {
|
|
189
|
-
let restoreCloseTimeoutMs = 0;
|
|
190
|
-
let restoreWorkerThread = false;
|
|
191
|
-
beforeEach(() => {
|
|
192
|
-
restoreWorkerThread = setJsEvalWorkerThreadForTests(true);
|
|
193
|
-
// Shrink the graceful-close grace period so the "close acked but the worker
|
|
194
|
-
// never exits -> force terminate" contract is proven without a real 1s wait.
|
|
195
|
-
restoreCloseTimeoutMs = setWorkerCloseTimeoutMsForTests(1);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
afterEach(async () => {
|
|
199
|
-
// Dispose while the shrunk timeout is still active so a hung worker's afterEach
|
|
200
|
-
// close also force-terminates instantly, then restore the production default.
|
|
201
|
-
await disposeAllVmContexts();
|
|
202
|
-
setWorkerCloseTimeoutMsForTests(restoreCloseTimeoutMs);
|
|
203
|
-
Object.defineProperty(globalThis, "Worker", {
|
|
204
|
-
configurable: true,
|
|
205
|
-
writable: true,
|
|
206
|
-
value: originalWorker,
|
|
207
|
-
});
|
|
208
|
-
setJsEvalWorkerThreadForTests(restoreWorkerThread);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("exits a real worker on graceful close even with ref'ed user handles", async () => {
|
|
212
|
-
using tempDir = TempDir.createSync("@omp-js-worker-real-close-");
|
|
213
|
-
|
|
214
|
-
await waitForRealWorkerExitAfterClose(tempDir.path());
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it("waits for the worker to close on reset instead of force-terminating it", async () => {
|
|
218
|
-
using tempDir = TempDir.createSync("@omp-js-worker-close-");
|
|
219
|
-
const stats: FakeWorkerStats = { closeRequests: 0, terminateCalls: 0 };
|
|
220
|
-
installFakeWorker(stats, { exitOnClose: true, settleRuns: true });
|
|
221
|
-
|
|
222
|
-
const session = makeSession(tempDir.path());
|
|
223
|
-
const sessionId = `js-close:${crypto.randomUUID()}`;
|
|
224
|
-
|
|
225
|
-
const first = await executeJs("globalThis.marker = 1;", { cwd: tempDir.path(), sessionId, session });
|
|
226
|
-
expect(first.exitCode).toBe(0);
|
|
227
|
-
|
|
228
|
-
const second = await executeJs("globalThis.marker = 2;", {
|
|
229
|
-
cwd: tempDir.path(),
|
|
230
|
-
sessionId,
|
|
231
|
-
session,
|
|
232
|
-
reset: true,
|
|
233
|
-
});
|
|
234
|
-
expect(second.exitCode).toBe(0);
|
|
235
|
-
expect(stats.closeRequests).toBe(1);
|
|
236
|
-
expect(stats.terminateCalls).toBe(0);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("terminates when close is acknowledged but the worker does not exit", async () => {
|
|
240
|
-
using tempDir = TempDir.createSync("@omp-js-worker-close-hung-");
|
|
241
|
-
const stats: FakeWorkerStats = { closeRequests: 0, terminateCalls: 0 };
|
|
242
|
-
installFakeWorker(stats, { exitOnClose: false, settleRuns: true });
|
|
243
|
-
|
|
244
|
-
const session = makeSession(tempDir.path());
|
|
245
|
-
const sessionId = `js-close-hung:${crypto.randomUUID()}`;
|
|
246
|
-
|
|
247
|
-
const first = await executeJs("globalThis.marker = 1;", { cwd: tempDir.path(), sessionId, session });
|
|
248
|
-
expect(first.exitCode).toBe(0);
|
|
249
|
-
|
|
250
|
-
const second = await executeJs("globalThis.marker = 2;", {
|
|
251
|
-
cwd: tempDir.path(),
|
|
252
|
-
sessionId,
|
|
253
|
-
session,
|
|
254
|
-
reset: true,
|
|
255
|
-
});
|
|
256
|
-
expect(second.exitCode).toBe(0);
|
|
257
|
-
expect(stats.closeRequests).toBe(1);
|
|
258
|
-
expect(stats.terminateCalls).toBe(1);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it("force-terminates instead of closing when an in-flight run is aborted", async () => {
|
|
262
|
-
using tempDir = TempDir.createSync("@omp-js-worker-abort-");
|
|
263
|
-
const stats: FakeWorkerStats = { closeRequests: 0, terminateCalls: 0 };
|
|
264
|
-
installFakeWorker(stats, { exitOnClose: true, settleRuns: false });
|
|
265
|
-
|
|
266
|
-
const session = makeSession(tempDir.path());
|
|
267
|
-
const sessionId = `js-abort:${crypto.randomUUID()}`;
|
|
268
|
-
const controller = new AbortController();
|
|
269
|
-
const resultPromise = executeJs("globalThis.neverFinishes = true;", {
|
|
270
|
-
cwd: tempDir.path(),
|
|
271
|
-
sessionId,
|
|
272
|
-
session,
|
|
273
|
-
signal: controller.signal,
|
|
274
|
-
});
|
|
275
|
-
setTimeout(() => controller.abort(new DOMException("Execution aborted", "AbortError")), 0);
|
|
276
|
-
|
|
277
|
-
const result = await resultPromise;
|
|
278
|
-
expect(result.cancelled).toBe(true);
|
|
279
|
-
expect(stats.closeRequests).toBe(0);
|
|
280
|
-
expect(stats.terminateCalls).toBe(1);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it("falls back to a Bun Worker when the subprocess cannot spawn", async () => {
|
|
284
|
-
using tempDir = TempDir.createSync("@omp-js-spawn-fallback-");
|
|
285
|
-
// Exercise the production ladder (process -> worker -> inline), not the
|
|
286
|
-
// worker-thread test seam the surrounding describe enables.
|
|
287
|
-
setJsEvalWorkerThreadForTests(false);
|
|
288
|
-
const stats: FakeWorkerStats = { closeRequests: 0, terminateCalls: 0 };
|
|
289
|
-
installFakeWorker(stats, { exitOnClose: true, settleRuns: true });
|
|
290
|
-
const originalSpawn = Bun.spawn;
|
|
291
|
-
let spawnAttempts = 0;
|
|
292
|
-
Bun.spawn = ((): never => {
|
|
293
|
-
spawnAttempts++;
|
|
294
|
-
throw new Error("subprocess spawn unavailable");
|
|
295
|
-
}) as unknown as typeof Bun.spawn;
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
const session = makeSession(tempDir.path());
|
|
299
|
-
const sessionId = `js-spawn-fallback:${crypto.randomUUID()}`;
|
|
300
|
-
// The fake Worker settles runs without executing the cell, so an empty
|
|
301
|
-
// output proves the middle rung handled it — the inline fallback would
|
|
302
|
-
// have actually evaluated the expression and printed 42.
|
|
303
|
-
const result = await executeJs("return String(6 * 7);", { cwd: tempDir.path(), sessionId, session });
|
|
304
|
-
expect(result.exitCode).toBe(0);
|
|
305
|
-
expect(result.output.trim()).toBe("");
|
|
306
|
-
expect(spawnAttempts).toBe(1);
|
|
307
|
-
} finally {
|
|
308
|
-
Bun.spawn = originalSpawn;
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("falls back to a Bun Worker when the subprocess fails during initialization", async () => {
|
|
313
|
-
using tempDir = TempDir.createSync("@omp-js-init-fallback-");
|
|
314
|
-
// Exercise the production ladder (process -> worker -> inline), not the
|
|
315
|
-
// worker-thread test seam the surrounding describe enables.
|
|
316
|
-
setJsEvalWorkerThreadForTests(false);
|
|
317
|
-
const stats: FakeWorkerStats = { closeRequests: 0, terminateCalls: 0 };
|
|
318
|
-
installFakeWorker(stats, { exitOnClose: true, settleRuns: true });
|
|
319
|
-
const originalSpawn = Bun.spawn;
|
|
320
|
-
let spawnAttempts = 0;
|
|
321
|
-
Bun.spawn = ((options: unknown) => {
|
|
322
|
-
spawnAttempts++;
|
|
323
|
-
const spawnOptions = options as {
|
|
324
|
-
onExit?: (proc: unknown, exitCode: number | null, signalCode: string | null) => void;
|
|
325
|
-
};
|
|
326
|
-
const fakeProcess = {
|
|
327
|
-
send: () => undefined,
|
|
328
|
-
kill: () => undefined,
|
|
329
|
-
unref: () => undefined,
|
|
330
|
-
};
|
|
331
|
-
queueMicrotask(() => spawnOptions.onExit?.(fakeProcess, 1, null));
|
|
332
|
-
return fakeProcess;
|
|
333
|
-
}) as unknown as typeof Bun.spawn;
|
|
334
|
-
|
|
335
|
-
try {
|
|
336
|
-
const session = makeSession(tempDir.path());
|
|
337
|
-
const sessionId = `js-init-fallback:${crypto.randomUUID()}`;
|
|
338
|
-
// The fake Worker settles runs without executing the cell, so empty
|
|
339
|
-
// output proves the middle rung handled the retry. Inline execution
|
|
340
|
-
// would evaluate the expression and print 42.
|
|
341
|
-
const result = await executeJs("return String(6 * 7);", { cwd: tempDir.path(), sessionId, session });
|
|
342
|
-
expect(result.exitCode).toBe(0);
|
|
343
|
-
expect(result.output.trim()).toBe("");
|
|
344
|
-
expect(spawnAttempts).toBe(1);
|
|
345
|
-
} finally {
|
|
346
|
-
Bun.spawn = originalSpawn;
|
|
347
|
-
}
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it("falls back to the inline worker when the spawned worker errors during startup", async () => {
|
|
351
|
-
using tempDir = TempDir.createSync("@omp-js-worker-error-");
|
|
352
|
-
const stats: FakeWorkerStats = { closeRequests: 0, terminateCalls: 0 };
|
|
353
|
-
installFakeWorker(stats, { exitOnClose: true, settleRuns: true, errorOnStart: true });
|
|
354
|
-
|
|
355
|
-
const session = makeSession(tempDir.path());
|
|
356
|
-
const sessionId = `js-worker-error:${crypto.randomUUID()}`;
|
|
357
|
-
|
|
358
|
-
// The spawned worker emits an `error` event instead of `ready`. Without fail-fast
|
|
359
|
-
// error handling the handshake would stall until WORKER_INIT_TIMEOUT_MS (15s); with
|
|
360
|
-
// it, the handshake rejects at once and the inline worker runs the cell.
|
|
361
|
-
const result = await executeJs("return String(6 * 7);", { cwd: tempDir.path(), sessionId, session });
|
|
362
|
-
expect(result.exitCode).toBe(0);
|
|
363
|
-
expect(result.output.trim()).toBe("42");
|
|
364
|
-
// The errored primary worker is torn down before the inline retry takes over.
|
|
365
|
-
expect(stats.terminateCalls).toBe(1);
|
|
366
|
-
});
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
describe.skipIf(process.platform === "win32")("JavaScript eval process isolation", () => {
|
|
370
|
-
afterEach(async () => {
|
|
371
|
-
await disposeAllVmContexts();
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
it("runs spawned commands in the isolated POSIX process group", async () => {
|
|
375
|
-
using tempDir = TempDir.createSync("@omp-js-process-isolation-");
|
|
376
|
-
const session = makeSession(tempDir.path());
|
|
377
|
-
const evalSessionId = `js-isolation:${crypto.randomUUID()}`;
|
|
378
|
-
const result = await executeJs(
|
|
379
|
-
[
|
|
380
|
-
`const child = Bun.spawn(["/bin/sh", "-c", 'pgid=$(ps -o pgid= -p $$); printf "%s %s\\n" "$pgid" "$PPID"'], { stdout: "pipe" });`,
|
|
381
|
-
"return await new Response(child.stdout).text();",
|
|
382
|
-
].join("\n"),
|
|
383
|
-
{ cwd: tempDir.path(), sessionId: evalSessionId, session },
|
|
384
|
-
);
|
|
385
|
-
const [processGroupId, parentProcessId] = result.output.trim().split(/\s+/).map(Number);
|
|
386
|
-
expect(parentProcessId).not.toBe(process.pid);
|
|
387
|
-
expect(processGroupId).toBe(parentProcessId);
|
|
388
|
-
|
|
389
|
-
await executeJs("var saved = 41; function increment(value) { return value + 1; }", {
|
|
390
|
-
cwd: tempDir.path(),
|
|
391
|
-
sessionId: evalSessionId,
|
|
392
|
-
session,
|
|
393
|
-
});
|
|
394
|
-
const reused = await executeJs("return increment(saved);", {
|
|
395
|
-
cwd: tempDir.path(),
|
|
396
|
-
sessionId: evalSessionId,
|
|
397
|
-
session,
|
|
398
|
-
});
|
|
399
|
-
expect(reused.output.trim()).toBe("42");
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it("mirrors the session cwd onto the subprocess's real cwd", async () => {
|
|
403
|
-
using tempDir = TempDir.createSync("@omp-js-process-cwd-");
|
|
404
|
-
const session = makeSession(tempDir.path());
|
|
405
|
-
const evalSessionId = `js-cwd:${crypto.randomUUID()}`;
|
|
406
|
-
const result = await executeJs("return process.cwd();", {
|
|
407
|
-
cwd: tempDir.path(),
|
|
408
|
-
sessionId: evalSessionId,
|
|
409
|
-
session,
|
|
410
|
-
});
|
|
411
|
-
// process.chdir resolves symlinks (macOS tempdirs live under /var ->
|
|
412
|
-
// /private/var), so compare physical paths.
|
|
413
|
-
expect(result.output.trim()).toBe(fs.realpathSync(tempDir.path()));
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
it("still runs cells when the session cwd does not exist", async () => {
|
|
417
|
-
using tempDir = TempDir.createSync("@omp-js-process-cwd-missing-");
|
|
418
|
-
const missingCwd = path.join(tempDir.path(), "deleted");
|
|
419
|
-
const session = makeSession(missingCwd);
|
|
420
|
-
const result = await executeJs("return String(6 * 7);", {
|
|
421
|
-
cwd: missingCwd,
|
|
422
|
-
sessionId: `js-cwd-missing:${crypto.randomUUID()}`,
|
|
423
|
-
session,
|
|
424
|
-
});
|
|
425
|
-
expect(result.exitCode).toBe(0);
|
|
426
|
-
expect(result.output.trim()).toBe("42");
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
it("keeps the isolated process alive after handled and stackless floated rejections", async () => {
|
|
430
|
-
using tempDir = TempDir.createSync("@omp-js-process-rejection-");
|
|
431
|
-
const session = makeSession(tempDir.path());
|
|
432
|
-
const evalSessionId = `js-rejection:${crypto.randomUUID()}`;
|
|
433
|
-
const handled = await executeJs('await Promise.reject("handled rejection").catch(() => undefined); return 42;', {
|
|
434
|
-
cwd: tempDir.path(),
|
|
435
|
-
sessionId: evalSessionId,
|
|
436
|
-
session,
|
|
437
|
-
});
|
|
438
|
-
expect(handled.exitCode).toBe(0);
|
|
439
|
-
expect(handled.output.trim()).toBe("42");
|
|
440
|
-
|
|
441
|
-
const rejected = await executeJs(
|
|
442
|
-
'var savedAfterRejection = 41; Promise.reject("stackless rejection"); await Bun.sleep(10);',
|
|
443
|
-
{ cwd: tempDir.path(), sessionId: evalSessionId, session },
|
|
444
|
-
);
|
|
445
|
-
expect(rejected.exitCode).toBe(1);
|
|
446
|
-
expect(rejected.output).toContain("Unhandled rejection (missing await?): stackless rejection");
|
|
447
|
-
|
|
448
|
-
const reused = await executeJs("return savedAfterRejection + 1;", {
|
|
449
|
-
cwd: tempDir.path(),
|
|
450
|
-
sessionId: evalSessionId,
|
|
451
|
-
session,
|
|
452
|
-
});
|
|
453
|
-
expect(reused.exitCode).toBe(0);
|
|
454
|
-
expect(reused.output.trim()).toBe("42");
|
|
455
|
-
});
|
|
456
|
-
});
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { $which, TempDir } from "@oh-my-pi/pi-utils";
|
|
4
|
-
import { disposeJuliaKernelSessionsByOwner, executeJulia } from "../jl/executor";
|
|
5
|
-
|
|
6
|
-
const HAS_JULIA = Boolean($which("julia"));
|
|
7
|
-
const OWNER_ID = "julia-prelude-tests";
|
|
8
|
-
|
|
9
|
-
describe.skipIf(!HAS_JULIA)("eval Julia prelude helpers", () => {
|
|
10
|
-
afterEach(async () => {
|
|
11
|
-
await disposeJuliaKernelSessionsByOwner(OWNER_ID);
|
|
12
|
-
}, 30_000);
|
|
13
|
-
|
|
14
|
-
it("supports output ranges, JSON queries, metadata, and ANSI stripping", async () => {
|
|
15
|
-
using tempDir = TempDir.createSync("@omp-eval-julia-output-");
|
|
16
|
-
const artifactsDir = path.join(tempDir.path(), "session-artifacts");
|
|
17
|
-
await Bun.write(path.join(artifactsDir, "alpha.md"), "one\ntwo\nthree\nfour");
|
|
18
|
-
await Bun.write(path.join(artifactsDir, "json.md"), JSON.stringify({ items: [{ name: "a" }, { name: "b" }] }));
|
|
19
|
-
await Bun.write(path.join(artifactsDir, "ansi.md"), "\u001b[31mred\u001b[0m");
|
|
20
|
-
|
|
21
|
-
const result = await executeJulia(
|
|
22
|
-
`
|
|
23
|
-
println("RANGE=", replace(output("alpha", offset=2, limit=2), "\\n" => "|"))
|
|
24
|
-
println("QUERY=", output("json", query=".items[1].name"))
|
|
25
|
-
println("STRIPPED=", output("ansi", format="stripped"))
|
|
26
|
-
meta = output("alpha", format="json")
|
|
27
|
-
println("META=", meta["id"], ":", meta["char_count"] > 0)
|
|
28
|
-
multi = output("alpha", "json")
|
|
29
|
-
println("MULTI=", length(multi), ":", multi[1]["id"], ":", multi[2]["id"])
|
|
30
|
-
nothing
|
|
31
|
-
`,
|
|
32
|
-
{
|
|
33
|
-
cwd: tempDir.path(),
|
|
34
|
-
artifactsDir,
|
|
35
|
-
sessionId: `julia-prelude-output:${crypto.randomUUID()}`,
|
|
36
|
-
kernelOwnerId: OWNER_ID,
|
|
37
|
-
reset: true,
|
|
38
|
-
},
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
expect(result.exitCode).toBe(0);
|
|
42
|
-
expect(result.output).toContain("RANGE=two|three");
|
|
43
|
-
expect(result.output).toContain('QUERY="b"');
|
|
44
|
-
expect(result.output).toContain("STRIPPED=red");
|
|
45
|
-
expect(result.output).toContain("META=alpha:true");
|
|
46
|
-
expect(result.output).toContain("MULTI=2:alpha:json");
|
|
47
|
-
}, 60_000);
|
|
48
|
-
|
|
49
|
-
it("surfaces the exception type and message in the error output, not just stack frames", async () => {
|
|
50
|
-
using tempDir = TempDir.createSync("@omp-eval-julia-error-");
|
|
51
|
-
const result = await executeJulia(`println("="^8)\nmissing_var_xyz + 1`, {
|
|
52
|
-
cwd: tempDir.path(),
|
|
53
|
-
sessionId: `julia-prelude-error:${crypto.randomUUID()}`,
|
|
54
|
-
kernelOwnerId: OWNER_ID,
|
|
55
|
-
reset: true,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// The rendered error must carry the actual exception, not only the
|
|
59
|
-
// runner-internal backtrace frames (regression: traceback-only output
|
|
60
|
-
// hid `ename`/`evalue`).
|
|
61
|
-
expect(result.output).toContain("UndefVarError");
|
|
62
|
-
expect(result.output).toContain("missing_var_xyz");
|
|
63
|
-
// Frames are still present alongside the message.
|
|
64
|
-
expect(result.output).toContain("top-level scope");
|
|
65
|
-
}, 30_000);
|
|
66
|
-
});
|