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,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
|
+
});
|