import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import inspector from "node:inspector"; import inspectorPromises from "node:inspector/promises"; describe("node:inspector", () => { describe("Session", () => { let session: inspector.Session; beforeEach(() => { session = new inspector.Session(); }); afterEach(() => { try { session.disconnect(); } catch { // Ignore if already disconnected } }); test("Session is a constructor", () => { expect(inspector.Session).toBeInstanceOf(Function); expect(session).toBeInstanceOf(inspector.Session); }); test("Session extends EventEmitter", () => { expect(typeof session.on).toBe("function"); expect(typeof session.emit).toBe("function"); expect(typeof session.removeListener).toBe("function"); }); test("connect() establishes connection", () => { expect(() => session.connect()).not.toThrow(); }); test("connect() throws if already connected", () => { session.connect(); expect(() => session.connect()).toThrow("already connected"); }); test("connectToMainThread() works like connect()", () => { expect(() => session.connectToMainThread()).not.toThrow(); }); test("disconnect() closes connection cleanly", () => { session.connect(); expect(() => session.disconnect()).not.toThrow(); }); test("disconnect() is a no-op if not connected", () => { expect(() => session.disconnect()).not.toThrow(); }); test("post() throws if not connected", () => { expect(() => session.post("Profiler.enable")).toThrow("not connected"); }); test("post() with callback calls callback with error if not connected", async () => { const { promise, resolve, reject } = Promise.withResolvers(); session.post("Profiler.enable", err => { if (err) resolve(err); else reject(new Error("Expected error")); }); const error = await promise; expect(error.message).toContain("not connected"); }); }); describe("Profiler", () => { let session: inspector.Session; beforeEach(() => { session = new inspector.Session(); session.connect(); }); afterEach(() => { try { session.disconnect(); } catch { // Ignore } }); test("Profiler.enable succeeds", () => { const result = session.post("Profiler.enable"); expect(result).toEqual({}); }); test("Profiler.disable succeeds", () => { session.post("Profiler.enable"); const result = session.post("Profiler.disable"); expect(result).toEqual({}); }); test("Profiler.start without enable throws", () => { expect(() => session.post("Profiler.start")).toThrow("not enabled"); }); test("Profiler.start after enable succeeds", () => { session.post("Profiler.enable"); const result = session.post("Profiler.start"); expect(result).toEqual({}); }); test("Profiler.stop without start throws", () => { session.post("Profiler.enable"); expect(() => session.post("Profiler.stop")).toThrow("not started"); }); test("Profiler.stop returns valid profile", () => { session.post("Profiler.enable"); session.post("Profiler.start"); // Do some work to generate profile data let sum = 0; for (let i = 0; i < 10000; i++) { sum += Math.sqrt(i); } const result = session.post("Profiler.stop"); expect(result).toHaveProperty("profile"); const profile = result.profile; // Validate profile structure expect(profile).toHaveProperty("nodes"); expect(profile).toHaveProperty("startTime"); expect(profile).toHaveProperty("endTime"); expect(profile).toHaveProperty("samples"); expect(profile).toHaveProperty("timeDeltas"); expect(profile.nodes).toBeArray(); expect(profile.nodes.length).toBeGreaterThanOrEqual(1); // First node should be (root) const rootNode = profile.nodes[0]; expect(rootNode).toHaveProperty("id", 1); expect(rootNode).toHaveProperty("callFrame"); expect(rootNode.callFrame).toHaveProperty("functionName", "(root)"); expect(rootNode.callFrame).toHaveProperty("scriptId", "0"); expect(rootNode.callFrame).toHaveProperty("url", ""); expect(rootNode.callFrame).toHaveProperty("lineNumber", -1); expect(rootNode.callFrame).toHaveProperty("columnNumber", -1); }); test("complete enable->start->stop workflow", () => { // Enable profiler const enableResult = session.post("Profiler.enable"); expect(enableResult).toEqual({}); // Start profiling const startResult = session.post("Profiler.start"); expect(startResult).toEqual({}); // Do some work function fibonacci(n: number): number { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2); } fibonacci(20); // Stop profiling const stopResult = session.post("Profiler.stop"); expect(stopResult).toHaveProperty("profile"); // Disable profiler const disableResult = session.post("Profiler.disable"); expect(disableResult).toEqual({}); }); test("samples and timeDeltas have same length", () => { session.post("Profiler.enable"); session.post("Profiler.start"); // Do some work let sum = 0; for (let i = 0; i < 5000; i++) { sum += Math.sqrt(i); } const result = session.post("Profiler.stop"); const profile = result.profile; expect(profile.samples.length).toBe(profile.timeDeltas.length); }); test("samples reference valid node IDs", () => { session.post("Profiler.enable"); session.post("Profiler.start"); // Do some work let sum = 0; for (let i = 0; i < 5000; i++) { sum += Math.sqrt(i); } const result = session.post("Profiler.stop"); const profile = result.profile; const nodeIds = new Set(profile.nodes.map((n: any) => n.id)); for (const sample of profile.samples) { expect(nodeIds.has(sample)).toBe(true); } }); test("Profiler.setSamplingInterval works", () => { session.post("Profiler.enable"); const result = session.post("Profiler.setSamplingInterval", { interval: 500 }); expect(result).toEqual({}); }); test("Profiler.setSamplingInterval throws if profiler is running", () => { session.post("Profiler.enable"); session.post("Profiler.start"); expect(() => session.post("Profiler.setSamplingInterval", { interval: 500 })).toThrow( "Cannot change sampling interval while profiler is running", ); session.post("Profiler.stop"); }); test("Profiler.setSamplingInterval requires positive interval", () => { session.post("Profiler.enable"); expect(() => session.post("Profiler.setSamplingInterval", { interval: 0 })).toThrow(); expect(() => session.post("Profiler.setSamplingInterval", { interval: -1 })).toThrow(); }); test("double Profiler.start is a no-op", () => { session.post("Profiler.enable"); session.post("Profiler.start"); const result = session.post("Profiler.start"); expect(result).toEqual({}); session.post("Profiler.stop"); }); test("profiler can be restarted after stop", () => { // First run session.post("Profiler.enable"); session.post("Profiler.start"); let sum = 0; for (let i = 0; i < 1000; i++) sum += i; const result1 = session.post("Profiler.stop"); expect(result1).toHaveProperty("profile"); // Second run session.post("Profiler.start"); for (let i = 0; i < 1000; i++) sum += i; const result2 = session.post("Profiler.stop"); expect(result2).toHaveProperty("profile"); // Both profiles should be valid expect(result1.profile.nodes.length).toBeGreaterThanOrEqual(1); expect(result2.profile.nodes.length).toBeGreaterThanOrEqual(1); }); test("disconnect() stops running profiler", () => { session.post("Profiler.enable"); session.post("Profiler.start"); session.disconnect(); // Create new session and verify profiler was stopped const session2 = new inspector.Session(); session2.connect(); session2.post("Profiler.enable"); // This should work without error (profiler is not running) const result = session2.post("Profiler.setSamplingInterval", { interval: 500 }); expect(result).toEqual({}); session2.disconnect(); }); }); describe("callback API", () => { test("post() with callback receives result", async () => { const session = new inspector.Session(); session.connect(); const { promise, resolve } = Promise.withResolvers(); session.post("Profiler.enable", (err, result) => { resolve({ err, result }); }); const { err, result } = await promise; expect(err).toBeNull(); expect(result).toEqual({}); session.disconnect(); }); test("post() with callback receives error", async () => { const session = new inspector.Session(); session.connect(); const { promise, resolve } = Promise.withResolvers(); session.post("Profiler.start", (err, result) => { resolve({ err, result }); }); const { err, result } = await promise; expect(err).toBeInstanceOf(Error); expect(err.message).toContain("not enabled"); session.disconnect(); }); }); describe("unsupported methods", () => { test("unsupported method throws", () => { const session = new inspector.Session(); session.connect(); expect(() => session.post("Runtime.evaluate")).toThrow("not supported"); session.disconnect(); }); test("coverage APIs throw not supported", () => { const session = new inspector.Session(); session.connect(); session.post("Profiler.enable"); expect(() => session.post("Profiler.getBestEffortCoverage")).toThrow("not supported"); expect(() => session.post("Profiler.startPreciseCoverage")).toThrow("not supported"); expect(() => session.post("Profiler.stopPreciseCoverage")).toThrow("not supported"); expect(() => session.post("Profiler.takePreciseCoverage")).toThrow("not supported"); session.disconnect(); }); }); describe("exports", () => { test("url() returns undefined", () => { expect(inspector.url()).toBeUndefined(); }); test("console is exported", () => { expect(inspector.console).toBeObject(); expect(inspector.console.log).toBe(globalThis.console.log); }); test("open() throws not implemented", () => { expect(() => inspector.open()).toThrow(); }); test("close() throws not implemented", () => { expect(() => inspector.close()).toThrow(); }); test("waitForDebugger() throws not implemented", () => { expect(() => inspector.waitForDebugger()).toThrow(); }); }); }); describe("node:inspector/promises", () => { test("Session is exported", () => { expect(inspectorPromises.Session).toBeInstanceOf(Function); }); test("post() returns a Promise", async () => { const session = new inspectorPromises.Session(); session.connect(); const result = session.post("Profiler.enable"); expect(result).toBeInstanceOf(Promise); await expect(result).resolves.toEqual({}); session.disconnect(); }); test("post() rejects on error", async () => { const session = new inspectorPromises.Session(); session.connect(); await expect(session.post("Profiler.start")).rejects.toThrow("not enabled"); session.disconnect(); }); test("complete profiling workflow with promises", async () => { const session = new inspectorPromises.Session(); session.connect(); await session.post("Profiler.enable"); await session.post("Profiler.start"); // Do some work function work(n: number): number { if (n <= 1) return n; return work(n - 1) + work(n - 2); } work(15); const result = await session.post("Profiler.stop"); expect(result).toHaveProperty("profile"); expect(result.profile.nodes).toBeArray(); await session.post("Profiler.disable"); session.disconnect(); }); test("other exports are the same as node:inspector", () => { expect(inspectorPromises.url).toBe(inspector.url); expect(inspectorPromises.console).toBe(inspector.console); expect(inspectorPromises.open).toBe(inspector.open); expect(inspectorPromises.close).toBe(inspector.close); expect(inspectorPromises.waitForDebugger).toBe(inspector.waitForDebugger); }); });