@bastani/atomic 0.6.5-0 → 0.6.6-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/.agents/skills/ado-commit/SKILL.md +2 -0
  2. package/.agents/skills/ado-create-pr/SKILL.md +2 -0
  3. package/.agents/skills/advanced-evaluation/SKILL.md +2 -0
  4. package/.agents/skills/ast-grep/SKILL.md +2 -0
  5. package/.agents/skills/bdi-mental-states/SKILL.md +2 -0
  6. package/.agents/skills/bun/SKILL.md +156 -122
  7. package/.agents/skills/context-compression/SKILL.md +2 -0
  8. package/.agents/skills/context-degradation/SKILL.md +2 -0
  9. package/.agents/skills/context-fundamentals/SKILL.md +2 -0
  10. package/.agents/skills/context-optimization/SKILL.md +2 -0
  11. package/.agents/skills/create-spec/SKILL.md +2 -0
  12. package/.agents/skills/docx/SKILL.md +2 -0
  13. package/.agents/skills/evaluation/SKILL.md +2 -0
  14. package/.agents/skills/explain-code/SKILL.md +2 -0
  15. package/.agents/skills/filesystem-context/SKILL.md +2 -0
  16. package/.agents/skills/find-skills/SKILL.md +2 -0
  17. package/.agents/skills/gh-commit/SKILL.md +2 -0
  18. package/.agents/skills/gh-create-pr/SKILL.md +2 -0
  19. package/.agents/skills/hosted-agents/SKILL.md +2 -0
  20. package/.agents/skills/impeccable/SKILL.md +117 -304
  21. package/.agents/skills/impeccable/agents/openai.yaml +4 -0
  22. package/.agents/skills/{adapt/SKILL.md → impeccable/reference/adapt.md} +2 -11
  23. package/.agents/skills/{animate/SKILL.md → impeccable/reference/animate.md} +15 -15
  24. package/.agents/skills/{audit/SKILL.md → impeccable/reference/audit.md} +8 -22
  25. package/.agents/skills/{bolder/SKILL.md → impeccable/reference/bolder.md} +9 -13
  26. package/.agents/skills/impeccable/reference/brand.md +114 -0
  27. package/.agents/skills/{clarify/SKILL.md → impeccable/reference/clarify.md} +2 -11
  28. package/.agents/skills/{colorize/SKILL.md → impeccable/reference/colorize.md} +23 -12
  29. package/.agents/skills/impeccable/reference/craft.md +152 -29
  30. package/.agents/skills/{critique/SKILL.md → impeccable/reference/critique.md} +25 -37
  31. package/.agents/skills/{delight/SKILL.md → impeccable/reference/delight.md} +9 -11
  32. package/.agents/skills/{distill/SKILL.md → impeccable/reference/distill.md} +2 -13
  33. package/.agents/skills/impeccable/reference/document.md +427 -0
  34. package/.agents/skills/impeccable/reference/extract.md +1 -1
  35. package/.agents/skills/{harden/SKILL.md → impeccable/reference/harden.md} +1 -43
  36. package/.agents/skills/{layout/SKILL.md → impeccable/reference/layout.md} +27 -11
  37. package/.agents/skills/impeccable/reference/live.md +594 -0
  38. package/.agents/skills/impeccable/reference/motion-design.md +12 -2
  39. package/.agents/skills/impeccable/reference/onboard.md +234 -0
  40. package/.agents/skills/{optimize/SKILL.md → impeccable/reference/optimize.md} +4 -12
  41. package/.agents/skills/{overdrive/SKILL.md → impeccable/reference/overdrive.md} +9 -21
  42. package/.agents/skills/{critique → impeccable}/reference/personas.md +1 -1
  43. package/.agents/skills/{polish/SKILL.md → impeccable/reference/polish.md} +31 -23
  44. package/.agents/skills/impeccable/reference/product.md +62 -0
  45. package/.agents/skills/{quieter/SKILL.md → impeccable/reference/quieter.md} +7 -11
  46. package/.agents/skills/impeccable/reference/shape.md +151 -0
  47. package/.agents/skills/impeccable/reference/teach.md +156 -0
  48. package/.agents/skills/{typeset/SKILL.md → impeccable/reference/typeset.md} +19 -11
  49. package/.agents/skills/impeccable/reference/typography.md +31 -14
  50. package/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs +87 -17
  51. package/.agents/skills/impeccable/scripts/command-metadata.json +94 -0
  52. package/.agents/skills/impeccable/scripts/design-parser.mjs +820 -0
  53. package/.agents/skills/impeccable/scripts/detect-csp.mjs +198 -0
  54. package/.agents/skills/impeccable/scripts/is-generated.mjs +69 -0
  55. package/.agents/skills/impeccable/scripts/live-accept.mjs +595 -0
  56. package/.agents/skills/impeccable/scripts/live-browser.js +4781 -0
  57. package/.agents/skills/impeccable/scripts/live-inject.mjs +445 -0
  58. package/.agents/skills/impeccable/scripts/live-poll.mjs +186 -0
  59. package/.agents/skills/impeccable/scripts/live-server.mjs +694 -0
  60. package/.agents/skills/impeccable/scripts/live-wrap.mjs +571 -0
  61. package/.agents/skills/impeccable/scripts/live.mjs +247 -0
  62. package/.agents/skills/impeccable/scripts/load-context.mjs +141 -0
  63. package/.agents/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
  64. package/.agents/skills/impeccable/scripts/pin.mjs +214 -0
  65. package/.agents/skills/init/SKILL.md +2 -0
  66. package/.agents/skills/liteparse/SKILL.md +1 -0
  67. package/.agents/skills/memory-systems/SKILL.md +2 -0
  68. package/.agents/skills/multi-agent-patterns/SKILL.md +2 -0
  69. package/.agents/skills/opentui/SKILL.md +1 -0
  70. package/.agents/skills/pdf/SKILL.md +2 -0
  71. package/.agents/skills/playwright-cli/SKILL.md +51 -5
  72. package/.agents/skills/playwright-cli/references/playwright-tests.md +1 -1
  73. package/.agents/skills/playwright-cli/references/running-code.md +10 -0
  74. package/.agents/skills/playwright-cli/references/session-management.md +56 -0
  75. package/.agents/skills/playwright-cli/references/spec-driven-testing.md +305 -0
  76. package/.agents/skills/playwright-cli/references/test-generation.md +49 -3
  77. package/.agents/skills/pptx/SKILL.md +2 -0
  78. package/.agents/skills/project-development/SKILL.md +2 -0
  79. package/.agents/skills/prompt-engineer/SKILL.md +2 -0
  80. package/.agents/skills/research-codebase/SKILL.md +2 -0
  81. package/.agents/skills/ripgrep/SKILL.md +2 -0
  82. package/.agents/skills/skill-creator/LICENSE.txt +1 -1
  83. package/.agents/skills/skill-creator/SKILL.md +2 -0
  84. package/.agents/skills/sl-commit/SKILL.md +2 -0
  85. package/.agents/skills/sl-submit-diff/SKILL.md +2 -0
  86. package/.agents/skills/tdd/SKILL.md +4 -0
  87. package/.agents/skills/tool-design/SKILL.md +2 -0
  88. package/.agents/skills/typescript-advanced-types/SKILL.md +2 -1
  89. package/.agents/skills/typescript-expert/SKILL.md +7 -1
  90. package/.agents/skills/typescript-react-reviewer/SKILL.md +2 -1
  91. package/.agents/skills/workflow-creator/SKILL.md +75 -72
  92. package/.agents/skills/workflow-creator/references/session-config.md +48 -1
  93. package/.agents/skills/xlsx/SKILL.md +2 -0
  94. package/.opencode/opencode.json +4 -2
  95. package/dist/sdk/runtime/executor.d.ts +8 -0
  96. package/dist/sdk/runtime/executor.d.ts.map +1 -1
  97. package/dist/sdk/runtime/port-discovery.d.ts +71 -0
  98. package/dist/sdk/runtime/port-discovery.d.ts.map +1 -0
  99. package/dist/sdk/runtime/tmux.d.ts +10 -0
  100. package/dist/sdk/runtime/tmux.d.ts.map +1 -1
  101. package/dist/sdk/types.d.ts +1 -0
  102. package/dist/sdk/types.d.ts.map +1 -1
  103. package/dist/sdk/workflows/builtin/deep-research-codebase/opencode/index.d.ts.map +1 -1
  104. package/dist/sdk/workflows/builtin/open-claude-design/opencode/index.d.ts.map +1 -1
  105. package/dist/sdk/workflows/builtin/ralph/claude/index.d.ts.map +1 -1
  106. package/dist/sdk/workflows/builtin/ralph/copilot/index.d.ts.map +1 -1
  107. package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts +15 -0
  108. package/dist/sdk/workflows/builtin/ralph/helpers/prompts.d.ts.map +1 -1
  109. package/dist/sdk/workflows/builtin/ralph/opencode/index.d.ts.map +1 -1
  110. package/package.json +1 -1
  111. package/src/sdk/runtime/executor.test.ts +254 -1
  112. package/src/sdk/runtime/executor.ts +135 -89
  113. package/src/sdk/runtime/port-discovery.test.ts +573 -0
  114. package/src/sdk/runtime/port-discovery.ts +496 -0
  115. package/src/sdk/runtime/tmux.ts +16 -0
  116. package/src/sdk/types.ts +1 -0
  117. package/src/sdk/workflows/builtin/deep-research-codebase/opencode/index.ts +24 -6
  118. package/src/sdk/workflows/builtin/open-claude-design/opencode/index.ts +52 -13
  119. package/src/sdk/workflows/builtin/ralph/claude/index.ts +31 -3
  120. package/src/sdk/workflows/builtin/ralph/copilot/index.ts +16 -0
  121. package/src/sdk/workflows/builtin/ralph/helpers/prompts.ts +70 -3
  122. package/src/sdk/workflows/builtin/ralph/opencode/index.ts +50 -6
  123. package/.agents/skills/shape/SKILL.md +0 -96
  124. /package/.agents/skills/{critique → impeccable}/reference/cognitive-load.md +0 -0
  125. /package/.agents/skills/{critique → impeccable}/reference/heuristics-scoring.md +0 -0
@@ -0,0 +1,573 @@
1
+ /**
2
+ * Tests for the cross-platform TCP port discovery module.
3
+ *
4
+ * Parser functions are tested with synthetic data to keep tests OS-independent.
5
+ * The end-to-end test uses a real server and only runs on Linux/macOS.
6
+ */
7
+
8
+ import { test, expect, describe, mock, beforeAll, afterAll } from "bun:test";
9
+ import {
10
+ _linuxGetListeningPort,
11
+ _macosGetChildren,
12
+ _macosReadListeningPort,
13
+ _parseLinuxTcpLine,
14
+ _parseLinuxTcpTable,
15
+ _getLinuxPidSocketInodes,
16
+ _parseMacosLsofOutput,
17
+ _parseWindowsPowerShellOutput,
18
+ _parseWindowsNetstatOutput,
19
+ _readListeningPortForPid,
20
+ _setPortDiscoverySpawnSyncForTest,
21
+ _windowsGetChildren,
22
+ _windowsReadListeningPort,
23
+ getListeningPortForPid,
24
+ PORT_DISCOVERY_TIMEOUT_MS,
25
+ } from "./port-discovery.ts";
26
+ import * as net from "node:net";
27
+ import { Buffer } from "node:buffer";
28
+
29
+ type Platform = typeof process.platform;
30
+ type SpawnResult = {
31
+ stdout: Buffer;
32
+ stderr: Buffer;
33
+ success: boolean;
34
+ };
35
+
36
+ function withPlatform<T>(platform: Platform, fn: () => T): T {
37
+ const descriptor = Object.getOwnPropertyDescriptor(process, "platform");
38
+ Object.defineProperty(process, "platform", { configurable: true, value: platform });
39
+ try {
40
+ return fn();
41
+ } finally {
42
+ if (descriptor) {
43
+ Object.defineProperty(process, "platform", descriptor);
44
+ }
45
+ }
46
+ }
47
+
48
+ function spawnResult(stdout = "", stderr = "", success = true): SpawnResult {
49
+ return {
50
+ stdout: Buffer.from(stdout),
51
+ stderr: Buffer.from(stderr),
52
+ success,
53
+ };
54
+ }
55
+
56
+ function withSpawnSyncMock<T>(handler: (cmd: string[]) => SpawnResult, fn: () => T): T {
57
+ const restore = _setPortDiscoverySpawnSyncForTest(mock((options) => handler(options.cmd)));
58
+ try {
59
+ return fn();
60
+ } finally {
61
+ restore();
62
+ }
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // 1. Linux /proc parser
67
+ // ---------------------------------------------------------------------------
68
+
69
+ describe("_parseLinuxTcpLine", () => {
70
+ test("returns null for header line", () => {
71
+ const line = " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode";
72
+ expect(_parseLinuxTcpLine(line)).toBeNull();
73
+ });
74
+
75
+ test("returns null for non-LISTEN state (ESTABLISHED = 01)", () => {
76
+ const line = " 0: 0100007F:1F90 0200007F:C000 01 00000000:00000000 00:00000000 00000000 1000 0 12345 1 0000000000000000 20 4 24 10 -1";
77
+ expect(_parseLinuxTcpLine(line)).toBeNull();
78
+ });
79
+
80
+ test("parses LISTEN state (0A) and decodes port hex :1F90 → 8080", () => {
81
+ // 0x1F90 = 8080
82
+ const line = " 0: 0100007F:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 99001 1 0000000000000000 100 0 0 10 0";
83
+ const result = _parseLinuxTcpLine(line);
84
+ expect(result).not.toBeNull();
85
+ expect(result!.port).toBe(8080);
86
+ expect(result!.inode).toBe(99001);
87
+ });
88
+
89
+ test("parses port :50FF → 20735", () => {
90
+ // 0x50FF = 20735
91
+ const line = " 1: 00000000:50FF 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 55555 1 0000000000000000 100 0 0 10 0";
92
+ const result = _parseLinuxTcpLine(line);
93
+ expect(result).not.toBeNull();
94
+ expect(result!.port).toBe(0x50ff);
95
+ });
96
+
97
+ test("parses port :0050 → 80", () => {
98
+ // 0x0050 = 80
99
+ const line = " 2: 00000000:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1001 1 0000000000000000 100 0 0 10 0";
100
+ const result = _parseLinuxTcpLine(line);
101
+ expect(result).not.toBeNull();
102
+ expect(result!.port).toBe(80);
103
+ });
104
+
105
+ test("returns null for empty line", () => {
106
+ expect(_parseLinuxTcpLine("")).toBeNull();
107
+ expect(_parseLinuxTcpLine(" ")).toBeNull();
108
+ });
109
+
110
+ test("returns null for truncated line", () => {
111
+ expect(_parseLinuxTcpLine(" 0: 0100007F:1F90 00000000:0000 0A")).toBeNull();
112
+ });
113
+
114
+ test("returns null when port hex is 0000", () => {
115
+ const line = " 0: 00000000:0000 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 1 1 0000000000000000 100 0 0 10 0";
116
+ expect(_parseLinuxTcpLine(line)).toBeNull();
117
+ });
118
+ });
119
+
120
+ describe("_parseLinuxTcpTable", () => {
121
+ const sampleTcp = [
122
+ " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode",
123
+ " 0: 0100007F:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 99001 1 0000000000000000 100 0 0 10 0",
124
+ " 1: 0200007F:23FB 00000000:0000 01 00000000:00000000 00:00000000 00000000 1000 0 99002 1 0000000000000000 100 0 0 10 0",
125
+ " 2: 00000000:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 99003 1 0000000000000000 100 0 0 10 0",
126
+ ].join("\n");
127
+
128
+ test("only includes LISTEN (0A) entries", () => {
129
+ const table = _parseLinuxTcpTable(sampleTcp);
130
+ // inode 99002 is ESTABLISHED (01) — must not appear
131
+ expect(table.has(99002)).toBe(false);
132
+ });
133
+
134
+ test("maps inode 99001 → port 8080", () => {
135
+ const table = _parseLinuxTcpTable(sampleTcp);
136
+ expect(table.get(99001)).toBe(8080);
137
+ });
138
+
139
+ test("maps inode 99003 → port 80", () => {
140
+ const table = _parseLinuxTcpTable(sampleTcp);
141
+ expect(table.get(99003)).toBe(80);
142
+ });
143
+
144
+ test("returns empty map for empty content", () => {
145
+ expect(_parseLinuxTcpTable("").size).toBe(0);
146
+ });
147
+ });
148
+
149
+ // Note: _getLinuxPidSocketInodes relies on real /proc fs; tested in e2e section.
150
+
151
+ describe("Linux discovery helpers", () => {
152
+ test("returns IPv4 port before IPv6 for matching socket inode", () => {
153
+ const tcp = "0: 0100007F:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 111 1";
154
+ const tcp6 = "0: 00000000000000000000000001000000:23FB 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 111 1";
155
+
156
+ expect(_linuxGetListeningPort(tcp, tcp6, new Set([111]))).toBe(8080);
157
+ });
158
+
159
+ test("returns IPv6 port when no IPv4 table entry matches", () => {
160
+ const tcp6 = "0: 00000000000000000000000001000000:23FB 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 222 1";
161
+
162
+ expect(_linuxGetListeningPort("", tcp6, new Set([222]))).toBe(9211);
163
+ });
164
+
165
+ test("platform dispatch uses Linux reader without subprocesses", () => {
166
+ const port = withPlatform("linux", () => _readListeningPortForPid(2_000_000_000));
167
+
168
+ expect(port).toBeNull();
169
+ });
170
+ });
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // 2. macOS lsof parser
174
+ // ---------------------------------------------------------------------------
175
+
176
+ describe("_parseMacosLsofOutput", () => {
177
+ const sampleLsof = [
178
+ "COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME",
179
+ "node 12345 user 23u IPv4 0x0000000000000000 0t0 TCP 127.0.0.1:8080 (LISTEN)",
180
+ "node 12345 user 24u IPv4 0x0000000000000001 0t0 TCP 0.0.0.0:9090 (LISTEN)",
181
+ ].join("\n");
182
+
183
+ test("extracts port from 127.0.0.1:8080", () => {
184
+ expect(_parseMacosLsofOutput(sampleLsof)).toBe(8080);
185
+ });
186
+
187
+ test("prefers loopback over wildcard when loopback appears first", () => {
188
+ const output = [
189
+ "COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME",
190
+ "node 1 user 23u IPv4 0 0t0 TCP 127.0.0.1:8080 (LISTEN)",
191
+ "node 1 user 24u IPv4 0 0t0 TCP 0.0.0.0:9090 (LISTEN)",
192
+ ].join("\n");
193
+ expect(_parseMacosLsofOutput(output)).toBe(8080);
194
+ });
195
+
196
+ test("returns wildcard port when only 0.0.0.0 binding exists", () => {
197
+ const output = [
198
+ "COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME",
199
+ "node 1 user 23u IPv4 0 0t0 TCP 0.0.0.0:3000 (LISTEN)",
200
+ ].join("\n");
201
+ expect(_parseMacosLsofOutput(output)).toBe(3000);
202
+ });
203
+
204
+ test("returns null for empty output", () => {
205
+ expect(_parseMacosLsofOutput("")).toBeNull();
206
+ });
207
+
208
+ test("returns null for header-only output", () => {
209
+ expect(_parseMacosLsofOutput("COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME")).toBeNull();
210
+ });
211
+
212
+ test("handles ::1 IPv6 loopback", () => {
213
+ const output = [
214
+ "COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME",
215
+ "node 1 user 10u IPv6 0 0t0 TCP [::1]:4000 (LISTEN)",
216
+ ].join("\n");
217
+ // [::1]:4000 — lastIndexOf(':') points to the ':' before port
218
+ // After stripping brackets, host becomes "[::1]" which won't match our preferred list.
219
+ // The parser sees last colon, gets port 4000, host "[::1]" — falls to fallback candidates.
220
+ const port = _parseMacosLsofOutput(output);
221
+ expect(port).toBe(4000);
222
+ });
223
+
224
+ test("handles :: IPv6 wildcard", () => {
225
+ const output = [
226
+ "COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME",
227
+ "node 1 user 10u IPv6 0 0t0 TCP [::]:5000 (LISTEN)",
228
+ ].join("\n");
229
+ const port = _parseMacosLsofOutput(output);
230
+ expect(port).toBe(5000);
231
+ });
232
+
233
+ test("returns null when no valid port lines", () => {
234
+ expect(_parseMacosLsofOutput("COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\njunk line here")).toBeNull();
235
+ });
236
+ });
237
+
238
+ describe("macOS discovery helpers", () => {
239
+ const lsofOutput = "COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\nbun 123 user 10u IPv4 0 0t0 TCP 127.0.0.1:4567 (LISTEN)";
240
+
241
+ test("reads a port from lsof output", () => {
242
+ const port = withSpawnSyncMock(
243
+ () => spawnResult(lsofOutput),
244
+ () => _macosReadListeningPort(123, 0),
245
+ );
246
+
247
+ expect(port).toBe(4567);
248
+ });
249
+
250
+ test("walks child PIDs when parent has no listening port", () => {
251
+ const seenCommands: string[][] = [];
252
+ const port = withSpawnSyncMock(
253
+ (cmd) => {
254
+ seenCommands.push(cmd);
255
+ if (cmd[0] === "pgrep") return spawnResult("456\n");
256
+ if (cmd.includes("456")) return spawnResult(lsofOutput.replace("4567", "5678"));
257
+ return spawnResult("");
258
+ },
259
+ () => _macosReadListeningPort(123, 0),
260
+ );
261
+
262
+ expect(port).toBe(5678);
263
+ expect(seenCommands.some((cmd) => cmd[0] === "pgrep")).toBe(true);
264
+ });
265
+
266
+ test("returns null after macOS child traversal depth limit", () => {
267
+ const port = withSpawnSyncMock(
268
+ () => spawnResult(lsofOutput),
269
+ () => _macosReadListeningPort(123, 4),
270
+ );
271
+
272
+ expect(port).toBeNull();
273
+ });
274
+
275
+ test("parses pgrep child output and filters invalid values", () => {
276
+ const children = withSpawnSyncMock(
277
+ () => spawnResult("123\nnot-a-pid\n0\n456\n"),
278
+ () => _macosGetChildren(99),
279
+ );
280
+
281
+ expect(children).toEqual([123, 456]);
282
+ });
283
+
284
+ test("platform dispatch uses macOS reader", () => {
285
+ const port = withPlatform("darwin", () =>
286
+ withSpawnSyncMock(() => spawnResult(lsofOutput), () => _readListeningPortForPid(123)),
287
+ );
288
+
289
+ expect(port).toBe(4567);
290
+ });
291
+ });
292
+
293
+ // ---------------------------------------------------------------------------
294
+ // 3. Windows PowerShell parser
295
+ // ---------------------------------------------------------------------------
296
+
297
+ describe("_parseWindowsPowerShellOutput", () => {
298
+ test("returns null for empty string", () => {
299
+ expect(_parseWindowsPowerShellOutput("")).toBeNull();
300
+ });
301
+
302
+ test("returns null for literal null string", () => {
303
+ expect(_parseWindowsPowerShellOutput("null")).toBeNull();
304
+ });
305
+
306
+ test("returns null for whitespace-only string", () => {
307
+ expect(_parseWindowsPowerShellOutput(" ")).toBeNull();
308
+ });
309
+
310
+ test("parses single object {LocalPort: 12345}", () => {
311
+ expect(_parseWindowsPowerShellOutput('{"LocalPort":12345}')).toBe(12345);
312
+ });
313
+
314
+ test("parses array and returns first port", () => {
315
+ const json = '[{"LocalPort":8080},{"LocalPort":9090}]';
316
+ expect(_parseWindowsPowerShellOutput(json)).toBe(8080);
317
+ });
318
+
319
+ test("parses array with single element", () => {
320
+ expect(_parseWindowsPowerShellOutput('[{"LocalPort":3000}]')).toBe(3000);
321
+ });
322
+
323
+ test("returns null for invalid JSON", () => {
324
+ expect(_parseWindowsPowerShellOutput("not json")).toBeNull();
325
+ });
326
+
327
+ test("returns null for empty array", () => {
328
+ expect(_parseWindowsPowerShellOutput("[]")).toBeNull();
329
+ });
330
+
331
+ test("returns null for object without LocalPort", () => {
332
+ expect(_parseWindowsPowerShellOutput('{"OtherField":1234}')).toBeNull();
333
+ });
334
+ });
335
+
336
+ describe("Windows discovery helpers", () => {
337
+ test("reads a port from PowerShell output", () => {
338
+ const port = withSpawnSyncMock(
339
+ () => spawnResult('{"LocalPort":7001}'),
340
+ () => _windowsReadListeningPort(321, 0),
341
+ );
342
+
343
+ expect(port).toBe(7001);
344
+ });
345
+
346
+ test("falls back to netstat when PowerShell is unavailable", () => {
347
+ const port = withSpawnSyncMock(
348
+ (cmd) => {
349
+ if (cmd[0] === "cmd") {
350
+ return spawnResult("TCP 0.0.0.0:7002 0.0.0.0:0 LISTENING 321");
351
+ }
352
+ return spawnResult("", "CommandNotFoundException", false);
353
+ },
354
+ () => _windowsReadListeningPort(321, 0),
355
+ );
356
+
357
+ expect(port).toBe(7002);
358
+ });
359
+
360
+ test("walks child PIDs when parent has no listening port", () => {
361
+ const port = withSpawnSyncMock(
362
+ (cmd) => {
363
+ const command = cmd.join(" ");
364
+ if (command.includes("Get-NetTCPConnection") && command.includes("654")) {
365
+ return spawnResult('{"LocalPort":7003}');
366
+ }
367
+ if (command.includes("Win32_Process")) {
368
+ return spawnResult('{"ProcessId":654}');
369
+ }
370
+ return spawnResult("null");
371
+ },
372
+ () => _windowsReadListeningPort(321, 0),
373
+ );
374
+
375
+ expect(port).toBe(7003);
376
+ });
377
+
378
+ test("returns null after Windows child traversal depth limit", () => {
379
+ const port = withSpawnSyncMock(
380
+ () => spawnResult('{"LocalPort":7004}'),
381
+ () => _windowsReadListeningPort(321, 4),
382
+ );
383
+
384
+ expect(port).toBeNull();
385
+ });
386
+
387
+ test("parses Windows child PID JSON arrays and objects", () => {
388
+ const arrayChildren = withSpawnSyncMock(
389
+ () => spawnResult('[{"ProcessId":111},{"ProcessId":"222"},{"Other":333},{"ProcessId":0}]'),
390
+ () => _windowsGetChildren(321),
391
+ );
392
+ const objectChildren = withSpawnSyncMock(
393
+ () => spawnResult('{"ProcessId":444}'),
394
+ () => _windowsGetChildren(321),
395
+ );
396
+
397
+ expect(arrayChildren).toEqual([111, 222]);
398
+ expect(objectChildren).toEqual([444]);
399
+ });
400
+
401
+ test("falls back to wmic when child PowerShell is unavailable", () => {
402
+ const children = withSpawnSyncMock(
403
+ (cmd) => {
404
+ if (cmd[0] === "wmic") return spawnResult("ProcessId\n111\nbad\n222\n");
405
+ return spawnResult("", "is not recognized", false);
406
+ },
407
+ () => _windowsGetChildren(321),
408
+ );
409
+
410
+ expect(children).toEqual([111, 222]);
411
+ });
412
+
413
+ test("platform dispatch uses Windows reader for non-Linux and non-macOS platforms", () => {
414
+ const port = withPlatform("win32", () =>
415
+ withSpawnSyncMock(() => spawnResult('{"LocalPort":7005}'), () => _readListeningPortForPid(321)),
416
+ );
417
+
418
+ expect(port).toBe(7005);
419
+ });
420
+ });
421
+
422
+ // ---------------------------------------------------------------------------
423
+ // 4. Windows netstat parser
424
+ // ---------------------------------------------------------------------------
425
+
426
+ describe("_parseWindowsNetstatOutput", () => {
427
+ const sampleNetstat = [
428
+ "",
429
+ "Active Connections",
430
+ "",
431
+ " Proto Local Address Foreign Address State PID",
432
+ " TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 884",
433
+ " TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 1234",
434
+ " TCP 0.0.0.0:9090 0.0.0.0:0 LISTENING 5678",
435
+ " TCP 127.0.0.1:54321 127.0.0.1:12345 ESTABLISHED 1234",
436
+ " UDP 0.0.0.0:3702 *:* 1234",
437
+ ].join("\n");
438
+
439
+ test("returns port 8080 for PID 1234", () => {
440
+ expect(_parseWindowsNetstatOutput(sampleNetstat, 1234)).toBe(8080);
441
+ });
442
+
443
+ test("returns port 9090 for PID 5678", () => {
444
+ expect(_parseWindowsNetstatOutput(sampleNetstat, 5678)).toBe(9090);
445
+ });
446
+
447
+ test("ignores ESTABLISHED connections", () => {
448
+ // PID 1234 has ESTABLISHED on 54321 too, but we should get the LISTENING one first
449
+ expect(_parseWindowsNetstatOutput(sampleNetstat, 1234)).toBe(8080);
450
+ });
451
+
452
+ test("returns null for unknown PID", () => {
453
+ expect(_parseWindowsNetstatOutput(sampleNetstat, 9999)).toBeNull();
454
+ });
455
+
456
+ test("returns null for empty output", () => {
457
+ expect(_parseWindowsNetstatOutput("", 1234)).toBeNull();
458
+ });
459
+
460
+ test("handles IPv6 addresses", () => {
461
+ const output = [
462
+ " Proto Local Address Foreign Address State PID",
463
+ " TCP6 [::]:8080 [::]:0 LISTENING 4242",
464
+ ].join("\n");
465
+ expect(_parseWindowsNetstatOutput(output, 4242)).toBe(8080);
466
+ });
467
+ });
468
+
469
+ // ---------------------------------------------------------------------------
470
+ // 5. Polling loop (dependency-injection)
471
+ // ---------------------------------------------------------------------------
472
+
473
+ describe("getListeningPortForPid polling loop", () => {
474
+ test("returns port on first non-null read", async () => {
475
+ // We can't inject the resolver directly, but we can test via E2E with a
476
+ // very short timeout on the real process — testing the loop logic is
477
+ // validated through integration (see section 6). Here we test the contract:
478
+ // if the process is the current process and it has a listening socket,
479
+ // we get a port.
480
+ //
481
+ // For pure unit testing of the loop, we verify the timeout path.
482
+ const result = await getListeningPortForPid(-999999, {
483
+ timeoutMs: 50,
484
+ pollIntervalMs: 10,
485
+ });
486
+ // PID -999999 doesn't exist, so either process-alive check returns false
487
+ // or we timeout. Either way, null.
488
+ expect(result).toBeNull();
489
+ });
490
+
491
+ test("returns null on timeout for non-listening PID", async () => {
492
+ // Use a real but non-listening PID (our parent shell or similar)
493
+ // The process exists but has no listening TCP socket.
494
+ // With a very short timeout we should get null quickly.
495
+ const result = await getListeningPortForPid(process.ppid ?? 1, {
496
+ timeoutMs: 200,
497
+ pollIntervalMs: 50,
498
+ });
499
+ // Either it times out (null) or parent happens to listen (number).
500
+ // We can't assert exact value but we can assert type.
501
+ expect(result === null || typeof result === "number").toBe(true);
502
+ });
503
+
504
+ test("returns null immediately for dead PID", async () => {
505
+ // Use a very large PID unlikely to exist
506
+ const start = Date.now();
507
+ const result = await getListeningPortForPid(2_000_000_000, {
508
+ timeoutMs: 5_000,
509
+ pollIntervalMs: 100,
510
+ });
511
+ const elapsed = Date.now() - start;
512
+ expect(result).toBeNull();
513
+ // Should exit well before the 5s timeout once it detects the process is dead
514
+ // (On Linux, /proc/2_000_000_000 won't exist; on macOS/Windows, kill(0) throws)
515
+ expect(elapsed).toBeLessThan(4_000);
516
+ });
517
+ });
518
+
519
+ // ---------------------------------------------------------------------------
520
+ // 6. End-to-end (Linux/macOS only)
521
+ // ---------------------------------------------------------------------------
522
+
523
+ const isLinuxOrMac = process.platform === "linux" || process.platform === "darwin";
524
+
525
+ describe("getListeningPortForPid end-to-end", () => {
526
+ let server: net.Server;
527
+ let serverPort: number;
528
+
529
+ beforeAll(async () => {
530
+ if (!isLinuxOrMac) return;
531
+ server = net.createServer();
532
+ await new Promise<void>((resolve) => {
533
+ server.listen(0, "127.0.0.1", () => resolve());
534
+ });
535
+ const addr = server.address();
536
+ serverPort = typeof addr === "object" && addr !== null ? addr.port : 0;
537
+ });
538
+
539
+ afterAll(async () => {
540
+ if (!isLinuxOrMac) return;
541
+ await new Promise<void>((resolve) => server.close(() => resolve()));
542
+ });
543
+
544
+ test(
545
+ "discovers own listening port via getListeningPortForPid",
546
+ async () => {
547
+ if (!isLinuxOrMac) {
548
+ return; // Skip on Windows
549
+ }
550
+ expect(serverPort).toBeGreaterThan(0);
551
+
552
+ const discovered = await getListeningPortForPid(process.pid, {
553
+ timeoutMs: 5_000,
554
+ pollIntervalMs: 100,
555
+ });
556
+
557
+ // The discovered port should match what the server is listening on.
558
+ // Note: process may have multiple listening sockets (e.g., Bun test runner).
559
+ // We verify that discovered is a valid port number.
560
+ expect(discovered).not.toBeNull();
561
+ expect(typeof discovered).toBe("number");
562
+ expect(discovered!).toBeGreaterThan(0);
563
+ },
564
+ 10_000,
565
+ );
566
+
567
+ test(
568
+ "PORT_DISCOVERY_TIMEOUT_MS is 15000",
569
+ () => {
570
+ expect(PORT_DISCOVERY_TIMEOUT_MS).toBe(15_000);
571
+ },
572
+ );
573
+ });