// ***************************************************************************** // Copyright (C) 2026 EclipseSource GmbH and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; const disableJSDOM = enableJSDOM(); FrontendApplicationConfigProvider.set({}); import { Container } from '@theia/core/shared/inversify'; import { CommandService, Emitter } from '@theia/core/lib/common'; import { LabelProvider, OpenerService } from '@theia/core/lib/browser'; import { StorageService } from '@theia/core/lib/browser/storage-service'; import URI from '@theia/core/lib/common/uri'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { FileChangesEvent, FileChangeType } from '@theia/filesystem/lib/common/files'; import { expect } from 'chai'; import { BreakpointManager, SourceBreakpointsChangeEvent, FunctionBreakpointsChangeEvent, InstructionBreakpointsChangeEvent, DataBreakpointsChangeEvent } from './breakpoint-manager'; import { SourceBreakpoint, FunctionBreakpoint, DataBreakpoint, DataBreakpointSourceType } from './breakpoint-marker'; import { DebugDataBreakpoint } from '../model/debug-data-breakpoint'; import { BPSessionData } from '../model/debug-breakpoint'; import { DebugProtocol } from '@vscode/debugprotocol'; disableJSDOM(); // ── Helpers ── const FILE_A = new URI('file:///workspace/a.ts'); const FILE_B = new URI('file:///workspace/b.ts'); function makeSourceBreakpoint(uri: URI, line: number, opts?: Partial): SourceBreakpoint { return SourceBreakpoint.create( uri, { line, column: opts?.column, condition: opts?.condition, hitCondition: opts?.hitCondition, logMessage: opts?.logMessage }, opts?.id ? { id: opts.id, uri: uri.toString(), enabled: opts?.enabled ?? true, raw: { line } } as SourceBreakpoint : undefined ); } function makeFunctionBreakpoint(name: string): FunctionBreakpoint { return FunctionBreakpoint.create({ name }); } function makeDataBreakpoint(dataId: string, description = 'some var'): DataBreakpoint { return DataBreakpoint.create( { dataId, accessType: 'write' }, { dataId, description, accessTypes: ['read', 'write'], canPersist: true }, { type: DataBreakpointSourceType.Variable, variable: description } ); } const defaultCapabilities: DebugProtocol.Capabilities = {}; function makeSessionData(overrides: Partial = {}): Omit { return { id: 1, verified: true, line: 5, supportsConditionalBreakpoints: false, supportsHitConditionalBreakpoints: false, supportsLogPoints: false, supportsFunctionBreakpoints: false, supportsDataBreakpoints: false, supportsInstructionBreakpoints: false, ...overrides, }; } // ── Test Setup ── function createManager(): { manager: BreakpointManager; storageData: Record; fileChangeEmitter: Emitter } { const container = new Container(); const storageData: Record = {}; const fileChangeEmitter = new Emitter(); container.bind(LabelProvider).toConstantValue({ getName: (uri: URI) => uri.path.base, getLongName: (uri: URI) => uri.path.toString(), } as unknown as LabelProvider); container.bind(OpenerService).toConstantValue({} as unknown as OpenerService); container.bind(CommandService).toConstantValue({} as unknown as CommandService); container.bind(StorageService).toConstantValue({ getData: async (key: string, defaultValue: T) => (storageData[key] as T) ?? defaultValue, setData: async (key: string, data: T) => { storageData[key] = data; }, } as StorageService); container.bind(FileService).toConstantValue({ onDidFilesChange: fileChangeEmitter.event, } as unknown as FileService); container.bind(BreakpointManager).toSelf().inSingletonScope(); const manager = container.get(BreakpointManager); return { manager, storageData, fileChangeEmitter }; } // ── Tests ── describe('DebugBreakpoint.update() — session data lifecycle', () => { let manager: BreakpointManager; beforeEach(() => { ({ manager } = createManager()); }); it('adding session data for a session updates _raw', () => { const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10)); bp.update('session-1', makeSessionData({ verified: true, line: 10 })); expect(bp.raw).to.not.be.undefined; expect(bp.raw!.sessionId).to.equal('session-1'); expect(bp.raw!.line).to.equal(10); }); it('verified single session: verified returns true', () => { const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10)); bp.update('session-1', makeSessionData({ verified: true })); expect(bp.verified).to.be.true; }); it('unverified single session: verified returns false, _raw still available', () => { const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10)); bp.update('session-1', makeSessionData({ verified: false, message: 'not yet' })); expect(bp.verified).to.be.false; expect(bp.raw).to.not.be.undefined; expect(bp.raw!.message).to.equal('not yet'); }); it('multiple sessions, one verified: _raw picks the verified one', () => { const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10)); bp.update('session-1', makeSessionData({ verified: false, line: 10 })); bp.update('session-2', makeSessionData({ verified: true, line: 11 })); expect(bp.verified).to.be.true; expect(bp.raw!.sessionId).to.equal('session-2'); expect(bp.raw!.line).to.equal(11); }); it('multiple sessions, both verified at same location: _raw picks one', () => { const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10)); bp.update('session-1', makeSessionData({ verified: true, line: 10, column: 1 })); bp.update('session-2', makeSessionData({ verified: true, line: 10, column: 1 })); expect(bp.verified).to.be.true; // Both agree on location, so verifiedLocations.size === 1 → picks that one expect(bp.raw).to.not.be.undefined; }); it('multiple sessions, verified at different locations: _raw cleared, verified stays true', () => { const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10)); bp.update('session-1', makeSessionData({ verified: true, line: 10 })); bp.update('session-2', makeSessionData({ verified: true, line: 20 })); // Sessions disagree → _raw is undefined so the breakpoint falls back // to its user-set position, but still shows as verified (VSCode semantics). expect(bp.raw).to.be.undefined; expect(bp.verified).to.be.true; expect(bp.installed).to.be.true; // Per-session data is still accessible for callers that need it. expect(bp.getDebugProtocolBreakpoint('session-1')!.line).to.equal(10); expect(bp.getDebugProtocolBreakpoint('session-2')!.line).to.equal(20); }); it('removing session data for a session that contributed: data is deleted, _raw recomputed', () => { const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10)); bp.update('session-1', makeSessionData({ verified: true, line: 10 })); bp.update('session-2', makeSessionData({ verified: true, line: 20 })); bp.update('session-1', undefined); expect(bp.raw).to.not.be.undefined; expect(bp.raw!.sessionId).to.equal('session-2'); }); it('removing session data for a session that never contributed: no-op', () => { const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10)); bp.update('session-1', makeSessionData({ verified: true, line: 10 })); const rawBefore = bp.raw; bp.update('session-never', undefined); expect(bp.raw).to.equal(rawBefore); }); it('removing all session data: _raw becomes undefined, verified defaults to true', () => { const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10)); bp.update('session-1', makeSessionData({ verified: true, line: 10 })); bp.update('session-1', undefined); expect(bp.raw).to.be.undefined; // No session data at all → verified defaults to true (no adapter has // said otherwise), installed is false (no session has reported). expect(bp.verified).to.be.true; expect(bp.installed).to.be.false; }); it('before any session: verified defaults to true, installed is false', () => { const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10)); expect(bp.raw).to.be.undefined; expect(bp.verified).to.be.true; expect(bp.installed).to.be.false; expect(bp.enabled).to.be.true; }); it('single session unverified: installed is true, verified is false', () => { const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10)); bp.update('session-1', makeSessionData({ verified: false })); expect(bp.installed).to.be.true; expect(bp.verified).to.be.false; }); it('disagreement resolved when one session removed: _raw restored from remaining', () => { const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10)); bp.update('session-1', makeSessionData({ verified: true, line: 10 })); bp.update('session-2', makeSessionData({ verified: true, line: 20 })); expect(bp.raw).to.be.undefined; // disagreement bp.update('session-1', undefined); // Only session-2 remains → single verified location → _raw restored expect(bp.raw).to.not.be.undefined; expect(bp.raw!.sessionId).to.equal('session-2'); expect(bp.raw!.line).to.equal(20); }); it('getIdForSession returns the adapter id', () => { const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10)); bp.update('session-1', makeSessionData({ id: 42 })); expect(bp.getIdForSession('session-1')).to.equal(42); expect(bp.getIdForSession('unknown')).to.be.undefined; }); it('getDebugProtocolBreakpoint returns protocol data for known session', () => { const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10)); bp.update('session-1', makeSessionData({ id: 7, verified: true, line: 10, message: 'ok' })); const proto = bp.getDebugProtocolBreakpoint('session-1'); expect(proto).to.deep.include({ id: 7, verified: true, line: 10, message: 'ok' }); }); it('getDebugProtocolBreakpoint returns undefined for unknown session', () => { const bp = manager.addBreakpoint(makeSourceBreakpoint(FILE_A, 10)); expect(bp.getDebugProtocolBreakpoint('nope')).to.be.undefined; }); }); describe('BreakpointManager — source breakpoint identity preservation', () => { let manager: BreakpointManager; beforeEach(() => { ({ manager } = createManager()); }); it('setBreakpoints with matching ID reuses existing wrapper', () => { const original = makeSourceBreakpoint(FILE_A, 10); manager.setBreakpoints(FILE_A, [original]); const wrapper1 = manager.getBreakpoints(FILE_A)[0]; // Now "move" the breakpoint to line 20 but keep the same ID const moved: SourceBreakpoint = { ...original, raw: { ...original.raw, line: 20 } }; manager.setBreakpoints(FILE_A, [moved]); const wrapper2 = manager.getBreakpoints(FILE_A)[0]; expect(wrapper2).to.equal(wrapper1); // same object identity expect(wrapper2.line).to.equal(20); }); it('session data survives a position change via setBreakpoints', () => { const original = makeSourceBreakpoint(FILE_A, 10); manager.setBreakpoints(FILE_A, [original]); const wrapper = manager.getBreakpoints(FILE_A)[0]; wrapper.update('s1', makeSessionData({ verified: true, line: 10 })); expect(wrapper.verified).to.be.true; // Move to line 20 const moved: SourceBreakpoint = { ...original, raw: { ...original.raw, line: 20 } }; manager.setBreakpoints(FILE_A, [moved]); const wrapper2 = manager.getBreakpoints(FILE_A)[0]; expect(wrapper2).to.equal(wrapper); expect(wrapper2.verified).to.be.true; // session data survived }); it('setBreakpoints with a genuinely new breakpoint creates a new wrapper', () => { const bp1 = makeSourceBreakpoint(FILE_A, 10); manager.setBreakpoints(FILE_A, [bp1]); const wrapper1 = manager.getBreakpoints(FILE_A)[0]; const bp2 = makeSourceBreakpoint(FILE_A, 20); manager.setBreakpoints(FILE_A, [bp1, bp2]); const wrappers = manager.getBreakpoints(FILE_A); expect(wrappers).to.have.length(2); expect(wrappers[0]).to.equal(wrapper1); expect(wrappers[1]).to.not.equal(wrapper1); }); it('setBreakpoints deduplicates by position', () => { const bp1 = makeSourceBreakpoint(FILE_A, 10); const bp2 = makeSourceBreakpoint(FILE_A, 10); // different id, same position manager.setBreakpoints(FILE_A, [bp1, bp2]); expect(manager.getBreakpoints(FILE_A)).to.have.length(1); }); it('addBreakpoint with a positional duplicate returns the existing wrapper', () => { const bp1 = makeSourceBreakpoint(FILE_A, 10); manager.setBreakpoints(FILE_A, [bp1]); const wrapper1 = manager.getBreakpoints(FILE_A)[0]; const bp2 = makeSourceBreakpoint(FILE_A, 10); const result = manager.addBreakpoint(bp2); expect(result).to.equal(wrapper1); expect(manager.getBreakpoints(FILE_A)).to.have.length(1); }); it('removeBreakpoint removes by identity and fires correct events', () => { const bp1 = makeSourceBreakpoint(FILE_A, 10); const bp2 = makeSourceBreakpoint(FILE_A, 20); manager.setBreakpoints(FILE_A, [bp1, bp2]); const events: SourceBreakpointsChangeEvent[] = []; manager.onDidChangeBreakpoints(e => events.push(e)); const wrapper = manager.getBreakpoints(FILE_A)[0]; manager.removeBreakpoint(wrapper); expect(manager.getBreakpoints(FILE_A)).to.have.length(1); expect(manager.getBreakpoints(FILE_A)[0].line).to.equal(20); // At least one event should have the removed breakpoint const removeEvent = events.find(e => e.removed.length > 0); expect(removeEvent).to.not.be.undefined; expect(removeEvent!.removed[0]).to.equal(wrapper); }); it('applySourceBreakpoints fires onDidChangeBreakpoints with correct added/removed/changed', () => { const bp1 = makeSourceBreakpoint(FILE_A, 10); const bp2 = makeSourceBreakpoint(FILE_A, 20); manager.setBreakpoints(FILE_A, [bp1, bp2]); const events: SourceBreakpointsChangeEvent[] = []; manager.onDidChangeBreakpoints(e => events.push(e)); const bp3 = makeSourceBreakpoint(FILE_A, 30); manager.setBreakpoints(FILE_A, [bp1, bp3]); // bp2 removed, bp3 added, bp1 changed (same identity) expect(events).to.have.length(1); const event = events[0]; expect(event.added).to.have.length(1); expect(event.added[0].line).to.equal(30); expect(event.removed).to.have.length(1); expect(event.removed[0].line).to.equal(20); expect(event.changed).to.have.length(1); }); it('setBreakpoints sorts by line then column', () => { const bp30 = makeSourceBreakpoint(FILE_A, 30); const bp10 = makeSourceBreakpoint(FILE_A, 10); const bp20 = makeSourceBreakpoint(FILE_A, 20); manager.setBreakpoints(FILE_A, [bp30, bp10, bp20]); const lines = manager.getBreakpoints(FILE_A).map(bp => bp.line); expect(lines).to.deep.equal([10, 20, 30]); }); }); describe('BreakpointManager — enable/disable', () => { let manager: BreakpointManager; beforeEach(() => { ({ manager } = createManager()); }); it('enableAllBreakpoints(true) enables all breakpoint types', () => { const bp = makeSourceBreakpoint(FILE_A, 10, { enabled: false }); manager.setBreakpoints(FILE_A, [bp]); const fbp = makeFunctionBreakpoint('myFunc'); fbp.enabled = false; manager.addFunctionBreakpoint(fbp); manager.addInstructionBreakpoint('0xDEAD', 0); const dbp = makeDataBreakpoint('data1'); dbp.enabled = false; manager.addDataBreakpoint(dbp); // Disable all first manager.enableAllBreakpoints(false); const sourceEvents: SourceBreakpointsChangeEvent[] = []; const funcEvents: FunctionBreakpointsChangeEvent[] = []; const instrEvents: InstructionBreakpointsChangeEvent[] = []; const dataEvents: DataBreakpointsChangeEvent[] = []; manager.onDidChangeBreakpoints(e => sourceEvents.push(e)); manager.onDidChangeFunctionBreakpoints(e => funcEvents.push(e)); manager.onDidChangeInstructionBreakpoints(e => instrEvents.push(e)); manager.onDidChangeDataBreakpoints(e => dataEvents.push(e)); manager.enableAllBreakpoints(true); expect(manager.getBreakpoints(FILE_A)[0].origin.enabled).to.be.true; expect(manager.getFunctionBreakpoints()[0].origin.enabled).to.be.true; expect(manager.getInstructionBreakpoints()[0].origin.enabled).to.be.true; expect(manager.getDataBreakpoints()[0].origin.enabled).to.be.true; expect(sourceEvents).to.have.length.greaterThan(0); expect(funcEvents).to.have.length.greaterThan(0); expect(instrEvents).to.have.length.greaterThan(0); expect(dataEvents).to.have.length.greaterThan(0); }); it('enableAllBreakpoints does not fire for types already in target state', () => { const bp = makeSourceBreakpoint(FILE_A, 10, { enabled: true }); manager.setBreakpoints(FILE_A, [bp]); // They're already enabled — should still fire because didChange is always true // in current implementation (identity-based). This test verifies the function // breakpoints emitter doesn't fire when there are no function breakpoints. const funcEvents: FunctionBreakpointsChangeEvent[] = []; manager.onDidChangeFunctionBreakpoints(e => funcEvents.push(e)); manager.enableAllBreakpoints(true); expect(funcEvents).to.have.length(0); }); it('set breakpointsEnabled fires onDidChangeMarkers for all synthetic URIs', () => { manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]); manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn')); const markerUris: string[] = []; manager.onDidChangeMarkers(uri => markerUris.push(uri.toString())); manager.breakpointsEnabled = false; expect(markerUris).to.include(FILE_A.toString()); expect(markerUris).to.include(BreakpointManager.FUNCTION_URI.toString()); expect(markerUris).to.include(BreakpointManager.INSTRUCTION_URI.toString()); expect(markerUris).to.include(BreakpointManager.DATA_URI.toString()); expect(markerUris).to.include(BreakpointManager.EXCEPTION_URI.toString()); }); it('set breakpointsEnabled does not fire if value unchanged', () => { const markerUris: string[] = []; manager.onDidChangeMarkers(uri => markerUris.push(uri.toString())); manager.breakpointsEnabled = true; // already true expect(markerUris).to.have.length(0); }); it('enableBreakpoint fires fireBreakpointChanged', () => { manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]); const wrapper = manager.getBreakpoints(FILE_A)[0]; const events: SourceBreakpointsChangeEvent[] = []; manager.onDidChangeBreakpoints(e => events.push(e)); manager.enableBreakpoint(wrapper, false); expect(wrapper.origin.enabled).to.be.false; expect(events).to.have.length(1); expect(events[0].changed).to.include(wrapper); }); it('enableBreakpoint does not fire if already at target state', () => { manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]); const wrapper = manager.getBreakpoints(FILE_A)[0]; const events: SourceBreakpointsChangeEvent[] = []; manager.onDidChangeBreakpoints(e => events.push(e)); manager.enableBreakpoint(wrapper, true); // already enabled expect(events).to.have.length(0); }); }); describe('BreakpointManager — non-source breakpoint types', () => { let manager: BreakpointManager; beforeEach(() => { ({ manager } = createManager()); }); // Function breakpoints it('addFunctionBreakpoint fires correct events', () => { const events: FunctionBreakpointsChangeEvent[] = []; manager.onDidChangeFunctionBreakpoints(e => events.push(e)); const bp = makeFunctionBreakpoint('myFunction'); manager.addFunctionBreakpoint(bp); expect(manager.getFunctionBreakpoints()).to.have.length(1); expect(manager.getFunctionBreakpoints()[0].name).to.equal('myFunction'); expect(events).to.have.length(1); expect(events[0].added).to.have.length(1); }); it('addFunctionBreakpoint with duplicate name is a no-op', () => { manager.addFunctionBreakpoint(makeFunctionBreakpoint('myFunc')); manager.addFunctionBreakpoint(makeFunctionBreakpoint('myFunc')); expect(manager.getFunctionBreakpoints()).to.have.length(1); }); it('removeFunctionBreakpoint fires correct events', () => { manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn')); const wrapper = manager.getFunctionBreakpoints()[0]; const events: FunctionBreakpointsChangeEvent[] = []; manager.onDidChangeFunctionBreakpoints(e => events.push(e)); manager.removeFunctionBreakpoint(wrapper); expect(manager.getFunctionBreakpoints()).to.have.length(0); expect(events).to.have.length(1); expect(events[0].removed).to.include(wrapper); }); it('updateFunctionBreakpoint with a name collision removes the colliding breakpoint', () => { manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn1')); manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn2')); const fn1 = manager.getFunctionBreakpoints().find(b => b.name === 'fn1')!; const events: FunctionBreakpointsChangeEvent[] = []; manager.onDidChangeFunctionBreakpoints(e => events.push(e)); manager.updateFunctionBreakpoint(fn1, { name: 'fn2' }); expect(manager.getFunctionBreakpoints()).to.have.length(1); expect(manager.getFunctionBreakpoints()[0].name).to.equal('fn2'); // The event should have a removed entry for the colliding breakpoint const removeEvent = events.find(e => e.removed.length > 0); expect(removeEvent).to.not.be.undefined; }); // Instruction breakpoints it('addInstructionBreakpoint fires correct events', () => { const events: InstructionBreakpointsChangeEvent[] = []; manager.onDidChangeInstructionBreakpoints(e => events.push(e)); manager.addInstructionBreakpoint('0xDEAD', 0); expect(manager.getInstructionBreakpoints()).to.have.length(1); expect(events).to.have.length(1); expect(events[0].added).to.have.length(1); }); it('addInstructionBreakpoint with duplicate address+offset is a no-op', () => { manager.addInstructionBreakpoint('0xBEEF', 4); manager.addInstructionBreakpoint('0xBEEF', 4); expect(manager.getInstructionBreakpoints()).to.have.length(1); }); it('addInstructionBreakpoint with same address but different offset creates new', () => { manager.addInstructionBreakpoint('0xBEEF', 0); manager.addInstructionBreakpoint('0xBEEF', 4); expect(manager.getInstructionBreakpoints()).to.have.length(2); }); it('removeInstructionBreakpoint fires correct events', () => { manager.addInstructionBreakpoint('0xCAFE', 0); const wrapper = manager.getInstructionBreakpoints()[0]; const events: InstructionBreakpointsChangeEvent[] = []; manager.onDidChangeInstructionBreakpoints(e => events.push(e)); manager.removeInstructionBreakpoint(wrapper); expect(manager.getInstructionBreakpoints()).to.have.length(0); expect(events).to.have.length(1); expect(events[0].removed).to.include(wrapper); }); // Data breakpoints it('addDataBreakpoint fires correct events', () => { const events: DataBreakpointsChangeEvent[] = []; manager.onDidChangeDataBreakpoints(e => events.push(e)); manager.addDataBreakpoint(makeDataBreakpoint('data-1')); expect(manager.getDataBreakpoints()).to.have.length(1); expect(events).to.have.length(1); expect(events[0].added).to.have.length(1); }); it('addDataBreakpoint with duplicate dataId is a no-op', () => { manager.addDataBreakpoint(makeDataBreakpoint('data-1')); manager.addDataBreakpoint(makeDataBreakpoint('data-1')); expect(manager.getDataBreakpoints()).to.have.length(1); }); it('removeDataBreakpoint fires correct events', () => { manager.addDataBreakpoint(makeDataBreakpoint('data-1')); const wrapper = manager.getDataBreakpoints()[0]; const events: DataBreakpointsChangeEvent[] = []; manager.onDidChangeDataBreakpoints(e => events.push(e)); manager.removeDataBreakpoint(wrapper); expect(manager.getDataBreakpoints()).to.have.length(0); expect(events).to.have.length(1); expect(events[0].removed).to.include(wrapper); }); // removeBreakpointsById it('removeBreakpointsById removes across all types', () => { manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]); manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn')); manager.addInstructionBreakpoint('0xABC', 0); manager.addDataBreakpoint(makeDataBreakpoint('d1')); const srcId = manager.getBreakpoints(FILE_A)[0].id; const fnId = manager.getFunctionBreakpoints()[0].id; const instrId = manager.getInstructionBreakpoints()[0].id; const dataId = manager.getDataBreakpoints()[0].id; manager.removeBreakpointsById([srcId, fnId, instrId, dataId]); expect(manager.getBreakpoints(FILE_A)).to.have.length(0); expect(manager.getFunctionBreakpoints()).to.have.length(0); expect(manager.getInstructionBreakpoints()).to.have.length(0); expect(manager.getDataBreakpoints()).to.have.length(0); }); }); describe('BreakpointManager — updateSessionData', () => { let manager: BreakpointManager; beforeEach(() => { ({ manager } = createManager()); }); it('with bps map: only matching breakpoints get updated', () => { const sb1 = makeSourceBreakpoint(FILE_A, 10); const sb2 = makeSourceBreakpoint(FILE_A, 20); manager.setBreakpoints(FILE_A, [sb1, sb2]); const wrappers = manager.getBreakpoints(FILE_A); const bpsMap = new Map(); bpsMap.set(wrappers[0].id, { id: 1, verified: true, line: 10 }); manager.updateSessionData('s1', defaultCapabilities, bpsMap); expect(wrappers[0].installed).to.be.true; expect(wrappers[0].verified).to.be.true; expect(wrappers[1].installed).to.be.false; // not in the map — no session touched it }); it('without bps map: all breakpoints have the session removed', () => { const sb = makeSourceBreakpoint(FILE_A, 10); manager.setBreakpoints(FILE_A, [sb]); const wrapper = manager.getBreakpoints(FILE_A)[0]; // First add session data const bpsMap = new Map(); bpsMap.set(wrapper.id, { id: 1, verified: true, line: 10 }); manager.updateSessionData('s1', defaultCapabilities, bpsMap); expect(wrapper.installed).to.be.true; expect(wrapper.verified).to.be.true; // Now cleanup (no bps map) manager.updateSessionData('s1', defaultCapabilities, undefined); expect(wrapper.installed).to.be.false; expect(wrapper.raw).to.be.undefined; // verified defaults to true when no session has weighed in expect(wrapper.verified).to.be.true; }); it('cleanup short-circuits for breakpoints that never had data from the session', () => { const sb = makeSourceBreakpoint(FILE_A, 10); manager.setBreakpoints(FILE_A, [sb]); const wrapper = manager.getBreakpoints(FILE_A)[0]; // Add data from session-1 const bpsMap = new Map(); bpsMap.set(wrapper.id, { id: 1, verified: true, line: 10 }); manager.updateSessionData('s1', defaultCapabilities, bpsMap); // Cleanup session-2 (never contributed) — wrapper should still have s1 data manager.updateSessionData('s2', defaultCapabilities, undefined); expect(wrapper.verified).to.be.true; expect(wrapper.raw!.sessionId).to.equal('s1'); }); it('capabilities are correctly extracted and merged into BPSessionData', () => { const sb = makeSourceBreakpoint(FILE_A, 10); manager.setBreakpoints(FILE_A, [sb]); const wrapper = manager.getBreakpoints(FILE_A)[0]; const caps: DebugProtocol.Capabilities = { supportsConditionalBreakpoints: true, supportsHitConditionalBreakpoints: true, supportsLogPoints: false, }; const bpsMap = new Map(); bpsMap.set(wrapper.id, { id: 1, verified: true, line: 10 }); manager.updateSessionData('s1', caps, bpsMap); expect(wrapper.raw!.supportsConditionalBreakpoints).to.be.true; expect(wrapper.raw!.supportsHitConditionalBreakpoints).to.be.true; expect(wrapper.raw!.supportsLogPoints).to.be.false; }); it('typed events fire grouped by URI', () => { manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]); manager.setBreakpoints(FILE_B, [makeSourceBreakpoint(FILE_B, 5)]); manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn')); const sourceEvents: SourceBreakpointsChangeEvent[] = []; const funcEvents: FunctionBreakpointsChangeEvent[] = []; manager.onDidChangeBreakpoints(e => sourceEvents.push(e)); manager.onDidChangeFunctionBreakpoints(e => funcEvents.push(e)); const bpsMap = new Map(); for (const bp of manager.getBreakpoints()) { bpsMap.set(bp.id, { id: 1, verified: true, line: bp.line }); } for (const bp of manager.getFunctionBreakpoints()) { bpsMap.set(bp.id, { id: 2, verified: true }); } manager.updateSessionData('s1', defaultCapabilities, bpsMap); // Source breakpoints for two different URIs → two events expect(sourceEvents).to.have.length(2); const uris = sourceEvents.map(e => e.uri.toString()).sort(); expect(uris).to.deep.equal([FILE_A.toString(), FILE_B.toString()].sort()); // Function breakpoint → one event expect(funcEvents).to.have.length(1); }); }); describe('BreakpointManager — exception breakpoints', () => { let manager: BreakpointManager; beforeEach(() => { ({ manager } = createManager()); }); it('addExceptionBreakpoints creates new for unknown filters', () => { const filter: DebugProtocol.ExceptionBreakpointsFilter = { filter: 'all', label: 'All Exceptions' }; manager.addExceptionBreakpoints([filter], 'session-1'); expect(manager.getExceptionBreakpoints()).to.have.length(1); expect(manager.getExceptionBreakpoints()[0].origin.raw.filter).to.equal('all'); }); it('addExceptionBreakpoints reuses existing for known filters', () => { const filter: DebugProtocol.ExceptionBreakpointsFilter = { filter: 'all', label: 'All Exceptions' }; manager.addExceptionBreakpoints([filter], 'session-1'); const first = manager.getExceptionBreakpoints()[0]; manager.addExceptionBreakpoints([filter], 'session-2'); expect(manager.getExceptionBreakpoints()).to.have.length(1); expect(manager.getExceptionBreakpoints()[0]).to.equal(first); }); it('clearExceptionSessionEnablement removes the session from all enablement sets', () => { const filter: DebugProtocol.ExceptionBreakpointsFilter = { filter: 'all', label: 'All Exceptions' }; manager.addExceptionBreakpoints([filter], 'session-1'); const bp = manager.getExceptionBreakpoints()[0]; expect(bp.isEnabledForSession('session-1')).to.be.true; manager.clearExceptionSessionEnablement('session-1'); expect(bp.isEnabledForSession('session-1')).to.be.false; }); it('persistentlyVisible remains true after session cleanup for filters that were visible', () => { const filter: DebugProtocol.ExceptionBreakpointsFilter = { filter: 'all', label: 'All Exceptions' }; manager.addExceptionBreakpoints([filter], 'session-1'); const bp = manager.getExceptionBreakpoints()[0]; // addExceptionBreakpoints calls doUpdateExceptionBreakpointVisibility which sets persistent visibility expect(bp.isPersistentlyVisible()).to.be.true; // Clearing session enablement does NOT clear persistent visibility manager.clearExceptionSessionEnablement('session-1'); expect(bp.isPersistentlyVisible()).to.be.true; }); it('getExceptionBreakpoint finds by filter match', () => { const filter: DebugProtocol.ExceptionBreakpointsFilter = { filter: 'uncaught', label: 'Uncaught Exceptions' }; manager.addExceptionBreakpoints([filter], 'session-1'); const found = manager.getExceptionBreakpoint(filter); expect(found).to.not.be.undefined; expect(found!.origin.raw.filter).to.equal('uncaught'); const notFound = manager.getExceptionBreakpoint({ filter: 'other', label: 'Other' }); expect(notFound).to.be.undefined; }); }); describe('BreakpointManager — persistence', () => { let manager: BreakpointManager; let storageData: Record; beforeEach(() => { ({ manager, storageData } = createManager()); }); it('save() extracts origin from all wrapper types', () => { manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]); manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn')); manager.addInstructionBreakpoint('0xDEAD', 0); manager.addDataBreakpoint(makeDataBreakpoint('d1')); // Add an exception breakpoint that's persistently visible const filter: DebugProtocol.ExceptionBreakpointsFilter = { filter: 'all', label: 'All Exceptions' }; manager.addExceptionBreakpoints([filter], 'session-1'); manager.save(); const data = storageData['breakpoints'] as BreakpointManager.Data; expect(data).to.not.be.undefined; expect(Object.keys(data.breakpoints)).to.have.length(1); expect(data.breakpoints[FILE_A.toString()]).to.have.length(1); expect(data.functionBreakpoints).to.have.length(1); expect(data.instructionBreakpoints).to.have.length(1); expect(data.dataBreakpoints).to.have.length(1); expect(data.exceptionBreakpoints).to.have.length(1); }); it('round-trip: save then load produces equivalent breakpoints', async () => { const sbp = makeSourceBreakpoint(FILE_A, 42, { condition: 'x > 5' }); manager.setBreakpoints(FILE_A, [sbp]); manager.addFunctionBreakpoint(makeFunctionBreakpoint('myFn')); manager.breakpointsEnabled = false; manager.save(); // Create a fresh manager and load const { manager: manager2 } = createManager(); // Copy stored data to the new manager's storage const freshStorageData = manager2['storage']; await freshStorageData.setData('breakpoints', storageData['breakpoints']); await manager2.load(); expect(manager2.breakpointsEnabled).to.be.false; const loaded = manager2.getBreakpoints(FILE_A); expect(loaded).to.have.length(1); expect(loaded[0].origin.raw.line).to.equal(42); expect(loaded[0].origin.raw.condition).to.equal('x > 5'); expect(manager2.getFunctionBreakpoints()).to.have.length(1); expect(manager2.getFunctionBreakpoints()[0].name).to.equal('myFn'); }); it('exception breakpoints: only persistentlyVisible ones are saved', () => { const filter1: DebugProtocol.ExceptionBreakpointsFilter = { filter: 'all', label: 'All' }; const filter2: DebugProtocol.ExceptionBreakpointsFilter = { filter: 'uncaught', label: 'Uncaught' }; manager.addExceptionBreakpoints([filter1], 'session-1'); manager.addExceptionBreakpoints([filter2], 'session-1'); // filter1 and filter2 are persistentlyVisible after addExceptionBreakpoints // Now mark one as not persistently visible const bp2 = manager.getExceptionBreakpoints()[1]; bp2.setPersistentVisibility(false); manager.save(); const data = storageData['breakpoints'] as BreakpointManager.Data; expect(data.exceptionBreakpoints).to.have.length(1); expect(data.exceptionBreakpoints![0].raw.filter).to.equal('all'); }); }); describe('BreakpointManager — file deletion', () => { let manager: BreakpointManager; let fileChangeEmitter: Emitter; beforeEach(() => { ({ manager, fileChangeEmitter } = createManager()); }); it('when a file is deleted, its source breakpoints are removed and onDidChangeMarkers fires', () => { manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]); manager.setBreakpoints(FILE_B, [makeSourceBreakpoint(FILE_B, 5)]); expect(manager.getBreakpoints(FILE_A)).to.have.length(1); const markerUris: string[] = []; manager.onDidChangeMarkers(uri => markerUris.push(uri.toString())); fileChangeEmitter.fire(new FileChangesEvent([{ resource: FILE_A, type: FileChangeType.DELETED, }])); expect(manager.getBreakpoints(FILE_A)).to.have.length(0); expect(markerUris).to.include(FILE_A.toString()); // FILE_B should be untouched expect(manager.getBreakpoints(FILE_B)).to.have.length(1); }); }); describe('BreakpointManager — fireTypedBreakpointEvent dispatch', () => { let manager: BreakpointManager; beforeEach(() => { ({ manager } = createManager()); }); it('fires onDidChangeBreakpoints for DebugSourceBreakpoint instances', () => { manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]); const events: SourceBreakpointsChangeEvent[] = []; manager.onDidChangeBreakpoints(e => events.push(e)); const wrapper = manager.getBreakpoints(FILE_A)[0]; manager.fireBreakpointChanged(wrapper); expect(events).to.have.length(1); expect(events[0].changed).to.include(wrapper); }); it('fires onDidChangeFunctionBreakpoints for DebugFunctionBreakpoint instances', () => { manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn')); const events: FunctionBreakpointsChangeEvent[] = []; manager.onDidChangeFunctionBreakpoints(e => events.push(e)); const wrapper = manager.getFunctionBreakpoints()[0]; manager.fireBreakpointChanged(wrapper); expect(events).to.have.length(1); expect(events[0].changed[0]).to.equal(wrapper); }); it('fires onDidChangeInstructionBreakpoints for DebugInstructionBreakpoint instances', () => { manager.addInstructionBreakpoint('0xABC', 0); const events: InstructionBreakpointsChangeEvent[] = []; manager.onDidChangeInstructionBreakpoints(e => events.push(e)); const wrapper = manager.getInstructionBreakpoints()[0]; manager.fireBreakpointChanged(wrapper); expect(events).to.have.length(1); expect(events[0].changed[0]).to.equal(wrapper); }); it('fires onDidChangeDataBreakpoints for DebugDataBreakpoint instances', () => { manager.addDataBreakpoint(makeDataBreakpoint('d1')); const events: DataBreakpointsChangeEvent[] = []; manager.onDidChangeDataBreakpoints(e => events.push(e)); const wrapper = manager.getDataBreakpoints()[0]; manager.fireBreakpointChanged(wrapper); expect(events).to.have.length(1); expect(events[0].changed[0]).to.equal(wrapper); }); }); describe('BreakpointManager — query helpers', () => { let manager: BreakpointManager; beforeEach(() => { ({ manager } = createManager()); }); it('getLineBreakpoints returns breakpoints at a specific line', () => { manager.setBreakpoints(FILE_A, [ makeSourceBreakpoint(FILE_A, 10), makeSourceBreakpoint(FILE_A, 20), makeSourceBreakpoint(FILE_A, 10, { column: 5 }), ]); const atLine10 = manager.getLineBreakpoints(FILE_A, 10); expect(atLine10).to.have.length(2); }); it('getBreakpointById finds across all types', () => { manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]); manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn')); manager.addInstructionBreakpoint('0x1', 0); manager.addDataBreakpoint(makeDataBreakpoint('d1')); const srcBp = manager.getBreakpoints(FILE_A)[0]; const fnBp = manager.getFunctionBreakpoints()[0]; const instrBp = manager.getInstructionBreakpoints()[0]; const dataBp = manager.getDataBreakpoints()[0]; expect(manager.getBreakpointById(srcBp.id)).to.equal(srcBp); expect(manager.getBreakpointById(fnBp.id)).to.equal(fnBp); expect(manager.getBreakpointById(instrBp.id)).to.equal(instrBp); expect(manager.getBreakpointById(dataBp.id)).to.equal(dataBp); expect(manager.getBreakpointById('nonexistent')).to.be.undefined; }); it('allBreakpoints yields all types', () => { manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]); manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn')); manager.addInstructionBreakpoint('0x1', 0); manager.addDataBreakpoint(makeDataBreakpoint('d1')); manager.addExceptionBreakpoints([{ filter: 'all', label: 'All' }], 's1'); const all = [...manager.allBreakpoints()]; expect(all).to.have.length(5); const types = all.map(bp => bp.constructor.name); expect(types).to.include('DebugSourceBreakpoint'); expect(types).to.include('DebugFunctionBreakpoint'); expect(types).to.include('DebugInstructionBreakpoint'); expect(types).to.include('DebugDataBreakpoint'); expect(types).to.include('DebugExceptionBreakpoint'); }); it('hasBreakpoints returns true when any type exists', () => { expect(manager.hasBreakpoints()).to.be.false; manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn')); expect(manager.hasBreakpoints()).to.be.true; }); it('getUris returns all URIs with source breakpoints', () => { manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]); manager.setBreakpoints(FILE_B, [makeSourceBreakpoint(FILE_B, 5)]); const uris = [...manager.getUris()]; expect(uris).to.have.length(2); expect(uris).to.include(FILE_A.toString()); expect(uris).to.include(FILE_B.toString()); }); it('getBreakpoints with no URI returns all source breakpoints', () => { manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]); manager.setBreakpoints(FILE_B, [makeSourceBreakpoint(FILE_B, 5)]); const all = manager.getBreakpoints(); expect(all).to.have.length(2); }); it('removeBreakpoints clears all breakpoints of all types', () => { manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 10)]); manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn')); manager.addInstructionBreakpoint('0x1', 0); manager.addDataBreakpoint(makeDataBreakpoint('d1')); manager.removeBreakpoints(); expect(manager.getBreakpoints()).to.have.length(0); expect(manager.getFunctionBreakpoints()).to.have.length(0); expect(manager.getInstructionBreakpoints()).to.have.length(0); expect(manager.getDataBreakpoints()).to.have.length(0); }); }); describe('BreakpointManager — updateBreakpoint', () => { let manager: BreakpointManager; beforeEach(() => { ({ manager } = createManager()); }); it('updateBreakpoint merges partial raw and fires changed event', () => { const bp = makeSourceBreakpoint(FILE_A, 10); manager.setBreakpoints(FILE_A, [bp]); const wrapper = manager.getBreakpoints(FILE_A)[0]; const events: SourceBreakpointsChangeEvent[] = []; manager.onDidChangeBreakpoints(e => events.push(e)); manager.updateBreakpoint(wrapper, { condition: 'x > 10' }); expect(wrapper.origin.raw.condition).to.equal('x > 10'); expect(wrapper.origin.raw.line).to.equal(10); // line preserved expect(events).to.have.length(1); }); it('updateDataBreakpoint updates enabled and raw fields', () => { manager.addDataBreakpoint(makeDataBreakpoint('d1')); const wrapper = manager.getDataBreakpoints()[0]; const events: DataBreakpointsChangeEvent[] = []; manager.onDidChangeDataBreakpoints(e => events.push(e)); manager.updateDataBreakpoint(wrapper, { enabled: false, raw: { condition: 'val > 0' } }); expect(wrapper.origin.enabled).to.be.false; expect(wrapper.origin.raw.condition).to.equal('val > 0'); expect(events).to.have.length(1); }); it('updateDataBreakpoint on unknown breakpoint is a no-op', () => { const orphan = DebugDataBreakpoint.create(makeDataBreakpoint('orphan'), manager.getBreakpointOptions()); const events: DataBreakpointsChangeEvent[] = []; manager.onDidChangeDataBreakpoints(e => events.push(e)); manager.updateDataBreakpoint(orphan, { enabled: false }); expect(events).to.have.length(0); }); }); describe('BreakpointManager — onDidChangeMarkers catch-all', () => { let manager: BreakpointManager; beforeEach(() => { ({ manager } = createManager()); }); it('fires when a source breakpoint is added', () => { const uris: URI[] = []; manager.onDidChangeMarkers(uri => uris.push(uri)); manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 1)]); expect(uris).to.have.length(1); expect(uris[0].toString()).to.equal(FILE_A.toString()); }); it('fires when a function breakpoint is added', () => { const uris: URI[] = []; manager.onDidChangeMarkers(uri => uris.push(uri)); manager.addFunctionBreakpoint(makeFunctionBreakpoint('myFunc')); expect(uris).to.have.length(1); expect(uris[0].toString()).to.equal(BreakpointManager.FUNCTION_URI.toString()); }); it('fires when an instruction breakpoint is added', () => { const uris: URI[] = []; manager.onDidChangeMarkers(uri => uris.push(uri)); manager.addInstructionBreakpoint('0xDEAD', 0); expect(uris).to.have.length(1); expect(uris[0].toString()).to.equal(BreakpointManager.INSTRUCTION_URI.toString()); }); it('fires when a data breakpoint is added', () => { const uris: URI[] = []; manager.onDidChangeMarkers(uri => uris.push(uri)); manager.addDataBreakpoint(makeDataBreakpoint('d1')); expect(uris).to.have.length(1); expect(uris[0].toString()).to.equal(BreakpointManager.DATA_URI.toString()); }); it('fires when a function breakpoint is removed', () => { manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn')); const wrapper = manager.getFunctionBreakpoints()[0]; const uris: URI[] = []; manager.onDidChangeMarkers(uri => uris.push(uri)); manager.removeFunctionBreakpoint(wrapper); expect(uris).to.have.length(1); }); it('fires for each type during enableAllBreakpoints', () => { manager.setBreakpoints(FILE_A, [makeSourceBreakpoint(FILE_A, 1, { enabled: false })]); manager.addFunctionBreakpoint(makeFunctionBreakpoint('fn')); manager.addInstructionBreakpoint('0xABC', 0); manager.addDataBreakpoint(makeDataBreakpoint('d1')); // Disable all first so enableAllBreakpoints has work to do manager.enableAllBreakpoints(false); const uris: URI[] = []; manager.onDidChangeMarkers(uri => uris.push(uri)); manager.enableAllBreakpoints(true); // Should fire once per breakpoint type that changed expect(uris).to.have.length.greaterThanOrEqual(4); }); });