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,209 @@
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("Inspection commands", () => {
19
+ test("getSource returns lines around pause location with current marker", async () => {
20
+ const session = new DebugSession("test-source-basic");
21
+ try {
22
+ await session.launch(["node", "tests/fixtures/step-app.js"], { brk: true });
23
+ await waitForState(session, "paused");
24
+ expect(session.sessionState).toBe("paused");
25
+
26
+ const result = await session.getSource();
27
+ expect(result.url).toBeDefined();
28
+ expect(result.lines.length).toBeGreaterThan(0);
29
+
30
+ // There should be exactly one line marked as current
31
+ const currentLines = result.lines.filter((l) => l.current === true);
32
+ expect(currentLines.length).toBe(1);
33
+
34
+ // All lines should have line numbers and text
35
+ for (const line of result.lines) {
36
+ expect(typeof line.line).toBe("number");
37
+ expect(typeof line.text).toBe("string");
38
+ }
39
+ } finally {
40
+ await session.stop();
41
+ }
42
+ });
43
+
44
+ test("getSource with file option shows source of specified file", async () => {
45
+ const session = new DebugSession("test-source-file");
46
+ try {
47
+ await session.launch(["node", "tests/fixtures/step-app.js"], { brk: true });
48
+ await waitForState(session, "paused");
49
+ expect(session.sessionState).toBe("paused");
50
+
51
+ const result = await session.getSource({ file: "step-app.js" });
52
+ expect(result.url).toContain("step-app.js");
53
+ expect(result.lines.length).toBeGreaterThan(0);
54
+ } finally {
55
+ await session.stop();
56
+ }
57
+ });
58
+
59
+ test("getSource with all option returns entire file", async () => {
60
+ const session = new DebugSession("test-source-all");
61
+ try {
62
+ await session.launch(["node", "tests/fixtures/step-app.js"], { brk: true });
63
+ await waitForState(session, "paused");
64
+ expect(session.sessionState).toBe("paused");
65
+
66
+ const result = await session.getSource({ all: true });
67
+ expect(result.lines.length).toBeGreaterThan(0);
68
+
69
+ // step-app.js has 13 lines of content; with all, we should see all of them
70
+ // The file has content on lines including function helper, const a, etc.
71
+ expect(result.lines.length).toBeGreaterThanOrEqual(10);
72
+ } finally {
73
+ await session.stop();
74
+ }
75
+ });
76
+
77
+ test("getScripts lists loaded scripts including step-app.js", async () => {
78
+ const session = new DebugSession("test-scripts-list");
79
+ try {
80
+ await session.launch(["node", "tests/fixtures/step-app.js"], { brk: true });
81
+ await waitForState(session, "paused");
82
+ expect(session.sessionState).toBe("paused");
83
+
84
+ const scripts = session.getScripts();
85
+ expect(scripts.length).toBeGreaterThan(0);
86
+
87
+ // step-app.js should be in the list
88
+ const stepApp = scripts.find((s) => s.url.includes("step-app.js"));
89
+ expect(stepApp).toBeDefined();
90
+ expect(stepApp!.scriptId).toBeDefined();
91
+ expect(stepApp!.url).toContain("step-app.js");
92
+ } finally {
93
+ await session.stop();
94
+ }
95
+ });
96
+
97
+ test("getScripts with filter narrows results", async () => {
98
+ const session = new DebugSession("test-scripts-filter");
99
+ try {
100
+ await session.launch(["node", "tests/fixtures/step-app.js"], { brk: true });
101
+ await waitForState(session, "paused");
102
+ expect(session.sessionState).toBe("paused");
103
+
104
+ const allScripts = session.getScripts();
105
+ const filtered = session.getScripts("step-app");
106
+
107
+ // Filtered should contain step-app.js
108
+ expect(filtered.length).toBeGreaterThan(0);
109
+ const stepApp = filtered.find((s) => s.url.includes("step-app.js"));
110
+ expect(stepApp).toBeDefined();
111
+
112
+ // Filtered should be a subset of all scripts
113
+ expect(filtered.length).toBeLessThanOrEqual(allScripts.length);
114
+ } finally {
115
+ await session.stop();
116
+ }
117
+ });
118
+
119
+ test("getStack returns stack frames with refs and correct format", async () => {
120
+ const session = new DebugSession("test-stack-basic");
121
+ try {
122
+ await session.launch(["node", "tests/fixtures/step-app.js"], { brk: true });
123
+ await waitForState(session, "paused");
124
+ expect(session.sessionState).toBe("paused");
125
+
126
+ const stack = session.getStack();
127
+ expect(stack.length).toBeGreaterThan(0);
128
+
129
+ // Each frame should have ref, functionName, file, line
130
+ for (const frame of stack) {
131
+ expect(frame.ref).toMatch(/^@f\d+$/);
132
+ expect(typeof frame.functionName).toBe("string");
133
+ expect(typeof frame.file).toBe("string");
134
+ expect(typeof frame.line).toBe("number");
135
+ expect(frame.line).toBeGreaterThan(0); // 1-based
136
+ }
137
+ } finally {
138
+ await session.stop();
139
+ }
140
+ });
141
+
142
+ test("getStack while inside a function shows multiple frames", async () => {
143
+ const session = new DebugSession("test-stack-nested");
144
+ try {
145
+ await session.launch(["node", "tests/fixtures/step-app.js"], { brk: true });
146
+ await waitForState(session, "paused");
147
+
148
+ // Step to the helper() call and step into it
149
+ let currentLine = session.getStatus().pauseInfo?.line ?? 0;
150
+ while (currentLine < 10 && session.sessionState === "paused") {
151
+ await session.step("over");
152
+ currentLine = session.getStatus().pauseInfo?.line ?? currentLine;
153
+ }
154
+
155
+ await session.step("into");
156
+ const line = session.getStatus().pauseInfo?.line;
157
+ if (line !== undefined && line >= 10) {
158
+ await session.step("into");
159
+ }
160
+
161
+ expect(session.sessionState).toBe("paused");
162
+
163
+ const stack = session.getStack();
164
+ // Should have at least 2 frames: helper + main module
165
+ expect(stack.length).toBeGreaterThanOrEqual(2);
166
+
167
+ // Top frame should be the helper function
168
+ const topFrame = stack[0];
169
+ expect(topFrame).toBeDefined();
170
+ expect(topFrame!.functionName).toBe("helper");
171
+ } finally {
172
+ await session.stop();
173
+ }
174
+ });
175
+
176
+ test("searchInScripts finds a string in step-app.js", async () => {
177
+ const session = new DebugSession("test-search-basic");
178
+ try {
179
+ await session.launch(["node", "tests/fixtures/step-app.js"], { brk: true });
180
+ await waitForState(session, "paused");
181
+ expect(session.sessionState).toBe("paused");
182
+
183
+ const results = await session.searchInScripts("helper");
184
+ expect(results.length).toBeGreaterThan(0);
185
+
186
+ // At least one match should be in step-app.js
187
+ const stepAppMatch = results.find((r) => r.url.includes("step-app.js"));
188
+ expect(stepAppMatch).toBeDefined();
189
+ expect(stepAppMatch!.line).toBeGreaterThan(0);
190
+ expect(stepAppMatch!.content).toContain("helper");
191
+ } finally {
192
+ await session.stop();
193
+ }
194
+ });
195
+
196
+ test("searchInScripts with no matches returns empty array", async () => {
197
+ const session = new DebugSession("test-search-empty");
198
+ try {
199
+ await session.launch(["node", "tests/fixtures/step-app.js"], { brk: true });
200
+ await waitForState(session, "paused");
201
+ expect(session.sessionState).toBe("paused");
202
+
203
+ const results = await session.searchInScripts("xyzzy_nonexistent_string_12345");
204
+ expect(results.length).toBe(0);
205
+ } finally {
206
+ await session.stop();
207
+ }
208
+ });
209
+ });
@@ -0,0 +1,214 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { DebugSession } from "../../src/daemon/session.ts";
3
+
4
+ async function waitForState(
5
+ session: DebugSession,
6
+ state: "idle" | "running" | "paused",
7
+ timeoutMs = 5000,
8
+ ): Promise<void> {
9
+ const deadline = Date.now() + timeoutMs;
10
+ while (session.sessionState !== state && Date.now() < deadline) {
11
+ await Bun.sleep(50);
12
+ }
13
+ }
14
+
15
+ describe("Source map integration", () => {
16
+ test("stack trace shows .ts paths after source map resolution", async () => {
17
+ const session = new DebugSession("test-sm-stack");
18
+ try {
19
+ await session.launch(["node", "tests/fixtures/ts-app/dist/app.js"], { brk: true });
20
+ await waitForState(session, "paused");
21
+ expect(session.sessionState).toBe("paused");
22
+
23
+ // Wait a moment for source maps to load
24
+ await Bun.sleep(100);
25
+
26
+ // Set breakpoint inside the greet function (line 8 of app.ts)
27
+ const bp = await session.setBreakpoint("app.ts", 8);
28
+ expect(bp.ref).toMatch(/^BP#\d+$/);
29
+
30
+ // Continue to hit the breakpoint
31
+ await session.continue();
32
+ await waitForState(session, "paused");
33
+ expect(session.sessionState).toBe("paused");
34
+
35
+ const stack = session.getStack();
36
+ expect(stack.length).toBeGreaterThan(0);
37
+
38
+ // Top frame should reference .ts source
39
+ const topFrame = stack[0];
40
+ expect(topFrame).toBeDefined();
41
+ expect(topFrame!.file).toContain("app.ts");
42
+ } finally {
43
+ await session.stop();
44
+ }
45
+ });
46
+
47
+ test("setBreakpoint on .ts file works via source map translation", async () => {
48
+ const session = new DebugSession("test-sm-break");
49
+ try {
50
+ await session.launch(["node", "tests/fixtures/ts-app/dist/app.js"], { brk: true });
51
+ await waitForState(session, "paused");
52
+
53
+ // Wait for source maps to load
54
+ await Bun.sleep(100);
55
+
56
+ // Set breakpoint on TS file line (add function at line 13 of app.ts)
57
+ const bp = await session.setBreakpoint("app.ts", 13);
58
+ expect(bp.ref).toMatch(/^BP#\d+$/);
59
+ // The location should report the original .ts file
60
+ expect(bp.location.url).toContain("app.ts");
61
+ expect(bp.location.line).toBe(13);
62
+
63
+ // Continue and it should actually pause
64
+ await session.continue();
65
+ await waitForState(session, "paused");
66
+ expect(session.sessionState).toBe("paused");
67
+ } finally {
68
+ await session.stop();
69
+ }
70
+ });
71
+
72
+ test("getSource shows original TypeScript source with type annotations", async () => {
73
+ const session = new DebugSession("test-sm-source");
74
+ try {
75
+ await session.launch(["node", "tests/fixtures/ts-app/dist/app.js"], { brk: true });
76
+ await waitForState(session, "paused");
77
+
78
+ // Wait for source maps to load
79
+ await Bun.sleep(100);
80
+
81
+ const source = await session.getSource({ file: "app.ts", all: true });
82
+ expect(source.lines.length).toBeGreaterThan(0);
83
+
84
+ // Should contain TypeScript type annotations
85
+ const allText = source.lines.map((l) => l.text).join("\n");
86
+ expect(allText).toContain("Person");
87
+ expect(allText).toContain(": string");
88
+ expect(allText).toContain(": number");
89
+ } finally {
90
+ await session.stop();
91
+ }
92
+ });
93
+
94
+ test("buildState shows source-mapped .ts location", async () => {
95
+ const session = new DebugSession("test-sm-state");
96
+ try {
97
+ await session.launch(["node", "tests/fixtures/ts-app/dist/app.js"], { brk: true });
98
+ await waitForState(session, "paused");
99
+
100
+ // Wait for source maps to load
101
+ await Bun.sleep(100);
102
+
103
+ // Set breakpoint and continue to it
104
+ await session.setBreakpoint("app.ts", 8);
105
+ await session.continue();
106
+ await waitForState(session, "paused");
107
+
108
+ const state = await session.buildState();
109
+ expect(state.status).toBe("paused");
110
+ expect(state.location).toBeDefined();
111
+ expect(state.location!.url).toContain("app.ts");
112
+ } finally {
113
+ await session.stop();
114
+ }
115
+ });
116
+
117
+ test("buildState source shows TypeScript content", async () => {
118
+ const session = new DebugSession("test-sm-state-source");
119
+ try {
120
+ await session.launch(["node", "tests/fixtures/ts-app/dist/app.js"], { brk: true });
121
+ await waitForState(session, "paused");
122
+
123
+ // Wait for source maps to load
124
+ await Bun.sleep(100);
125
+
126
+ // Set breakpoint and continue to it
127
+ await session.setBreakpoint("app.ts", 8);
128
+ await session.continue();
129
+ await waitForState(session, "paused");
130
+
131
+ const state = await session.buildState({ code: true });
132
+ expect(state.source).toBeDefined();
133
+ expect(state.source!.lines.length).toBeGreaterThan(0);
134
+
135
+ // Source should contain TS type annotations
136
+ const allText = state.source!.lines.map((l) => l.text).join("\n");
137
+ expect(allText).toContain("Person");
138
+ } finally {
139
+ await session.stop();
140
+ }
141
+ });
142
+
143
+ test("listBreakpoints shows .ts file locations", async () => {
144
+ const session = new DebugSession("test-sm-breakls");
145
+ try {
146
+ await session.launch(["node", "tests/fixtures/ts-app/dist/app.js"], { brk: true });
147
+ await waitForState(session, "paused");
148
+
149
+ // Wait for source maps to load
150
+ await Bun.sleep(100);
151
+
152
+ await session.setBreakpoint("app.ts", 8);
153
+
154
+ const bps = session.listBreakpoints();
155
+ expect(bps.length).toBe(1);
156
+ const bp = bps[0];
157
+ expect(bp).toBeDefined();
158
+ // URL should be the original .ts file
159
+ expect(bp!.url).toContain("app.ts");
160
+ expect(bp!.originalUrl).toContain("app.ts");
161
+ expect(bp!.originalLine).toBe(8);
162
+ } finally {
163
+ await session.stop();
164
+ }
165
+ });
166
+
167
+ test("graceful fallback: plain .js files work exactly as before", async () => {
168
+ const session = new DebugSession("test-sm-fallback");
169
+ try {
170
+ await session.launch(["node", "tests/fixtures/simple-app.js"], { brk: true });
171
+ await waitForState(session, "paused");
172
+
173
+ // Set breakpoint on plain JS file — should work as before
174
+ const bp = await session.setBreakpoint("simple-app.js", 5);
175
+ expect(bp.ref).toMatch(/^BP#\d+$/);
176
+ expect(bp.location.url).toContain("simple-app.js");
177
+
178
+ await session.continue();
179
+ await waitForState(session, "paused");
180
+ expect(session.sessionState).toBe("paused");
181
+
182
+ const stack = session.getStack();
183
+ expect(stack.length).toBeGreaterThan(0);
184
+ expect(stack[0]!.file).toContain("simple-app.js");
185
+
186
+ const state = await session.buildState();
187
+ expect(state.location!.url).toContain("simple-app.js");
188
+ } finally {
189
+ await session.stop();
190
+ }
191
+ });
192
+
193
+ test("source map info is available via resolver", async () => {
194
+ const session = new DebugSession("test-sm-info");
195
+ try {
196
+ await session.launch(["node", "tests/fixtures/ts-app/dist/app.js"], { brk: true });
197
+ await waitForState(session, "paused");
198
+
199
+ // Wait for source maps to load
200
+ await Bun.sleep(100);
201
+
202
+ const infos = session.sourceMapResolver.getAllInfos();
203
+ expect(infos.length).toBeGreaterThan(0);
204
+
205
+ const appInfo = infos.find((i) => i.generatedUrl.includes("app.js"));
206
+ expect(appInfo).toBeDefined();
207
+ expect(appInfo!.sources.length).toBeGreaterThan(0);
208
+ expect(appInfo!.sources.some((s) => s.includes("app.ts"))).toBe(true);
209
+ expect(appInfo!.hasSourcesContent).toBe(true);
210
+ } finally {
211
+ await session.stop();
212
+ }
213
+ });
214
+ });
@@ -0,0 +1,208 @@
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 = 2000,
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("buildState integration", () => {
19
+ test("state returns source, locals, and stack when paused", async () => {
20
+ const session = new DebugSession("test-state-full");
21
+ try {
22
+ await session.launch(["node", "tests/fixtures/simple-app.js"], {
23
+ brk: true,
24
+ });
25
+ await waitForState(session, "paused");
26
+
27
+ const snapshot = await session.buildState();
28
+
29
+ expect(snapshot.status).toBe("paused");
30
+ expect(snapshot.reason).toBeDefined();
31
+ expect(snapshot.location).toBeDefined();
32
+ expect(snapshot.location?.line).toBeGreaterThan(0);
33
+
34
+ // Source should be present with lines around the current position
35
+ expect(snapshot.source).toBeDefined();
36
+ expect(snapshot.source?.lines.length).toBeGreaterThan(0);
37
+ // At least one line should be marked as current
38
+ const currentLine = snapshot.source?.lines.find((l) => l.current === true);
39
+ expect(currentLine).toBeDefined();
40
+
41
+ // Stack should be present
42
+ expect(snapshot.stack).toBeDefined();
43
+ expect(snapshot.stack?.length).toBeGreaterThan(0);
44
+ // First frame should have a ref starting with @f
45
+ const firstFrame = snapshot.stack?.[0];
46
+ expect(firstFrame?.ref).toMatch(/^@f/);
47
+ expect(firstFrame?.line).toBeGreaterThan(0);
48
+
49
+ // Locals should be present (may be empty at entry point, but the array should exist)
50
+ expect(snapshot.locals).toBeDefined();
51
+
52
+ // Breakpoint count should be present
53
+ expect(snapshot.breakpointCount).toBeDefined();
54
+ expect(snapshot.breakpointCount).toBeGreaterThanOrEqual(0);
55
+ } finally {
56
+ await session.stop();
57
+ }
58
+ });
59
+
60
+ test("state returns running status when not paused", async () => {
61
+ const session = new DebugSession("test-state-running");
62
+ try {
63
+ await session.launch(["node", "-e", "setTimeout(() => {}, 30000)"], {
64
+ brk: false,
65
+ });
66
+
67
+ const snapshot = await session.buildState();
68
+
69
+ expect(snapshot.status).toBe("running");
70
+ // When running, no source/locals/stack should be present
71
+ expect(snapshot.source).toBeUndefined();
72
+ expect(snapshot.locals).toBeUndefined();
73
+ expect(snapshot.stack).toBeUndefined();
74
+ } finally {
75
+ await session.stop();
76
+ }
77
+ });
78
+
79
+ test("state with vars filter returns only locals", async () => {
80
+ const session = new DebugSession("test-state-vars");
81
+ try {
82
+ await session.launch(["node", "tests/fixtures/simple-app.js"], {
83
+ brk: true,
84
+ });
85
+ await waitForState(session, "paused");
86
+
87
+ const snapshot = await session.buildState({ vars: true });
88
+
89
+ expect(snapshot.status).toBe("paused");
90
+ // Locals should be present
91
+ expect(snapshot.locals).toBeDefined();
92
+ // Source and stack should NOT be present (filtered out)
93
+ expect(snapshot.source).toBeUndefined();
94
+ expect(snapshot.stack).toBeUndefined();
95
+ expect(snapshot.breakpointCount).toBeUndefined();
96
+ } finally {
97
+ await session.stop();
98
+ }
99
+ });
100
+
101
+ test("state with stack filter returns only stack", async () => {
102
+ const session = new DebugSession("test-state-stack");
103
+ try {
104
+ await session.launch(["node", "tests/fixtures/simple-app.js"], {
105
+ brk: true,
106
+ });
107
+ await waitForState(session, "paused");
108
+
109
+ const snapshot = await session.buildState({ stack: true });
110
+
111
+ expect(snapshot.status).toBe("paused");
112
+ // Stack should be present
113
+ expect(snapshot.stack).toBeDefined();
114
+ expect(snapshot.stack?.length).toBeGreaterThan(0);
115
+ // Source, locals, and breakpoints should NOT be present (filtered out)
116
+ expect(snapshot.source).toBeUndefined();
117
+ expect(snapshot.locals).toBeUndefined();
118
+ expect(snapshot.breakpointCount).toBeUndefined();
119
+ } finally {
120
+ await session.stop();
121
+ }
122
+ });
123
+
124
+ test("state with code filter returns only source", async () => {
125
+ const session = new DebugSession("test-state-code");
126
+ try {
127
+ await session.launch(["node", "tests/fixtures/simple-app.js"], {
128
+ brk: true,
129
+ });
130
+ await waitForState(session, "paused");
131
+
132
+ const snapshot = await session.buildState({ code: true });
133
+
134
+ expect(snapshot.status).toBe("paused");
135
+ // Source should be present
136
+ expect(snapshot.source).toBeDefined();
137
+ expect(snapshot.source?.lines.length).toBeGreaterThan(0);
138
+ // Locals, stack, and breakpoints should NOT be present (filtered out)
139
+ expect(snapshot.locals).toBeUndefined();
140
+ expect(snapshot.stack).toBeUndefined();
141
+ expect(snapshot.breakpointCount).toBeUndefined();
142
+ } finally {
143
+ await session.stop();
144
+ }
145
+ });
146
+
147
+ test("state returns idle status when no target", async () => {
148
+ const session = new DebugSession("test-state-idle");
149
+ const snapshot = await session.buildState();
150
+ expect(snapshot.status).toBe("idle");
151
+ });
152
+
153
+ test("state assigns refs to variables and frames", async () => {
154
+ const session = new DebugSession("test-state-refs");
155
+ try {
156
+ await session.launch(["node", "tests/fixtures/simple-app.js"], {
157
+ brk: true,
158
+ });
159
+ await waitForState(session, "paused");
160
+
161
+ const snapshot = await session.buildState();
162
+
163
+ // Check that variable refs are assigned
164
+ if (snapshot.locals && snapshot.locals.length > 0) {
165
+ const firstLocal = snapshot.locals[0];
166
+ expect(firstLocal?.ref).toMatch(/^@v\d+$/);
167
+ expect(firstLocal?.name).toBeDefined();
168
+ expect(firstLocal?.value).toBeDefined();
169
+
170
+ // The ref should be resolvable in the ref table
171
+ const entry = session.refs.resolve(firstLocal?.ref ?? "");
172
+ expect(entry).toBeDefined();
173
+ }
174
+
175
+ // Check that frame refs are assigned
176
+ if (snapshot.stack && snapshot.stack.length > 0) {
177
+ const firstFrame = snapshot.stack[0];
178
+ expect(firstFrame?.ref).toMatch(/^@f\d+$/);
179
+
180
+ // The ref should be resolvable in the ref table
181
+ const entry = session.refs.resolve(firstFrame?.ref ?? "");
182
+ expect(entry).toBeDefined();
183
+ }
184
+ } finally {
185
+ await session.stop();
186
+ }
187
+ });
188
+
189
+ test("state with custom lines context", async () => {
190
+ const session = new DebugSession("test-state-lines");
191
+ try {
192
+ await session.launch(["node", "tests/fixtures/simple-app.js"], {
193
+ brk: true,
194
+ });
195
+ await waitForState(session, "paused");
196
+
197
+ // Request more context lines
198
+ const snapshot = await session.buildState({ code: true, lines: 5 });
199
+
200
+ expect(snapshot.source).toBeDefined();
201
+ // With 5 lines context, we should get up to 11 lines (5 above + current + 5 below)
202
+ // but may be fewer if near the start/end of file
203
+ expect(snapshot.source?.lines.length).toBeGreaterThan(0);
204
+ } finally {
205
+ await session.stop();
206
+ }
207
+ });
208
+ });