@theia/debug 1.70.0-next.81 → 1.71.0-next.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 (100) hide show
  1. package/lib/browser/breakpoint/breakpoint-manager.d.ts +80 -45
  2. package/lib/browser/breakpoint/breakpoint-manager.d.ts.map +1 -1
  3. package/lib/browser/breakpoint/breakpoint-manager.js +553 -170
  4. package/lib/browser/breakpoint/breakpoint-manager.js.map +1 -1
  5. package/lib/browser/breakpoint/breakpoint-manager.spec.d.ts +2 -0
  6. package/lib/browser/breakpoint/breakpoint-manager.spec.d.ts.map +1 -0
  7. package/lib/browser/breakpoint/breakpoint-manager.spec.js +861 -0
  8. package/lib/browser/breakpoint/breakpoint-manager.spec.js.map +1 -0
  9. package/lib/browser/breakpoint/breakpoint-marker.d.ts +7 -10
  10. package/lib/browser/breakpoint/breakpoint-marker.d.ts.map +1 -1
  11. package/lib/browser/breakpoint/breakpoint-marker.js +14 -11
  12. package/lib/browser/breakpoint/breakpoint-marker.js.map +1 -1
  13. package/lib/browser/breakpoint/debug-data-breakpoint-actions.js +1 -1
  14. package/lib/browser/breakpoint/debug-data-breakpoint-actions.js.map +1 -1
  15. package/lib/browser/debug-frontend-application-contribution.d.ts +1 -2
  16. package/lib/browser/debug-frontend-application-contribution.d.ts.map +1 -1
  17. package/lib/browser/debug-frontend-application-contribution.js +13 -21
  18. package/lib/browser/debug-frontend-application-contribution.js.map +1 -1
  19. package/lib/browser/debug-frontend-module.d.ts.map +1 -1
  20. package/lib/browser/debug-frontend-module.js +3 -0
  21. package/lib/browser/debug-frontend-module.js.map +1 -1
  22. package/lib/browser/debug-session-manager.d.ts +8 -27
  23. package/lib/browser/debug-session-manager.d.ts.map +1 -1
  24. package/lib/browser/debug-session-manager.js +14 -132
  25. package/lib/browser/debug-session-manager.js.map +1 -1
  26. package/lib/browser/debug-session.d.ts +1 -21
  27. package/lib/browser/debug-session.d.ts.map +1 -1
  28. package/lib/browser/debug-session.js +72 -203
  29. package/lib/browser/debug-session.js.map +1 -1
  30. package/lib/browser/disassembly-view/disassembly-view-breakpoint-renderer.js +1 -1
  31. package/lib/browser/disassembly-view/disassembly-view-breakpoint-renderer.js.map +1 -1
  32. package/lib/browser/disassembly-view/disassembly-view-widget.d.ts.map +1 -1
  33. package/lib/browser/disassembly-view/disassembly-view-widget.js +17 -24
  34. package/lib/browser/disassembly-view/disassembly-view-widget.js.map +1 -1
  35. package/lib/browser/editor/debug-editor-model.d.ts +15 -5
  36. package/lib/browser/editor/debug-editor-model.d.ts.map +1 -1
  37. package/lib/browser/editor/debug-editor-model.js +56 -32
  38. package/lib/browser/editor/debug-editor-model.js.map +1 -1
  39. package/lib/browser/model/debug-breakpoint-opener.d.ts +14 -0
  40. package/lib/browser/model/debug-breakpoint-opener.d.ts.map +1 -0
  41. package/lib/browser/model/debug-breakpoint-opener.js +67 -0
  42. package/lib/browser/model/debug-breakpoint-opener.js.map +1 -0
  43. package/lib/browser/model/debug-breakpoint.d.ts +32 -13
  44. package/lib/browser/model/debug-breakpoint.d.ts.map +1 -1
  45. package/lib/browser/model/debug-breakpoint.js +76 -16
  46. package/lib/browser/model/debug-breakpoint.js.map +1 -1
  47. package/lib/browser/model/debug-data-breakpoint.d.ts +1 -0
  48. package/lib/browser/model/debug-data-breakpoint.d.ts.map +1 -1
  49. package/lib/browser/model/debug-data-breakpoint.js +6 -5
  50. package/lib/browser/model/debug-data-breakpoint.js.map +1 -1
  51. package/lib/browser/model/debug-function-breakpoint.d.ts +4 -1
  52. package/lib/browser/model/debug-function-breakpoint.d.ts.map +1 -1
  53. package/lib/browser/model/debug-function-breakpoint.js +20 -29
  54. package/lib/browser/model/debug-function-breakpoint.js.map +1 -1
  55. package/lib/browser/model/debug-instruction-breakpoint.d.ts +2 -1
  56. package/lib/browser/model/debug-instruction-breakpoint.d.ts.map +1 -1
  57. package/lib/browser/model/debug-instruction-breakpoint.js +8 -8
  58. package/lib/browser/model/debug-instruction-breakpoint.js.map +1 -1
  59. package/lib/browser/model/debug-source-breakpoint.d.ts +6 -15
  60. package/lib/browser/model/debug-source-breakpoint.d.ts.map +1 -1
  61. package/lib/browser/model/debug-source-breakpoint.js +16 -90
  62. package/lib/browser/model/debug-source-breakpoint.js.map +1 -1
  63. package/lib/browser/view/debug-breakpoints-source.d.ts +0 -2
  64. package/lib/browser/view/debug-breakpoints-source.d.ts.map +1 -1
  65. package/lib/browser/view/debug-breakpoints-source.js +2 -10
  66. package/lib/browser/view/debug-breakpoints-source.js.map +1 -1
  67. package/lib/browser/view/debug-breakpoints-widget.d.ts +2 -0
  68. package/lib/browser/view/debug-breakpoints-widget.d.ts.map +1 -1
  69. package/lib/browser/view/debug-breakpoints-widget.js +3 -0
  70. package/lib/browser/view/debug-breakpoints-widget.js.map +1 -1
  71. package/lib/browser/view/debug-exception-breakpoint.d.ts +18 -11
  72. package/lib/browser/view/debug-exception-breakpoint.d.ts.map +1 -1
  73. package/lib/browser/view/debug-exception-breakpoint.js +58 -24
  74. package/lib/browser/view/debug-exception-breakpoint.js.map +1 -1
  75. package/lib/browser/view/debug-view-model.d.ts +8 -4
  76. package/lib/browser/view/debug-view-model.d.ts.map +1 -1
  77. package/lib/browser/view/debug-view-model.js +16 -9
  78. package/lib/browser/view/debug-view-model.js.map +1 -1
  79. package/package.json +16 -16
  80. package/src/browser/breakpoint/breakpoint-manager.spec.ts +1106 -0
  81. package/src/browser/breakpoint/breakpoint-manager.ts +583 -194
  82. package/src/browser/breakpoint/breakpoint-marker.ts +21 -15
  83. package/src/browser/breakpoint/debug-data-breakpoint-actions.ts +1 -1
  84. package/src/browser/debug-frontend-application-contribution.ts +18 -23
  85. package/src/browser/debug-frontend-module.ts +5 -1
  86. package/src/browser/debug-session-manager.ts +15 -147
  87. package/src/browser/debug-session.tsx +71 -221
  88. package/src/browser/disassembly-view/disassembly-view-breakpoint-renderer.ts +1 -1
  89. package/src/browser/disassembly-view/disassembly-view-widget.ts +17 -23
  90. package/src/browser/editor/debug-editor-model.ts +58 -35
  91. package/src/browser/model/debug-breakpoint-opener.ts +51 -0
  92. package/src/browser/model/debug-breakpoint.tsx +101 -20
  93. package/src/browser/model/debug-data-breakpoint.tsx +8 -5
  94. package/src/browser/model/debug-function-breakpoint.tsx +18 -29
  95. package/src/browser/model/debug-instruction-breakpoint.tsx +10 -8
  96. package/src/browser/model/debug-source-breakpoint.tsx +23 -101
  97. package/src/browser/view/debug-breakpoints-source.tsx +2 -9
  98. package/src/browser/view/debug-breakpoints-widget.ts +6 -0
  99. package/src/browser/view/debug-exception-breakpoint.tsx +66 -27
  100. package/src/browser/view/debug-view-model.ts +21 -13
@@ -0,0 +1,1106 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
18
+ import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
19
+ const disableJSDOM = enableJSDOM();
20
+ FrontendApplicationConfigProvider.set({});
21
+
22
+ import { Container } from '@theia/core/shared/inversify';
23
+ import { CommandService, Emitter } from '@theia/core/lib/common';
24
+ import { LabelProvider, OpenerService } from '@theia/core/lib/browser';
25
+ import { StorageService } from '@theia/core/lib/browser/storage-service';
26
+ import URI from '@theia/core/lib/common/uri';
27
+ import { FileService } from '@theia/filesystem/lib/browser/file-service';
28
+ import { FileChangesEvent, FileChangeType } from '@theia/filesystem/lib/common/files';
29
+ import { expect } from 'chai';
30
+ import {
31
+ BreakpointManager,
32
+ SourceBreakpointsChangeEvent,
33
+ FunctionBreakpointsChangeEvent,
34
+ InstructionBreakpointsChangeEvent,
35
+ DataBreakpointsChangeEvent
36
+ } from './breakpoint-manager';
37
+ import {
38
+ SourceBreakpoint, FunctionBreakpoint,
39
+ DataBreakpoint, DataBreakpointSourceType
40
+ } from './breakpoint-marker';
41
+ import { DebugDataBreakpoint } from '../model/debug-data-breakpoint';
42
+ import { BPSessionData } from '../model/debug-breakpoint';
43
+ import { DebugProtocol } from '@vscode/debugprotocol';
44
+
45
+ disableJSDOM();
46
+
47
+ // ── Helpers ──
48
+
49
+ const FILE_A = new URI('file:///workspace/a.ts');
50
+ const FILE_B = new URI('file:///workspace/b.ts');
51
+
52
+ function makeSourceBreakpoint(uri: URI, line: number, opts?: Partial<DebugProtocol.SourceBreakpoint & { id: string; enabled: boolean; column: number }>): SourceBreakpoint {
53
+ return SourceBreakpoint.create(
54
+ uri,
55
+ { line, column: opts?.column, condition: opts?.condition, hitCondition: opts?.hitCondition, logMessage: opts?.logMessage },
56
+ opts?.id ? { id: opts.id, uri: uri.toString(), enabled: opts?.enabled ?? true, raw: { line } } as SourceBreakpoint : undefined
57
+ );
58
+ }
59
+
60
+ function makeFunctionBreakpoint(name: string): FunctionBreakpoint {
61
+ return FunctionBreakpoint.create({ name });
62
+ }
63
+
64
+ function makeDataBreakpoint(dataId: string, description = 'some var'): DataBreakpoint {
65
+ return DataBreakpoint.create(
66
+ { dataId, accessType: 'write' },
67
+ { dataId, description, accessTypes: ['read', 'write'], canPersist: true },
68
+ { type: DataBreakpointSourceType.Variable, variable: description }
69
+ );
70
+ }
71
+
72
+ const defaultCapabilities: DebugProtocol.Capabilities = {};
73
+
74
+ function makeSessionData(overrides: Partial<BPSessionData> = {}): Omit<BPSessionData, 'sessionId'> {
75
+ return {
76
+ id: 1,
77
+ verified: true,
78
+ line: 5,
79
+ supportsConditionalBreakpoints: false,
80
+ supportsHitConditionalBreakpoints: false,
81
+ supportsLogPoints: false,
82
+ supportsFunctionBreakpoints: false,
83
+ supportsDataBreakpoints: false,
84
+ supportsInstructionBreakpoints: false,
85
+ ...overrides,
86
+ };
87
+ }
88
+
89
+ // ── Test Setup ──
90
+
91
+ function createManager(): { manager: BreakpointManager; storageData: Record<string, unknown>; fileChangeEmitter: Emitter<FileChangesEvent> } {
92
+ const container = new Container();
93
+
94
+ const storageData: Record<string, unknown> = {};
95
+ const fileChangeEmitter = new Emitter<FileChangesEvent>();
96
+
97
+ container.bind(LabelProvider).toConstantValue({
98
+ getName: (uri: URI) => uri.path.base,
99
+ getLongName: (uri: URI) => uri.path.toString(),
100
+ } as unknown as LabelProvider);
101
+
102
+ container.bind(OpenerService).toConstantValue({} as unknown as OpenerService);
103
+ container.bind(CommandService).toConstantValue({} as unknown as CommandService);
104
+
105
+ container.bind(StorageService).toConstantValue({
106
+ getData: async <T>(key: string, defaultValue: T) => (storageData[key] as T) ?? defaultValue,
107
+ setData: async <T>(key: string, data: T) => { storageData[key] = data; },
108
+ } as StorageService);
109
+
110
+ container.bind(FileService).toConstantValue({
111
+ onDidFilesChange: fileChangeEmitter.event,
112
+ } as unknown as FileService);
113
+
114
+ container.bind(BreakpointManager).toSelf().inSingletonScope();
115
+
116
+ const manager = container.get(BreakpointManager);
117
+
118
+ return { manager, storageData, fileChangeEmitter };
119
+ }
120
+
121
+ // ── Tests ──
122
+
123
+ describe('DebugBreakpoint.update() — session data lifecycle', () => {
124
+
125
+ let manager: BreakpointManager;
126
+
127
+ beforeEach(() => {
128
+ ({ manager } = createManager());
129
+ });
130
+
131
+ it('adding session data for a session updates _raw', () => {
132
+ const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10));
133
+ bp.update('session-1', makeSessionData({ verified: true, line: 10 }));
134
+ expect(bp.raw).to.not.be.undefined;
135
+ expect(bp.raw!.sessionId).to.equal('session-1');
136
+ expect(bp.raw!.line).to.equal(10);
137
+ });
138
+
139
+ it('verified single session: verified returns true', () => {
140
+ const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10));
141
+ bp.update('session-1', makeSessionData({ verified: true }));
142
+ expect(bp.verified).to.be.true;
143
+ });
144
+
145
+ it('unverified single session: verified returns false, _raw still available', () => {
146
+ const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10));
147
+ bp.update('session-1', makeSessionData({ verified: false, message: 'not yet' }));
148
+ expect(bp.verified).to.be.false;
149
+ expect(bp.raw).to.not.be.undefined;
150
+ expect(bp.raw!.message).to.equal('not yet');
151
+ });
152
+
153
+ it('multiple sessions, one verified: _raw picks the verified one', () => {
154
+ const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10));
155
+ bp.update('session-1', makeSessionData({ verified: false, line: 10 }));
156
+ bp.update('session-2', makeSessionData({ verified: true, line: 11 }));
157
+ expect(bp.verified).to.be.true;
158
+ expect(bp.raw!.sessionId).to.equal('session-2');
159
+ expect(bp.raw!.line).to.equal(11);
160
+ });
161
+
162
+ it('multiple sessions, both verified at same location: _raw picks one', () => {
163
+ const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10));
164
+ bp.update('session-1', makeSessionData({ verified: true, line: 10, column: 1 }));
165
+ bp.update('session-2', makeSessionData({ verified: true, line: 10, column: 1 }));
166
+ expect(bp.verified).to.be.true;
167
+ // Both agree on location, so verifiedLocations.size === 1 → picks that one
168
+ expect(bp.raw).to.not.be.undefined;
169
+ });
170
+
171
+ it('multiple sessions, verified at different locations: _raw cleared, verified stays true', () => {
172
+ const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10));
173
+ bp.update('session-1', makeSessionData({ verified: true, line: 10 }));
174
+ bp.update('session-2', makeSessionData({ verified: true, line: 20 }));
175
+ // Sessions disagree → _raw is undefined so the breakpoint falls back
176
+ // to its user-set position, but still shows as verified (VSCode semantics).
177
+ expect(bp.raw).to.be.undefined;
178
+ expect(bp.verified).to.be.true;
179
+ expect(bp.installed).to.be.true;
180
+ // Per-session data is still accessible for callers that need it.
181
+ expect(bp.getDebugProtocolBreakpoint('session-1')!.line).to.equal(10);
182
+ expect(bp.getDebugProtocolBreakpoint('session-2')!.line).to.equal(20);
183
+ });
184
+
185
+ it('removing session data for a session that contributed: data is deleted, _raw recomputed', () => {
186
+ const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10));
187
+ bp.update('session-1', makeSessionData({ verified: true, line: 10 }));
188
+ bp.update('session-2', makeSessionData({ verified: true, line: 20 }));
189
+ bp.update('session-1', undefined);
190
+ expect(bp.raw).to.not.be.undefined;
191
+ expect(bp.raw!.sessionId).to.equal('session-2');
192
+ });
193
+
194
+ it('removing session data for a session that never contributed: no-op', () => {
195
+ const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10));
196
+ bp.update('session-1', makeSessionData({ verified: true, line: 10 }));
197
+ const rawBefore = bp.raw;
198
+ bp.update('session-never', undefined);
199
+ expect(bp.raw).to.equal(rawBefore);
200
+ });
201
+
202
+ it('removing all session data: _raw becomes undefined, verified defaults to true', () => {
203
+ const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10));
204
+ bp.update('session-1', makeSessionData({ verified: true, line: 10 }));
205
+ bp.update('session-1', undefined);
206
+ expect(bp.raw).to.be.undefined;
207
+ // No session data at all → verified defaults to true (no adapter has
208
+ // said otherwise), installed is false (no session has reported).
209
+ expect(bp.verified).to.be.true;
210
+ expect(bp.installed).to.be.false;
211
+ });
212
+
213
+ it('before any session: verified defaults to true, installed is false', () => {
214
+ const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10));
215
+ expect(bp.raw).to.be.undefined;
216
+ expect(bp.verified).to.be.true;
217
+ expect(bp.installed).to.be.false;
218
+ expect(bp.enabled).to.be.true;
219
+ });
220
+
221
+ it('single session unverified: installed is true, verified is false', () => {
222
+ const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10));
223
+ bp.update('session-1', makeSessionData({ verified: false }));
224
+ expect(bp.installed).to.be.true;
225
+ expect(bp.verified).to.be.false;
226
+ });
227
+
228
+ it('disagreement resolved when one session removed: _raw restored from remaining', () => {
229
+ const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10));
230
+ bp.update('session-1', makeSessionData({ verified: true, line: 10 }));
231
+ bp.update('session-2', makeSessionData({ verified: true, line: 20 }));
232
+ expect(bp.raw).to.be.undefined; // disagreement
233
+
234
+ bp.update('session-1', undefined);
235
+ // Only session-2 remains → single verified location → _raw restored
236
+ expect(bp.raw).to.not.be.undefined;
237
+ expect(bp.raw!.sessionId).to.equal('session-2');
238
+ expect(bp.raw!.line).to.equal(20);
239
+ });
240
+
241
+ it('getIdForSession returns the adapter id', () => {
242
+ const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10));
243
+ bp.update('session-1', makeSessionData({ id: 42 }));
244
+ expect(bp.getIdForSession('session-1')).to.equal(42);
245
+ expect(bp.getIdForSession('unknown')).to.be.undefined;
246
+ });
247
+
248
+ it('getDebugProtocolBreakpoint returns protocol data for known session', () => {
249
+ const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10));
250
+ bp.update('session-1', makeSessionData({ id: 7, verified: true, line: 10, message: 'ok' }));
251
+ const proto = bp.getDebugProtocolBreakpoint('session-1');
252
+ expect(proto).to.deep.include({ id: 7, verified: true, line: 10, message: 'ok' });
253
+ });
254
+
255
+ it('getDebugProtocolBreakpoint returns undefined for unknown session', () => {
256
+ const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10));
257
+ expect(bp.getDebugProtocolBreakpoint('nope')).to.be.undefined;
258
+ });
259
+ });
260
+
261
+ describe('BreakpointManager — source breakpoint identity preservation', () => {
262
+
263
+ let manager: BreakpointManager;
264
+
265
+ beforeEach(() => {
266
+ ({ manager } = createManager());
267
+ });
268
+
269
+ it('setBreakpoints with matching ID reuses existing wrapper', () => {
270
+ const original = makeSourceBreakpoint(FILE_A, 10);
271
+ manager.setBreakpoints(FILE_A, [original]);
272
+ const wrapper1 = manager.getBreakpoints(FILE_A)[0];
273
+
274
+ // Now "move" the breakpoint to line 20 but keep the same ID
275
+ const moved: SourceBreakpoint = { ...original, raw: { ...original.raw, line: 20 } };
276
+ manager.setBreakpoints(FILE_A, [moved]);
277
+ const wrapper2 = manager.getBreakpoints(FILE_A)[0];
278
+
279
+ expect(wrapper2).to.equal(wrapper1); // same object identity
280
+ expect(wrapper2.line).to.equal(20);
281
+ });
282
+
283
+ it('session data survives a position change via setBreakpoints', () => {
284
+ const original = makeSourceBreakpoint(FILE_A, 10);
285
+ manager.setBreakpoints(FILE_A, [original]);
286
+ const wrapper = manager.getBreakpoints(FILE_A)[0];
287
+ wrapper.update('s1', makeSessionData({ verified: true, line: 10 }));
288
+ expect(wrapper.verified).to.be.true;
289
+
290
+ // Move to line 20
291
+ const moved: SourceBreakpoint = { ...original, raw: { ...original.raw, line: 20 } };
292
+ manager.setBreakpoints(FILE_A, [moved]);
293
+ const wrapper2 = manager.getBreakpoints(FILE_A)[0];
294
+ expect(wrapper2).to.equal(wrapper);
295
+ expect(wrapper2.verified).to.be.true; // session data survived
296
+ });
297
+
298
+ it('setBreakpoints with a genuinely new breakpoint creates a new wrapper', () => {
299
+ const bp1 = makeSourceBreakpoint(FILE_A, 10);
300
+ manager.setBreakpoints(FILE_A, [bp1]);
301
+ const wrapper1 = manager.getBreakpoints(FILE_A)[0];
302
+
303
+ const bp2 = makeSourceBreakpoint(FILE_A, 20);
304
+ manager.setBreakpoints(FILE_A, [bp1, bp2]);
305
+ const wrappers = manager.getBreakpoints(FILE_A);
306
+ expect(wrappers).to.have.length(2);
307
+ expect(wrappers[0]).to.equal(wrapper1);
308
+ expect(wrappers[1]).to.not.equal(wrapper1);
309
+ });
310
+
311
+ it('setBreakpoints deduplicates by position', () => {
312
+ const bp1 = makeSourceBreakpoint(FILE_A, 10);
313
+ const bp2 = makeSourceBreakpoint(FILE_A, 10); // different id, same position
314
+ manager.setBreakpoints(FILE_A, [bp1, bp2]);
315
+ expect(manager.getBreakpoints(FILE_A)).to.have.length(1);
316
+ });
317
+
318
+ it('addBreakpoint with a positional duplicate returns the existing wrapper', () => {
319
+ const bp1 = makeSourceBreakpoint(FILE_A, 10);
320
+ manager.setBreakpoints(FILE_A, [bp1]);
321
+ const wrapper1 = manager.getBreakpoints(FILE_A)[0];
322
+
323
+ const bp2 = makeSourceBreakpoint(FILE_A, 10);
324
+ const result = manager.addBreakpoint(bp2);
325
+ expect(result).to.equal(wrapper1);
326
+ expect(manager.getBreakpoints(FILE_A)).to.have.length(1);
327
+ });
328
+
329
+ it('removeBreakpoint removes by identity and fires correct events', () => {
330
+ const bp1 = makeSourceBreakpoint(FILE_A, 10);
331
+ const bp2 = makeSourceBreakpoint(FILE_A, 20);
332
+ manager.setBreakpoints(FILE_A, [bp1, bp2]);
333
+
334
+ const events: SourceBreakpointsChangeEvent[] = [];
335
+ manager.onDidChangeBreakpoints(e => events.push(e));
336
+
337
+ const wrapper = manager.getBreakpoints(FILE_A)[0];
338
+ manager.removeBreakpoint(wrapper);
339
+
340
+ expect(manager.getBreakpoints(FILE_A)).to.have.length(1);
341
+ expect(manager.getBreakpoints(FILE_A)[0].line).to.equal(20);
342
+ // At least one event should have the removed breakpoint
343
+ const removeEvent = events.find(e => e.removed.length > 0);
344
+ expect(removeEvent).to.not.be.undefined;
345
+ expect(removeEvent!.removed[0]).to.equal(wrapper);
346
+ });
347
+
348
+ it('applySourceBreakpoints fires onDidChangeBreakpoints with correct added/removed/changed', () => {
349
+ const bp1 = makeSourceBreakpoint(FILE_A, 10);
350
+ const bp2 = makeSourceBreakpoint(FILE_A, 20);
351
+ manager.setBreakpoints(FILE_A, [bp1, bp2]);
352
+
353
+ const events: SourceBreakpointsChangeEvent[] = [];
354
+ manager.onDidChangeBreakpoints(e => events.push(e));
355
+
356
+ const bp3 = makeSourceBreakpoint(FILE_A, 30);
357
+ manager.setBreakpoints(FILE_A, [bp1, bp3]); // bp2 removed, bp3 added, bp1 changed (same identity)
358
+
359
+ expect(events).to.have.length(1);
360
+ const event = events[0];
361
+ expect(event.added).to.have.length(1);
362
+ expect(event.added[0].line).to.equal(30);
363
+ expect(event.removed).to.have.length(1);
364
+ expect(event.removed[0].line).to.equal(20);
365
+ expect(event.changed).to.have.length(1);
366
+ });
367
+
368
+ it('setBreakpoints sorts by line then column', () => {
369
+ const bp30 = makeSourceBreakpoint(FILE_A, 30);
370
+ const bp10 = makeSourceBreakpoint(FILE_A, 10);
371
+ const bp20 = makeSourceBreakpoint(FILE_A, 20);
372
+ manager.setBreakpoints(FILE_A, [bp30, bp10, bp20]);
373
+ const lines = manager.getBreakpoints(FILE_A).map(bp => bp.line);
374
+ expect(lines).to.deep.equal([10, 20, 30]);
375
+ });
376
+ });
377
+
378
+ describe('BreakpointManager — enable/disable', () => {
379
+
380
+ let manager: BreakpointManager;
381
+
382
+ beforeEach(() => {
383
+ ({ manager } = createManager());
384
+ });
385
+
386
+ it('enableAllBreakpoints(true) enables all breakpoint types', () => {
387
+ const bp = makeSourceBreakpoint(FILE_A, 10, { enabled: false });
388
+ manager.setBreakpoints(FILE_A, [bp]);
389
+ const fbp = makeFunctionBreakpoint('myFunc');
390
+ fbp.enabled = false;
391
+ manager.addFunctionBreakpoint(fbp);
392
+ manager.addInstructionBreakpoint('0xDEAD', 0);
393
+ const dbp = makeDataBreakpoint('data1');
394
+ dbp.enabled = false;
395
+ manager.addDataBreakpoint(dbp);
396
+
397
+ // Disable all first
398
+ manager.enableAllBreakpoints(false);
399
+
400
+ const sourceEvents: SourceBreakpointsChangeEvent[] = [];
401
+ const funcEvents: FunctionBreakpointsChangeEvent[] = [];
402
+ const instrEvents: InstructionBreakpointsChangeEvent[] = [];
403
+ const dataEvents: DataBreakpointsChangeEvent[] = [];
404
+ manager.onDidChangeBreakpoints(e => sourceEvents.push(e));
405
+ manager.onDidChangeFunctionBreakpoints(e => funcEvents.push(e));
406
+ manager.onDidChangeInstructionBreakpoints(e => instrEvents.push(e));
407
+ manager.onDidChangeDataBreakpoints(e => dataEvents.push(e));
408
+
409
+ manager.enableAllBreakpoints(true);
410
+
411
+ expect(manager.getBreakpoints(FILE_A)[0].origin.enabled).to.be.true;
412
+ expect(manager.getFunctionBreakpoints()[0].origin.enabled).to.be.true;
413
+ expect(manager.getInstructionBreakpoints()[0].origin.enabled).to.be.true;
414
+ expect(manager.getDataBreakpoints()[0].origin.enabled).to.be.true;
415
+
416
+ expect(sourceEvents).to.have.length.greaterThan(0);
417
+ expect(funcEvents).to.have.length.greaterThan(0);
418
+ expect(instrEvents).to.have.length.greaterThan(0);
419
+ expect(dataEvents).to.have.length.greaterThan(0);
420
+ });
421
+
422
+ it('enableAllBreakpoints does not fire for types already in target state', () => {
423
+ const bp = makeSourceBreakpoint(FILE_A, 10, { enabled: true });
424
+ manager.setBreakpoints(FILE_A, [bp]);
425
+
426
+ // They're already enabled — should still fire because didChange is always true
427
+ // in current implementation (identity-based). This test verifies the function
428
+ // breakpoints emitter doesn't fire when there are no function breakpoints.
429
+ const funcEvents: FunctionBreakpointsChangeEvent[] = [];
430
+ manager.onDidChangeFunctionBreakpoints(e => funcEvents.push(e));
431
+
432
+ manager.enableAllBreakpoints(true);
433
+ expect(funcEvents).to.have.length(0);
434
+ });
435
+
436
+ it('set breakpointsEnabled fires onDidChangeMarkers for all synthetic URIs', () => {
437
+ manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]);
438
+ manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn'));
439
+
440
+ const markerUris: string[] = [];
441
+ manager.onDidChangeMarkers(uri => markerUris.push(uri.toString()));
442
+
443
+ manager.breakpointsEnabled = false;
444
+
445
+ expect(markerUris).to.include(FILE_A.toString());
446
+ expect(markerUris).to.include(BreakpointManager.FUNCTION_URI.toString());
447
+ expect(markerUris).to.include(BreakpointManager.INSTRUCTION_URI.toString());
448
+ expect(markerUris).to.include(BreakpointManager.DATA_URI.toString());
449
+ expect(markerUris).to.include(BreakpointManager.EXCEPTION_URI.toString());
450
+ });
451
+
452
+ it('set breakpointsEnabled does not fire if value unchanged', () => {
453
+ const markerUris: string[] = [];
454
+ manager.onDidChangeMarkers(uri => markerUris.push(uri.toString()));
455
+
456
+ manager.breakpointsEnabled = true; // already true
457
+ expect(markerUris).to.have.length(0);
458
+ });
459
+
460
+ it('enableBreakpoint fires fireBreakpointChanged', () => {
461
+ manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]);
462
+ const wrapper = manager.getBreakpoints(FILE_A)[0];
463
+
464
+ const events: SourceBreakpointsChangeEvent[] = [];
465
+ manager.onDidChangeBreakpoints(e => events.push(e));
466
+
467
+ manager.enableBreakpoint(wrapper, false);
468
+ expect(wrapper.origin.enabled).to.be.false;
469
+ expect(events).to.have.length(1);
470
+ expect(events[0].changed).to.include(wrapper);
471
+ });
472
+
473
+ it('enableBreakpoint does not fire if already at target state', () => {
474
+ manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]);
475
+ const wrapper = manager.getBreakpoints(FILE_A)[0];
476
+
477
+ const events: SourceBreakpointsChangeEvent[] = [];
478
+ manager.onDidChangeBreakpoints(e => events.push(e));
479
+
480
+ manager.enableBreakpoint(wrapper, true); // already enabled
481
+ expect(events).to.have.length(0);
482
+ });
483
+ });
484
+
485
+ describe('BreakpointManager — non-source breakpoint types', () => {
486
+
487
+ let manager: BreakpointManager;
488
+
489
+ beforeEach(() => {
490
+ ({ manager } = createManager());
491
+ });
492
+
493
+ // Function breakpoints
494
+
495
+ it('addFunctionBreakpoint fires correct events', () => {
496
+ const events: FunctionBreakpointsChangeEvent[] = [];
497
+ manager.onDidChangeFunctionBreakpoints(e => events.push(e));
498
+
499
+ const bp = makeFunctionBreakpoint('myFunction');
500
+ manager.addFunctionBreakpoint(bp);
501
+
502
+ expect(manager.getFunctionBreakpoints()).to.have.length(1);
503
+ expect(manager.getFunctionBreakpoints()[0].name).to.equal('myFunction');
504
+ expect(events).to.have.length(1);
505
+ expect(events[0].added).to.have.length(1);
506
+ });
507
+
508
+ it('addFunctionBreakpoint with duplicate name is a no-op', () => {
509
+ manager.addFunctionBreakpoint(makeFunctionBreakpoint('myFunc'));
510
+ manager.addFunctionBreakpoint(makeFunctionBreakpoint('myFunc'));
511
+ expect(manager.getFunctionBreakpoints()).to.have.length(1);
512
+ });
513
+
514
+ it('removeFunctionBreakpoint fires correct events', () => {
515
+ manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn'));
516
+ const wrapper = manager.getFunctionBreakpoints()[0];
517
+
518
+ const events: FunctionBreakpointsChangeEvent[] = [];
519
+ manager.onDidChangeFunctionBreakpoints(e => events.push(e));
520
+
521
+ manager.removeFunctionBreakpoint(wrapper);
522
+ expect(manager.getFunctionBreakpoints()).to.have.length(0);
523
+ expect(events).to.have.length(1);
524
+ expect(events[0].removed).to.include(wrapper);
525
+ });
526
+
527
+ it('updateFunctionBreakpoint with a name collision removes the colliding breakpoint', () => {
528
+ manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn1'));
529
+ manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn2'));
530
+
531
+ const fn1 = manager.getFunctionBreakpoints().find(b => b.name === 'fn1')!;
532
+
533
+ const events: FunctionBreakpointsChangeEvent[] = [];
534
+ manager.onDidChangeFunctionBreakpoints(e => events.push(e));
535
+
536
+ manager.updateFunctionBreakpoint(fn1, { name: 'fn2' });
537
+
538
+ expect(manager.getFunctionBreakpoints()).to.have.length(1);
539
+ expect(manager.getFunctionBreakpoints()[0].name).to.equal('fn2');
540
+ // The event should have a removed entry for the colliding breakpoint
541
+ const removeEvent = events.find(e => e.removed.length > 0);
542
+ expect(removeEvent).to.not.be.undefined;
543
+ });
544
+
545
+ // Instruction breakpoints
546
+
547
+ it('addInstructionBreakpoint fires correct events', () => {
548
+ const events: InstructionBreakpointsChangeEvent[] = [];
549
+ manager.onDidChangeInstructionBreakpoints(e => events.push(e));
550
+
551
+ manager.addInstructionBreakpoint('0xDEAD', 0);
552
+
553
+ expect(manager.getInstructionBreakpoints()).to.have.length(1);
554
+ expect(events).to.have.length(1);
555
+ expect(events[0].added).to.have.length(1);
556
+ });
557
+
558
+ it('addInstructionBreakpoint with duplicate address+offset is a no-op', () => {
559
+ manager.addInstructionBreakpoint('0xBEEF', 4);
560
+ manager.addInstructionBreakpoint('0xBEEF', 4);
561
+ expect(manager.getInstructionBreakpoints()).to.have.length(1);
562
+ });
563
+
564
+ it('addInstructionBreakpoint with same address but different offset creates new', () => {
565
+ manager.addInstructionBreakpoint('0xBEEF', 0);
566
+ manager.addInstructionBreakpoint('0xBEEF', 4);
567
+ expect(manager.getInstructionBreakpoints()).to.have.length(2);
568
+ });
569
+
570
+ it('removeInstructionBreakpoint fires correct events', () => {
571
+ manager.addInstructionBreakpoint('0xCAFE', 0);
572
+ const wrapper = manager.getInstructionBreakpoints()[0];
573
+
574
+ const events: InstructionBreakpointsChangeEvent[] = [];
575
+ manager.onDidChangeInstructionBreakpoints(e => events.push(e));
576
+
577
+ manager.removeInstructionBreakpoint(wrapper);
578
+ expect(manager.getInstructionBreakpoints()).to.have.length(0);
579
+ expect(events).to.have.length(1);
580
+ expect(events[0].removed).to.include(wrapper);
581
+ });
582
+
583
+ // Data breakpoints
584
+
585
+ it('addDataBreakpoint fires correct events', () => {
586
+ const events: DataBreakpointsChangeEvent[] = [];
587
+ manager.onDidChangeDataBreakpoints(e => events.push(e));
588
+
589
+ manager.addDataBreakpoint(makeDataBreakpoint('data-1'));
590
+
591
+ expect(manager.getDataBreakpoints()).to.have.length(1);
592
+ expect(events).to.have.length(1);
593
+ expect(events[0].added).to.have.length(1);
594
+ });
595
+
596
+ it('addDataBreakpoint with duplicate dataId is a no-op', () => {
597
+ manager.addDataBreakpoint(makeDataBreakpoint('data-1'));
598
+ manager.addDataBreakpoint(makeDataBreakpoint('data-1'));
599
+ expect(manager.getDataBreakpoints()).to.have.length(1);
600
+ });
601
+
602
+ it('removeDataBreakpoint fires correct events', () => {
603
+ manager.addDataBreakpoint(makeDataBreakpoint('data-1'));
604
+ const wrapper = manager.getDataBreakpoints()[0];
605
+
606
+ const events: DataBreakpointsChangeEvent[] = [];
607
+ manager.onDidChangeDataBreakpoints(e => events.push(e));
608
+
609
+ manager.removeDataBreakpoint(wrapper);
610
+ expect(manager.getDataBreakpoints()).to.have.length(0);
611
+ expect(events).to.have.length(1);
612
+ expect(events[0].removed).to.include(wrapper);
613
+ });
614
+
615
+ // removeBreakpointsById
616
+
617
+ it('removeBreakpointsById removes across all types', () => {
618
+ manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]);
619
+ manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn'));
620
+ manager.addInstructionBreakpoint('0xABC', 0);
621
+ manager.addDataBreakpoint(makeDataBreakpoint('d1'));
622
+
623
+ const srcId = manager.getBreakpoints(FILE_A)[0].id;
624
+ const fnId = manager.getFunctionBreakpoints()[0].id;
625
+ const instrId = manager.getInstructionBreakpoints()[0].id;
626
+ const dataId = manager.getDataBreakpoints()[0].id;
627
+
628
+ manager.removeBreakpointsById([srcId, fnId, instrId, dataId]);
629
+
630
+ expect(manager.getBreakpoints(FILE_A)).to.have.length(0);
631
+ expect(manager.getFunctionBreakpoints()).to.have.length(0);
632
+ expect(manager.getInstructionBreakpoints()).to.have.length(0);
633
+ expect(manager.getDataBreakpoints()).to.have.length(0);
634
+ });
635
+ });
636
+
637
+ describe('BreakpointManager — updateSessionData', () => {
638
+
639
+ let manager: BreakpointManager;
640
+
641
+ beforeEach(() => {
642
+ ({ manager } = createManager());
643
+ });
644
+
645
+ it('with bps map: only matching breakpoints get updated', () => {
646
+ const sb1 = makeSourceBreakpoint(FILE_A, 10);
647
+ const sb2 = makeSourceBreakpoint(FILE_A, 20);
648
+ manager.setBreakpoints(FILE_A, [sb1, sb2]);
649
+ const wrappers = manager.getBreakpoints(FILE_A);
650
+
651
+ const bpsMap = new Map<string, DebugProtocol.Breakpoint>();
652
+ bpsMap.set(wrappers[0].id, { id: 1, verified: true, line: 10 });
653
+
654
+ manager.updateSessionData('s1', defaultCapabilities, bpsMap);
655
+
656
+ expect(wrappers[0].installed).to.be.true;
657
+ expect(wrappers[0].verified).to.be.true;
658
+ expect(wrappers[1].installed).to.be.false; // not in the map — no session touched it
659
+ });
660
+
661
+ it('without bps map: all breakpoints have the session removed', () => {
662
+ const sb = makeSourceBreakpoint(FILE_A, 10);
663
+ manager.setBreakpoints(FILE_A, [sb]);
664
+ const wrapper = manager.getBreakpoints(FILE_A)[0];
665
+
666
+ // First add session data
667
+ const bpsMap = new Map<string, DebugProtocol.Breakpoint>();
668
+ bpsMap.set(wrapper.id, { id: 1, verified: true, line: 10 });
669
+ manager.updateSessionData('s1', defaultCapabilities, bpsMap);
670
+ expect(wrapper.installed).to.be.true;
671
+ expect(wrapper.verified).to.be.true;
672
+
673
+ // Now cleanup (no bps map)
674
+ manager.updateSessionData('s1', defaultCapabilities, undefined);
675
+ expect(wrapper.installed).to.be.false;
676
+ expect(wrapper.raw).to.be.undefined;
677
+ // verified defaults to true when no session has weighed in
678
+ expect(wrapper.verified).to.be.true;
679
+ });
680
+
681
+ it('cleanup short-circuits for breakpoints that never had data from the session', () => {
682
+ const sb = makeSourceBreakpoint(FILE_A, 10);
683
+ manager.setBreakpoints(FILE_A, [sb]);
684
+ const wrapper = manager.getBreakpoints(FILE_A)[0];
685
+
686
+ // Add data from session-1
687
+ const bpsMap = new Map<string, DebugProtocol.Breakpoint>();
688
+ bpsMap.set(wrapper.id, { id: 1, verified: true, line: 10 });
689
+ manager.updateSessionData('s1', defaultCapabilities, bpsMap);
690
+
691
+ // Cleanup session-2 (never contributed) — wrapper should still have s1 data
692
+ manager.updateSessionData('s2', defaultCapabilities, undefined);
693
+ expect(wrapper.verified).to.be.true;
694
+ expect(wrapper.raw!.sessionId).to.equal('s1');
695
+ });
696
+
697
+ it('capabilities are correctly extracted and merged into BPSessionData', () => {
698
+ const sb = makeSourceBreakpoint(FILE_A, 10);
699
+ manager.setBreakpoints(FILE_A, [sb]);
700
+ const wrapper = manager.getBreakpoints(FILE_A)[0];
701
+
702
+ const caps: DebugProtocol.Capabilities = {
703
+ supportsConditionalBreakpoints: true,
704
+ supportsHitConditionalBreakpoints: true,
705
+ supportsLogPoints: false,
706
+ };
707
+ const bpsMap = new Map<string, DebugProtocol.Breakpoint>();
708
+ bpsMap.set(wrapper.id, { id: 1, verified: true, line: 10 });
709
+ manager.updateSessionData('s1', caps, bpsMap);
710
+
711
+ expect(wrapper.raw!.supportsConditionalBreakpoints).to.be.true;
712
+ expect(wrapper.raw!.supportsHitConditionalBreakpoints).to.be.true;
713
+ expect(wrapper.raw!.supportsLogPoints).to.be.false;
714
+ });
715
+
716
+ it('typed events fire grouped by URI', () => {
717
+ manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]);
718
+ manager.setBreakpoints(FILE_B, [makeSourceBreakpoint(FILE_B, 5)]);
719
+ manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn'));
720
+
721
+ const sourceEvents: SourceBreakpointsChangeEvent[] = [];
722
+ const funcEvents: FunctionBreakpointsChangeEvent[] = [];
723
+ manager.onDidChangeBreakpoints(e => sourceEvents.push(e));
724
+ manager.onDidChangeFunctionBreakpoints(e => funcEvents.push(e));
725
+
726
+ const bpsMap = new Map<string, DebugProtocol.Breakpoint>();
727
+ for (const bp of manager.getBreakpoints()) {
728
+ bpsMap.set(bp.id, { id: 1, verified: true, line: bp.line });
729
+ }
730
+ for (const bp of manager.getFunctionBreakpoints()) {
731
+ bpsMap.set(bp.id, { id: 2, verified: true });
732
+ }
733
+
734
+ manager.updateSessionData('s1', defaultCapabilities, bpsMap);
735
+
736
+ // Source breakpoints for two different URIs → two events
737
+ expect(sourceEvents).to.have.length(2);
738
+ const uris = sourceEvents.map(e => e.uri.toString()).sort();
739
+ expect(uris).to.deep.equal([FILE_A.toString(), FILE_B.toString()].sort());
740
+
741
+ // Function breakpoint → one event
742
+ expect(funcEvents).to.have.length(1);
743
+ });
744
+ });
745
+
746
+ describe('BreakpointManager — exception breakpoints', () => {
747
+
748
+ let manager: BreakpointManager;
749
+
750
+ beforeEach(() => {
751
+ ({ manager } = createManager());
752
+ });
753
+
754
+ it('addExceptionBreakpoints creates new for unknown filters', () => {
755
+ const filter: DebugProtocol.ExceptionBreakpointsFilter = { filter: 'all', label: 'All Exceptions' };
756
+ manager.addExceptionBreakpoints([filter], 'session-1');
757
+ expect(manager.getExceptionBreakpoints()).to.have.length(1);
758
+ expect(manager.getExceptionBreakpoints()[0].origin.raw.filter).to.equal('all');
759
+ });
760
+
761
+ it('addExceptionBreakpoints reuses existing for known filters', () => {
762
+ const filter: DebugProtocol.ExceptionBreakpointsFilter = { filter: 'all', label: 'All Exceptions' };
763
+ manager.addExceptionBreakpoints([filter], 'session-1');
764
+ const first = manager.getExceptionBreakpoints()[0];
765
+ manager.addExceptionBreakpoints([filter], 'session-2');
766
+ expect(manager.getExceptionBreakpoints()).to.have.length(1);
767
+ expect(manager.getExceptionBreakpoints()[0]).to.equal(first);
768
+ });
769
+
770
+ it('clearExceptionSessionEnablement removes the session from all enablement sets', () => {
771
+ const filter: DebugProtocol.ExceptionBreakpointsFilter = { filter: 'all', label: 'All Exceptions' };
772
+ manager.addExceptionBreakpoints([filter], 'session-1');
773
+ const bp = manager.getExceptionBreakpoints()[0];
774
+ expect(bp.isEnabledForSession('session-1')).to.be.true;
775
+
776
+ manager.clearExceptionSessionEnablement('session-1');
777
+ expect(bp.isEnabledForSession('session-1')).to.be.false;
778
+ });
779
+
780
+ it('persistentlyVisible remains true after session cleanup for filters that were visible', () => {
781
+ const filter: DebugProtocol.ExceptionBreakpointsFilter = { filter: 'all', label: 'All Exceptions' };
782
+ manager.addExceptionBreakpoints([filter], 'session-1');
783
+ const bp = manager.getExceptionBreakpoints()[0];
784
+
785
+ // addExceptionBreakpoints calls doUpdateExceptionBreakpointVisibility which sets persistent visibility
786
+ expect(bp.isPersistentlyVisible()).to.be.true;
787
+
788
+ // Clearing session enablement does NOT clear persistent visibility
789
+ manager.clearExceptionSessionEnablement('session-1');
790
+ expect(bp.isPersistentlyVisible()).to.be.true;
791
+ });
792
+
793
+ it('getExceptionBreakpoint finds by filter match', () => {
794
+ const filter: DebugProtocol.ExceptionBreakpointsFilter = { filter: 'uncaught', label: 'Uncaught Exceptions' };
795
+ manager.addExceptionBreakpoints([filter], 'session-1');
796
+
797
+ const found = manager.getExceptionBreakpoint(filter);
798
+ expect(found).to.not.be.undefined;
799
+ expect(found!.origin.raw.filter).to.equal('uncaught');
800
+
801
+ const notFound = manager.getExceptionBreakpoint({ filter: 'other', label: 'Other' });
802
+ expect(notFound).to.be.undefined;
803
+ });
804
+ });
805
+
806
+ describe('BreakpointManager — persistence', () => {
807
+
808
+ let manager: BreakpointManager;
809
+ let storageData: Record<string, unknown>;
810
+
811
+ beforeEach(() => {
812
+ ({ manager, storageData } = createManager());
813
+ });
814
+
815
+ it('save() extracts origin from all wrapper types', () => {
816
+ manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]);
817
+ manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn'));
818
+ manager.addInstructionBreakpoint('0xDEAD', 0);
819
+ manager.addDataBreakpoint(makeDataBreakpoint('d1'));
820
+
821
+ // Add an exception breakpoint that's persistently visible
822
+ const filter: DebugProtocol.ExceptionBreakpointsFilter = { filter: 'all', label: 'All Exceptions' };
823
+ manager.addExceptionBreakpoints([filter], 'session-1');
824
+
825
+ manager.save();
826
+
827
+ const data = storageData['breakpoints'] as BreakpointManager.Data;
828
+ expect(data).to.not.be.undefined;
829
+ expect(Object.keys(data.breakpoints)).to.have.length(1);
830
+ expect(data.breakpoints[FILE_A.toString()]).to.have.length(1);
831
+ expect(data.functionBreakpoints).to.have.length(1);
832
+ expect(data.instructionBreakpoints).to.have.length(1);
833
+ expect(data.dataBreakpoints).to.have.length(1);
834
+ expect(data.exceptionBreakpoints).to.have.length(1);
835
+ });
836
+
837
+ it('round-trip: save then load produces equivalent breakpoints', async () => {
838
+ const sbp = makeSourceBreakpoint(FILE_A, 42, { condition: 'x > 5' });
839
+ manager.setBreakpoints(FILE_A, [sbp]);
840
+ manager.addFunctionBreakpoint(makeFunctionBreakpoint('myFn'));
841
+ manager.breakpointsEnabled = false;
842
+ manager.save();
843
+
844
+ // Create a fresh manager and load
845
+ const { manager: manager2 } = createManager();
846
+ // Copy stored data to the new manager's storage
847
+ const freshStorageData = manager2['storage'];
848
+ await freshStorageData.setData('breakpoints', storageData['breakpoints']);
849
+ await manager2.load();
850
+
851
+ expect(manager2.breakpointsEnabled).to.be.false;
852
+ const loaded = manager2.getBreakpoints(FILE_A);
853
+ expect(loaded).to.have.length(1);
854
+ expect(loaded[0].origin.raw.line).to.equal(42);
855
+ expect(loaded[0].origin.raw.condition).to.equal('x > 5');
856
+ expect(manager2.getFunctionBreakpoints()).to.have.length(1);
857
+ expect(manager2.getFunctionBreakpoints()[0].name).to.equal('myFn');
858
+ });
859
+
860
+ it('exception breakpoints: only persistentlyVisible ones are saved', () => {
861
+ const filter1: DebugProtocol.ExceptionBreakpointsFilter = { filter: 'all', label: 'All' };
862
+ const filter2: DebugProtocol.ExceptionBreakpointsFilter = { filter: 'uncaught', label: 'Uncaught' };
863
+ manager.addExceptionBreakpoints([filter1], 'session-1');
864
+ manager.addExceptionBreakpoints([filter2], 'session-1');
865
+
866
+ // filter1 and filter2 are persistentlyVisible after addExceptionBreakpoints
867
+ // Now mark one as not persistently visible
868
+ const bp2 = manager.getExceptionBreakpoints()[1];
869
+ bp2.setPersistentVisibility(false);
870
+
871
+ manager.save();
872
+ const data = storageData['breakpoints'] as BreakpointManager.Data;
873
+ expect(data.exceptionBreakpoints).to.have.length(1);
874
+ expect(data.exceptionBreakpoints![0].raw.filter).to.equal('all');
875
+ });
876
+ });
877
+
878
+ describe('BreakpointManager — file deletion', () => {
879
+
880
+ let manager: BreakpointManager;
881
+ let fileChangeEmitter: Emitter<FileChangesEvent>;
882
+
883
+ beforeEach(() => {
884
+ ({ manager, fileChangeEmitter } = createManager());
885
+ });
886
+
887
+ it('when a file is deleted, its source breakpoints are removed and onDidChangeMarkers fires', () => {
888
+ manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]);
889
+ manager.setBreakpoints(FILE_B, [makeSourceBreakpoint(FILE_B, 5)]);
890
+
891
+ expect(manager.getBreakpoints(FILE_A)).to.have.length(1);
892
+
893
+ const markerUris: string[] = [];
894
+ manager.onDidChangeMarkers(uri => markerUris.push(uri.toString()));
895
+
896
+ fileChangeEmitter.fire(new FileChangesEvent([{
897
+ resource: FILE_A,
898
+ type: FileChangeType.DELETED,
899
+ }]));
900
+
901
+ expect(manager.getBreakpoints(FILE_A)).to.have.length(0);
902
+ expect(markerUris).to.include(FILE_A.toString());
903
+ // FILE_B should be untouched
904
+ expect(manager.getBreakpoints(FILE_B)).to.have.length(1);
905
+ });
906
+ });
907
+
908
+ describe('BreakpointManager — fireTypedBreakpointEvent dispatch', () => {
909
+
910
+ let manager: BreakpointManager;
911
+
912
+ beforeEach(() => {
913
+ ({ manager } = createManager());
914
+ });
915
+
916
+ it('fires onDidChangeBreakpoints for DebugSourceBreakpoint instances', () => {
917
+ manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]);
918
+
919
+ const events: SourceBreakpointsChangeEvent[] = [];
920
+ manager.onDidChangeBreakpoints(e => events.push(e));
921
+
922
+ const wrapper = manager.getBreakpoints(FILE_A)[0];
923
+ manager.fireBreakpointChanged(wrapper);
924
+
925
+ expect(events).to.have.length(1);
926
+ expect(events[0].changed).to.include(wrapper);
927
+ });
928
+
929
+ it('fires onDidChangeFunctionBreakpoints for DebugFunctionBreakpoint instances', () => {
930
+ manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn'));
931
+
932
+ const events: FunctionBreakpointsChangeEvent[] = [];
933
+ manager.onDidChangeFunctionBreakpoints(e => events.push(e));
934
+
935
+ const wrapper = manager.getFunctionBreakpoints()[0];
936
+ manager.fireBreakpointChanged(wrapper);
937
+
938
+ expect(events).to.have.length(1);
939
+ expect(events[0].changed[0]).to.equal(wrapper);
940
+ });
941
+
942
+ it('fires onDidChangeInstructionBreakpoints for DebugInstructionBreakpoint instances', () => {
943
+ manager.addInstructionBreakpoint('0xABC', 0);
944
+
945
+ const events: InstructionBreakpointsChangeEvent[] = [];
946
+ manager.onDidChangeInstructionBreakpoints(e => events.push(e));
947
+
948
+ const wrapper = manager.getInstructionBreakpoints()[0];
949
+ manager.fireBreakpointChanged(wrapper);
950
+
951
+ expect(events).to.have.length(1);
952
+ expect(events[0].changed[0]).to.equal(wrapper);
953
+ });
954
+
955
+ it('fires onDidChangeDataBreakpoints for DebugDataBreakpoint instances', () => {
956
+ manager.addDataBreakpoint(makeDataBreakpoint('d1'));
957
+
958
+ const events: DataBreakpointsChangeEvent[] = [];
959
+ manager.onDidChangeDataBreakpoints(e => events.push(e));
960
+
961
+ const wrapper = manager.getDataBreakpoints()[0];
962
+ manager.fireBreakpointChanged(wrapper);
963
+
964
+ expect(events).to.have.length(1);
965
+ expect(events[0].changed[0]).to.equal(wrapper);
966
+ });
967
+ });
968
+
969
+ describe('BreakpointManager — query helpers', () => {
970
+
971
+ let manager: BreakpointManager;
972
+
973
+ beforeEach(() => {
974
+ ({ manager } = createManager());
975
+ });
976
+
977
+ it('getLineBreakpoints returns breakpoints at a specific line', () => {
978
+ manager.setBreakpoints(FILE_A, [
979
+ makeSourceBreakpoint(FILE_A, 10),
980
+ makeSourceBreakpoint(FILE_A, 20),
981
+ makeSourceBreakpoint(FILE_A, 10, { column: 5 }),
982
+ ]);
983
+ const atLine10 = manager.getLineBreakpoints(FILE_A, 10);
984
+ expect(atLine10).to.have.length(2);
985
+ });
986
+
987
+ it('getBreakpointById finds across all types', () => {
988
+ manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]);
989
+ manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn'));
990
+ manager.addInstructionBreakpoint('0x1', 0);
991
+ manager.addDataBreakpoint(makeDataBreakpoint('d1'));
992
+
993
+ const srcBp = manager.getBreakpoints(FILE_A)[0];
994
+ const fnBp = manager.getFunctionBreakpoints()[0];
995
+ const instrBp = manager.getInstructionBreakpoints()[0];
996
+ const dataBp = manager.getDataBreakpoints()[0];
997
+
998
+ expect(manager.getBreakpointById(srcBp.id)).to.equal(srcBp);
999
+ expect(manager.getBreakpointById(fnBp.id)).to.equal(fnBp);
1000
+ expect(manager.getBreakpointById(instrBp.id)).to.equal(instrBp);
1001
+ expect(manager.getBreakpointById(dataBp.id)).to.equal(dataBp);
1002
+ expect(manager.getBreakpointById('nonexistent')).to.be.undefined;
1003
+ });
1004
+
1005
+ it('allBreakpoints yields all types', () => {
1006
+ manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]);
1007
+ manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn'));
1008
+ manager.addInstructionBreakpoint('0x1', 0);
1009
+ manager.addDataBreakpoint(makeDataBreakpoint('d1'));
1010
+ manager.addExceptionBreakpoints([{ filter: 'all', label: 'All' }], 's1');
1011
+
1012
+ const all = [...manager.allBreakpoints()];
1013
+ expect(all).to.have.length(5);
1014
+
1015
+ const types = all.map(bp => bp.constructor.name);
1016
+ expect(types).to.include('DebugSourceBreakpoint');
1017
+ expect(types).to.include('DebugFunctionBreakpoint');
1018
+ expect(types).to.include('DebugInstructionBreakpoint');
1019
+ expect(types).to.include('DebugDataBreakpoint');
1020
+ expect(types).to.include('DebugExceptionBreakpoint');
1021
+ });
1022
+
1023
+ it('hasBreakpoints returns true when any type exists', () => {
1024
+ expect(manager.hasBreakpoints()).to.be.false;
1025
+ manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn'));
1026
+ expect(manager.hasBreakpoints()).to.be.true;
1027
+ });
1028
+
1029
+ it('getUris returns all URIs with source breakpoints', () => {
1030
+ manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]);
1031
+ manager.setBreakpoints(FILE_B, [makeSourceBreakpoint(FILE_B, 5)]);
1032
+ const uris = [...manager.getUris()];
1033
+ expect(uris).to.have.length(2);
1034
+ expect(uris).to.include(FILE_A.toString());
1035
+ expect(uris).to.include(FILE_B.toString());
1036
+ });
1037
+
1038
+ it('getBreakpoints with no URI returns all source breakpoints', () => {
1039
+ manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]);
1040
+ manager.setBreakpoints(FILE_B, [makeSourceBreakpoint(FILE_B, 5)]);
1041
+ const all = manager.getBreakpoints();
1042
+ expect(all).to.have.length(2);
1043
+ });
1044
+
1045
+ it('removeBreakpoints clears all breakpoints of all types', () => {
1046
+ manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]);
1047
+ manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn'));
1048
+ manager.addInstructionBreakpoint('0x1', 0);
1049
+ manager.addDataBreakpoint(makeDataBreakpoint('d1'));
1050
+
1051
+ manager.removeBreakpoints();
1052
+
1053
+ expect(manager.getBreakpoints()).to.have.length(0);
1054
+ expect(manager.getFunctionBreakpoints()).to.have.length(0);
1055
+ expect(manager.getInstructionBreakpoints()).to.have.length(0);
1056
+ expect(manager.getDataBreakpoints()).to.have.length(0);
1057
+ });
1058
+ });
1059
+
1060
+ describe('BreakpointManager — updateBreakpoint', () => {
1061
+
1062
+ let manager: BreakpointManager;
1063
+
1064
+ beforeEach(() => {
1065
+ ({ manager } = createManager());
1066
+ });
1067
+
1068
+ it('updateBreakpoint merges partial raw and fires changed event', () => {
1069
+ const bp = makeSourceBreakpoint(FILE_A, 10);
1070
+ manager.setBreakpoints(FILE_A, [bp]);
1071
+ const wrapper = manager.getBreakpoints(FILE_A)[0];
1072
+
1073
+ const events: SourceBreakpointsChangeEvent[] = [];
1074
+ manager.onDidChangeBreakpoints(e => events.push(e));
1075
+
1076
+ manager.updateBreakpoint(wrapper, { condition: 'x > 10' });
1077
+
1078
+ expect(wrapper.origin.raw.condition).to.equal('x > 10');
1079
+ expect(wrapper.origin.raw.line).to.equal(10); // line preserved
1080
+ expect(events).to.have.length(1);
1081
+ });
1082
+
1083
+ it('updateDataBreakpoint updates enabled and raw fields', () => {
1084
+ manager.addDataBreakpoint(makeDataBreakpoint('d1'));
1085
+ const wrapper = manager.getDataBreakpoints()[0];
1086
+
1087
+ const events: DataBreakpointsChangeEvent[] = [];
1088
+ manager.onDidChangeDataBreakpoints(e => events.push(e));
1089
+
1090
+ manager.updateDataBreakpoint(wrapper, { enabled: false, raw: { condition: 'val > 0' } });
1091
+
1092
+ expect(wrapper.origin.enabled).to.be.false;
1093
+ expect(wrapper.origin.raw.condition).to.equal('val > 0');
1094
+ expect(events).to.have.length(1);
1095
+ });
1096
+
1097
+ it('updateDataBreakpoint on unknown breakpoint is a no-op', () => {
1098
+ const orphan = DebugDataBreakpoint.create(makeDataBreakpoint('orphan'), manager.getBreakpointOptions());
1099
+
1100
+ const events: DataBreakpointsChangeEvent[] = [];
1101
+ manager.onDidChangeDataBreakpoints(e => events.push(e));
1102
+
1103
+ manager.updateDataBreakpoint(orphan, { enabled: false });
1104
+ expect(events).to.have.length(0);
1105
+ });
1106
+ });