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.
- package/.bin/ndbg +0 -0
- package/.claude/settings.local.json +21 -0
- package/.claude/skills/ndbg-debugger/ndbg-debugger/SKILL.md +116 -0
- package/.claude/skills/ndbg-debugger/ndbg-debugger/references/commands.md +173 -0
- package/CLAUDE.md +43 -0
- package/PROGRESS.md +261 -0
- package/README.md +67 -0
- package/biome.json +41 -0
- package/ndbg-spec.md +958 -0
- package/package.json +30 -0
- package/src/cdp/client.ts +198 -0
- package/src/cdp/types.ts +16 -0
- package/src/cli/parser.ts +287 -0
- package/src/cli/registry.ts +7 -0
- package/src/cli/types.ts +24 -0
- package/src/commands/attach.ts +47 -0
- package/src/commands/blackbox-ls.ts +38 -0
- package/src/commands/blackbox-rm.ts +57 -0
- package/src/commands/blackbox.ts +48 -0
- package/src/commands/break-ls.ts +57 -0
- package/src/commands/break-rm.ts +40 -0
- package/src/commands/break-toggle.ts +42 -0
- package/src/commands/break.ts +145 -0
- package/src/commands/breakable.ts +69 -0
- package/src/commands/catch.ts +38 -0
- package/src/commands/console.ts +61 -0
- package/src/commands/continue.ts +46 -0
- package/src/commands/eval.ts +70 -0
- package/src/commands/exceptions.ts +61 -0
- package/src/commands/hotpatch.ts +67 -0
- package/src/commands/launch.ts +69 -0
- package/src/commands/logpoint.ts +78 -0
- package/src/commands/pause.ts +46 -0
- package/src/commands/props.ts +77 -0
- package/src/commands/restart-frame.ts +36 -0
- package/src/commands/run-to.ts +70 -0
- package/src/commands/scripts.ts +57 -0
- package/src/commands/search.ts +73 -0
- package/src/commands/sessions.ts +71 -0
- package/src/commands/set-return.ts +49 -0
- package/src/commands/set.ts +61 -0
- package/src/commands/source.ts +59 -0
- package/src/commands/sourcemap.ts +66 -0
- package/src/commands/stack.ts +64 -0
- package/src/commands/state.ts +124 -0
- package/src/commands/status.ts +57 -0
- package/src/commands/step.ts +50 -0
- package/src/commands/stop.ts +27 -0
- package/src/commands/vars.ts +71 -0
- package/src/daemon/client.ts +147 -0
- package/src/daemon/entry.ts +242 -0
- package/src/daemon/paths.ts +26 -0
- package/src/daemon/server.ts +185 -0
- package/src/daemon/session-blackbox.ts +41 -0
- package/src/daemon/session-breakpoints.ts +492 -0
- package/src/daemon/session-execution.ts +121 -0
- package/src/daemon/session-inspection.ts +701 -0
- package/src/daemon/session-mutation.ts +197 -0
- package/src/daemon/session-state.ts +258 -0
- package/src/daemon/session.ts +938 -0
- package/src/daemon/spawn.ts +53 -0
- package/src/formatter/errors.ts +15 -0
- package/src/formatter/source.ts +74 -0
- package/src/formatter/stack.ts +70 -0
- package/src/formatter/values.ts +269 -0
- package/src/formatter/variables.ts +20 -0
- package/src/main.ts +45 -0
- package/src/protocol/messages.ts +316 -0
- package/src/refs/ref-table.ts +120 -0
- package/src/refs/resolver.ts +24 -0
- package/src/sourcemap/resolver.ts +318 -0
- package/tests/fixtures/async-app.js +34 -0
- package/tests/fixtures/console-app.js +12 -0
- package/tests/fixtures/error-app.js +28 -0
- package/tests/fixtures/exception-app.js +6 -0
- package/tests/fixtures/inspect-app.js +10 -0
- package/tests/fixtures/mutation-app.js +9 -0
- package/tests/fixtures/simple-app.js +50 -0
- package/tests/fixtures/step-app.js +13 -0
- package/tests/fixtures/ts-app/src/app.ts +21 -0
- package/tests/fixtures/ts-app/tsconfig.json +14 -0
- package/tests/integration/blackbox.test.ts +135 -0
- package/tests/integration/break-extras.test.ts +241 -0
- package/tests/integration/breakpoint.test.ts +217 -0
- package/tests/integration/console.test.ts +275 -0
- package/tests/integration/execution.test.ts +247 -0
- package/tests/integration/inspection.test.ts +311 -0
- package/tests/integration/mutation.test.ts +178 -0
- package/tests/integration/session.test.ts +223 -0
- package/tests/integration/source.test.ts +209 -0
- package/tests/integration/sourcemap.test.ts +214 -0
- package/tests/integration/state.test.ts +208 -0
- package/tests/unit/cdp-client.test.ts +422 -0
- package/tests/unit/daemon.test.ts +286 -0
- package/tests/unit/formatter.test.ts +716 -0
- package/tests/unit/parser.test.ts +105 -0
- package/tests/unit/refs.test.ts +383 -0
- package/tests/unit/sourcemap.test.ts +236 -0
- 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
|
+
});
|