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,318 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import {
3
+ generatedPositionFor,
4
+ LEAST_UPPER_BOUND,
5
+ originalPositionFor,
6
+ TraceMap,
7
+ } from "@jridgewell/trace-mapping";
8
+
9
+ export interface OriginalPosition {
10
+ source: string;
11
+ line: number;
12
+ column: number;
13
+ name: string | null;
14
+ }
15
+
16
+ export interface GeneratedPosition {
17
+ scriptId: string;
18
+ line: number;
19
+ column: number;
20
+ }
21
+
22
+ export interface SourceMapInfo {
23
+ scriptId: string;
24
+ generatedUrl: string;
25
+ mapUrl: string;
26
+ sources: string[];
27
+ hasSourcesContent: boolean;
28
+ }
29
+
30
+ interface LoadedMap {
31
+ traceMap: TraceMap;
32
+ scriptId: string;
33
+ generatedUrl: string;
34
+ mapUrl: string;
35
+ sources: string[];
36
+ resolvedSources: string[];
37
+ hasSourcesContent: boolean;
38
+ }
39
+
40
+ export class SourceMapResolver {
41
+ private maps: Map<string, LoadedMap> = new Map();
42
+ // Reverse lookup: resolved source path → { scriptId, sourceIndex }
43
+ private sourceIndex: Map<string, { scriptId: string; sourceIndex: number }> = new Map();
44
+ private disabled = false;
45
+
46
+ async loadSourceMap(scriptId: string, scriptUrl: string, sourceMapURL: string): Promise<boolean> {
47
+ if (this.disabled) return false;
48
+
49
+ try {
50
+ let rawMap: string;
51
+
52
+ if (sourceMapURL.startsWith("data:")) {
53
+ // Inline data: URI
54
+ const commaIndex = sourceMapURL.indexOf(",");
55
+ if (commaIndex === -1) return false;
56
+ const header = sourceMapURL.slice(0, commaIndex);
57
+ const data = sourceMapURL.slice(commaIndex + 1);
58
+
59
+ if (header.includes("base64")) {
60
+ rawMap = Buffer.from(data, "base64").toString("utf-8");
61
+ } else {
62
+ rawMap = decodeURIComponent(data);
63
+ }
64
+ } else {
65
+ // File-based source map — resolve relative to the script
66
+ let mapPath: string;
67
+ const scriptPath = scriptUrl.startsWith("file://") ? scriptUrl.slice(7) : scriptUrl;
68
+
69
+ if (sourceMapURL.startsWith("/")) {
70
+ mapPath = sourceMapURL;
71
+ } else {
72
+ mapPath = resolve(dirname(scriptPath), sourceMapURL);
73
+ }
74
+
75
+ const file = Bun.file(mapPath);
76
+ if (!(await file.exists())) return false;
77
+ rawMap = await file.text();
78
+ }
79
+
80
+ const parsed = JSON.parse(rawMap);
81
+ const traceMap = new TraceMap(parsed);
82
+
83
+ const sources: string[] = (traceMap.sources as string[]) ?? [];
84
+
85
+ // Resolve source paths relative to the script location
86
+ const scriptPath = scriptUrl.startsWith("file://") ? scriptUrl.slice(7) : scriptUrl;
87
+ const scriptDir = dirname(scriptPath);
88
+
89
+ const resolvedSources = sources.map((s) => {
90
+ if (s.startsWith("/")) return s;
91
+ return resolve(scriptDir, s);
92
+ });
93
+
94
+ const entry: LoadedMap = {
95
+ traceMap,
96
+ scriptId,
97
+ generatedUrl: scriptUrl,
98
+ mapUrl: sourceMapURL,
99
+ sources,
100
+ resolvedSources,
101
+ hasSourcesContent:
102
+ Array.isArray(traceMap.sourcesContent) && traceMap.sourcesContent.some((c) => c != null),
103
+ };
104
+
105
+ this.maps.set(scriptId, entry);
106
+
107
+ // Build reverse lookup for each source
108
+ for (let i = 0; i < sources.length; i++) {
109
+ const rawSource = sources[i];
110
+ const resolvedSource = resolvedSources[i];
111
+
112
+ if (rawSource) {
113
+ this.sourceIndex.set(rawSource, { scriptId, sourceIndex: i });
114
+ }
115
+ if (resolvedSource && resolvedSource !== rawSource) {
116
+ this.sourceIndex.set(resolvedSource, { scriptId, sourceIndex: i });
117
+ }
118
+ }
119
+
120
+ return true;
121
+ } catch {
122
+ return false;
123
+ }
124
+ }
125
+
126
+ toOriginal(scriptId: string, line: number, column: number): OriginalPosition | null {
127
+ if (this.disabled) return null;
128
+
129
+ const entry = this.maps.get(scriptId);
130
+ if (!entry) return null;
131
+
132
+ const result = originalPositionFor(entry.traceMap, {
133
+ line,
134
+ column,
135
+ });
136
+
137
+ if (result.source == null) return null;
138
+
139
+ return {
140
+ source: result.source,
141
+ line: result.line ?? line,
142
+ column: result.column ?? column,
143
+ name: result.name,
144
+ };
145
+ }
146
+
147
+ toGenerated(source: string, line: number, column: number): GeneratedPosition | null {
148
+ if (this.disabled) return null;
149
+
150
+ // Try direct lookup first
151
+ const indexEntry = this.sourceIndex.get(source);
152
+ if (indexEntry) {
153
+ const entry = this.maps.get(indexEntry.scriptId);
154
+ if (entry) {
155
+ const sourceName = entry.sources[indexEntry.sourceIndex];
156
+ if (sourceName) {
157
+ const result = this.tryGeneratedPosition(entry.traceMap, sourceName, line, column);
158
+ if (result) {
159
+ return { scriptId: indexEntry.scriptId, ...result };
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ // Try suffix matching
166
+ const match = this.findScriptForSource(source);
167
+ if (match) {
168
+ const entry = this.maps.get(match.scriptId);
169
+ if (entry) {
170
+ // Find the matching source name in the map
171
+ for (const s of entry.sources) {
172
+ if (s && (s === source || s.endsWith(source) || source.endsWith(s))) {
173
+ const result = this.tryGeneratedPosition(entry.traceMap, s, line, column);
174
+ if (result) {
175
+ return { scriptId: match.scriptId, ...result };
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ return null;
183
+ }
184
+
185
+ private tryGeneratedPosition(
186
+ traceMap: TraceMap,
187
+ source: string,
188
+ line: number,
189
+ column: number,
190
+ ): { line: number; column: number } | null {
191
+ // Try exact match first
192
+ const exact = generatedPositionFor(traceMap, { source, line, column });
193
+ if (exact.line != null) {
194
+ return { line: exact.line, column: exact.column ?? 0 };
195
+ }
196
+
197
+ // Fallback: use LEAST_UPPER_BOUND to find the nearest mapping on this line
198
+ const approx = generatedPositionFor(traceMap, {
199
+ source,
200
+ line,
201
+ column,
202
+ bias: LEAST_UPPER_BOUND,
203
+ });
204
+ if (approx.line != null) {
205
+ return { line: approx.line, column: approx.column ?? 0 };
206
+ }
207
+
208
+ return null;
209
+ }
210
+
211
+ getOriginalSource(scriptId: string, sourcePath: string): string | null {
212
+ if (this.disabled) return null;
213
+
214
+ const entry = this.maps.get(scriptId);
215
+ if (!entry) return null;
216
+
217
+ const sourcesContent = entry.traceMap.sourcesContent as (string | null)[] | undefined;
218
+ if (!sourcesContent) return null;
219
+
220
+ // Match by raw source path or resolved path
221
+ for (let i = 0; i < entry.sources.length; i++) {
222
+ const raw = entry.sources[i];
223
+ const resolved = entry.resolvedSources[i];
224
+ if (
225
+ (raw && (raw === sourcePath || raw.endsWith(sourcePath) || sourcePath.endsWith(raw))) ||
226
+ (resolved &&
227
+ (resolved === sourcePath ||
228
+ resolved.endsWith(sourcePath) ||
229
+ sourcePath.endsWith(resolved)))
230
+ ) {
231
+ return sourcesContent[i] ?? null;
232
+ }
233
+ }
234
+
235
+ return null;
236
+ }
237
+
238
+ findScriptForSource(path: string): { scriptId: string; url: string } | null {
239
+ if (this.disabled) return null;
240
+
241
+ // Try direct lookup first
242
+ const direct = this.sourceIndex.get(path);
243
+ if (direct) {
244
+ const entry = this.maps.get(direct.scriptId);
245
+ if (entry) {
246
+ return { scriptId: direct.scriptId, url: entry.generatedUrl };
247
+ }
248
+ }
249
+
250
+ // Try suffix matching against all sources
251
+ for (const [scriptId, entry] of this.maps) {
252
+ for (let i = 0; i < entry.sources.length; i++) {
253
+ const raw = entry.sources[i];
254
+ const resolved = entry.resolvedSources[i];
255
+ if (raw && (raw.endsWith(path) || path.endsWith(raw))) {
256
+ return { scriptId, url: entry.generatedUrl };
257
+ }
258
+ if (resolved && (resolved.endsWith(path) || path.endsWith(resolved))) {
259
+ return { scriptId, url: entry.generatedUrl };
260
+ }
261
+ }
262
+ }
263
+
264
+ return null;
265
+ }
266
+
267
+ /**
268
+ * Returns the primary original source URL for a script that has a source map,
269
+ * regardless of whether a specific line has a mapping. Used for Option A:
270
+ * always show .ts path when source map exists.
271
+ */
272
+ getScriptOriginalUrl(scriptId: string): string | null {
273
+ if (this.disabled) return null;
274
+ const entry = this.maps.get(scriptId);
275
+ if (!entry) return null;
276
+ return entry.sources[0] ?? null;
277
+ }
278
+
279
+ getInfo(scriptId: string): SourceMapInfo | null {
280
+ const entry = this.maps.get(scriptId);
281
+ if (!entry) return null;
282
+
283
+ return {
284
+ scriptId: entry.scriptId,
285
+ generatedUrl: entry.generatedUrl,
286
+ mapUrl: entry.mapUrl,
287
+ sources: [...entry.sources],
288
+ hasSourcesContent: entry.hasSourcesContent,
289
+ };
290
+ }
291
+
292
+ getAllInfos(): SourceMapInfo[] {
293
+ const result: SourceMapInfo[] = [];
294
+ for (const entry of this.maps.values()) {
295
+ result.push({
296
+ scriptId: entry.scriptId,
297
+ generatedUrl: entry.generatedUrl,
298
+ mapUrl: entry.mapUrl,
299
+ sources: [...entry.sources],
300
+ hasSourcesContent: entry.hasSourcesContent,
301
+ });
302
+ }
303
+ return result;
304
+ }
305
+
306
+ setDisabled(disabled: boolean): void {
307
+ this.disabled = disabled;
308
+ }
309
+
310
+ isDisabled(): boolean {
311
+ return this.disabled;
312
+ }
313
+
314
+ clear(): void {
315
+ this.maps.clear();
316
+ this.sourceIndex.clear();
317
+ }
318
+ }
@@ -0,0 +1,34 @@
1
+ // Test fixture with async patterns for ndbg integration tests
2
+ // Launch with: node --inspect-brk tests/fixtures/async-app.js
3
+
4
+ async function delay(ms) {
5
+ return new Promise((resolve) => setTimeout(resolve, ms));
6
+ }
7
+
8
+ async function processItem(item) {
9
+ await delay(10);
10
+ const result = { ...item, processed: true, timestamp: Date.now() };
11
+ return result;
12
+ }
13
+
14
+ async function processQueue(items) {
15
+ const results = [];
16
+ for (const item of items) {
17
+ const result = await processItem(item);
18
+ results.push(result);
19
+ }
20
+ return results;
21
+ }
22
+
23
+ const queue = [
24
+ { id: 1, name: "alpha" },
25
+ { id: 2, name: "beta" },
26
+ { id: 3, name: "gamma" },
27
+ ];
28
+
29
+ processQueue(queue).then((results) => {
30
+ console.log(`Processed ${results.length} items`);
31
+ for (const r of results) {
32
+ console.log(` ${r.id}: ${r.name} (processed: ${r.processed})`);
33
+ }
34
+ });
@@ -0,0 +1,12 @@
1
+ console.log("hello from app");
2
+ console.warn("warning message");
3
+ console.error("error message");
4
+ const obj = { key: "value" };
5
+ console.log("object:", obj);
6
+ try {
7
+ throw new Error("test error");
8
+ } catch (e) {
9
+ /* swallowed */
10
+ }
11
+ debugger;
12
+ console.log("after debugger");
@@ -0,0 +1,28 @@
1
+ // Test fixture with errors for ndbg integration tests
2
+ // Launch with: node --inspect-brk tests/fixtures/error-app.js
3
+
4
+ function riskyOperation(input) {
5
+ if (!input) {
6
+ throw new Error("Input is required");
7
+ }
8
+ return input.toUpperCase();
9
+ }
10
+
11
+ function handleError() {
12
+ try {
13
+ riskyOperation(null);
14
+ } catch (err) {
15
+ console.error("Caught:", err.message);
16
+ return { error: err.message };
17
+ }
18
+ }
19
+
20
+ // Uncaught error path
21
+ const mode = process.argv[2] || "caught";
22
+
23
+ if (mode === "caught") {
24
+ const result = handleError();
25
+ console.log("Result:", result);
26
+ } else if (mode === "uncaught") {
27
+ riskyOperation(null); // This will throw uncaught
28
+ }
@@ -0,0 +1,6 @@
1
+ // This script throws an uncaught exception after a short delay.
2
+ // The process will crash, but the Runtime.exceptionThrown event
3
+ // will be captured before the process exits.
4
+ setTimeout(() => {
5
+ throw new Error("uncaught!");
6
+ }, 50);
@@ -0,0 +1,10 @@
1
+ const obj = { name: "test", count: 42, nested: { deep: true } };
2
+ const arr = [1, 2, 3];
3
+ const str = "hello";
4
+ const num = 123;
5
+ function greet(who) {
6
+ return "hi " + who;
7
+ }
8
+ const promise = Promise.resolve(42);
9
+ debugger; // pause here
10
+ console.log(obj, arr);
@@ -0,0 +1,9 @@
1
+ let counter = 0;
2
+ const name = "original";
3
+ function increment() {
4
+ counter++;
5
+ return counter;
6
+ }
7
+ debugger;
8
+ const result = increment();
9
+ console.log(name, result);
@@ -0,0 +1,50 @@
1
+ // Simple test fixture for ndbg integration tests
2
+ // Launch with: node --inspect-brk tests/fixtures/simple-app.js
3
+
4
+ function greet(name) {
5
+ const message = `Hello, ${name}!`;
6
+ console.log(message);
7
+ return message;
8
+ }
9
+
10
+ function add(a, b) {
11
+ const result = a + b;
12
+ return result;
13
+ }
14
+
15
+ async function fetchData(id) {
16
+ const data = { id, name: "test", items: [1, 2, 3] };
17
+ await new Promise((resolve) => setTimeout(resolve, 10));
18
+ return data;
19
+ }
20
+
21
+ class Counter {
22
+ constructor(initial = 0) {
23
+ this.count = initial;
24
+ }
25
+
26
+ increment() {
27
+ this.count++;
28
+ return this.count;
29
+ }
30
+
31
+ decrement() {
32
+ this.count--;
33
+ return this.count;
34
+ }
35
+ }
36
+
37
+ // Main execution
38
+ const counter = new Counter(10);
39
+ const greeting = greet("World");
40
+ const sum = add(2, 3);
41
+
42
+ counter.increment();
43
+ counter.increment();
44
+ counter.decrement();
45
+
46
+ fetchData("test-123").then((data) => {
47
+ console.log("Data:", JSON.stringify(data));
48
+ console.log("Counter:", counter.count);
49
+ console.log("Sum:", sum);
50
+ });
@@ -0,0 +1,13 @@
1
+ // Test fixture for execution control tests
2
+ // Each line is a separate statement for stepping tests
3
+
4
+ function helper(x) {
5
+ const doubled = x * 2;
6
+ return doubled;
7
+ }
8
+
9
+ const a = 1;
10
+ const b = 2;
11
+ const c = helper(a);
12
+ const d = a + b + c;
13
+ console.log(d);
@@ -0,0 +1,21 @@
1
+ // TypeScript test fixture for source map integration tests
2
+ interface Person {
3
+ name: string;
4
+ age: number;
5
+ }
6
+
7
+ function greet(person: Person): string {
8
+ const message: string = `Hello, ${person.name}! Age: ${person.age}`;
9
+ return message;
10
+ }
11
+
12
+ function add(a: number, b: number): number {
13
+ const result: number = a + b;
14
+ return result;
15
+ }
16
+
17
+ const alice: Person = { name: "Alice", age: 30 };
18
+ const greeting: string = greet(alice);
19
+ const sum: number = add(2, 3);
20
+ console.log(greeting);
21
+ console.log("Sum:", sum);
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "outDir": "./dist",
6
+ "rootDir": "./src",
7
+ "sourceMap": true,
8
+ "inlineSources": true,
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "declaration": false
12
+ },
13
+ "include": ["src/**/*.ts"]
14
+ }
@@ -0,0 +1,135 @@
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
+ describe("Blackbox patterns", () => {
19
+ test("add blackbox patterns", async () => {
20
+ const session = new DebugSession("test-blackbox-add");
21
+ try {
22
+ await session.launch(["node", "tests/fixtures/step-app.js"], {
23
+ brk: true,
24
+ });
25
+ await waitForState(session, "paused");
26
+
27
+ const result = await session.addBlackbox(["node_modules", "internal"]);
28
+ expect(result).toEqual(["node_modules", "internal"]);
29
+ expect(session.listBlackbox()).toEqual(["node_modules", "internal"]);
30
+ } finally {
31
+ await session.stop();
32
+ }
33
+ });
34
+
35
+ test("list blackbox patterns", async () => {
36
+ const session = new DebugSession("test-blackbox-list");
37
+ try {
38
+ await session.launch(["node", "tests/fixtures/step-app.js"], {
39
+ brk: true,
40
+ });
41
+ await waitForState(session, "paused");
42
+
43
+ // Initially empty
44
+ expect(session.listBlackbox()).toEqual([]);
45
+
46
+ // Add some patterns
47
+ await session.addBlackbox(["node_modules", "vendor"]);
48
+
49
+ // Verify they are listed
50
+ const patterns = session.listBlackbox();
51
+ expect(patterns).toEqual(["node_modules", "vendor"]);
52
+ } finally {
53
+ await session.stop();
54
+ }
55
+ });
56
+
57
+ test("remove specific pattern", async () => {
58
+ const session = new DebugSession("test-blackbox-rm-specific");
59
+ try {
60
+ await session.launch(["node", "tests/fixtures/step-app.js"], {
61
+ brk: true,
62
+ });
63
+ await waitForState(session, "paused");
64
+
65
+ await session.addBlackbox(["node_modules", "vendor"]);
66
+ expect(session.listBlackbox()).toEqual(["node_modules", "vendor"]);
67
+
68
+ const result = await session.removeBlackbox(["node_modules"]);
69
+ expect(result).toEqual(["vendor"]);
70
+ expect(session.listBlackbox()).toEqual(["vendor"]);
71
+ } finally {
72
+ await session.stop();
73
+ }
74
+ });
75
+
76
+ test("remove all patterns", async () => {
77
+ const session = new DebugSession("test-blackbox-rm-all");
78
+ try {
79
+ await session.launch(["node", "tests/fixtures/step-app.js"], {
80
+ brk: true,
81
+ });
82
+ await waitForState(session, "paused");
83
+
84
+ await session.addBlackbox(["node_modules", "vendor", "internal"]);
85
+ expect(session.listBlackbox()).toHaveLength(3);
86
+
87
+ const result = await session.removeBlackbox(["all"]);
88
+ expect(result).toEqual([]);
89
+ expect(session.listBlackbox()).toEqual([]);
90
+ } finally {
91
+ await session.stop();
92
+ }
93
+ });
94
+
95
+ test("blackbox persists across continue", async () => {
96
+ const session = new DebugSession("test-blackbox-persist");
97
+ try {
98
+ await session.launch(["node", "tests/fixtures/step-app.js"], {
99
+ brk: true,
100
+ });
101
+ await waitForState(session, "paused");
102
+
103
+ // Set a breakpoint further in the file so we can continue to it
104
+ await session.setBreakpoint("step-app.js", 12);
105
+
106
+ // Add blackbox patterns
107
+ await session.addBlackbox(["node_modules"]);
108
+ expect(session.listBlackbox()).toEqual(["node_modules"]);
109
+
110
+ // Continue to the breakpoint
111
+ await session.continue();
112
+ await waitForState(session, "paused");
113
+
114
+ // Patterns should still be present
115
+ expect(session.listBlackbox()).toEqual(["node_modules"]);
116
+ } finally {
117
+ await session.stop();
118
+ }
119
+ });
120
+
121
+ test("blackbox throws when no session", async () => {
122
+ const session = new DebugSession("test-blackbox-no-session");
123
+ try {
124
+ await expect(session.addBlackbox(["node_modules"])).rejects.toThrow(
125
+ "No active debug session",
126
+ );
127
+
128
+ await expect(session.removeBlackbox(["node_modules"])).rejects.toThrow(
129
+ "No active debug session",
130
+ );
131
+ } finally {
132
+ await session.stop();
133
+ }
134
+ });
135
+ });