@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.
Files changed (244) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-5k4dq4g6.md} +54 -0
  3. package/dist/cli.js +3342 -3274
  4. package/dist/types/capability/index.d.ts +1 -1
  5. package/dist/types/capability/types.d.ts +23 -1
  6. package/dist/types/config/settings-schema.d.ts +39 -2
  7. package/dist/types/cursor.d.ts +2 -1
  8. package/dist/types/extensibility/extensions/runner.d.ts +4 -0
  9. package/dist/types/extensibility/shared-events.d.ts +18 -1
  10. package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
  11. package/dist/types/internal-urls/parse.d.ts +12 -0
  12. package/dist/types/internal-urls/router.d.ts +6 -0
  13. package/dist/types/internal-urls/types.d.ts +6 -0
  14. package/dist/types/lsp/config.d.ts +1 -0
  15. package/dist/types/lsp/types.d.ts +2 -0
  16. package/dist/types/mcp/manager.d.ts +5 -0
  17. package/dist/types/mcp/tool-bridge.d.ts +13 -0
  18. package/dist/types/modes/components/custom-editor.d.ts +5 -0
  19. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  20. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
  21. package/dist/types/sdk.d.ts +3 -1
  22. package/dist/types/session/agent-session-types.d.ts +8 -6
  23. package/dist/types/session/agent-session.d.ts +28 -1
  24. package/dist/types/session/model-controls.d.ts +4 -1
  25. package/dist/types/session/session-advisors.d.ts +7 -1
  26. package/dist/types/session/session-tools.d.ts +44 -6
  27. package/dist/types/session/streaming-output.d.ts +7 -2
  28. package/dist/types/session/tool-choice-queue.d.ts +6 -4
  29. package/dist/types/session/turn-recovery.d.ts +5 -3
  30. package/dist/types/task/index.d.ts +1 -1
  31. package/dist/types/task/types.d.ts +3 -11
  32. package/dist/types/thinking.d.ts +21 -2
  33. package/dist/types/tools/index.d.ts +8 -3
  34. package/dist/types/tools/output-meta.d.ts +5 -0
  35. package/dist/types/tools/read.d.ts +9 -1
  36. package/dist/types/tools/xdev.d.ts +53 -67
  37. package/dist/types/utils/cpuprofile.d.ts +51 -0
  38. package/dist/types/utils/inspect-image-mode.d.ts +29 -0
  39. package/dist/types/utils/profile-tree.d.ts +47 -0
  40. package/dist/types/utils/sample-profile.d.ts +67 -0
  41. package/dist/types/utils/title-generator.d.ts +17 -16
  42. package/package.json +12 -12
  43. package/src/capability/index.ts +43 -12
  44. package/src/capability/mcp.ts +21 -0
  45. package/src/capability/types.ts +20 -1
  46. package/src/cli/read-cli.ts +44 -2
  47. package/src/config/settings-schema.ts +42 -2
  48. package/src/config/settings.ts +35 -0
  49. package/src/cursor.ts +4 -3
  50. package/src/discovery/builtin-rules/index.ts +2 -0
  51. package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
  52. package/src/eval/py/runner.py +16 -2
  53. package/src/extensibility/extensions/runner.ts +117 -5
  54. package/src/extensibility/extensions/types.ts +0 -1
  55. package/src/extensibility/extensions/wrapper.ts +74 -42
  56. package/src/extensibility/hooks/tool-wrapper.ts +11 -4
  57. package/src/extensibility/hooks/types.ts +0 -1
  58. package/src/extensibility/shared-events.ts +18 -1
  59. package/src/internal-urls/mcp-protocol.ts +17 -3
  60. package/src/internal-urls/parse.ts +31 -0
  61. package/src/internal-urls/router.ts +24 -4
  62. package/src/internal-urls/types.ts +6 -0
  63. package/src/live/transport.ts +2 -2
  64. package/src/lsp/client.ts +2 -2
  65. package/src/lsp/config.ts +4 -0
  66. package/src/lsp/types.ts +2 -0
  67. package/src/mcp/config.ts +26 -14
  68. package/src/mcp/manager.ts +26 -9
  69. package/src/mcp/tool-bridge.ts +52 -1
  70. package/src/memories/index.ts +25 -6
  71. package/src/modes/components/custom-editor.ts +39 -16
  72. package/src/modes/components/status-line/segments.ts +3 -1
  73. package/src/modes/components/tips.txt +2 -1
  74. package/src/modes/components/tool-execution.ts +8 -7
  75. package/src/modes/controllers/command-controller.ts +10 -10
  76. package/src/modes/controllers/extension-ui-controller.ts +7 -7
  77. package/src/modes/controllers/selector-controller.ts +7 -2
  78. package/src/modes/rpc/rpc-mode.ts +60 -47
  79. package/src/prompts/steering/user-interjection.md +2 -5
  80. package/src/prompts/tools/task.md +4 -2
  81. package/src/sdk.ts +64 -58
  82. package/src/session/agent-session-types.ts +8 -5
  83. package/src/session/agent-session.ts +144 -11
  84. package/src/session/model-controls.ts +48 -12
  85. package/src/session/session-advisors.ts +30 -14
  86. package/src/session/session-listing.ts +66 -4
  87. package/src/session/session-tools.ts +162 -54
  88. package/src/session/streaming-output.ts +18 -6
  89. package/src/session/tool-choice-queue.ts +19 -4
  90. package/src/session/turn-recovery.ts +25 -6
  91. package/src/slash-commands/builtin-registry.ts +69 -0
  92. package/src/task/executor.ts +11 -3
  93. package/src/task/index.ts +43 -32
  94. package/src/task/types.ts +12 -17
  95. package/src/thinking.ts +68 -5
  96. package/src/tools/bash.ts +16 -9
  97. package/src/tools/index.ts +36 -16
  98. package/src/tools/output-meta.ts +20 -0
  99. package/src/tools/read.ts +89 -15
  100. package/src/tools/write.ts +16 -7
  101. package/src/tools/xdev.ts +198 -210
  102. package/src/utils/cpuprofile.ts +235 -0
  103. package/src/utils/inspect-image-mode.ts +39 -0
  104. package/src/utils/profile-tree.ts +111 -0
  105. package/src/utils/sample-profile.ts +437 -0
  106. package/src/utils/title-generator.ts +88 -34
  107. package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
  108. package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
  109. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
  110. package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
  111. package/dist/types/cli/update-cli.test.d.ts +0 -1
  112. package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
  113. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
  114. package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
  115. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
  116. package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
  117. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
  118. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
  119. package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
  120. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
  121. package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
  122. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
  123. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
  124. package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
  125. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
  126. package/dist/types/hindsight/client.test.d.ts +0 -1
  127. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
  128. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
  129. package/dist/types/launch/broker-list-order.test.d.ts +0 -1
  130. package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
  131. package/dist/types/launch/protocol.test.d.ts +0 -1
  132. package/dist/types/launch/spawn-options.test.d.ts +0 -1
  133. package/dist/types/launch/terminal-output.test.d.ts +0 -1
  134. package/dist/types/live/protocol.test.d.ts +0 -1
  135. package/dist/types/mcp/config-writer.test.d.ts +0 -1
  136. package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
  137. package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
  138. package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
  139. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
  140. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
  141. package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
  142. package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
  143. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
  144. package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
  145. package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
  146. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
  147. package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
  148. package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
  149. package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
  150. package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
  151. package/dist/types/modes/print-mode.test.d.ts +0 -1
  152. package/dist/types/modes/session-teardown.test.d.ts +0 -1
  153. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
  154. package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
  155. package/dist/types/modes/warp-events.test.d.ts +0 -1
  156. package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
  157. package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
  158. package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
  159. package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
  160. package/dist/types/session/blob-store.test.d.ts +0 -1
  161. package/dist/types/session/messages.test.d.ts +0 -1
  162. package/dist/types/session/session-context.test.d.ts +0 -1
  163. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
  164. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
  165. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
  166. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
  167. package/dist/types/system-prompt.test.d.ts +0 -1
  168. package/dist/types/task/render.test.d.ts +0 -1
  169. package/dist/types/task/spawn-policy.test.d.ts +0 -1
  170. package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
  171. package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
  172. package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
  173. package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
  174. package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
  175. package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
  176. package/src/advisor/__tests__/advisor.test.ts +0 -4889
  177. package/src/advisor/__tests__/config.test.ts +0 -349
  178. package/src/advisor/__tests__/emission-guard.test.ts +0 -147
  179. package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
  180. package/src/cli/update-cli.test.ts +0 -28
  181. package/src/config/__tests__/model-registry.test.ts +0 -182
  182. package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
  183. package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
  184. package/src/eval/__tests__/budget-bridge.test.ts +0 -80
  185. package/src/eval/__tests__/completion-bridge.test.ts +0 -412
  186. package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
  187. package/src/eval/__tests__/idle-timeout.test.ts +0 -80
  188. package/src/eval/__tests__/js-context-manager.test.ts +0 -456
  189. package/src/eval/__tests__/julia-prelude.test.ts +0 -66
  190. package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
  191. package/src/eval/__tests__/prelude-agent.test.ts +0 -156
  192. package/src/eval/__tests__/process-entry-import.test.ts +0 -137
  193. package/src/eval/py/__tests__/prelude.test.ts +0 -104
  194. package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
  195. package/src/hindsight/client.test.ts +0 -75
  196. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
  197. package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
  198. package/src/launch/broker-list-order.test.ts +0 -89
  199. package/src/launch/broker-output-snapshot.test.ts +0 -126
  200. package/src/launch/protocol.test.ts +0 -59
  201. package/src/launch/spawn-options.test.ts +0 -31
  202. package/src/launch/terminal-output.test.ts +0 -107
  203. package/src/live/protocol.test.ts +0 -140
  204. package/src/mcp/config-writer.test.ts +0 -43
  205. package/src/mcp/smithery-auth.test.ts +0 -29
  206. package/src/mcp/smithery-registry.test.ts +0 -51
  207. package/src/mcp/transports/stdio.test.ts +0 -427
  208. package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
  209. package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
  210. package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
  211. package/src/modes/components/__tests__/skill-message.test.ts +0 -94
  212. package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
  213. package/src/modes/components/custom-editor.test.ts +0 -510
  214. package/src/modes/components/login-dialog.test.ts +0 -56
  215. package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
  216. package/src/modes/components/status-line/component.test.ts +0 -84
  217. package/src/modes/components/tool-execution.test.ts +0 -162
  218. package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
  219. package/src/modes/noninteractive-dispose.test.ts +0 -73
  220. package/src/modes/print-mode.test.ts +0 -71
  221. package/src/modes/session-teardown.test.ts +0 -219
  222. package/src/modes/theme/mermaid-rendering.test.ts +0 -53
  223. package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
  224. package/src/modes/warp-events.test.ts +0 -794
  225. package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
  226. package/src/plan-mode/model-transition.test.ts +0 -60
  227. package/src/plan-mode/reentry-prompt.test.ts +0 -41
  228. package/src/session/agent-session-error-log.test.ts +0 -59
  229. package/src/session/blob-store.test.ts +0 -56
  230. package/src/session/messages.test.ts +0 -282
  231. package/src/session/session-context.test.ts +0 -384
  232. package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
  233. package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
  234. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
  235. package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
  236. package/src/system-prompt.test.ts +0 -236
  237. package/src/task/render.test.ts +0 -290
  238. package/src/task/spawn-policy.test.ts +0 -62
  239. package/src/tools/__tests__/eval-description.test.ts +0 -18
  240. package/src/tools/__tests__/glob.test.ts +0 -37
  241. package/src/tools/__tests__/json-tree.test.ts +0 -35
  242. package/src/tools/__tests__/vibe-render.test.ts +0 -210
  243. package/src/tools/hub/launch-compat.test.ts +0 -40
  244. 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
- });