agent-dbg 0.1.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 (99) hide show
  1. package/.bin/ndbg +0 -0
  2. package/.claude/settings.local.json +21 -0
  3. package/.claude/skills/ndbg-debugger/ndbg-debugger/SKILL.md +116 -0
  4. package/.claude/skills/ndbg-debugger/ndbg-debugger/references/commands.md +173 -0
  5. package/CLAUDE.md +43 -0
  6. package/PROGRESS.md +261 -0
  7. package/README.md +67 -0
  8. package/biome.json +41 -0
  9. package/ndbg-spec.md +958 -0
  10. package/package.json +30 -0
  11. package/src/cdp/client.ts +198 -0
  12. package/src/cdp/types.ts +16 -0
  13. package/src/cli/parser.ts +287 -0
  14. package/src/cli/registry.ts +7 -0
  15. package/src/cli/types.ts +24 -0
  16. package/src/commands/attach.ts +47 -0
  17. package/src/commands/blackbox-ls.ts +38 -0
  18. package/src/commands/blackbox-rm.ts +57 -0
  19. package/src/commands/blackbox.ts +48 -0
  20. package/src/commands/break-ls.ts +57 -0
  21. package/src/commands/break-rm.ts +40 -0
  22. package/src/commands/break-toggle.ts +42 -0
  23. package/src/commands/break.ts +145 -0
  24. package/src/commands/breakable.ts +69 -0
  25. package/src/commands/catch.ts +38 -0
  26. package/src/commands/console.ts +61 -0
  27. package/src/commands/continue.ts +46 -0
  28. package/src/commands/eval.ts +70 -0
  29. package/src/commands/exceptions.ts +61 -0
  30. package/src/commands/hotpatch.ts +67 -0
  31. package/src/commands/launch.ts +69 -0
  32. package/src/commands/logpoint.ts +78 -0
  33. package/src/commands/pause.ts +46 -0
  34. package/src/commands/props.ts +77 -0
  35. package/src/commands/restart-frame.ts +36 -0
  36. package/src/commands/run-to.ts +70 -0
  37. package/src/commands/scripts.ts +57 -0
  38. package/src/commands/search.ts +73 -0
  39. package/src/commands/sessions.ts +71 -0
  40. package/src/commands/set-return.ts +49 -0
  41. package/src/commands/set.ts +61 -0
  42. package/src/commands/source.ts +59 -0
  43. package/src/commands/sourcemap.ts +66 -0
  44. package/src/commands/stack.ts +64 -0
  45. package/src/commands/state.ts +124 -0
  46. package/src/commands/status.ts +57 -0
  47. package/src/commands/step.ts +50 -0
  48. package/src/commands/stop.ts +27 -0
  49. package/src/commands/vars.ts +71 -0
  50. package/src/daemon/client.ts +147 -0
  51. package/src/daemon/entry.ts +242 -0
  52. package/src/daemon/paths.ts +26 -0
  53. package/src/daemon/server.ts +185 -0
  54. package/src/daemon/session-blackbox.ts +41 -0
  55. package/src/daemon/session-breakpoints.ts +492 -0
  56. package/src/daemon/session-execution.ts +121 -0
  57. package/src/daemon/session-inspection.ts +701 -0
  58. package/src/daemon/session-mutation.ts +197 -0
  59. package/src/daemon/session-state.ts +258 -0
  60. package/src/daemon/session.ts +938 -0
  61. package/src/daemon/spawn.ts +53 -0
  62. package/src/formatter/errors.ts +15 -0
  63. package/src/formatter/source.ts +74 -0
  64. package/src/formatter/stack.ts +70 -0
  65. package/src/formatter/values.ts +269 -0
  66. package/src/formatter/variables.ts +20 -0
  67. package/src/main.ts +45 -0
  68. package/src/protocol/messages.ts +316 -0
  69. package/src/refs/ref-table.ts +120 -0
  70. package/src/refs/resolver.ts +24 -0
  71. package/src/sourcemap/resolver.ts +318 -0
  72. package/tests/fixtures/async-app.js +34 -0
  73. package/tests/fixtures/console-app.js +12 -0
  74. package/tests/fixtures/error-app.js +28 -0
  75. package/tests/fixtures/exception-app.js +6 -0
  76. package/tests/fixtures/inspect-app.js +10 -0
  77. package/tests/fixtures/mutation-app.js +9 -0
  78. package/tests/fixtures/simple-app.js +50 -0
  79. package/tests/fixtures/step-app.js +13 -0
  80. package/tests/fixtures/ts-app/src/app.ts +21 -0
  81. package/tests/fixtures/ts-app/tsconfig.json +14 -0
  82. package/tests/integration/blackbox.test.ts +135 -0
  83. package/tests/integration/break-extras.test.ts +241 -0
  84. package/tests/integration/breakpoint.test.ts +217 -0
  85. package/tests/integration/console.test.ts +275 -0
  86. package/tests/integration/execution.test.ts +247 -0
  87. package/tests/integration/inspection.test.ts +311 -0
  88. package/tests/integration/mutation.test.ts +178 -0
  89. package/tests/integration/session.test.ts +223 -0
  90. package/tests/integration/source.test.ts +209 -0
  91. package/tests/integration/sourcemap.test.ts +214 -0
  92. package/tests/integration/state.test.ts +208 -0
  93. package/tests/unit/cdp-client.test.ts +422 -0
  94. package/tests/unit/daemon.test.ts +286 -0
  95. package/tests/unit/formatter.test.ts +716 -0
  96. package/tests/unit/parser.test.ts +105 -0
  97. package/tests/unit/refs.test.ts +383 -0
  98. package/tests/unit/sourcemap.test.ts +236 -0
  99. package/tsconfig.json +32 -0
@@ -0,0 +1,311 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { DebugSession } from "../../src/daemon/session.ts";
3
+
4
+ /**
5
+ * Polls until the session reaches the expected state, or times out.
6
+ */
7
+ async function waitForState(
8
+ session: DebugSession,
9
+ state: "idle" | "running" | "paused",
10
+ timeoutMs = 5000,
11
+ ): Promise<void> {
12
+ const deadline = Date.now() + timeoutMs;
13
+ while (session.sessionState !== state && Date.now() < deadline) {
14
+ await Bun.sleep(50);
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Launch and advance to the debugger statement in inspect-app.js (line 7).
20
+ */
21
+ async function launchAndPauseAtDebugger(sessionName: string): Promise<DebugSession> {
22
+ const session = new DebugSession(sessionName);
23
+ await session.launch(["node", "tests/fixtures/inspect-app.js"], {
24
+ brk: true,
25
+ });
26
+ await waitForState(session, "paused");
27
+
28
+ // Continue past initial brk pause to the `debugger;` statement
29
+ await session.continue();
30
+ await waitForState(session, "paused");
31
+
32
+ return session;
33
+ }
34
+
35
+ describe("Inspection: eval", () => {
36
+ test("eval evaluates a simple expression", async () => {
37
+ const session = await launchAndPauseAtDebugger("test-eval-simple");
38
+ try {
39
+ expect(session.sessionState).toBe("paused");
40
+
41
+ const result = await session.eval("1 + 2");
42
+ expect(result.ref).toMatch(/^@v/);
43
+ expect(result.type).toBe("number");
44
+ expect(result.value).toBe("3");
45
+ } finally {
46
+ await session.stop();
47
+ }
48
+ });
49
+
50
+ test("eval accesses local variables", async () => {
51
+ const session = await launchAndPauseAtDebugger("test-eval-locals");
52
+ try {
53
+ const result = await session.eval("num");
54
+ expect(result.ref).toMatch(/^@v/);
55
+ expect(result.type).toBe("number");
56
+ expect(result.value).toBe("123");
57
+ } finally {
58
+ await session.stop();
59
+ }
60
+ });
61
+
62
+ test("eval accesses object properties", async () => {
63
+ const session = await launchAndPauseAtDebugger("test-eval-obj-prop");
64
+ try {
65
+ const result = await session.eval("obj.name");
66
+ expect(result.ref).toMatch(/^@v/);
67
+ expect(result.type).toBe("string");
68
+ expect(result.value).toContain("test");
69
+ } finally {
70
+ await session.stop();
71
+ }
72
+ });
73
+
74
+ test("eval with string concatenation", async () => {
75
+ const session = await launchAndPauseAtDebugger("test-eval-concat");
76
+ try {
77
+ const result = await session.eval("str + ' world'");
78
+ expect(result.type).toBe("string");
79
+ expect(result.value).toContain("hello world");
80
+ } finally {
81
+ await session.stop();
82
+ }
83
+ });
84
+
85
+ test("eval returns object with objectId", async () => {
86
+ const session = await launchAndPauseAtDebugger("test-eval-object");
87
+ try {
88
+ const result = await session.eval("obj");
89
+ expect(result.ref).toMatch(/^@v/);
90
+ expect(result.type).toBe("object");
91
+ expect(result.objectId).toBeDefined();
92
+ expect(result.value).toContain("name");
93
+ } finally {
94
+ await session.stop();
95
+ }
96
+ });
97
+
98
+ test("eval syntax error throws", async () => {
99
+ const session = await launchAndPauseAtDebugger("test-eval-syntax-err");
100
+ try {
101
+ await expect(session.eval("if (")).rejects.toThrow();
102
+ } finally {
103
+ await session.stop();
104
+ }
105
+ });
106
+
107
+ test("eval throws when not paused", async () => {
108
+ const session = new DebugSession("test-eval-not-paused");
109
+ try {
110
+ await session.launch(["node", "-e", "setInterval(() => {}, 100)"], { brk: false });
111
+ expect(session.sessionState).toBe("running");
112
+
113
+ await expect(session.eval("1 + 1")).rejects.toThrow("not paused");
114
+ } finally {
115
+ await session.stop();
116
+ }
117
+ });
118
+
119
+ test("eval with @ref interpolation", async () => {
120
+ const session = await launchAndPauseAtDebugger("test-eval-ref-interp");
121
+ try {
122
+ // First get vars to establish refs
123
+ const vars = await session.getVars();
124
+ const objVar = vars.find((v) => v.name === "obj");
125
+ expect(objVar).toBeDefined();
126
+
127
+ if (objVar) {
128
+ // Use the ref in an expression
129
+ const result = await session.eval(`${objVar.ref}.count`);
130
+ expect(result.type).toBe("number");
131
+ expect(result.value).toBe("42");
132
+ }
133
+ } finally {
134
+ await session.stop();
135
+ }
136
+ });
137
+ });
138
+
139
+ describe("Inspection: vars", () => {
140
+ test("getVars returns local variables with refs", async () => {
141
+ const session = await launchAndPauseAtDebugger("test-vars-basic");
142
+ try {
143
+ const vars = await session.getVars();
144
+
145
+ expect(vars.length).toBeGreaterThan(0);
146
+
147
+ // Check that expected variables are present
148
+ const names = vars.map((v) => v.name);
149
+ expect(names).toContain("obj");
150
+ expect(names).toContain("arr");
151
+ expect(names).toContain("str");
152
+ expect(names).toContain("num");
153
+
154
+ // Each variable should have a ref
155
+ for (const v of vars) {
156
+ expect(v.ref).toMatch(/^@v/);
157
+ expect(v.type).toBeDefined();
158
+ expect(v.value).toBeDefined();
159
+ }
160
+
161
+ // Check specific values
162
+ const numVar = vars.find((v) => v.name === "num");
163
+ expect(numVar?.type).toBe("number");
164
+ expect(numVar?.value).toBe("123");
165
+
166
+ const strVar = vars.find((v) => v.name === "str");
167
+ expect(strVar?.type).toBe("string");
168
+ expect(strVar?.value).toContain("hello");
169
+ } finally {
170
+ await session.stop();
171
+ }
172
+ });
173
+
174
+ test("getVars with name filter", async () => {
175
+ const session = await launchAndPauseAtDebugger("test-vars-filter");
176
+ try {
177
+ const vars = await session.getVars({ names: ["num", "str"] });
178
+
179
+ expect(vars.length).toBe(2);
180
+ const names = vars.map((v) => v.name);
181
+ expect(names).toContain("num");
182
+ expect(names).toContain("str");
183
+ } finally {
184
+ await session.stop();
185
+ }
186
+ });
187
+
188
+ test("getVars throws when not paused", async () => {
189
+ const session = new DebugSession("test-vars-not-paused");
190
+ try {
191
+ await session.launch(["node", "-e", "setInterval(() => {}, 100)"], { brk: false });
192
+ expect(session.sessionState).toBe("running");
193
+
194
+ await expect(session.getVars()).rejects.toThrow("not paused");
195
+ } finally {
196
+ await session.stop();
197
+ }
198
+ });
199
+ });
200
+
201
+ describe("Inspection: props", () => {
202
+ test("getProps expands an object", async () => {
203
+ const session = await launchAndPauseAtDebugger("test-props-basic");
204
+ try {
205
+ // First get vars to get a ref for obj
206
+ const vars = await session.getVars();
207
+ const objVar = vars.find((v) => v.name === "obj");
208
+ expect(objVar).toBeDefined();
209
+
210
+ if (objVar) {
211
+ const props = await session.getProps(objVar.ref);
212
+
213
+ expect(props.length).toBeGreaterThan(0);
214
+
215
+ const propNames = props.map((p) => p.name);
216
+ expect(propNames).toContain("name");
217
+ expect(propNames).toContain("count");
218
+ expect(propNames).toContain("nested");
219
+
220
+ // Check property values
221
+ const nameProp = props.find((p) => p.name === "name");
222
+ expect(nameProp?.type).toBe("string");
223
+ expect(nameProp?.value).toContain("test");
224
+
225
+ const countProp = props.find((p) => p.name === "count");
226
+ expect(countProp?.type).toBe("number");
227
+ expect(countProp?.value).toBe("42");
228
+
229
+ // nested should be an object with a ref
230
+ const nestedProp = props.find((p) => p.name === "nested");
231
+ expect(nestedProp?.type).toBe("object");
232
+ expect(nestedProp?.ref).toMatch(/^@o/);
233
+ }
234
+ } finally {
235
+ await session.stop();
236
+ }
237
+ });
238
+
239
+ test("getProps assigns @o refs for object-type properties", async () => {
240
+ const session = await launchAndPauseAtDebugger("test-props-orefs");
241
+ try {
242
+ const vars = await session.getVars();
243
+ const objVar = vars.find((v) => v.name === "obj");
244
+ expect(objVar).toBeDefined();
245
+
246
+ if (objVar) {
247
+ const props = await session.getProps(objVar.ref);
248
+
249
+ // nested is an object, should have @o ref
250
+ const nestedProp = props.find((p) => p.name === "nested");
251
+ expect(nestedProp?.ref).toMatch(/^@o/);
252
+
253
+ // Primitive properties may or may not have refs depending on V8
254
+ // but object-type ones definitely should
255
+ if (nestedProp?.ref) {
256
+ // Can expand the nested object further
257
+ const nestedProps = await session.getProps(nestedProp.ref);
258
+ const deepProp = nestedProps.find((p) => p.name === "deep");
259
+ expect(deepProp).toBeDefined();
260
+ expect(deepProp?.value).toBe("true");
261
+ }
262
+ }
263
+ } finally {
264
+ await session.stop();
265
+ }
266
+ });
267
+
268
+ test("getProps expands an array", async () => {
269
+ const session = await launchAndPauseAtDebugger("test-props-array");
270
+ try {
271
+ const vars = await session.getVars();
272
+ const arrVar = vars.find((v) => v.name === "arr");
273
+ expect(arrVar).toBeDefined();
274
+
275
+ if (arrVar) {
276
+ const props = await session.getProps(arrVar.ref);
277
+ expect(props.length).toBeGreaterThan(0);
278
+
279
+ // Array should have numeric indices
280
+ const zeroProp = props.find((p) => p.name === "0");
281
+ expect(zeroProp?.value).toBe("1");
282
+ }
283
+ } finally {
284
+ await session.stop();
285
+ }
286
+ });
287
+
288
+ test("getProps on unknown ref throws", async () => {
289
+ const session = await launchAndPauseAtDebugger("test-props-unknown");
290
+ try {
291
+ await expect(session.getProps("@v999")).rejects.toThrow("Unknown ref");
292
+ } finally {
293
+ await session.stop();
294
+ }
295
+ });
296
+
297
+ test("getProps on primitive ref throws gracefully", async () => {
298
+ const session = await launchAndPauseAtDebugger("test-props-primitive");
299
+ try {
300
+ const vars = await session.getVars();
301
+ const numVar = vars.find((v) => v.name === "num");
302
+ expect(numVar).toBeDefined();
303
+
304
+ if (numVar) {
305
+ await expect(session.getProps(numVar.ref)).rejects.toThrow("primitive");
306
+ }
307
+ } finally {
308
+ await session.stop();
309
+ }
310
+ });
311
+ });
@@ -0,0 +1,178 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { DebugSession } from "../../src/daemon/session.ts";
3
+
4
+ /**
5
+ * Polls until the session reaches the expected state, or times out.
6
+ */
7
+ async function waitForState(
8
+ session: DebugSession,
9
+ state: "idle" | "running" | "paused",
10
+ timeoutMs = 5000,
11
+ ): Promise<void> {
12
+ const deadline = Date.now() + timeoutMs;
13
+ while (session.sessionState !== state && Date.now() < deadline) {
14
+ await Bun.sleep(50);
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Launch and advance to the debugger statement in mutation-app.js (line 7).
20
+ */
21
+ async function launchAndPauseAtDebugger(sessionName: string): Promise<DebugSession> {
22
+ const session = new DebugSession(sessionName);
23
+ await session.launch(["node", "tests/fixtures/mutation-app.js"], {
24
+ brk: true,
25
+ });
26
+ await waitForState(session, "paused");
27
+
28
+ // Continue past initial brk pause to the `debugger;` statement
29
+ await session.continue();
30
+ await waitForState(session, "paused");
31
+
32
+ return session;
33
+ }
34
+
35
+ describe("Mutation: setVariable", () => {
36
+ test("set variable changes value", async () => {
37
+ const session = await launchAndPauseAtDebugger("test-set-var");
38
+ try {
39
+ expect(session.sessionState).toBe("paused");
40
+
41
+ // Set counter to 42
42
+ const result = await session.setVariable("counter", "42");
43
+ expect(result.name).toBe("counter");
44
+ expect(result.newValue).toBe("42");
45
+ expect(result.type).toBe("number");
46
+
47
+ // Verify the value was actually changed
48
+ const evalResult = await session.eval("counter");
49
+ expect(evalResult.value).toBe("42");
50
+ } finally {
51
+ await session.stop();
52
+ }
53
+ });
54
+
55
+ test("set variable returns old value", async () => {
56
+ const session = await launchAndPauseAtDebugger("test-set-var-old");
57
+ try {
58
+ const result = await session.setVariable("counter", "99");
59
+ expect(result.oldValue).toBe("0");
60
+ expect(result.newValue).toBe("99");
61
+ } finally {
62
+ await session.stop();
63
+ }
64
+ });
65
+
66
+ test("set variable throws when not paused", async () => {
67
+ const session = new DebugSession("test-set-var-not-paused");
68
+ try {
69
+ await session.launch(["node", "-e", "setInterval(() => {}, 100)"], { brk: false });
70
+ expect(session.sessionState).toBe("running");
71
+
72
+ await expect(session.setVariable("x", "1")).rejects.toThrow("not paused");
73
+ } finally {
74
+ await session.stop();
75
+ }
76
+ });
77
+ });
78
+
79
+ describe("Mutation: setReturnValue", () => {
80
+ test("set-return changes return value", async () => {
81
+ const session = await launchAndPauseAtDebugger("test-set-return");
82
+ try {
83
+ expect(session.sessionState).toBe("paused");
84
+
85
+ // We are paused at `debugger;` (line 7).
86
+ // Step into goes to `const result = increment();` (line 8).
87
+ await session.step("into");
88
+ await waitForState(session, "paused");
89
+
90
+ // Step into enters the `increment` function body.
91
+ await session.step("into");
92
+ await waitForState(session, "paused");
93
+
94
+ // Step over `counter++` to reach `return counter;`
95
+ await session.step("over");
96
+ await waitForState(session, "paused");
97
+
98
+ // Step over the return statement -- this evaluates `return counter`
99
+ // and pauses at the return point (frame about to pop).
100
+ await session.step("over");
101
+ await waitForState(session, "paused");
102
+
103
+ // Now at return position, set the return value to 99
104
+ const result = await session.setReturnValue("99");
105
+ expect(result.value).toBe("99");
106
+ expect(result.type).toBe("number");
107
+ } finally {
108
+ await session.stop();
109
+ }
110
+ });
111
+ });
112
+
113
+ describe("Mutation: hotpatch", () => {
114
+ test("hotpatch replaces script source", async () => {
115
+ const session = await launchAndPauseAtDebugger("test-hotpatch");
116
+ try {
117
+ expect(session.sessionState).toBe("paused");
118
+
119
+ // Get the current source
120
+ const source = await session.getSource({ file: "mutation-app.js", all: true });
121
+ expect(source.url).toContain("mutation-app.js");
122
+
123
+ // Build modified source: change the increment function to add 10 instead of 1
124
+ const originalText = source.lines.map((l) => l.text).join("\n");
125
+ const modifiedSource = originalText.replace("counter++", "counter += 10");
126
+
127
+ // Apply hotpatch
128
+ const result = await session.hotpatch("mutation-app.js", modifiedSource);
129
+ expect(result.status).toBeDefined();
130
+
131
+ // Verify the modified function by checking the updated source
132
+ const newSource = await session.getSource({ file: "mutation-app.js", all: true });
133
+ const newText = newSource.lines.map((l) => l.text).join("\n");
134
+ expect(newText).toContain("counter += 10");
135
+ } finally {
136
+ await session.stop();
137
+ }
138
+ });
139
+
140
+ test("hotpatch dry-run does not modify source", async () => {
141
+ const session = await launchAndPauseAtDebugger("test-hotpatch-dry");
142
+ try {
143
+ expect(session.sessionState).toBe("paused");
144
+
145
+ // Get the current source
146
+ const source = await session.getSource({ file: "mutation-app.js", all: true });
147
+ const originalText = source.lines.map((l) => l.text).join("\n");
148
+
149
+ // Build modified source
150
+ const modifiedSource = originalText.replace("counter++", "counter += 100");
151
+
152
+ // Apply hotpatch with dryRun
153
+ const result = await session.hotpatch("mutation-app.js", modifiedSource, {
154
+ dryRun: true,
155
+ });
156
+ expect(result.status).toBeDefined();
157
+
158
+ // Verify the source was NOT changed
159
+ const afterSource = await session.getSource({ file: "mutation-app.js", all: true });
160
+ const afterText = afterSource.lines.map((l) => l.text).join("\n");
161
+ expect(afterText).not.toContain("counter += 100");
162
+ expect(afterText).toContain("counter++");
163
+ } finally {
164
+ await session.stop();
165
+ }
166
+ });
167
+
168
+ test("hotpatch throws for unknown file", async () => {
169
+ const session = await launchAndPauseAtDebugger("test-hotpatch-unknown");
170
+ try {
171
+ await expect(session.hotpatch("nonexistent-file.js", "// new source")).rejects.toThrow(
172
+ "No loaded script",
173
+ );
174
+ } finally {
175
+ await session.stop();
176
+ }
177
+ });
178
+ });
@@ -0,0 +1,223 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { DebugSession } from "../../src/daemon/session.ts";
3
+
4
+ /**
5
+ * Polls until the session reaches the expected state, or times out.
6
+ * This is needed because Debugger.paused events arrive asynchronously
7
+ * over WebSocket and may not have been processed when launch() returns.
8
+ */
9
+ async function waitForState(
10
+ session: DebugSession,
11
+ state: "idle" | "running" | "paused",
12
+ timeoutMs = 2000,
13
+ ): Promise<void> {
14
+ const deadline = Date.now() + timeoutMs;
15
+ while (session.sessionState !== state && Date.now() < deadline) {
16
+ await Bun.sleep(50);
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Reads stderr from a spawned process until it finds the inspector URL line.
22
+ * Returns the accumulated stderr output.
23
+ */
24
+ async function readStderrUntilInspector(stderr: ReadableStream<Uint8Array>): Promise<string> {
25
+ const reader = stderr.getReader();
26
+ const decoder = new TextDecoder();
27
+ let output = "";
28
+ try {
29
+ while (true) {
30
+ const { done, value } = await reader.read();
31
+ if (done) break;
32
+ output += decoder.decode(value, { stream: true });
33
+ if (output.includes("Debugger listening on")) break;
34
+ }
35
+ } finally {
36
+ reader.releaseLock();
37
+ }
38
+ return output;
39
+ }
40
+
41
+ describe("DebugSession integration", () => {
42
+ test("launch with brk pauses at first line", async () => {
43
+ const session = new DebugSession("test-launch");
44
+ try {
45
+ const result = await session.launch(["node", "tests/fixtures/simple-app.js"], {
46
+ brk: true,
47
+ });
48
+ // Allow time for the Debugger.paused event to arrive via WebSocket
49
+ await waitForState(session, "paused");
50
+
51
+ expect(result.pid).toBeGreaterThan(0);
52
+ expect(result.wsUrl).toMatch(/^ws:\/\//);
53
+ expect(session.sessionState).toBe("paused");
54
+ } finally {
55
+ await session.stop();
56
+ }
57
+ });
58
+
59
+ test("launch without brk starts running", async () => {
60
+ const session = new DebugSession("test-nobrk");
61
+ try {
62
+ const result = await session.launch(["node", "-e", "setTimeout(() => {}, 30000)"], {
63
+ brk: false,
64
+ });
65
+ expect(result.pid).toBeGreaterThan(0);
66
+ expect(result.paused).toBe(false);
67
+ expect(session.sessionState).toBe("running");
68
+ } finally {
69
+ await session.stop();
70
+ }
71
+ });
72
+
73
+ test("getStatus returns correct info after launch", async () => {
74
+ const session = new DebugSession("test-status");
75
+ try {
76
+ await session.launch(["node", "tests/fixtures/simple-app.js"], { brk: true });
77
+ // Allow time for the Debugger.paused event to arrive via WebSocket
78
+ await waitForState(session, "paused");
79
+
80
+ const status = session.getStatus();
81
+ expect(status.session).toBe("test-status");
82
+ expect(status.state).toBe("paused");
83
+ expect(status.pid).toBeGreaterThan(0);
84
+ expect(status.wsUrl).toBeDefined();
85
+ expect(status.uptime).toBeGreaterThanOrEqual(0);
86
+ expect(status.scriptCount).toBeGreaterThan(0);
87
+ } finally {
88
+ await session.stop();
89
+ }
90
+ });
91
+
92
+ test("stop disconnects and kills process", async () => {
93
+ const session = new DebugSession("test-stop");
94
+ const result = await session.launch(["node", "-e", "setTimeout(() => {}, 30000)"], {
95
+ brk: true,
96
+ });
97
+ const pid = result.pid;
98
+
99
+ await session.stop();
100
+
101
+ expect(session.sessionState).toBe("idle");
102
+ expect(session.cdp).toBeNull();
103
+ expect(session.targetPid).toBeNull();
104
+
105
+ // Verify process is actually dead (give it a moment)
106
+ await Bun.sleep(100);
107
+ let alive = false;
108
+ try {
109
+ process.kill(pid, 0);
110
+ alive = true;
111
+ } catch {
112
+ alive = false;
113
+ }
114
+ expect(alive).toBe(false);
115
+ });
116
+
117
+ test("attach connects to running inspector", async () => {
118
+ // Start a node process with --inspect manually
119
+ const proc = Bun.spawn(["node", "--inspect=0", "-e", "setTimeout(() => {}, 30000)"], {
120
+ stdin: "ignore",
121
+ stdout: "pipe",
122
+ stderr: "pipe",
123
+ });
124
+
125
+ try {
126
+ const stderr = await readStderrUntilInspector(proc.stderr);
127
+ const match = /Debugger listening on (ws:\/\/\S+)/.exec(stderr);
128
+ expect(match).not.toBeNull();
129
+ const wsUrl = match?.[1] ?? "";
130
+ expect(wsUrl).toMatch(/^ws:\/\//);
131
+
132
+ const session = new DebugSession("test-attach");
133
+ try {
134
+ const result = await session.attach(wsUrl);
135
+ expect(result.wsUrl).toBe(wsUrl);
136
+ expect(session.sessionState).toBe("running");
137
+ } finally {
138
+ await session.stop();
139
+ }
140
+ } finally {
141
+ proc.kill();
142
+ }
143
+ });
144
+
145
+ test("attach by port discovers ws URL", async () => {
146
+ // Start node with --inspect=0 to get a random port
147
+ const proc = Bun.spawn(["node", "--inspect=0", "-e", "setTimeout(() => {}, 30000)"], {
148
+ stdin: "ignore",
149
+ stdout: "pipe",
150
+ stderr: "pipe",
151
+ });
152
+
153
+ try {
154
+ const stderr = await readStderrUntilInspector(proc.stderr);
155
+ const match = /ws:\/\/127\.0\.0\.1:(\d+)\//.exec(stderr);
156
+ expect(match).not.toBeNull();
157
+ const port = match?.[1] ?? "";
158
+ expect(port).toMatch(/^\d+$/);
159
+
160
+ const session = new DebugSession("test-attach-port");
161
+ try {
162
+ const result = await session.attach(port);
163
+ expect(result.wsUrl).toMatch(/^ws:\/\//);
164
+ } finally {
165
+ await session.stop();
166
+ }
167
+ } finally {
168
+ proc.kill();
169
+ }
170
+ });
171
+
172
+ test("launching twice throws error", async () => {
173
+ const session = new DebugSession("test-double");
174
+ try {
175
+ await session.launch(["node", "-e", "setTimeout(() => {}, 30000)"], { brk: true });
176
+ await expect(
177
+ session.launch(["node", "-e", "setTimeout(() => {}, 30000)"], { brk: true }),
178
+ ).rejects.toThrow("already has an active");
179
+ } finally {
180
+ await session.stop();
181
+ }
182
+ });
183
+
184
+ test("CDP connection is functional after launch", async () => {
185
+ const session = new DebugSession("test-cdp");
186
+ try {
187
+ await session.launch(["node", "-e", "debugger; setTimeout(() => {}, 30000)"], {
188
+ brk: true,
189
+ });
190
+ // Allow time for the Debugger.paused event to arrive via WebSocket
191
+ await waitForState(session, "paused");
192
+
193
+ // CDP should be connected
194
+ expect(session.cdp).not.toBeNull();
195
+ const cdp = session.cdp;
196
+ expect(cdp?.connected).toBe(true);
197
+
198
+ // Should be able to send CDP commands
199
+ const result = await cdp?.send("Runtime.evaluate", {
200
+ expression: "1 + 1",
201
+ });
202
+ const evalResult = result as { result: { value: number } };
203
+ expect(evalResult.result.value).toBe(2);
204
+ } finally {
205
+ await session.stop();
206
+ }
207
+ });
208
+
209
+ test("scripts are tracked after launch", async () => {
210
+ const session = new DebugSession("test-scripts");
211
+ try {
212
+ await session.launch(["node", "tests/fixtures/simple-app.js"], { brk: true });
213
+ // Allow time for script parsed events to arrive
214
+ await waitForState(session, "paused");
215
+
216
+ const status = session.getStatus();
217
+ // There should be several scripts loaded (at minimum the main script and node internals)
218
+ expect(status.scriptCount).toBeGreaterThan(0);
219
+ } finally {
220
+ await session.stop();
221
+ }
222
+ });
223
+ });