@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,427 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, spyOn } from "bun:test";
|
|
2
|
-
import * as fs from "node:fs/promises";
|
|
3
|
-
import * as os from "node:os";
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
|
|
6
|
-
import { resolveStdioSpawnCommand, StdioTransport, terminateStdioProcess } from "./stdio";
|
|
7
|
-
|
|
8
|
-
describe("resolveStdioSpawnCommand", () => {
|
|
9
|
-
it("hides Windows executable MCP servers when the host has no console", async () => {
|
|
10
|
-
// Hidden so a console-app child does not allocate a visible window when
|
|
11
|
-
// OMP is launched without a terminal console (#3536).
|
|
12
|
-
await expect(
|
|
13
|
-
resolveStdioSpawnCommand(
|
|
14
|
-
{ command: "server.exe", args: ["--stdio"] },
|
|
15
|
-
{ cwd: process.cwd(), env: {}, platform: "win32", hostHasInheritableConsole: false },
|
|
16
|
-
),
|
|
17
|
-
).resolves.toEqual({
|
|
18
|
-
cmd: ["server.exe", "--stdio"],
|
|
19
|
-
windowsHide: true,
|
|
20
|
-
detached: false,
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("inherits an attached Windows console instead of forcing CREATE_NO_WINDOW", async () => {
|
|
25
|
-
await expect(
|
|
26
|
-
resolveStdioSpawnCommand(
|
|
27
|
-
{ command: "server.exe", args: ["--stdio"] },
|
|
28
|
-
{ cwd: process.cwd(), env: {}, platform: "win32", hostHasInheritableConsole: true },
|
|
29
|
-
),
|
|
30
|
-
).resolves.toEqual({
|
|
31
|
-
cmd: ["server.exe", "--stdio"],
|
|
32
|
-
windowsHide: false,
|
|
33
|
-
detached: false,
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("keeps Darwin stdio MCP servers attached so TCC Apple Events prompts can resolve", async () => {
|
|
38
|
-
await expect(
|
|
39
|
-
resolveStdioSpawnCommand(
|
|
40
|
-
{ command: "xcrun", args: ["mcpbridge"] },
|
|
41
|
-
{ cwd: process.cwd(), env: {}, platform: "darwin" },
|
|
42
|
-
),
|
|
43
|
-
).resolves.toEqual({
|
|
44
|
-
cmd: ["xcrun", "mcpbridge"],
|
|
45
|
-
detached: false,
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("detaches off-Windows MCP servers so terminal job-control signals cannot stop them", async () => {
|
|
50
|
-
await expect(
|
|
51
|
-
resolveStdioSpawnCommand(
|
|
52
|
-
{ command: "server.exe", args: ["--stdio"] },
|
|
53
|
-
{ cwd: process.cwd(), env: {}, platform: "linux" },
|
|
54
|
-
),
|
|
55
|
-
).resolves.toEqual({
|
|
56
|
-
cmd: ["server.exe", "--stdio"],
|
|
57
|
-
detached: true,
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe("StdioTransport.connect", () => {
|
|
63
|
-
it("passes argv as Bun.spawn's first argument and process options as the second", async () => {
|
|
64
|
-
const cwd = process.cwd();
|
|
65
|
-
const envValue = "stdio-spawn-shape";
|
|
66
|
-
const argv = [process.execPath, "-e", "process.exit(0)"];
|
|
67
|
-
const transport = new StdioTransport({
|
|
68
|
-
command: argv[0],
|
|
69
|
-
args: argv.slice(1),
|
|
70
|
-
cwd,
|
|
71
|
-
env: {
|
|
72
|
-
OMP_STDIO_SPAWN_SHAPE: envValue,
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
const spawnSpy = spyOn(Bun, "spawn");
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
await transport.connect();
|
|
79
|
-
|
|
80
|
-
expect(spawnSpy).toHaveBeenCalledTimes(1);
|
|
81
|
-
const call = spawnSpy.mock.calls[0];
|
|
82
|
-
if (!call) throw new Error("expected StdioTransport.connect() to spawn exactly one subprocess");
|
|
83
|
-
|
|
84
|
-
const [spawnArgv, spawnOptions] = call;
|
|
85
|
-
expect(spawnArgv).toEqual(argv);
|
|
86
|
-
expect(spawnOptions).toEqual(
|
|
87
|
-
expect.objectContaining({
|
|
88
|
-
cwd,
|
|
89
|
-
detached: !(process.platform === "darwin" || process.platform === "win32"),
|
|
90
|
-
env: expect.objectContaining({
|
|
91
|
-
OMP_STDIO_SPAWN_SHAPE: envValue,
|
|
92
|
-
}),
|
|
93
|
-
stderr: "pipe",
|
|
94
|
-
stdin: "pipe",
|
|
95
|
-
stdout: "pipe",
|
|
96
|
-
windowsHide: process.platform === "win32" ? expect.any(Boolean) : undefined,
|
|
97
|
-
}),
|
|
98
|
-
);
|
|
99
|
-
} finally {
|
|
100
|
-
await transport.close();
|
|
101
|
-
spawnSpy.mockRestore();
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// Regression for #3945: request() awaited stdin.write/flush, so a child that
|
|
107
|
-
// stops draining stdin would park the async fn past the timeout timer and past
|
|
108
|
-
// `return promise`, orphaning the deferred rejection and hanging the caller
|
|
109
|
-
// forever. `sleep` is POSIX-only, so the check is scoped to non-Windows hosts.
|
|
110
|
-
describe.skipIf(process.platform === "win32")("StdioTransport request write stall", () => {
|
|
111
|
-
it("rejects with the timeout error when the child never drains stdin", async () => {
|
|
112
|
-
const timeoutMs = 400;
|
|
113
|
-
const orphaned: Error[] = [];
|
|
114
|
-
const captureOrphan = (reason: unknown) => {
|
|
115
|
-
if (reason instanceof Error) orphaned.push(reason);
|
|
116
|
-
};
|
|
117
|
-
process.on("unhandledRejection", captureOrphan);
|
|
118
|
-
|
|
119
|
-
// `sleep 60` accepts a stdin pipe but never reads it; a 1 MB payload
|
|
120
|
-
// overruns the OS pipe buffer plus any FileSink JS-side buffering, so
|
|
121
|
-
// Bun's write() returns a Promise that only settles if the child reads.
|
|
122
|
-
const transport = new StdioTransport({
|
|
123
|
-
command: "sleep",
|
|
124
|
-
args: ["60"],
|
|
125
|
-
timeout: timeoutMs,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
await transport.connect();
|
|
130
|
-
|
|
131
|
-
const bigParam = "x".repeat(1024 * 1024);
|
|
132
|
-
const started = performance.now();
|
|
133
|
-
const outcome = await transport.request("tools/call", { name: "noop", arguments: { blob: bigParam } }).then(
|
|
134
|
-
() => ({ kind: "resolved" as const }),
|
|
135
|
-
(error: unknown) => ({ kind: "rejected" as const, error }),
|
|
136
|
-
);
|
|
137
|
-
const elapsedMs = performance.now() - started;
|
|
138
|
-
|
|
139
|
-
expect(outcome.kind).toBe("rejected");
|
|
140
|
-
if (outcome.kind !== "rejected") return;
|
|
141
|
-
if (!(outcome.error instanceof Error)) {
|
|
142
|
-
throw new Error(`expected Error rejection, got ${String(outcome.error)}`);
|
|
143
|
-
}
|
|
144
|
-
expect(outcome.error.message).toContain(`Request timeout after ${timeoutMs}ms`);
|
|
145
|
-
// Generous ceiling: bare rejection latency plus room for slow CI. The
|
|
146
|
-
// pre-fix behavior was an unbounded hang, not a slightly-late reject.
|
|
147
|
-
expect(elapsedMs).toBeLessThan(timeoutMs + 1500);
|
|
148
|
-
expect(orphaned).toEqual([]);
|
|
149
|
-
} finally {
|
|
150
|
-
process.off("unhandledRejection", captureOrphan);
|
|
151
|
-
await transport.close();
|
|
152
|
-
}
|
|
153
|
-
}, 8000);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// `kill(pid, 0)` succeeds for a zombie too: a grandchild whose parent (the
|
|
157
|
-
// killed leader) is gone sits as <defunct> until whatever reaps orphans
|
|
158
|
-
// (init/subreaper) gets around to it — which can lag on some hosts. A
|
|
159
|
-
// zombie already received and honored the group SIGKILL; it is just not
|
|
160
|
-
// harvested yet, so treating it as "still alive" would make the group-kill
|
|
161
|
-
// assertions below flaky rather than testing what they claim to test.
|
|
162
|
-
function processExists(pid: number): boolean {
|
|
163
|
-
try {
|
|
164
|
-
process.kill(pid, 0);
|
|
165
|
-
} catch {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
const result = Bun.spawnSync(["ps", "-o", "stat=", "-p", String(pid)]);
|
|
169
|
-
const state = result.stdout.toString().trim();
|
|
170
|
-
return result.exitCode === 0 && state.length > 0 && !state.startsWith("Z");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Regression for #5578: `close()` used a bare `this.#process.kill()` (direct
|
|
174
|
-
// SIGTERM, no wait, no escalate, no process-group signal), so a detached
|
|
175
|
-
// session-leader child (or a grandchild it spawned) that ignores/traps
|
|
176
|
-
// SIGTERM survived host exit and became an orphan pinned to PID 1 on Linux.
|
|
177
|
-
// `sleep`/`bun`/POSIX signal semantics are exercised directly here rather
|
|
178
|
-
// than through `StdioTransport.connect()`, because `connect()` derives
|
|
179
|
-
// `detached` from `resolveStdioSpawnCommand()`, which is tied to the host's
|
|
180
|
-
// real `process.platform` — a POSIX detached session cannot be reproduced
|
|
181
|
-
// end-to-end through `connect()` on a non-Linux dev/CI host, but a real
|
|
182
|
-
// detached process group can still be spawned directly on any POSIX host.
|
|
183
|
-
describe.skipIf(process.platform === "win32")("terminateStdioProcess", () => {
|
|
184
|
-
it("escalates a detached child that traps SIGTERM to SIGKILL", async () => {
|
|
185
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-stdio-kill-solo-"));
|
|
186
|
-
const scriptPath = path.join(tempDir, "child.mjs");
|
|
187
|
-
const readyPath = path.join(tempDir, "ready");
|
|
188
|
-
await fs.writeFile(
|
|
189
|
-
scriptPath,
|
|
190
|
-
[
|
|
191
|
-
"import { writeFileSync } from 'node:fs';",
|
|
192
|
-
"process.on('SIGTERM', () => {});",
|
|
193
|
-
`writeFileSync(${JSON.stringify(readyPath)}, '1');`,
|
|
194
|
-
"setInterval(() => {}, 60_000);",
|
|
195
|
-
].join("\n"),
|
|
196
|
-
);
|
|
197
|
-
const proc = Bun.spawn(["bun", "run", scriptPath], {
|
|
198
|
-
stdin: "ignore",
|
|
199
|
-
stdout: "ignore",
|
|
200
|
-
stderr: "ignore",
|
|
201
|
-
detached: true,
|
|
202
|
-
});
|
|
203
|
-
try {
|
|
204
|
-
// Wait for the child to actually register its SIGTERM handler before
|
|
205
|
-
// signaling it: signaling too early races the child's startup and
|
|
206
|
-
// hits the default (terminate) action instead of exercising the trap.
|
|
207
|
-
for (let i = 0; i < 100; i++) {
|
|
208
|
-
try {
|
|
209
|
-
await fs.access(readyPath);
|
|
210
|
-
break;
|
|
211
|
-
} catch {
|
|
212
|
-
await Bun.sleep(20);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const started = performance.now();
|
|
217
|
-
await terminateStdioProcess(proc, true);
|
|
218
|
-
await proc.exited;
|
|
219
|
-
const elapsedMs = performance.now() - started;
|
|
220
|
-
|
|
221
|
-
expect(proc.signalCode).toBe("SIGKILL");
|
|
222
|
-
// Escalation only fires after the ~1s SIGTERM grace window elapses —
|
|
223
|
-
// a too-fast exit would mean SIGKILL fired without waiting.
|
|
224
|
-
expect(elapsedMs).toBeGreaterThanOrEqual(900);
|
|
225
|
-
} finally {
|
|
226
|
-
try {
|
|
227
|
-
process.kill(-proc.pid, "SIGKILL");
|
|
228
|
-
} catch {
|
|
229
|
-
// Already gone.
|
|
230
|
-
}
|
|
231
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
232
|
-
}
|
|
233
|
-
}, 5000);
|
|
234
|
-
|
|
235
|
-
it("reaches a SIGTERM-trapping grandchild through the group SIGKILL, not just the direct child", async () => {
|
|
236
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-stdio-group-kill-"));
|
|
237
|
-
const grandchildScriptPath = path.join(tempDir, "grandchild.mjs");
|
|
238
|
-
const parentScriptPath = path.join(tempDir, "parent.mjs");
|
|
239
|
-
const grandchildPidPath = path.join(tempDir, "grandchild.pid");
|
|
240
|
-
|
|
241
|
-
// Grandchild also traps SIGTERM, so only an unrestricted SIGKILL to the
|
|
242
|
-
// whole group — not a signal to the direct (parent) child alone — can
|
|
243
|
-
// reach and stop it.
|
|
244
|
-
await fs.writeFile(
|
|
245
|
-
grandchildScriptPath,
|
|
246
|
-
[
|
|
247
|
-
"import { writeFileSync } from 'node:fs';",
|
|
248
|
-
"process.on('SIGTERM', () => {});",
|
|
249
|
-
`writeFileSync(${JSON.stringify(grandchildPidPath)}, String(process.pid));`,
|
|
250
|
-
"setInterval(() => {}, 60_000);",
|
|
251
|
-
].join("\n"),
|
|
252
|
-
);
|
|
253
|
-
await fs.writeFile(
|
|
254
|
-
parentScriptPath,
|
|
255
|
-
[
|
|
256
|
-
"process.on('SIGTERM', () => {});",
|
|
257
|
-
`Bun.spawn(["bun", "run", ${JSON.stringify(grandchildScriptPath)}], { stdout: "ignore", stderr: "ignore", stdin: "ignore" });`,
|
|
258
|
-
"setInterval(() => {}, 60_000);",
|
|
259
|
-
].join("\n"),
|
|
260
|
-
);
|
|
261
|
-
|
|
262
|
-
const proc = Bun.spawn(["bun", "run", parentScriptPath], {
|
|
263
|
-
stdin: "ignore",
|
|
264
|
-
stdout: "ignore",
|
|
265
|
-
stderr: "ignore",
|
|
266
|
-
detached: true,
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
try {
|
|
270
|
-
// Polls real wall-clock time rather than an event/promise: the
|
|
271
|
-
// grandchild process is a real external OS process writing to a real
|
|
272
|
-
// file, with no in-process signal this test can `await` directly.
|
|
273
|
-
let grandchildPid: number | undefined;
|
|
274
|
-
for (let i = 0; i < 100 && grandchildPid === undefined; i++) {
|
|
275
|
-
try {
|
|
276
|
-
grandchildPid = Number.parseInt(await fs.readFile(grandchildPidPath, "utf8"), 10);
|
|
277
|
-
} catch {
|
|
278
|
-
await Bun.sleep(20);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
if (grandchildPid === undefined) throw new Error("grandchild never reported its pid");
|
|
282
|
-
expect(processExists(grandchildPid)).toBe(true);
|
|
283
|
-
|
|
284
|
-
await terminateStdioProcess(proc, true);
|
|
285
|
-
await proc.exited;
|
|
286
|
-
expect(proc.signalCode).toBe("SIGKILL");
|
|
287
|
-
|
|
288
|
-
// The group SIGKILL is delivered to every member simultaneously, but
|
|
289
|
-
// give the kernel a brief window to finish reaping before asserting.
|
|
290
|
-
let grandchildAlive = processExists(grandchildPid);
|
|
291
|
-
for (let i = 0; i < 25 && grandchildAlive; i++) {
|
|
292
|
-
await Bun.sleep(20);
|
|
293
|
-
grandchildAlive = processExists(grandchildPid);
|
|
294
|
-
}
|
|
295
|
-
expect(grandchildAlive).toBe(false);
|
|
296
|
-
} finally {
|
|
297
|
-
try {
|
|
298
|
-
process.kill(-proc.pid, "SIGKILL");
|
|
299
|
-
} catch {
|
|
300
|
-
// Already gone.
|
|
301
|
-
}
|
|
302
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
303
|
-
}
|
|
304
|
-
}, 8000);
|
|
305
|
-
|
|
306
|
-
it("still reaps a SIGTERM-trapping grandchild after the detached leader exits cooperatively", async () => {
|
|
307
|
-
// Regression: the leader exiting within the SIGTERM grace window used to
|
|
308
|
-
// be treated as proof the whole process group was gone, so `close()`
|
|
309
|
-
// returned early and never delivered a group SIGKILL — leaving exactly
|
|
310
|
-
// the orphaned grandchild this change is meant to reap. Unlike the
|
|
311
|
-
// group-SIGKILL test above, the leader here does NOT trap SIGTERM, so
|
|
312
|
-
// it exits promptly on its own; only the grandchild ignores signals.
|
|
313
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-stdio-leader-exit-group-kill-"));
|
|
314
|
-
const grandchildScriptPath = path.join(tempDir, "grandchild.mjs");
|
|
315
|
-
const parentScriptPath = path.join(tempDir, "parent.mjs");
|
|
316
|
-
const grandchildPidPath = path.join(tempDir, "grandchild.pid");
|
|
317
|
-
|
|
318
|
-
await fs.writeFile(
|
|
319
|
-
grandchildScriptPath,
|
|
320
|
-
[
|
|
321
|
-
"import { writeFileSync } from 'node:fs';",
|
|
322
|
-
"process.on('SIGTERM', () => {});",
|
|
323
|
-
`writeFileSync(${JSON.stringify(grandchildPidPath)}, String(process.pid));`,
|
|
324
|
-
"setInterval(() => {}, 60_000);",
|
|
325
|
-
].join("\n"),
|
|
326
|
-
);
|
|
327
|
-
// No SIGTERM handler here: the default action (terminate) fires as soon
|
|
328
|
-
// as the group SIGTERM lands, well inside TERM_GRACE_MS.
|
|
329
|
-
await fs.writeFile(
|
|
330
|
-
parentScriptPath,
|
|
331
|
-
[
|
|
332
|
-
`Bun.spawn(["bun", "run", ${JSON.stringify(grandchildScriptPath)}], { stdout: "ignore", stderr: "ignore", stdin: "ignore" });`,
|
|
333
|
-
"setInterval(() => {}, 60_000);",
|
|
334
|
-
].join("\n"),
|
|
335
|
-
);
|
|
336
|
-
|
|
337
|
-
const proc = Bun.spawn(["bun", "run", parentScriptPath], {
|
|
338
|
-
stdin: "ignore",
|
|
339
|
-
stdout: "ignore",
|
|
340
|
-
stderr: "ignore",
|
|
341
|
-
detached: true,
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
try {
|
|
345
|
-
let grandchildPid: number | undefined;
|
|
346
|
-
for (let i = 0; i < 100 && grandchildPid === undefined; i++) {
|
|
347
|
-
try {
|
|
348
|
-
grandchildPid = Number.parseInt(await fs.readFile(grandchildPidPath, "utf8"), 10);
|
|
349
|
-
} catch {
|
|
350
|
-
await Bun.sleep(20);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
if (grandchildPid === undefined) throw new Error("grandchild never reported its pid");
|
|
354
|
-
expect(processExists(grandchildPid)).toBe(true);
|
|
355
|
-
|
|
356
|
-
const started = performance.now();
|
|
357
|
-
await terminateStdioProcess(proc, true);
|
|
358
|
-
await proc.exited;
|
|
359
|
-
const elapsedMs = performance.now() - started;
|
|
360
|
-
|
|
361
|
-
// The leader exits on the initial SIGTERM (no trap), so this must not
|
|
362
|
-
// block for the ~1s TERM grace window before sweeping the group.
|
|
363
|
-
expect(elapsedMs).toBeLessThan(700);
|
|
364
|
-
|
|
365
|
-
let grandchildAlive = processExists(grandchildPid);
|
|
366
|
-
for (let i = 0; i < 25 && grandchildAlive; i++) {
|
|
367
|
-
await Bun.sleep(20);
|
|
368
|
-
grandchildAlive = processExists(grandchildPid);
|
|
369
|
-
}
|
|
370
|
-
expect(grandchildAlive).toBe(false);
|
|
371
|
-
} finally {
|
|
372
|
-
try {
|
|
373
|
-
process.kill(-proc.pid, "SIGKILL");
|
|
374
|
-
} catch {
|
|
375
|
-
// Already gone.
|
|
376
|
-
}
|
|
377
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
378
|
-
}
|
|
379
|
-
}, 8000);
|
|
380
|
-
|
|
381
|
-
it("never attempts a process-group signal when the transport did not spawn detached", async () => {
|
|
382
|
-
const proc = Bun.spawn(["bun", "-e", "await Bun.sleep(60_000)"], {
|
|
383
|
-
stdin: "ignore",
|
|
384
|
-
stdout: "ignore",
|
|
385
|
-
stderr: "ignore",
|
|
386
|
-
detached: false,
|
|
387
|
-
});
|
|
388
|
-
const killSpy = spyOn(process, "kill");
|
|
389
|
-
try {
|
|
390
|
-
await terminateStdioProcess(proc, false);
|
|
391
|
-
await proc.exited;
|
|
392
|
-
|
|
393
|
-
// Only the direct-child `Subprocess.kill()` path may run; the global
|
|
394
|
-
// `process.kill()` (used exclusively for the negative-pid group
|
|
395
|
-
// signal) must never be reached.
|
|
396
|
-
expect(killSpy).not.toHaveBeenCalled();
|
|
397
|
-
} finally {
|
|
398
|
-
killSpy.mockRestore();
|
|
399
|
-
try {
|
|
400
|
-
proc.kill("SIGKILL");
|
|
401
|
-
} catch {
|
|
402
|
-
// Already gone.
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}, 5000);
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
describe.skipIf(process.platform === "win32")("StdioTransport.close teardown", () => {
|
|
409
|
-
it("closes a well-behaved child promptly without escalating to SIGKILL", async () => {
|
|
410
|
-
const transport = new StdioTransport({
|
|
411
|
-
command: "bun",
|
|
412
|
-
args: ["-e", "await Bun.sleep(60_000)"],
|
|
413
|
-
});
|
|
414
|
-
try {
|
|
415
|
-
await transport.connect();
|
|
416
|
-
const started = performance.now();
|
|
417
|
-
await transport.close();
|
|
418
|
-
const elapsedMs = performance.now() - started;
|
|
419
|
-
|
|
420
|
-
// No SIGTERM trap => the child dies almost immediately; close() must
|
|
421
|
-
// not block for the full ~1s TERM grace window before returning.
|
|
422
|
-
expect(elapsedMs).toBeLessThan(700);
|
|
423
|
-
} finally {
|
|
424
|
-
await transport.close();
|
|
425
|
-
}
|
|
426
|
-
}, 5000);
|
|
427
|
-
});
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import * as fs from "node:fs/promises";
|
|
3
|
-
import * as os from "node:os";
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import { getThemeByName, setThemeInstance, theme } from "../../theme/theme";
|
|
6
|
-
import { DynamicBorder } from "../dynamic-border";
|
|
7
|
-
|
|
8
|
-
const componentPath = path.resolve(import.meta.dir, "../dynamic-border.ts");
|
|
9
|
-
|
|
10
|
-
describe("DynamicBorder", () => {
|
|
11
|
-
// Regression for #5366: extensions importing legacy pi UI components get a
|
|
12
|
-
// second `src` module graph whose module-level `theme` is never assigned by
|
|
13
|
-
// host startup. `render()` must degrade to plain glyphs instead of throwing
|
|
14
|
-
// "undefined is not an object (evaluating 'theme.boxRound')" and killing the
|
|
15
|
-
// TUI. Other test files in the same process initialize the theme module, so
|
|
16
|
-
// the uninitialized state is only reachable in a fresh module graph — a
|
|
17
|
-
// subprocess reproduces the actual reported scenario deterministically.
|
|
18
|
-
it("renders plain glyphs when the module-level theme is uninitialized", async () => {
|
|
19
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-dynamic-border-"));
|
|
20
|
-
try {
|
|
21
|
-
const script = [
|
|
22
|
-
`import { DynamicBorder } from ${JSON.stringify(componentPath)};`,
|
|
23
|
-
'const custom = new DynamicBorder(str => "<" + str + ">");',
|
|
24
|
-
`if (JSON.stringify(custom.render(4)) !== JSON.stringify(["<────>"])) process.exit(1);`,
|
|
25
|
-
"const plain = new DynamicBorder();",
|
|
26
|
-
`if (JSON.stringify(plain.render(3)) !== JSON.stringify(["───"])) process.exit(2);`,
|
|
27
|
-
"",
|
|
28
|
-
].join("\n");
|
|
29
|
-
const file = path.join(dir, "uninitialized-theme.ts");
|
|
30
|
-
await Bun.write(file, script);
|
|
31
|
-
const proc = Bun.spawn(["bun", file], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
32
|
-
const [exitCode, stderr] = await Promise.all([
|
|
33
|
-
proc.exited,
|
|
34
|
-
new Response(proc.stderr as ReadableStream<Uint8Array>).text(),
|
|
35
|
-
]);
|
|
36
|
-
expect(stderr).toBe("");
|
|
37
|
-
expect(exitCode).toBe(0);
|
|
38
|
-
} finally {
|
|
39
|
-
await fs.rm(dir, { recursive: true, force: true });
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("paints with theme.boxRound.horizontal once the theme is initialized", async () => {
|
|
44
|
-
const previous = theme;
|
|
45
|
-
const loaded = await getThemeByName("dark");
|
|
46
|
-
if (!loaded) throw new Error("theme unavailable");
|
|
47
|
-
setThemeInstance(loaded);
|
|
48
|
-
try {
|
|
49
|
-
const border = new DynamicBorder(str => `<${str}>`);
|
|
50
|
-
expect(border.render(3)).toEqual([`<${loaded.boxRound.horizontal.repeat(3)}>`]);
|
|
51
|
-
} finally {
|
|
52
|
-
if (previous) setThemeInstance(previous);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
});
|