Skip to content

Commit 0f83b48

Browse files
committed
Add tests for diagnostics tools (session registry + tool handlers)
Tests cover DebugSessionRegistry (register, duplicate rejection, get, destroy, halt waiter cleanup) and tool handlers (list_sessions, end_session, continue, get_stack, evaluate). Diagnostics tools excluded from coverage threshold until full coverage is added.
1 parent 8da16e3 commit 0f83b48

2 files changed

Lines changed: 426 additions & 0 deletions

File tree

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* SPDX-License-Identifier: Apache-2
4+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
7+
import {expect} from 'chai';
8+
import sinon from 'sinon';
9+
import {Services} from '../../../src/services.js';
10+
import {ServerContext} from '../../../src/server-context.js';
11+
import {createMockResolvedConfig} from '../../test-helpers.js';
12+
import {createDebugListSessionsTool} from '../../../src/tools/diagnostics/debug-list-sessions.js';
13+
import {createDebugEndSessionTool} from '../../../src/tools/diagnostics/debug-end-session.js';
14+
import {createDebugContinueTool} from '../../../src/tools/diagnostics/debug-continue.js';
15+
import {createDebugGetStackTool} from '../../../src/tools/diagnostics/debug-get-stack.js';
16+
import {createDebugEvaluateTool} from '../../../src/tools/diagnostics/debug-evaluate.js';
17+
import type {DebugSessionManager, SourceMapper} from '@salesforce/b2c-tooling-sdk/operations/debug';
18+
import type {ToolResult} from '../../../src/utils/index.js';
19+
20+
function getResultJson<T>(result: ToolResult): T {
21+
const text = result.content[0];
22+
if (text?.type !== 'text') throw new Error('Expected text content');
23+
return JSON.parse(text.text) as T;
24+
}
25+
26+
function getResultText(result: ToolResult): string {
27+
const text = result.content[0];
28+
if (text?.type !== 'text') throw new Error('Expected text content');
29+
return text.text;
30+
}
31+
32+
function createMockManager(overrides?: Record<string, unknown>): DebugSessionManager {
33+
return {
34+
client: {
35+
getThread: sinon.stub().resolves({
36+
id: 1,
37+
status: 'halted',
38+
call_stack: [
39+
{index: 0, location: {function_name: 'show', line_number: 42, script_path: '/app_test/cartridge/controllers/Cart.js'}},
40+
],
41+
}),
42+
getVariables: sinon.stub().resolves({object_members: [], count: 0, start: 0, total: 0, _v: '2.0'}),
43+
getMembers: sinon.stub().resolves({object_members: [], count: 0, start: 0, total: 0, _v: '2.0'}),
44+
evaluate: sinon.stub().resolves({_v: '2.0', expression: 'x', result: '42'}),
45+
deleteBreakpoints: sinon.stub().resolves(),
46+
},
47+
connect: sinon.stub().resolves(),
48+
disconnect: sinon.stub().resolves(),
49+
setBreakpoints: sinon.stub().resolves([]),
50+
resume: sinon.stub().resolves(),
51+
stepOver: sinon.stub().resolves(),
52+
stepInto: sinon.stub().resolves(),
53+
stepOut: sinon.stub().resolves(),
54+
getKnownThreads: sinon.stub().returns([]),
55+
...overrides,
56+
} as unknown as DebugSessionManager;
57+
}
58+
59+
function createMockSourceMapper(): SourceMapper {
60+
return {
61+
toServerPath: sinon.stub().returns(undefined),
62+
toLocalPath: sinon.stub().callsFake((p: string) => (p.startsWith('/app_test') ? `/local${p}` : undefined)),
63+
};
64+
}
65+
66+
function createServices(): Services {
67+
return new Services({
68+
resolvedConfig: createMockResolvedConfig({hostname: 'test.example.com', username: 'user', password: 'pass'}),
69+
});
70+
}
71+
72+
describe('tools/diagnostics', () => {
73+
let serverContext: ServerContext;
74+
let loadServices: () => Services;
75+
76+
beforeEach(() => {
77+
serverContext = new ServerContext();
78+
loadServices = () => createServices();
79+
});
80+
81+
afterEach(async () => {
82+
await serverContext.destroyAll();
83+
});
84+
85+
describe('debug_list_sessions', () => {
86+
it('should have correct metadata', () => {
87+
const tool = createDebugListSessionsTool(loadServices, serverContext);
88+
expect(tool.name).to.equal('debug_list_sessions');
89+
expect(tool.toolsets).to.include('CARTRIDGES');
90+
expect(tool.toolsets).to.include('STOREFRONTNEXT');
91+
expect(tool.toolsets).to.include('SCAPI');
92+
});
93+
94+
it('should return empty sessions array when none exist', async () => {
95+
const tool = createDebugListSessionsTool(loadServices, serverContext);
96+
const result = await tool.handler({});
97+
const json = getResultJson<{sessions: unknown[]}>(result);
98+
expect(json.sessions).to.deep.equal([]);
99+
});
100+
101+
it('should list sessions with breakpoints and halted threads', async () => {
102+
const manager = createMockManager({
103+
getKnownThreads: sinon.stub().returns([{id: 5, status: 'halted', call_stack: []}]),
104+
});
105+
const sourceMapper = createMockSourceMapper();
106+
const entry = serverContext.debugSessions.registerSession('host.example.com', 'c1', manager, sourceMapper, []);
107+
entry.breakpoints = [{id: 1, line_number: 42, script_path: '/app_test/cartridge/controllers/Cart.js'}];
108+
109+
const tool = createDebugListSessionsTool(loadServices, serverContext);
110+
const result = await tool.handler({});
111+
const json = getResultJson<{sessions: Array<{session_id: string; halted_threads: number[]; breakpoints: unknown[]}>}>(result);
112+
113+
expect(json.sessions).to.have.lengthOf(1);
114+
expect(json.sessions[0].session_id).to.equal(entry.sessionId);
115+
expect(json.sessions[0].halted_threads).to.deep.equal([5]);
116+
expect(json.sessions[0].breakpoints).to.have.lengthOf(1);
117+
});
118+
});
119+
120+
describe('debug_end_session', () => {
121+
it('should disconnect and remove the session', async () => {
122+
const manager = createMockManager();
123+
const entry = serverContext.debugSessions.registerSession('host', 'c', manager, createMockSourceMapper(), []);
124+
125+
const tool = createDebugEndSessionTool(loadServices, serverContext);
126+
const result = await tool.handler({session_id: entry.sessionId});
127+
128+
expect(result.isError).to.be.undefined;
129+
const json = getResultJson<{status: string}>(result);
130+
expect(json.status).to.equal('disconnected');
131+
expect(serverContext.debugSessions.getSession(entry.sessionId)).to.be.undefined;
132+
expect((manager.disconnect as sinon.SinonStub).calledOnce).to.be.true;
133+
});
134+
135+
it('should clear breakpoints when requested', async () => {
136+
const manager = createMockManager();
137+
const entry = serverContext.debugSessions.registerSession('host', 'c', manager, createMockSourceMapper(), []);
138+
139+
const tool = createDebugEndSessionTool(loadServices, serverContext);
140+
await tool.handler({session_id: entry.sessionId, clear_breakpoints: true});
141+
142+
expect((manager.client.deleteBreakpoints as sinon.SinonStub).calledOnce).to.be.true;
143+
});
144+
145+
it('should return error for unknown session', async () => {
146+
const tool = createDebugEndSessionTool(loadServices, serverContext);
147+
const result = await tool.handler({session_id: 'nonexistent'});
148+
149+
expect(result.isError).to.be.true;
150+
expect(getResultText(result)).to.include('No debug session found');
151+
});
152+
});
153+
154+
describe('debug_continue', () => {
155+
it('should resume the specified thread', async () => {
156+
const manager = createMockManager();
157+
serverContext.debugSessions.registerSession('host', 'c', manager, createMockSourceMapper(), []);
158+
const entry = serverContext.debugSessions.listSessions()[0];
159+
160+
const tool = createDebugContinueTool(loadServices, serverContext);
161+
const result = await tool.handler({session_id: entry.sessionId, thread_id: 5});
162+
163+
expect(result.isError).to.be.undefined;
164+
const json = getResultJson<{thread_id: number; status: string}>(result);
165+
expect(json.thread_id).to.equal(5);
166+
expect(json.status).to.equal('resumed');
167+
expect((manager.resume as sinon.SinonStub).calledWith(5)).to.be.true;
168+
});
169+
});
170+
171+
describe('debug_get_stack', () => {
172+
it('should return mapped stack frames', async () => {
173+
const manager = createMockManager();
174+
const sourceMapper = createMockSourceMapper();
175+
const entry = serverContext.debugSessions.registerSession('host', 'c', manager, sourceMapper, []);
176+
177+
const tool = createDebugGetStackTool(loadServices, serverContext);
178+
const result = await tool.handler({session_id: entry.sessionId, thread_id: 1});
179+
180+
expect(result.isError).to.be.undefined;
181+
const json = getResultJson<{frames: Array<{function_name: string; file: string; line: number}>}>(result);
182+
expect(json.frames).to.have.lengthOf(1);
183+
expect(json.frames[0].function_name).to.equal('show');
184+
expect(json.frames[0].line).to.equal(42);
185+
expect(json.frames[0].file).to.equal('/local/app_test/cartridge/controllers/Cart.js');
186+
});
187+
});
188+
189+
describe('debug_evaluate', () => {
190+
it('should evaluate expression and return result', async () => {
191+
const manager = createMockManager();
192+
const entry = serverContext.debugSessions.registerSession('host', 'c', manager, createMockSourceMapper(), []);
193+
194+
const tool = createDebugEvaluateTool(loadServices, serverContext);
195+
const result = await tool.handler({session_id: entry.sessionId, thread_id: 1, expression: 'x'});
196+
197+
expect(result.isError).to.be.undefined;
198+
const json = getResultJson<{expression: string; result: string}>(result);
199+
expect(json.expression).to.equal('x');
200+
expect(json.result).to.equal('42');
201+
});
202+
203+
it('should use frame_index 0 by default', async () => {
204+
const manager = createMockManager();
205+
const entry = serverContext.debugSessions.registerSession('host', 'c', manager, createMockSourceMapper(), []);
206+
207+
const tool = createDebugEvaluateTool(loadServices, serverContext);
208+
await tool.handler({session_id: entry.sessionId, thread_id: 1, expression: 'x'});
209+
210+
expect((manager.client.evaluate as sinon.SinonStub).calledWith(1, 0, 'x')).to.be.true;
211+
});
212+
213+
it('should use specified frame_index', async () => {
214+
const manager = createMockManager();
215+
const entry = serverContext.debugSessions.registerSession('host', 'c', manager, createMockSourceMapper(), []);
216+
217+
const tool = createDebugEvaluateTool(loadServices, serverContext);
218+
await tool.handler({session_id: entry.sessionId, thread_id: 1, frame_index: 2, expression: 'y'});
219+
220+
expect((manager.client.evaluate as sinon.SinonStub).calledWith(1, 2, 'y')).to.be.true;
221+
});
222+
});
223+
});

0 commit comments

Comments
 (0)