agent-dbg 0.1.8 → 0.2.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/.claude/skills/agent-dbg/SKILL.md +44 -14
- package/.claude/skills/agent-dbg/references/commands.md +4 -0
- package/README.md +65 -29
- package/dist/main.js +362 -98
- package/package.json +3 -3
- package/src/cdp/client.ts +67 -6
- package/src/cdp/jsc-client.ts +58 -0
- package/src/cdp/jsc-protocol.d.ts +2807 -0
- package/src/daemon/adapters/bun-adapter.ts +190 -0
- package/src/daemon/adapters/index.ts +13 -0
- package/src/daemon/adapters/node-adapter.ts +121 -0
- package/src/daemon/runtime-adapter.ts +50 -0
- package/src/daemon/session-blackbox.ts +2 -6
- package/src/daemon/session-breakpoints.ts +64 -53
- package/src/daemon/session-inspection.ts +2 -2
- package/src/daemon/session-state.ts +1 -1
- package/src/daemon/session.ts +46 -44
- package/src/formatter/source.ts +6 -2
- package/tests/fixtures/bun-simple.js +12 -0
- package/tests/integration/bun-launch.test.ts +135 -0
- package/tests/unit/cdp-client.test.ts +134 -10
- package/tests/unit/jsc-client.test.ts +165 -0
- package/demo/DEMO.md +0 -71
- package/demo/order-processor.js +0 -35
- package/tests/fixtures/dap/hello +0 -0
- package/tests/fixtures/dap/hello.dSYM/Contents/Info.plist +0 -20
- package/tests/fixtures/dap/hello.dSYM/Contents/Resources/DWARF/hello +0 -0
- package/tests/fixtures/dap/hello.dSYM/Contents/Resources/Relocations/aarch64/hello.yml +0 -5
package/src/daemon/session.ts
CHANGED
|
@@ -6,8 +6,10 @@ import type { RemoteObject } from "../formatter/values.ts";
|
|
|
6
6
|
import { formatValue } from "../formatter/values.ts";
|
|
7
7
|
import { RefTable } from "../refs/ref-table.ts";
|
|
8
8
|
import { SourceMapResolver } from "../sourcemap/resolver.ts";
|
|
9
|
+
import { createAdapter } from "./adapters/index.ts";
|
|
9
10
|
import { DaemonLogger } from "./logger.ts";
|
|
10
11
|
import { ensureSocketDir, getDaemonLogPath, getLogPath } from "./paths.ts";
|
|
12
|
+
import type { RuntimeAdapter } from "./runtime-adapter.ts";
|
|
11
13
|
import {
|
|
12
14
|
addBlackbox as addBlackboxImpl,
|
|
13
15
|
listBlackbox as listBlackboxImpl,
|
|
@@ -134,7 +136,9 @@ export interface SessionStatus {
|
|
|
134
136
|
scriptCount: number;
|
|
135
137
|
}
|
|
136
138
|
|
|
137
|
-
|
|
139
|
+
// Node.js: "Debugger listening on ws://..."
|
|
140
|
+
// Bun: " ws://localhost:PORT/ID" (on its own indented line)
|
|
141
|
+
const INSPECTOR_URL_REGEX = /(?:Debugger listening on\s+)?(wss?:\/\/\S+)/;
|
|
138
142
|
const INSPECTOR_TIMEOUT_MS = 5_000;
|
|
139
143
|
|
|
140
144
|
export class DebugSession {
|
|
@@ -157,6 +161,7 @@ export class DebugSession {
|
|
|
157
161
|
new Map();
|
|
158
162
|
launchCommand: string[] | null = null;
|
|
159
163
|
launchOptions: { brk?: boolean; port?: number } | null = null;
|
|
164
|
+
adapter: RuntimeAdapter;
|
|
160
165
|
cdpLogger: CdpLogger;
|
|
161
166
|
daemonLogger: DaemonLogger;
|
|
162
167
|
|
|
@@ -165,6 +170,13 @@ export class DebugSession {
|
|
|
165
170
|
ensureSocketDir();
|
|
166
171
|
this.cdpLogger = new CdpLogger(getLogPath(session));
|
|
167
172
|
this.daemonLogger = options?.daemonLogger ?? new DaemonLogger(getDaemonLogPath(session));
|
|
173
|
+
// Default to NodeAdapter; overridden in launch() when command is known
|
|
174
|
+
this.adapter = createAdapter(["node"]);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Detected runtime name — delegates to the adapter */
|
|
178
|
+
get runtime(): "node" | "bun" | "unknown" {
|
|
179
|
+
return this.adapter.name;
|
|
168
180
|
}
|
|
169
181
|
|
|
170
182
|
// ── Session lifecycle ─────────────────────────────────────────────
|
|
@@ -183,15 +195,19 @@ export class DebugSession {
|
|
|
183
195
|
|
|
184
196
|
this.launchCommand = command;
|
|
185
197
|
this.launchOptions = options;
|
|
198
|
+
this.adapter = createAdapter(command);
|
|
186
199
|
|
|
187
200
|
const brk = options.brk ?? true;
|
|
188
201
|
const port = options.port ?? 0;
|
|
202
|
+
|
|
203
|
+
// Both Bun and Node.js support --inspect-brk (Bun also has --inspect-wait
|
|
204
|
+
// but --inspect-brk works better for our pause strategy)
|
|
189
205
|
const inspectFlag = brk ? `--inspect-brk=${port}` : `--inspect=${port}`;
|
|
190
206
|
|
|
191
207
|
// Build the args: inject inspect flag after the runtime (first element)
|
|
192
|
-
const
|
|
208
|
+
const runtimeBin = command[0] as string;
|
|
193
209
|
const rest = command.slice(1);
|
|
194
|
-
const spawnArgs = [
|
|
210
|
+
const spawnArgs = [runtimeBin, inspectFlag, ...rest];
|
|
195
211
|
|
|
196
212
|
const proc = Bun.spawn(spawnArgs, {
|
|
197
213
|
stdin: "ignore",
|
|
@@ -236,7 +252,23 @@ export class DebugSession {
|
|
|
236
252
|
};
|
|
237
253
|
|
|
238
254
|
if (this.pauseInfo) {
|
|
239
|
-
|
|
255
|
+
// Source-map translate for display
|
|
256
|
+
const translated = { ...this.pauseInfo };
|
|
257
|
+
if (translated.scriptId && translated.line !== undefined) {
|
|
258
|
+
const resolved = this.resolveOriginalLocation(
|
|
259
|
+
translated.scriptId,
|
|
260
|
+
translated.line + 1, // pauseInfo.line is 0-based
|
|
261
|
+
translated.column ?? 0,
|
|
262
|
+
);
|
|
263
|
+
if (resolved) {
|
|
264
|
+
translated.url = resolved.url;
|
|
265
|
+
translated.line = resolved.line - 1;
|
|
266
|
+
if (resolved.column !== undefined) {
|
|
267
|
+
translated.column = resolved.column - 1;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
result.pauseInfo = translated;
|
|
240
272
|
}
|
|
241
273
|
|
|
242
274
|
return result;
|
|
@@ -652,21 +684,16 @@ export class DebugSession {
|
|
|
652
684
|
const settle = () => {
|
|
653
685
|
if (settled) return;
|
|
654
686
|
settled = true;
|
|
655
|
-
clearTimeout(timer);
|
|
656
687
|
clearInterval(pollTimer);
|
|
657
|
-
this.cdp?.off("Debugger.paused", handler);
|
|
658
688
|
this.onProcessExit = null;
|
|
659
689
|
resolve();
|
|
660
690
|
};
|
|
661
691
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
const handler = () => {
|
|
668
|
-
settle();
|
|
669
|
-
};
|
|
692
|
+
// Use waitFor for the event subscription + timeout
|
|
693
|
+
this.cdp
|
|
694
|
+
?.waitFor("Debugger.paused", { timeoutMs })
|
|
695
|
+
.then(() => settle())
|
|
696
|
+
.catch(() => settle()); // timeout — don't reject, just settle
|
|
670
697
|
|
|
671
698
|
// Poll as a fallback in case the event/callback is missed
|
|
672
699
|
// (e.g., process exits and monitorProcessExit runs before
|
|
@@ -677,7 +704,6 @@ export class DebugSession {
|
|
|
677
704
|
}
|
|
678
705
|
}, 100);
|
|
679
706
|
|
|
680
|
-
this.cdp?.on("Debugger.paused", handler);
|
|
681
707
|
// Also resolve if the process exits during execution
|
|
682
708
|
this.onProcessExit = settle;
|
|
683
709
|
});
|
|
@@ -724,32 +750,7 @@ export class DebugSession {
|
|
|
724
750
|
// ── Private helpers ───────────────────────────────────────────────
|
|
725
751
|
|
|
726
752
|
private async waitForBrkPause(): Promise<void> {
|
|
727
|
-
|
|
728
|
-
if (!this.isPaused()) {
|
|
729
|
-
await Bun.sleep(100);
|
|
730
|
-
}
|
|
731
|
-
// On Node.js v24+, --inspect-brk does not emit Debugger.paused when the
|
|
732
|
-
// debugger connects after the process is already paused. We request an
|
|
733
|
-
// explicit pause and then signal Runtime.runIfWaitingForDebugger so the
|
|
734
|
-
// process starts execution and immediately hits our pause request.
|
|
735
|
-
if (!this.isPaused() && this.cdp) {
|
|
736
|
-
await this.cdp.send("Debugger.pause");
|
|
737
|
-
await this.cdp.send("Runtime.runIfWaitingForDebugger");
|
|
738
|
-
const deadline = Date.now() + 2_000;
|
|
739
|
-
while (!this.isPaused() && Date.now() < deadline) {
|
|
740
|
-
await Bun.sleep(50);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
// On Node.js v24+, the initial --inspect-brk pause lands in an internal
|
|
744
|
-
// bootstrap module (node:internal/...) rather than the user script.
|
|
745
|
-
// Resume past internal pauses until we reach user code.
|
|
746
|
-
let skips = 0;
|
|
747
|
-
while (this.isPaused() && this.cdp && this.pauseInfo?.url?.startsWith("node:") && skips < 5) {
|
|
748
|
-
skips++;
|
|
749
|
-
const waiter = this.createPauseWaiter(5_000);
|
|
750
|
-
await this.cdp.send("Debugger.resume");
|
|
751
|
-
await waiter;
|
|
752
|
-
}
|
|
753
|
+
return this.adapter.waitForBrkPause(this);
|
|
753
754
|
}
|
|
754
755
|
|
|
755
756
|
private async connectCdp(wsUrl: string): Promise<void> {
|
|
@@ -761,13 +762,14 @@ export class DebugSession {
|
|
|
761
762
|
// Set up event handlers before enabling domains so we don't miss any events
|
|
762
763
|
this.setupCdpEventHandlers(cdp);
|
|
763
764
|
|
|
765
|
+
// Runtime-specific pre-enable hook (e.g. Bun needs Inspector.enable first)
|
|
766
|
+
await this.adapter.preEnable(cdp);
|
|
767
|
+
|
|
764
768
|
await cdp.enableDomains();
|
|
765
769
|
|
|
766
770
|
// Re-apply blackbox patterns if any exist
|
|
767
771
|
if (this.blackboxPatterns.length > 0) {
|
|
768
|
-
await
|
|
769
|
-
patterns: this.blackboxPatterns,
|
|
770
|
-
});
|
|
772
|
+
await this.adapter.setBlackboxPatterns(cdp, this.blackboxPatterns);
|
|
771
773
|
}
|
|
772
774
|
|
|
773
775
|
// Update state to running if not already paused
|
package/src/formatter/source.ts
CHANGED
|
@@ -66,8 +66,12 @@ export function formatSource(lines: SourceLine[]): string {
|
|
|
66
66
|
|
|
67
67
|
// Add column indicator under current line
|
|
68
68
|
if (line.isCurrent && trimmed.caretOffset !== undefined && trimmed.caretOffset >= 0) {
|
|
69
|
-
const
|
|
70
|
-
|
|
69
|
+
const gutter = " ".repeat(numWidth + 4); // marker(2) + space(1) + numWidth + │(1)
|
|
70
|
+
// Preserve tabs from source so ^ aligns in terminal
|
|
71
|
+
const indent = trimmed.text
|
|
72
|
+
.slice(0, trimmed.caretOffset)
|
|
73
|
+
.replace(/[^\t]/g, " ");
|
|
74
|
+
result.push(`${gutter}${indent}^`);
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
return result.join("\n");
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Simple test fixture for Bun debugging
|
|
2
|
+
debugger; // Should trigger a pause
|
|
3
|
+
|
|
4
|
+
function greet(name) {
|
|
5
|
+
const message = `Hello, ${name}!`;
|
|
6
|
+
console.log(message);
|
|
7
|
+
return message;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const x = 42;
|
|
11
|
+
const greeting = greet("Bun");
|
|
12
|
+
console.log("x =", x);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { DebugSession } from "../../src/daemon/session.ts";
|
|
3
|
+
|
|
4
|
+
function waitForState(
|
|
5
|
+
session: DebugSession,
|
|
6
|
+
target: "paused" | "running" | "idle",
|
|
7
|
+
timeoutMs = 5_000,
|
|
8
|
+
): Promise<void> {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
if (session.state === target) return resolve();
|
|
11
|
+
const deadline = Date.now() + timeoutMs;
|
|
12
|
+
const timer = setInterval(() => {
|
|
13
|
+
if (session.state === target) {
|
|
14
|
+
clearInterval(timer);
|
|
15
|
+
resolve();
|
|
16
|
+
} else if (Date.now() > deadline) {
|
|
17
|
+
clearInterval(timer);
|
|
18
|
+
reject(new Error(`Timed out waiting for state=${target}, current=${session.state}`));
|
|
19
|
+
}
|
|
20
|
+
}, 50);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("Bun debugging", () => {
|
|
25
|
+
let session: DebugSession;
|
|
26
|
+
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
if (session) {
|
|
29
|
+
await session.stop().catch(() => {});
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("launches and pauses with --inspect-brk", async () => {
|
|
34
|
+
session = new DebugSession("bun-test-launch");
|
|
35
|
+
const result = await session.launch(["bun", "tests/fixtures/simple-app.js"], { brk: true });
|
|
36
|
+
|
|
37
|
+
expect(result.paused).toBe(true);
|
|
38
|
+
expect(result.pid).toBeGreaterThan(0);
|
|
39
|
+
expect(result.wsUrl).toContain("ws://");
|
|
40
|
+
expect(session.state).toBe("paused");
|
|
41
|
+
expect(session.runtime).toBe("bun");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("detects bun runtime", async () => {
|
|
45
|
+
session = new DebugSession("bun-test-detect");
|
|
46
|
+
await session.launch(["bun", "tests/fixtures/simple-app.js"], { brk: true });
|
|
47
|
+
expect(session.runtime).toBe("bun");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("state includes source-mapped location", async () => {
|
|
51
|
+
session = new DebugSession("bun-test-state");
|
|
52
|
+
await session.launch(["bun", "tests/fixtures/simple-app.js"], { brk: true });
|
|
53
|
+
await Bun.sleep(200); // Wait for source maps to load
|
|
54
|
+
|
|
55
|
+
const state = await session.buildState({ code: true, stack: true });
|
|
56
|
+
expect(state.status).toBe("paused");
|
|
57
|
+
// Source-mapped location should point to original source
|
|
58
|
+
expect(state.location?.url).toContain("simple-app.js");
|
|
59
|
+
expect(state.location?.line).toBe(38); // const counter = new Counter(10)
|
|
60
|
+
expect(state.source?.lines?.some((l) => l.current)).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("eval works in paused context", async () => {
|
|
64
|
+
session = new DebugSession("bun-test-eval");
|
|
65
|
+
await session.launch(["bun", "tests/fixtures/simple-app.js"], { brk: true });
|
|
66
|
+
|
|
67
|
+
const r1 = await session.eval("1+1");
|
|
68
|
+
expect(r1.value).toBe("2");
|
|
69
|
+
|
|
70
|
+
const r2 = await session.eval("typeof Bun");
|
|
71
|
+
expect(r2.value).toBe('"object"');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("breakpoint by scriptId hits", async () => {
|
|
75
|
+
session = new DebugSession("bun-test-bp");
|
|
76
|
+
await session.launch(["bun", "tests/fixtures/simple-app.js"], { brk: true });
|
|
77
|
+
await Bun.sleep(200); // Wait for source maps
|
|
78
|
+
|
|
79
|
+
// Set breakpoint inside greet function (original line 6)
|
|
80
|
+
const bp = await session.setBreakpoint("tests/fixtures/simple-app.js", 6);
|
|
81
|
+
expect(bp.ref).toMatch(/^BP#/);
|
|
82
|
+
|
|
83
|
+
// Continue — should hit breakpoint
|
|
84
|
+
await session.continue();
|
|
85
|
+
await waitForState(session, "paused");
|
|
86
|
+
|
|
87
|
+
expect(session.state).toBe("paused");
|
|
88
|
+
expect(session.pauseInfo?.reason).toBe("Breakpoint");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("step over works", async () => {
|
|
92
|
+
session = new DebugSession("bun-test-step");
|
|
93
|
+
await session.launch(["bun", "tests/fixtures/simple-app.js"], { brk: true });
|
|
94
|
+
|
|
95
|
+
const initialLine = session.pauseInfo?.line;
|
|
96
|
+
await session.step("over");
|
|
97
|
+
expect(session.state).toBe("paused");
|
|
98
|
+
// Should have advanced at least one line
|
|
99
|
+
expect(session.pauseInfo?.line).not.toBe(initialLine);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("step into enters function", async () => {
|
|
103
|
+
session = new DebugSession("bun-test-step-into");
|
|
104
|
+
await session.launch(["bun", "tests/fixtures/simple-app.js"], { brk: true });
|
|
105
|
+
|
|
106
|
+
// Step over to reach line 30 (greet call) then step into
|
|
107
|
+
await session.step("over"); // past const counter
|
|
108
|
+
await session.step("into"); // into greet("World")
|
|
109
|
+
|
|
110
|
+
expect(session.state).toBe("paused");
|
|
111
|
+
// Should be inside greet function
|
|
112
|
+
const stack = session.getStack({});
|
|
113
|
+
expect(stack[0]?.functionName).toBe("greet");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("scripts list includes user script", async () => {
|
|
117
|
+
session = new DebugSession("bun-test-scripts");
|
|
118
|
+
await session.launch(["bun", "tests/fixtures/simple-app.js"], { brk: true });
|
|
119
|
+
|
|
120
|
+
const scripts = session.getScripts();
|
|
121
|
+
const userScript = scripts.find((s) => s.url.includes("simple-app.js"));
|
|
122
|
+
expect(userScript).toBeDefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("continue resumes execution", async () => {
|
|
126
|
+
session = new DebugSession("bun-test-continue");
|
|
127
|
+
await session.launch(["bun", "tests/fixtures/simple-app.js"], { brk: true });
|
|
128
|
+
|
|
129
|
+
// Set a breakpoint so continue stops again (otherwise continue() waits for next pause indefinitely)
|
|
130
|
+
await session.setBreakpoint("tests/fixtures/simple-app.js", 6);
|
|
131
|
+
await session.continue();
|
|
132
|
+
expect(session.state).toBe("paused");
|
|
133
|
+
expect(session.pauseInfo?.reason).toBe("Breakpoint");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -356,7 +356,7 @@ describe("CdpClient", () => {
|
|
|
356
356
|
});
|
|
357
357
|
|
|
358
358
|
describe("enableDomains", () => {
|
|
359
|
-
test("sends enable for
|
|
359
|
+
test("sends enable for required and optional domains", async () => {
|
|
360
360
|
const c = client!;
|
|
361
361
|
const sentMethods: string[] = [];
|
|
362
362
|
const originalSend = c["ws"].send.bind(c["ws"]);
|
|
@@ -364,24 +364,52 @@ describe("CdpClient", () => {
|
|
|
364
364
|
if (typeof data === "string") {
|
|
365
365
|
const parsed = JSON.parse(data);
|
|
366
366
|
sentMethods.push(parsed.method);
|
|
367
|
+
// Auto-respond with success
|
|
368
|
+
setTimeout(() => {
|
|
369
|
+
c.handleMessage(JSON.stringify({ id: parsed.id, result: {} }));
|
|
370
|
+
}, 0);
|
|
367
371
|
}
|
|
368
372
|
return originalSend(data as string);
|
|
369
373
|
};
|
|
370
374
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
// Respond to all four
|
|
374
|
-
c.handleMessage(JSON.stringify({ id: 1, result: {} }));
|
|
375
|
-
c.handleMessage(JSON.stringify({ id: 2, result: {} }));
|
|
376
|
-
c.handleMessage(JSON.stringify({ id: 3, result: {} }));
|
|
377
|
-
c.handleMessage(JSON.stringify({ id: 4, result: {} }));
|
|
378
|
-
|
|
379
|
-
await promise;
|
|
375
|
+
await c.enableDomains();
|
|
380
376
|
|
|
381
377
|
expect(sentMethods).toContain("Debugger.enable");
|
|
382
378
|
expect(sentMethods).toContain("Runtime.enable");
|
|
383
379
|
expect(sentMethods).toContain("Profiler.enable");
|
|
384
380
|
expect(sentMethods).toContain("HeapProfiler.enable");
|
|
381
|
+
expect(c.enabledDomains.has("Debugger")).toBe(true);
|
|
382
|
+
expect(c.enabledDomains.has("Runtime")).toBe(true);
|
|
383
|
+
expect(c.enabledDomains.has("Profiler")).toBe(true);
|
|
384
|
+
expect(c.enabledDomains.has("HeapProfiler")).toBe(true);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("tracks which optional domains succeed", async () => {
|
|
388
|
+
const c = client!;
|
|
389
|
+
const originalSend = c["ws"].send.bind(c["ws"]);
|
|
390
|
+
c["ws"].send = (data: unknown) => {
|
|
391
|
+
if (typeof data === "string") {
|
|
392
|
+
const parsed = JSON.parse(data);
|
|
393
|
+
setTimeout(() => {
|
|
394
|
+
if (parsed.method === "Profiler.enable" || parsed.method === "HeapProfiler.enable") {
|
|
395
|
+
// Simulate Bun: optional domains fail
|
|
396
|
+
c.handleMessage(
|
|
397
|
+
JSON.stringify({ id: parsed.id, error: { code: -32601, message: "not found" } }),
|
|
398
|
+
);
|
|
399
|
+
} else {
|
|
400
|
+
c.handleMessage(JSON.stringify({ id: parsed.id, result: {} }));
|
|
401
|
+
}
|
|
402
|
+
}, 0);
|
|
403
|
+
}
|
|
404
|
+
return originalSend(data as string);
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
await c.enableDomains();
|
|
408
|
+
|
|
409
|
+
expect(c.enabledDomains.has("Debugger")).toBe(true);
|
|
410
|
+
expect(c.enabledDomains.has("Runtime")).toBe(true);
|
|
411
|
+
expect(c.enabledDomains.has("Profiler")).toBe(false);
|
|
412
|
+
expect(c.enabledDomains.has("HeapProfiler")).toBe(false);
|
|
385
413
|
});
|
|
386
414
|
});
|
|
387
415
|
|
|
@@ -406,6 +434,102 @@ describe("CdpClient", () => {
|
|
|
406
434
|
});
|
|
407
435
|
});
|
|
408
436
|
|
|
437
|
+
describe("waitFor", () => {
|
|
438
|
+
test("resolves with event params when event fires", async () => {
|
|
439
|
+
const c = client!;
|
|
440
|
+
const promise = c.waitFor("Debugger.paused", { timeoutMs: 1_000 });
|
|
441
|
+
|
|
442
|
+
c.handleMessage(
|
|
443
|
+
JSON.stringify({
|
|
444
|
+
method: "Debugger.paused",
|
|
445
|
+
params: { reason: "other", callFrames: [] },
|
|
446
|
+
}),
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const result = await promise;
|
|
450
|
+
expect(result).toEqual({ reason: "other", callFrames: [] });
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("rejects on timeout", async () => {
|
|
454
|
+
const c = client!;
|
|
455
|
+
const promise = c.waitFor("Debugger.paused", { timeoutMs: 50 });
|
|
456
|
+
|
|
457
|
+
await expect(promise).rejects.toThrow("waitFor timed out: Debugger.paused");
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("filter skips non-matching events", async () => {
|
|
461
|
+
const c = client!;
|
|
462
|
+
const promise = c.waitFor("Debugger.paused", {
|
|
463
|
+
timeoutMs: 1_000,
|
|
464
|
+
filter: (p) => p.reason === "step",
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// This one should be skipped
|
|
468
|
+
c.handleMessage(
|
|
469
|
+
JSON.stringify({
|
|
470
|
+
method: "Debugger.paused",
|
|
471
|
+
params: { reason: "other", callFrames: [] },
|
|
472
|
+
}),
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
// This one should match
|
|
476
|
+
c.handleMessage(
|
|
477
|
+
JSON.stringify({
|
|
478
|
+
method: "Debugger.paused",
|
|
479
|
+
params: { reason: "step", callFrames: [] },
|
|
480
|
+
}),
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
const result = await promise;
|
|
484
|
+
expect(result).toEqual({ reason: "step", callFrames: [] });
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("cleans up listener after resolving", async () => {
|
|
488
|
+
const c = client!;
|
|
489
|
+
const promise = c.waitFor("Debugger.paused", { timeoutMs: 1_000 });
|
|
490
|
+
|
|
491
|
+
c.handleMessage(
|
|
492
|
+
JSON.stringify({
|
|
493
|
+
method: "Debugger.paused",
|
|
494
|
+
params: { reason: "other", callFrames: [] },
|
|
495
|
+
}),
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
await promise;
|
|
499
|
+
|
|
500
|
+
// Listener should be cleaned up
|
|
501
|
+
expect(c["listeners"].get("Debugger.paused")?.size ?? 0).toBe(0);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test("cleans up listener after timeout", async () => {
|
|
505
|
+
const c = client!;
|
|
506
|
+
const promise = c.waitFor("Debugger.paused", { timeoutMs: 50 });
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
await promise;
|
|
510
|
+
} catch {
|
|
511
|
+
// expected
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
expect(c["listeners"].get("Debugger.paused")?.size ?? 0).toBe(0);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test("works with untyped events", async () => {
|
|
518
|
+
const c = client!;
|
|
519
|
+
const promise = c.waitFor("Inspector.initialized", { timeoutMs: 1_000 });
|
|
520
|
+
|
|
521
|
+
c.handleMessage(
|
|
522
|
+
JSON.stringify({
|
|
523
|
+
method: "Inspector.initialized",
|
|
524
|
+
params: {},
|
|
525
|
+
}),
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
const result = await promise;
|
|
529
|
+
expect(result).toEqual({});
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
409
533
|
describe("malformed messages", () => {
|
|
410
534
|
test("invalid JSON is silently ignored", () => {
|
|
411
535
|
const c = client!;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { CdpClient } from "../../src/cdp/client.ts";
|
|
3
|
+
import { JscClient } from "../../src/cdp/jsc-client.ts";
|
|
4
|
+
|
|
5
|
+
let server: ReturnType<typeof Bun.serve> | null = null;
|
|
6
|
+
let cdp: CdpClient | null = null;
|
|
7
|
+
let jsc: JscClient | null = null;
|
|
8
|
+
|
|
9
|
+
async function createTestClient(): Promise<{ cdp: CdpClient; jsc: JscClient }> {
|
|
10
|
+
server = Bun.serve({
|
|
11
|
+
port: 0,
|
|
12
|
+
fetch(req, srv) {
|
|
13
|
+
if (srv.upgrade(req, { data: undefined })) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
return new Response("Not found", { status: 404 });
|
|
17
|
+
},
|
|
18
|
+
websocket: {
|
|
19
|
+
message() {},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const port = server.port;
|
|
23
|
+
const c = await CdpClient.connect(`ws://127.0.0.1:${port}`);
|
|
24
|
+
return { cdp: c, jsc: new JscClient(c) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
const clients = await createTestClient();
|
|
29
|
+
cdp = clients.cdp;
|
|
30
|
+
jsc = clients.jsc;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
if (cdp?.connected) {
|
|
35
|
+
cdp.disconnect();
|
|
36
|
+
}
|
|
37
|
+
cdp = null;
|
|
38
|
+
jsc = null;
|
|
39
|
+
if (server) {
|
|
40
|
+
server.stop(true);
|
|
41
|
+
server = null;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("JscClient", () => {
|
|
46
|
+
test("send routes through cdp.sendRaw", async () => {
|
|
47
|
+
const j = jsc!;
|
|
48
|
+
const c = cdp!;
|
|
49
|
+
|
|
50
|
+
const sentMethods: string[] = [];
|
|
51
|
+
const originalSend = c["ws"].send.bind(c["ws"]);
|
|
52
|
+
c["ws"].send = (data: unknown) => {
|
|
53
|
+
if (typeof data === "string") {
|
|
54
|
+
const parsed = JSON.parse(data);
|
|
55
|
+
sentMethods.push(parsed.method);
|
|
56
|
+
}
|
|
57
|
+
return originalSend(data as string);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const promise = j.send("Inspector.enable");
|
|
61
|
+
c.handleMessage(JSON.stringify({ id: 1, result: {} }));
|
|
62
|
+
await promise;
|
|
63
|
+
|
|
64
|
+
expect(sentMethods).toEqual(["Inspector.enable"]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("send with params passes them through", async () => {
|
|
68
|
+
const j = jsc!;
|
|
69
|
+
const c = cdp!;
|
|
70
|
+
|
|
71
|
+
const sentParams: unknown[] = [];
|
|
72
|
+
const originalSend = c["ws"].send.bind(c["ws"]);
|
|
73
|
+
c["ws"].send = (data: unknown) => {
|
|
74
|
+
if (typeof data === "string") {
|
|
75
|
+
const parsed = JSON.parse(data);
|
|
76
|
+
sentParams.push(parsed.params);
|
|
77
|
+
}
|
|
78
|
+
return originalSend(data as string);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const promise = j.send("Debugger.setBreakpointsActive", { active: true });
|
|
82
|
+
c.handleMessage(JSON.stringify({ id: 1, result: {} }));
|
|
83
|
+
await promise;
|
|
84
|
+
|
|
85
|
+
expect(sentParams[0]).toEqual({ active: true });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("send returns typed result", async () => {
|
|
89
|
+
const j = jsc!;
|
|
90
|
+
const c = cdp!;
|
|
91
|
+
|
|
92
|
+
const promise = j.send("Debugger.setBreakpointByUrl", {
|
|
93
|
+
urlRegex: "test\\.js$",
|
|
94
|
+
lineNumber: 1,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
c.handleMessage(
|
|
98
|
+
JSON.stringify({
|
|
99
|
+
id: 1,
|
|
100
|
+
result: {
|
|
101
|
+
breakpointId: "bp-1",
|
|
102
|
+
locations: [{ scriptId: "1", lineNumber: 1 }],
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const result = await promise;
|
|
108
|
+
expect(result.breakpointId).toBe("bp-1");
|
|
109
|
+
expect(result.locations).toHaveLength(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("exposes cdp property for shared commands", () => {
|
|
113
|
+
const j = jsc!;
|
|
114
|
+
expect(j.cdp).toBe(cdp!);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("connected delegates to cdp", () => {
|
|
118
|
+
const j = jsc!;
|
|
119
|
+
expect(j.connected).toBe(true);
|
|
120
|
+
j.disconnect();
|
|
121
|
+
expect(j.connected).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("on/off delegate to cdp", () => {
|
|
125
|
+
const j = jsc!;
|
|
126
|
+
const c = cdp!;
|
|
127
|
+
|
|
128
|
+
const results: unknown[] = [];
|
|
129
|
+
const handler = (params: unknown) => results.push(params);
|
|
130
|
+
|
|
131
|
+
j.on("Debugger.paused", handler);
|
|
132
|
+
c.handleMessage(
|
|
133
|
+
JSON.stringify({
|
|
134
|
+
method: "Debugger.paused",
|
|
135
|
+
params: { reason: "breakpoint" },
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
expect(results).toHaveLength(1);
|
|
139
|
+
|
|
140
|
+
j.off("Debugger.paused", handler);
|
|
141
|
+
c.handleMessage(
|
|
142
|
+
JSON.stringify({
|
|
143
|
+
method: "Debugger.paused",
|
|
144
|
+
params: { reason: "step" },
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
expect(results).toHaveLength(1); // no new event
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("waitFor delegates to cdp", async () => {
|
|
151
|
+
const j = jsc!;
|
|
152
|
+
const c = cdp!;
|
|
153
|
+
|
|
154
|
+
const promise = j.waitFor("Debugger.paused", { timeoutMs: 1_000 });
|
|
155
|
+
c.handleMessage(
|
|
156
|
+
JSON.stringify({
|
|
157
|
+
method: "Debugger.paused",
|
|
158
|
+
params: { reason: "Breakpoint", callFrames: [] },
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const result = await promise;
|
|
163
|
+
expect(result).toEqual({ reason: "Breakpoint", callFrames: [] });
|
|
164
|
+
});
|
|
165
|
+
});
|