diff --git a/.github/actions/run-integration-tests/action.yml b/.github/actions/run-integration-tests/action.yml index 3098632d..f5f1fa9f 100644 --- a/.github/actions/run-integration-tests/action.yml +++ b/.github/actions/run-integration-tests/action.yml @@ -31,7 +31,7 @@ runs: run: | set -o pipefail xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" \ - pnpm test:release:automated --exclude-label requires-extensions --exclude-label cursor 2>&1 | tee /tmp/test-report-automated.txt + pnpm test:release:automated 2>&1 | tee /tmp/test-report-automated.txt - name: Extract integration test stats id: stats diff --git a/packages/rangelink-vscode-extension/package.json b/packages/rangelink-vscode-extension/package.json index 3bc789af..aec785da 100644 --- a/packages/rangelink-vscode-extension/package.json +++ b/packages/rangelink-vscode-extension/package.json @@ -51,7 +51,7 @@ "test:coverage": "jest --coverage --coverageReporters=text --coverageReporters=text-summary --coverageReporters=html", "test:fast": "jest --coverage --testPathIgnorePatterns '/src/__integration-tests__/' '\\.integration\\.test\\.ts$'", "test:release": "./scripts/run-integration-tests.sh", - "test:release:automated": "./scripts/run-integration-tests.sh --automated", + "test:release:automated": "./scripts/run-integration-tests.sh --automated --exclude-label requires-extensions --exclude-label cursor", "test:release:grep": "./scripts/run-integration-tests.sh --grep", "test:release:prepare": "pnpm compile && rm -rf out/__integration-tests__ && tsc -p tsconfig.integration.json && node scripts/setup-integration-test-settings.js", "test:release:with-extensions": "./scripts/run-integration-tests.sh --with-extensions", diff --git a/packages/rangelink-vscode-extension/src/LogCapture.ts b/packages/rangelink-vscode-extension/src/LogCapture.ts index 9d2526dd..bea7ffd7 100644 --- a/packages/rangelink-vscode-extension/src/LogCapture.ts +++ b/packages/rangelink-vscode-extension/src/LogCapture.ts @@ -1,15 +1,9 @@ import type * as vscode from 'vscode'; +import { ENV_RANGELINK_CAPTURE_LOGS } from './constants'; import { RangeLinkExtensionError } from './errors/RangeLinkExtensionError'; import { RangeLinkExtensionErrorCodes } from './errors/RangeLinkExtensionErrorCodes'; -/** - * Environment variable that enables in-memory log capture. - * Set by the integration test runner before launching VS Code. - * When absent or not 'true', LogCapture is a transparent proxy with zero overhead. - */ -const ENV_CAPTURE_LOGS = 'RANGELINK_CAPTURE_LOGS'; - /** * Wraps an OutputChannel to optionally capture log lines in memory. * @@ -27,7 +21,7 @@ export class LogCapture { private readonly captureEnabled: boolean; constructor(private readonly outputChannel: vscode.OutputChannel) { - this.captureEnabled = process.env[ENV_CAPTURE_LOGS] === 'true'; + this.captureEnabled = process.env[ENV_RANGELINK_CAPTURE_LOGS] === 'true'; } /** @@ -91,7 +85,7 @@ export class LogCapture { if (!this.captureEnabled) { throw new RangeLinkExtensionError({ code: RangeLinkExtensionErrorCodes.LOG_CAPTURE_DISABLED, - message: `LogCapture.${methodName}() called without RANGELINK_CAPTURE_LOGS=true — this method is for integration tests only`, + message: `LogCapture.${methodName}() called without ${ENV_RANGELINK_CAPTURE_LOGS}=true — this method is for integration tests only`, functionName: `LogCapture.${methodName}`, }); } diff --git a/packages/rangelink-vscode-extension/src/__tests__/commands/BindToTextEditorCommand.test.ts b/packages/rangelink-vscode-extension/src/__tests__/commands/BindToTextEditorCommand.test.ts index 31ba7b13..c2442798 100644 --- a/packages/rangelink-vscode-extension/src/__tests__/commands/BindToTextEditorCommand.test.ts +++ b/packages/rangelink-vscode-extension/src/__tests__/commands/BindToTextEditorCommand.test.ts @@ -83,11 +83,7 @@ describe('BindToTextEditorCommand', () => { outcome: 'bound', bindInfo: { destinationName: 'app.ts', destinationKind: 'text-editor' }, }); - expect(mockDestinationManager.bind).toHaveBeenCalledWith({ - kind: 'text-editor', - uri: eligibleFile.uri, - viewColumn: 1, - }); + expect(mockDestinationManager.bind).toHaveBeenCalledWith(eligibleFile.bindOptions); expect(mockLogger.debug).toHaveBeenCalledWith( { fn: 'BindToTextEditorCommand.executeWithPicker', filename: 'app.ts' }, 'Single file, auto-binding', @@ -134,11 +130,7 @@ describe('BindToTextEditorCommand', () => { outcome: 'bound', bindInfo: { destinationName: 'utils.ts', destinationKind: 'text-editor' }, }); - expect(mockDestinationManager.bind).toHaveBeenCalledWith({ - kind: 'text-editor', - uri: eligibleFile2.uri, - viewColumn: 2, - }); + expect(mockDestinationManager.bind).toHaveBeenCalledWith(eligibleFile2.bindOptions); expect(mockLogger.debug).toHaveBeenCalledWith( { fn: 'BindToTextEditorCommand.executeWithPicker', fileCount: 2 }, 'Starting bind to text editor command', diff --git a/packages/rangelink-vscode-extension/src/__tests__/destinations/DestinationAvailabilityService.test.ts b/packages/rangelink-vscode-extension/src/__tests__/destinations/DestinationAvailabilityService.test.ts index 45cb34c5..d3168633 100644 --- a/packages/rangelink-vscode-extension/src/__tests__/destinations/DestinationAvailabilityService.test.ts +++ b/packages/rangelink-vscode-extension/src/__tests__/destinations/DestinationAvailabilityService.test.ts @@ -130,10 +130,8 @@ describe('DestinationAvailabilityService', () => { displayName: 'Terminal ("zsh")', bindOptions: { kind: 'terminal', terminal: terminal2 }, itemKind: 'bindable', - isActive: true, - boundState: 'not-bound', terminalInfo: { - terminal: terminal2, + bindOptions: { kind: 'terminal', terminal: terminal2 }, name: 'zsh', isActive: true, processId: undefined, @@ -145,10 +143,8 @@ describe('DestinationAvailabilityService', () => { displayName: 'Terminal ("bash")', bindOptions: { kind: 'terminal', terminal: terminal1 }, itemKind: 'bindable', - isActive: false, - boundState: 'not-bound', terminalInfo: { - terminal: terminal1, + bindOptions: { kind: 'terminal', terminal: terminal1 }, name: 'bash', isActive: false, processId: undefined, @@ -199,10 +195,8 @@ describe('DestinationAvailabilityService', () => { displayName: 'Terminal ("bash")', bindOptions: { kind: 'terminal', terminal }, itemKind: 'bindable', - isActive: true, - boundState: 'not-bound', terminalInfo: { - terminal, + bindOptions: { kind: 'terminal', terminal }, name: 'bash', isActive: true, processId: undefined, @@ -246,10 +240,8 @@ describe('DestinationAvailabilityService', () => { displayName: 'Terminal ("bash")', bindOptions: { kind: 'terminal', terminal }, itemKind: 'bindable', - isActive: true, - boundState: 'not-bound', terminalInfo: { - terminal, + bindOptions: { kind: 'terminal', terminal }, name: 'bash', isActive: true, processId: undefined, @@ -293,10 +285,8 @@ describe('DestinationAvailabilityService', () => { displayName: 'Terminal ("bash")', bindOptions: { kind: 'terminal', terminal }, itemKind: 'bindable', - isActive: true, - boundState: 'not-bound', terminalInfo: { - terminal, + bindOptions: { kind: 'terminal', terminal }, name: 'bash', isActive: true, processId: undefined, @@ -333,10 +323,8 @@ describe('DestinationAvailabilityService', () => { displayName: 'Terminal ("bash")', bindOptions: { kind: 'terminal', terminal }, itemKind: 'bindable', - isActive: true, - boundState: 'not-bound', terminalInfo: { - terminal, + bindOptions: { kind: 'terminal', terminal }, name: 'bash', isActive: true, processId: undefined, @@ -373,10 +361,8 @@ describe('DestinationAvailabilityService', () => { displayName: 'Terminal ("bash")', bindOptions: { kind: 'terminal', terminal }, itemKind: 'bindable', - isActive: true, - boundState: 'not-bound', terminalInfo: { - terminal, + bindOptions: { kind: 'terminal', terminal }, name: 'bash', isActive: true, processId: undefined, @@ -415,10 +401,8 @@ describe('DestinationAvailabilityService', () => { displayName: 'Terminal ("Terminal 1")', bindOptions: { kind: 'terminal', terminal: terminal1 }, itemKind: 'bindable', - isActive: true, - boundState: 'not-bound', terminalInfo: { - terminal: terminal1, + bindOptions: { kind: 'terminal', terminal: terminal1 }, name: 'Terminal 1', isActive: true, processId: undefined, @@ -430,10 +414,8 @@ describe('DestinationAvailabilityService', () => { displayName: 'Terminal ("Terminal 2")', bindOptions: { kind: 'terminal', terminal: terminal2 }, itemKind: 'bindable', - isActive: false, - boundState: 'not-bound', terminalInfo: { - terminal: terminal2, + bindOptions: { kind: 'terminal', terminal: terminal2 }, name: 'Terminal 2', isActive: false, processId: undefined, @@ -489,7 +471,7 @@ describe('DestinationAvailabilityService', () => { bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, itemKind: 'bindable', fileInfo: { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 1, @@ -497,7 +479,6 @@ describe('DestinationAvailabilityService', () => { isActiveEditor: false, boundState: 'not-bound', }, - boundState: 'not-bound', }, ]); expect(mockLogger.debug).toHaveBeenCalledWith( @@ -579,7 +560,7 @@ describe('DestinationAvailabilityService', () => { bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, itemKind: 'bindable', fileInfo: { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 1, @@ -587,7 +568,6 @@ describe('DestinationAvailabilityService', () => { isActiveEditor: false, boundState: 'bound', }, - boundState: 'bound', }, ]); expect(mockLogger.debug).toHaveBeenCalledWith( @@ -638,7 +618,7 @@ describe('DestinationAvailabilityService', () => { bindOptions: { kind: 'text-editor', uri: uri1, viewColumn: 1 }, itemKind: 'bindable', fileInfo: { - uri: uri1, + bindOptions: { kind: 'text-editor', uri: uri1, viewColumn: 1 }, filename: 'a.ts', displayPath: 'src/a.ts', viewColumn: 1, @@ -646,7 +626,6 @@ describe('DestinationAvailabilityService', () => { isActiveEditor: false, boundState: 'not-bound', }, - boundState: 'not-bound', }, ]); expect(result['file-more']).toStrictEqual({ @@ -703,7 +682,7 @@ describe('DestinationAvailabilityService', () => { bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, itemKind: 'bindable', fileInfo: { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 1, @@ -711,7 +690,6 @@ describe('DestinationAvailabilityService', () => { isActiveEditor: false, boundState: 'bound', }, - boundState: 'bound', }, { label: 'app.ts', @@ -720,7 +698,7 @@ describe('DestinationAvailabilityService', () => { bindOptions: { kind: 'text-editor', uri, viewColumn: 2 }, itemKind: 'bindable', fileInfo: { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 2 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 2, @@ -728,7 +706,6 @@ describe('DestinationAvailabilityService', () => { isActiveEditor: false, boundState: 'not-bound', }, - boundState: 'not-bound', }, ]); expect(mockLogger.debug).toHaveBeenCalledWith( @@ -778,7 +755,7 @@ describe('DestinationAvailabilityService', () => { bindOptions: { kind: 'text-editor', uri: uri1, viewColumn: 1 }, itemKind: 'bindable', fileInfo: { - uri: uri1, + bindOptions: { kind: 'text-editor', uri: uri1, viewColumn: 1 }, filename: 'a.ts', displayPath: 'src/a.ts', viewColumn: 1, @@ -786,7 +763,6 @@ describe('DestinationAvailabilityService', () => { isActiveEditor: false, boundState: 'not-bound', }, - boundState: 'not-bound', }, { label: 'b.ts', @@ -795,7 +771,7 @@ describe('DestinationAvailabilityService', () => { bindOptions: { kind: 'text-editor', uri: uri2, viewColumn: 2 }, itemKind: 'bindable', fileInfo: { - uri: uri2, + bindOptions: { kind: 'text-editor', uri: uri2, viewColumn: 2 }, filename: 'b.ts', displayPath: 'src/b.ts', viewColumn: 2, @@ -803,7 +779,6 @@ describe('DestinationAvailabilityService', () => { isActiveEditor: false, boundState: 'not-bound', }, - boundState: 'not-bound', }, ]); expect(result['file-more']).toBeUndefined(); @@ -872,7 +847,7 @@ describe('DestinationAvailabilityService', () => { bindOptions: { kind: 'text-editor', uri: uri1, viewColumn: 1 }, itemKind: 'bindable', fileInfo: { - uri: uri1, + bindOptions: { kind: 'text-editor', uri: uri1, viewColumn: 1 }, filename: 'a.ts', displayPath: 'src/a.ts', viewColumn: 1, @@ -880,7 +855,6 @@ describe('DestinationAvailabilityService', () => { isActiveEditor: false, boundState: 'not-bound', }, - boundState: 'not-bound', }, { label: 'b.ts', @@ -889,7 +863,7 @@ describe('DestinationAvailabilityService', () => { bindOptions: { kind: 'text-editor', uri: uri2, viewColumn: 1 }, itemKind: 'bindable', fileInfo: { - uri: uri2, + bindOptions: { kind: 'text-editor', uri: uri2, viewColumn: 1 }, filename: 'b.ts', displayPath: 'src/b.ts', viewColumn: 1, @@ -897,7 +871,6 @@ describe('DestinationAvailabilityService', () => { isActiveEditor: false, boundState: 'not-bound', }, - boundState: 'not-bound', }, ]); expect(mockLogger.debug).toHaveBeenCalledWith( @@ -932,7 +905,7 @@ describe('DestinationAvailabilityService', () => { bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, itemKind: 'bindable', fileInfo: { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 1, @@ -940,7 +913,6 @@ describe('DestinationAvailabilityService', () => { isActiveEditor: false, boundState: 'bound', }, - boundState: 'bound', }, ]); expect(mockLogger.debug).toHaveBeenCalledWith( @@ -977,7 +949,7 @@ describe('DestinationAvailabilityService', () => { bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, itemKind: 'bindable', fileInfo: { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 1, @@ -985,7 +957,6 @@ describe('DestinationAvailabilityService', () => { isActiveEditor: false, boundState: 'bound', }, - boundState: 'bound', }, { label: 'app.ts', @@ -994,7 +965,7 @@ describe('DestinationAvailabilityService', () => { bindOptions: { kind: 'text-editor', uri, viewColumn: 2 }, itemKind: 'bindable', fileInfo: { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 2 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 2, @@ -1002,7 +973,6 @@ describe('DestinationAvailabilityService', () => { isActiveEditor: false, boundState: 'not-bound', }, - boundState: 'not-bound', }, ]); expect(mockLogger.debug).toHaveBeenCalledWith( @@ -1039,7 +1009,7 @@ describe('DestinationAvailabilityService', () => { bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, itemKind: 'bindable', fileInfo: { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 1, @@ -1047,7 +1017,6 @@ describe('DestinationAvailabilityService', () => { isActiveEditor: false, boundState: 'bound', }, - boundState: 'bound', }, { label: 'app.ts', @@ -1056,7 +1025,7 @@ describe('DestinationAvailabilityService', () => { bindOptions: { kind: 'text-editor', uri, viewColumn: 2 }, itemKind: 'bindable', fileInfo: { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 2 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 2, @@ -1064,7 +1033,6 @@ describe('DestinationAvailabilityService', () => { isActiveEditor: false, boundState: 'bound', }, - boundState: 'bound', }, ]); expect(mockLogger.debug).toHaveBeenCalledWith( @@ -1101,7 +1069,7 @@ describe('DestinationAvailabilityService', () => { bindOptions: { kind: 'text-editor', uri: uri1, viewColumn: 1 }, itemKind: 'bindable', fileInfo: { - uri: uri1, + bindOptions: { kind: 'text-editor', uri: uri1, viewColumn: 1 }, filename: 'util.ts', displayPath: 'src/a/util.ts', viewColumn: 1, @@ -1109,7 +1077,6 @@ describe('DestinationAvailabilityService', () => { isActiveEditor: false, boundState: 'not-bound', }, - boundState: 'not-bound', }, { label: 'util.ts', @@ -1118,7 +1085,7 @@ describe('DestinationAvailabilityService', () => { bindOptions: { kind: 'text-editor', uri: uri2, viewColumn: 1 }, itemKind: 'bindable', fileInfo: { - uri: uri2, + bindOptions: { kind: 'text-editor', uri: uri2, viewColumn: 1 }, filename: 'util.ts', displayPath: 'src/b/util.ts', viewColumn: 1, @@ -1126,7 +1093,6 @@ describe('DestinationAvailabilityService', () => { isActiveEditor: false, boundState: 'not-bound', }, - boundState: 'not-bound', }, ]); expect(mockLogger.debug).toHaveBeenCalledWith( diff --git a/packages/rangelink-vscode-extension/src/__tests__/destinations/DestinationPicker.test.ts b/packages/rangelink-vscode-extension/src/__tests__/destinations/DestinationPicker.test.ts index 7b46f73b..ca5078b0 100644 --- a/packages/rangelink-vscode-extension/src/__tests__/destinations/DestinationPicker.test.ts +++ b/packages/rangelink-vscode-extension/src/__tests__/destinations/DestinationPicker.test.ts @@ -188,7 +188,7 @@ describe('DestinationPicker', () => { expect(showFilePickerSpy).toHaveBeenCalled(); expect(result).toStrictEqual({ outcome: 'selected', - bindOptions: { kind: 'text-editor', uri: fileInfo.uri, viewColumn: fileInfo.viewColumn }, + bindOptions: fileInfo.bindOptions, }); expect(mockLogger.debug).toHaveBeenCalledWith( { fn: 'DestinationPicker.handleQuickPickSelection' }, @@ -323,7 +323,7 @@ describe('DestinationPicker', () => { ): Promise => { capturedPlaceholder = handlers.getPlaceholder(); return handlers.onSelected({ - terminal: terminal2, + bindOptions: { kind: 'terminal', terminal: terminal2 }, name: 'Terminal 2', isActive: false, }); diff --git a/packages/rangelink-vscode-extension/src/__tests__/destinations/PasteDestinationManager.test.ts b/packages/rangelink-vscode-extension/src/__tests__/destinations/PasteDestinationManager.test.ts index a4195f0d..1c746c2c 100644 --- a/packages/rangelink-vscode-extension/src/__tests__/destinations/PasteDestinationManager.test.ts +++ b/packages/rangelink-vscode-extension/src/__tests__/destinations/PasteDestinationManager.test.ts @@ -81,23 +81,23 @@ const expectQuickPickConfirmation = ( showQuickPickMock: jest.Mock, expectedStrings: { currentDestination: string; newDestination: string }, ): void => { - expect(showQuickPickMock).toHaveBeenCalledTimes(1); - - const [items, options] = showQuickPickMock.mock.calls[0] as [ - vscode.QuickPickItem[], - vscode.QuickPickOptions, - ]; - - // Verify items structure - expect(items).toHaveLength(2); - expect(items[0].label).toContain('replace'); - expect(items[0].description).toContain(expectedStrings.currentDestination); - expect(items[0].description).toContain(expectedStrings.newDestination); - expect(items[1].label).toContain('keep'); - - // Verify placeholder - expect(options.placeHolder).toContain(expectedStrings.currentDestination); - expect(options.placeHolder).toContain(expectedStrings.newDestination); + expect(showQuickPickMock).toHaveBeenCalledWith( + [ + { + label: 'Yes, replace', + description: `Switch from ${expectedStrings.currentDestination} to ${expectedStrings.newDestination}`, + confirmed: true, + }, + { + label: 'No, keep current binding', + description: `Stay bound to ${expectedStrings.currentDestination}`, + confirmed: false, + }, + ], + { + placeHolder: `Already bound to ${expectedStrings.currentDestination}. Replace with ${expectedStrings.newDestination}?`, + }, + ); }; describe('PasteDestinationManager', () => { diff --git a/packages/rangelink-vscode-extension/src/__tests__/destinations/destinationBuilders.test.ts b/packages/rangelink-vscode-extension/src/__tests__/destinations/destinationBuilders.test.ts index 236ea459..0d0cb1e1 100644 --- a/packages/rangelink-vscode-extension/src/__tests__/destinations/destinationBuilders.test.ts +++ b/packages/rangelink-vscode-extension/src/__tests__/destinations/destinationBuilders.test.ts @@ -5,7 +5,6 @@ import { buildTerminalDestination, buildTextEditorDestination, createCustomAiAssistantBuilder, - GEMINI_CODE_ASSIST_FOCUS_COMMANDS, type DestinationBuilderContext, type DestinationBuilder, registerAllDestinationBuilders, @@ -785,13 +784,14 @@ describe('destinationBuilders', () => { const createCapabilityMock = context.factories.focusCapability .createAIAssistantCapability as jest.Mock; - expect(createCapabilityMock).toHaveBeenCalledTimes(1); - const [capabilities, getColdRefocusArg] = createCapabilityMock.mock.calls[0]; - expect(capabilities).toStrictEqual([ - 'claude-vscode.focus', - 'claude-vscode.sidebar.open', - 'claude-vscode.editor.open', - ]); + expect(createCapabilityMock).toHaveBeenCalledWith( + ['claude-vscode.focus', 'claude-vscode.sidebar.open', 'claude-vscode.editor.open'], + expect.any(Function), + ); + + const getColdRefocusArg = createCapabilityMock.mock.calls[0][1] as ( + ...args: unknown[] + ) => unknown; expect(typeof getColdRefocusArg).toBe('function'); const result = getColdRefocusArg(); @@ -808,9 +808,7 @@ describe('destinationBuilders', () => { const createCapabilityMock = context.factories.focusCapability .createAIAssistantCapability as jest.Mock; - expect(createCapabilityMock).toHaveBeenCalledTimes(1); - const [, getColdRefocusArg] = createCapabilityMock.mock.calls[0]; - expect(getColdRefocusArg).toBeUndefined(); + expect(createCapabilityMock).toHaveBeenCalledWith(expect.any(Array), undefined); }); it('uses config values from settings when valid', () => { @@ -870,9 +868,14 @@ describe('destinationBuilders', () => { const createCapabilityMock = context.factories.focusCapability .createAIAssistantCapability as jest.Mock; - expect(createCapabilityMock).toHaveBeenCalledTimes(1); - const [capabilities, getColdRefocusArg] = createCapabilityMock.mock.calls[0]; - expect(capabilities).toStrictEqual(GEMINI_CODE_ASSIST_FOCUS_COMMANDS); + expect(createCapabilityMock).toHaveBeenCalledWith( + ['cloudcode.gemini.chatView.focus'], + expect.any(Function), + ); + + const getColdRefocusArg = createCapabilityMock.mock.calls[0][1] as ( + ...args: unknown[] + ) => unknown; expect(typeof getColdRefocusArg).toBe('function'); const result = getColdRefocusArg(); diff --git a/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/buildDestinationQuickPickItems.test.ts b/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/buildDestinationQuickPickItems.test.ts index 1412da86..4b140777 100644 --- a/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/buildDestinationQuickPickItems.test.ts +++ b/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/buildDestinationQuickPickItems.test.ts @@ -84,9 +84,12 @@ describe('buildDestinationQuickPickItems', () => { label: 'Terminal "bash"', displayName: 'Terminal "bash"', bindOptions: { kind: 'terminal', terminal: mockTerminal }, - isActive: false, itemKind: 'bindable', - terminalInfo: { terminal: mockTerminal, name: 'bash', isActive: false }, + terminalInfo: { + bindOptions: { kind: 'terminal', terminal: mockTerminal }, + name: 'bash', + isActive: false, + }, }, ], }; @@ -149,9 +152,12 @@ describe('buildDestinationQuickPickItems', () => { label: 'Terminal "bash"', displayName: 'Terminal "bash"', bindOptions: { kind: 'terminal', terminal: mockTerminal }, - isActive: true, itemKind: 'bindable', - terminalInfo: { terminal: mockTerminal, name: 'bash', isActive: true }, + terminalInfo: { + bindOptions: { kind: 'terminal', terminal: mockTerminal }, + name: 'bash', + isActive: true, + }, }, ], }; @@ -172,16 +178,19 @@ describe('buildDestinationQuickPickItems', () => { label: 'Terminal "bash"', displayName: 'Terminal "bash"', bindOptions: { kind: 'terminal', terminal: mockTerminal }, - isActive: true, itemKind: 'bindable', description: 'active', - terminalInfo: { terminal: mockTerminal, name: 'bash', isActive: true }, + terminalInfo: { + bindOptions: { kind: 'terminal', terminal: mockTerminal }, + name: 'bash', + isActive: true, + }, }, separator('Files'), { label: 'app.ts', displayName: 'app.ts', - bindOptions: { kind: 'text-editor', uri: fileInfo.uri, viewColumn: 1 }, + bindOptions: fileInfo.bindOptions, itemKind: 'bindable', description: 'Tab Group 1', fileInfo, @@ -241,7 +250,11 @@ describe('buildDestinationQuickPickItems', () => { displayName: 'Terminal "bash"', bindOptions: { kind: 'terminal', terminal: mockTerminal }, itemKind: 'bindable', - terminalInfo: { terminal: mockTerminal, name: 'bash', isActive: false }, + terminalInfo: { + bindOptions: { kind: 'terminal', terminal: mockTerminal }, + name: 'bash', + isActive: false, + }, }, ], 'terminal-more': { @@ -263,7 +276,11 @@ describe('buildDestinationQuickPickItems', () => { bindOptions: { kind: 'terminal', terminal: mockTerminal }, itemKind: 'bindable', description: undefined, - terminalInfo: { terminal: mockTerminal, name: 'bash', isActive: false }, + terminalInfo: { + bindOptions: { kind: 'terminal', terminal: mockTerminal }, + name: 'bash', + isActive: false, + }, }, { label: 'More terminals...', @@ -276,7 +293,7 @@ describe('buildDestinationQuickPickItems', () => { { label: 'index.ts', displayName: 'index.ts', - bindOptions: { kind: 'text-editor', uri: fileInfo.uri, viewColumn: 1 }, + bindOptions: fileInfo.bindOptions, itemKind: 'bindable', description: 'Tab Group 1', fileInfo, @@ -310,11 +327,9 @@ describe('buildDestinationQuickPickItems', () => { label: 'Terminal "zsh"', displayName: 'Terminal "zsh"', bindOptions: { kind: 'terminal', terminal: mockTerminal }, - isActive: true, itemKind: 'bindable', - boundState: 'bound', terminalInfo: { - terminal: mockTerminal, + bindOptions: { kind: 'terminal', terminal: mockTerminal }, name: 'zsh', isActive: true, boundState: 'bound', @@ -336,11 +351,9 @@ describe('buildDestinationQuickPickItems', () => { label: 'Terminal "zsh"', displayName: 'Terminal "zsh"', bindOptions: { kind: 'terminal', terminal: mockTerminal }, - isActive: false, itemKind: 'bindable', - boundState: 'bound', terminalInfo: { - terminal: mockTerminal, + bindOptions: { kind: 'terminal', terminal: mockTerminal }, name: 'zsh', isActive: false, boundState: 'bound', @@ -362,11 +375,9 @@ describe('buildDestinationQuickPickItems', () => { label: 'Terminal "zsh"', displayName: 'Terminal "zsh"', bindOptions: { kind: 'terminal', terminal: mockTerminal }, - isActive: true, itemKind: 'bindable', - boundState: 'not-bound', terminalInfo: { - terminal: mockTerminal, + bindOptions: { kind: 'terminal', terminal: mockTerminal }, name: 'zsh', isActive: true, boundState: 'not-bound', @@ -388,11 +399,9 @@ describe('buildDestinationQuickPickItems', () => { label: 'Terminal "node"', displayName: 'Terminal "node"', bindOptions: { kind: 'terminal', terminal: mockTerminal }, - isActive: false, itemKind: 'bindable', - boundState: 'not-bound', terminalInfo: { - terminal: mockTerminal, + bindOptions: { kind: 'terminal', terminal: mockTerminal }, name: 'node', isActive: false, boundState: 'not-bound', @@ -438,9 +447,12 @@ describe('buildDestinationQuickPickItems', () => { label: 'Terminal "fish"', displayName: 'Terminal "fish"', bindOptions: { kind: 'terminal', terminal: mockTerminal }, - isActive: true, itemKind: 'bindable', - terminalInfo: { terminal: mockTerminal, name: 'fish', isActive: true }, + terminalInfo: { + bindOptions: { kind: 'terminal', terminal: mockTerminal }, + name: 'fish', + isActive: true, + }, }, ], }; @@ -451,10 +463,13 @@ describe('buildDestinationQuickPickItems', () => { label: ' $(arrow-right) Terminal "fish"', displayName: 'Terminal "fish"', bindOptions: { kind: 'terminal', terminal: mockTerminal }, - isActive: true, itemKind: 'bindable', description: 'active', - terminalInfo: { terminal: mockTerminal, name: 'fish', isActive: true }, + terminalInfo: { + bindOptions: { kind: 'terminal', terminal: mockTerminal }, + name: 'fish', + isActive: true, + }, }); }); @@ -502,10 +517,9 @@ describe('buildDestinationQuickPickItems', () => { label: 'app.ts', displayName: 'app.ts', description: 'bound · active · Tab Group 1', - bindOptions: { kind: 'text-editor', uri: fileInfo.uri, viewColumn: 1 }, + bindOptions: fileInfo.bindOptions, itemKind: 'bindable', fileInfo, - boundState: 'bound', }); }); @@ -555,7 +569,7 @@ describe('buildDestinationQuickPickItems', () => { { label: 'app.ts', displayName: 'app.ts', - bindOptions: { kind: 'text-editor', uri: fileInfo.uri, viewColumn: 1 }, + bindOptions: fileInfo.bindOptions, itemKind: 'bindable', description: 'Tab Group 1', fileInfo, diff --git a/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/buildTerminalPickerItems.test.ts b/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/buildTerminalPickerItems.test.ts index b735502c..5380dee5 100644 --- a/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/buildTerminalPickerItems.test.ts +++ b/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/buildTerminalPickerItems.test.ts @@ -72,7 +72,7 @@ describe('buildTerminalPickerItems', () => { expect(result[1].label).toBe('prefix-zsh'); }); - it('propagates boundState to output items', () => { + it('propagates boundState to output items via terminalInfo', () => { const items = [ createMockTerminalQuickPickItem(createMockTerminal({ name: 'bash' }), false, 'bound'), createMockTerminalQuickPickItem(createMockTerminal({ name: 'zsh' }), false, 'not-bound'), @@ -80,8 +80,8 @@ describe('buildTerminalPickerItems', () => { const result = buildTerminalPickerItems(items, identityLabel); - expect(result[0].boundState).toBe('bound'); - expect(result[1].boundState).toBe('not-bound'); + expect(result[0].terminalInfo.boundState).toBe('bound'); + expect(result[1].terminalInfo.boundState).toBe('not-bound'); }); it('returns complete item structure', () => { @@ -97,9 +97,12 @@ describe('buildTerminalPickerItems', () => { displayName: 'bash', bindOptions: { kind: 'terminal', terminal }, itemKind: 'bindable', - isActive: true, - boundState: 'bound', - terminalInfo: { terminal, name: 'bash', isActive: true, boundState: 'bound' }, + terminalInfo: { + bindOptions: { kind: 'terminal', terminal }, + name: 'bash', + isActive: true, + boundState: 'bound', + }, }, ]); }); diff --git a/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/getEligibleFiles.test.ts b/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/getEligibleFiles.test.ts index a28dcc1f..a2997509 100644 --- a/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/getEligibleFiles.test.ts +++ b/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/getEligibleFiles.test.ts @@ -30,7 +30,7 @@ describe('getEligibleFiles', () => { expect(result).toStrictEqual([ { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 1, @@ -52,7 +52,7 @@ describe('getEligibleFiles', () => { expect(result).toStrictEqual([ { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 1, @@ -60,7 +60,7 @@ describe('getEligibleFiles', () => { isActiveEditor: false, }, { - uri: uri2, + bindOptions: { kind: 'text-editor', uri: uri2, viewColumn: 2 }, filename: 'utils.ts', displayPath: 'src/utils.ts', viewColumn: 2, @@ -81,7 +81,7 @@ describe('getEligibleFiles', () => { expect(result).toStrictEqual([ { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 1, @@ -103,7 +103,7 @@ describe('getEligibleFiles', () => { expect(result).toStrictEqual([ { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 1, @@ -125,7 +125,7 @@ describe('getEligibleFiles', () => { expect(result).toStrictEqual([ { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 1, @@ -147,7 +147,7 @@ describe('getEligibleFiles', () => { expect(result).toStrictEqual([ { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 1, @@ -155,7 +155,7 @@ describe('getEligibleFiles', () => { isActiveEditor: false, }, { - uri: uri2, + bindOptions: { kind: 'text-editor', uri: uri2, viewColumn: 1 }, filename: 'utils.ts', displayPath: 'src/utils.ts', viewColumn: 1, @@ -178,7 +178,7 @@ describe('getEligibleFiles', () => { expect(result).toStrictEqual([ { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 1, @@ -202,7 +202,7 @@ describe('getEligibleFiles', () => { expect(result).toStrictEqual([ { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 1, @@ -221,7 +221,7 @@ describe('getEligibleFiles', () => { expect(result).toStrictEqual([ { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 1, @@ -267,7 +267,7 @@ describe('getEligibleFiles', () => { expect(result).toStrictEqual([ { - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: 1 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 1, diff --git a/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/getEligibleTerminals.test.ts b/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/getEligibleTerminals.test.ts index 186eee40..b8c28e99 100644 --- a/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/getEligibleTerminals.test.ts +++ b/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/getEligibleTerminals.test.ts @@ -26,7 +26,7 @@ describe('getEligibleTerminals', () => { expect(await getEligibleTerminals(ideAdapter)).toStrictEqual([ { - terminal, + bindOptions: { kind: 'terminal', terminal }, name: 'zsh', isActive: true, processId: 42, @@ -58,9 +58,24 @@ describe('getEligibleTerminals', () => { }); expect(await getEligibleTerminals(ideAdapter)).toStrictEqual([ - { terminal: terminal1, name: 'zsh', isActive: false, processId: 10 }, - { terminal: terminal2, name: 'Node.js Debug Console', isActive: true, processId: 20 }, - { terminal: terminal3, name: 'bash', isActive: false, processId: 30 }, + { + bindOptions: { kind: 'terminal', terminal: terminal1 }, + name: 'zsh', + isActive: false, + processId: 10, + }, + { + bindOptions: { kind: 'terminal', terminal: terminal2 }, + name: 'Node.js Debug Console', + isActive: true, + processId: 20, + }, + { + bindOptions: { kind: 'terminal', terminal: terminal3 }, + name: 'bash', + isActive: false, + processId: 30, + }, ]); }); @@ -83,8 +98,18 @@ describe('getEligibleTerminals', () => { }); expect(await getEligibleTerminals(ideAdapter)).toStrictEqual([ - { terminal: terminal1, name: 'zsh', isActive: false, processId: 10 }, - { terminal: terminal2, name: 'bash', isActive: false, processId: 20 }, + { + bindOptions: { kind: 'terminal', terminal: terminal1 }, + name: 'zsh', + isActive: false, + processId: 10, + }, + { + bindOptions: { kind: 'terminal', terminal: terminal2 }, + name: 'bash', + isActive: false, + processId: 20, + }, ]); }); }); @@ -129,7 +154,12 @@ describe('getEligibleTerminals', () => { }); expect(await getEligibleTerminals(ideAdapter)).toStrictEqual([ - { terminal: liveTerminal, name: 'live', isActive: true, processId: 100 }, + { + bindOptions: { kind: 'terminal', terminal: liveTerminal }, + name: 'live', + isActive: true, + processId: 100, + }, ]); }); @@ -170,7 +200,12 @@ describe('getEligibleTerminals', () => { }); expect(await getEligibleTerminals(ideAdapter)).toStrictEqual([ - { terminal: liveTerminal, name: 'live', isActive: false, processId: 100 }, + { + bindOptions: { kind: 'terminal', terminal: liveTerminal }, + name: 'live', + isActive: false, + processId: 100, + }, ]); }); }); @@ -197,7 +232,12 @@ describe('getEligibleTerminals', () => { }); expect(await getEligibleTerminals(ideAdapter)).toStrictEqual([ - { terminal: shellTerminal, name: 'zsh', isActive: true, processId: 10 }, + { + bindOptions: { kind: 'terminal', terminal: shellTerminal }, + name: 'zsh', + isActive: true, + processId: 10, + }, ]); }); @@ -244,7 +284,12 @@ describe('getEligibleTerminals', () => { }); expect(await getEligibleTerminals(ideAdapter)).toStrictEqual([ - { terminal: shellTerminal, name: 'zsh', isActive: true, processId: 10 }, + { + bindOptions: { kind: 'terminal', terminal: shellTerminal }, + name: 'zsh', + isActive: true, + processId: 10, + }, ]); }); }); diff --git a/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/markBoundFile.test.ts b/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/markBoundFile.test.ts index bc9f651a..08ff9c7a 100644 --- a/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/markBoundFile.test.ts +++ b/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/markBoundFile.test.ts @@ -8,8 +8,11 @@ describe('markBoundFile', () => { let fileC: EligibleFile; beforeEach(() => { + const uriA = createMockUri('/workspace/src/app.ts'); + const uriB = createMockUri('/workspace/src/utils.ts'); + const uriC = createMockUri('/workspace/src/index.ts'); fileA = { - uri: createMockUri('/workspace/src/app.ts'), + bindOptions: { kind: 'text-editor', uri: uriA, viewColumn: 1 }, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 1, @@ -17,7 +20,7 @@ describe('markBoundFile', () => { isActiveEditor: true, }; fileB = { - uri: createMockUri('/workspace/src/utils.ts'), + bindOptions: { kind: 'text-editor', uri: uriB, viewColumn: 1 }, filename: 'utils.ts', displayPath: 'src/utils.ts', viewColumn: 1, @@ -25,7 +28,7 @@ describe('markBoundFile', () => { isActiveEditor: false, }; fileC = { - uri: createMockUri('/workspace/src/index.ts'), + bindOptions: { kind: 'text-editor', uri: uriC, viewColumn: 2 }, filename: 'index.ts', displayPath: 'src/index.ts', viewColumn: 2, @@ -35,7 +38,7 @@ describe('markBoundFile', () => { }); it('marks matching file as bound and others as not-bound', () => { - const result = markBoundFile([fileA, fileB, fileC], fileB.uri.toString()); + const result = markBoundFile([fileA, fileB, fileC], fileB.bindOptions.uri.toString()); expect(result).toStrictEqual([ { ...fileA, boundState: 'not-bound' }, @@ -63,10 +66,10 @@ describe('markBoundFile', () => { }); it('preserves all original EligibleFile properties', () => { - const result = markBoundFile([fileA], fileA.uri.toString()); + const result = markBoundFile([fileA], fileA.bindOptions.uri.toString()); expect(result[0]).toStrictEqual({ - uri: fileA.uri, + bindOptions: fileA.bindOptions, filename: 'app.ts', displayPath: 'src/app.ts', viewColumn: 1, @@ -77,7 +80,7 @@ describe('markBoundFile', () => { }); it('does not mutate the input array', () => { - markBoundFile([fileA], fileA.uri.toString()); + markBoundFile([fileA], fileA.bindOptions.uri.toString()); expect(fileA.boundState).toBeUndefined(); }); @@ -87,9 +90,13 @@ describe('markBoundFile', () => { }); it('marks file as bound when URI and viewColumn both match', () => { - const fileAInColumn2: EligibleFile = { ...fileA, viewColumn: 2 }; + const fileAInColumn2: EligibleFile = { + ...fileA, + viewColumn: 2, + bindOptions: { ...fileA.bindOptions, viewColumn: 2 }, + }; - const result = markBoundFile([fileA, fileAInColumn2], fileA.uri.toString(), 1); + const result = markBoundFile([fileA, fileAInColumn2], fileA.bindOptions.uri.toString(), 1); expect(result).toStrictEqual([ { ...fileA, boundState: 'bound' }, @@ -98,9 +105,13 @@ describe('markBoundFile', () => { }); it('does not mark file as bound when URI matches but viewColumn differs', () => { - const fileAInColumn2: EligibleFile = { ...fileA, viewColumn: 2 }; + const fileAInColumn2: EligibleFile = { + ...fileA, + viewColumn: 2, + bindOptions: { ...fileA.bindOptions, viewColumn: 2 }, + }; - const result = markBoundFile([fileA, fileAInColumn2], fileA.uri.toString(), 2); + const result = markBoundFile([fileA, fileAInColumn2], fileA.bindOptions.uri.toString(), 2); expect(result).toStrictEqual([ { ...fileA, boundState: 'not-bound' }, diff --git a/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/markBoundTerminal.test.ts b/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/markBoundTerminal.test.ts index e4136978..e7e661b4 100644 --- a/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/markBoundTerminal.test.ts +++ b/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/markBoundTerminal.test.ts @@ -69,7 +69,7 @@ describe('markBoundTerminal', () => { const result = markBoundTerminal(terminals, 42); expect(result[0]).toStrictEqual({ - terminal, + bindOptions: { kind: 'terminal', terminal }, name: 'bash', isActive: true, processId: 42, diff --git a/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/showTerminalPicker.test.ts b/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/showTerminalPicker.test.ts index 588881c5..298d9104 100644 --- a/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/showTerminalPicker.test.ts +++ b/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/showTerminalPicker.test.ts @@ -33,8 +33,6 @@ describe('showTerminalPicker', () => { displayName: item.terminalInfo.name, bindOptions: item.bindOptions, itemKind: 'bindable', - isActive: item.isActive, - boundState: item.boundState, terminalInfo: item.terminalInfo, }); @@ -137,7 +135,7 @@ describe('showTerminalPicker', () => { it('marks active terminal with active description', async () => { const items = createTerminalItems(3); - const activeTerminal = items[1].terminalInfo.terminal; + const activeTerminal = items[1].terminalInfo.bindOptions.terminal; items[1] = createMockTerminalQuickPickItem(activeTerminal, true); const quickPickProvider = createMockQuickPickProvider(); quickPickProvider.showQuickPick.mockResolvedValueOnce(reformattedItem(items[1])); diff --git a/packages/rangelink-vscode-extension/src/__tests__/helpers/createMockBindableQuickPickItem.ts b/packages/rangelink-vscode-extension/src/__tests__/helpers/createMockBindableQuickPickItem.ts index 79db6689..90d22476 100644 --- a/packages/rangelink-vscode-extension/src/__tests__/helpers/createMockBindableQuickPickItem.ts +++ b/packages/rangelink-vscode-extension/src/__tests__/helpers/createMockBindableQuickPickItem.ts @@ -27,20 +27,21 @@ export const createMockTerminalQuickPickItem = ( terminal: vscode.Terminal, isActive = false, boundState?: EligibleTerminal['boundState'], -): TerminalBindableQuickPickItem => ({ - label: `Terminal ("${terminal.name}")`, - displayName: `Terminal ("${terminal.name}")`, - bindOptions: { kind: 'terminal', terminal }, - itemKind: 'bindable', - isActive, - ...(boundState !== undefined && { boundState }), - terminalInfo: createMockEligibleTerminal({ +): TerminalBindableQuickPickItem => { + const terminalInfo = createMockEligibleTerminal({ terminal, name: terminal.name, isActive, boundState, - }), -}); + }); + return { + label: `Terminal ("${terminal.name}")`, + displayName: `Terminal ("${terminal.name}")`, + bindOptions: terminalInfo.bindOptions, + itemKind: 'bindable', + terminalInfo, + }; +}; /** * Create a mock BindableQuickPickItem for an AI assistant. @@ -57,7 +58,6 @@ export const createMockAIAssistantQuickPickItem = ( displayName, bindOptions: { kind }, itemKind: 'bindable', - isActive: false, }); /** @@ -76,14 +76,9 @@ export const createMockTextEditorQuickPickItem = ( label: resolvedFileInfo.filename, displayName: resolvedFileInfo.filename, description, - bindOptions: { - kind: 'text-editor', - uri: resolvedFileInfo.uri, - viewColumn: resolvedFileInfo.viewColumn, - }, + bindOptions: resolvedFileInfo.bindOptions, itemKind: 'bindable', fileInfo: resolvedFileInfo, - ...(resolvedFileInfo.boundState !== undefined && { boundState: resolvedFileInfo.boundState }), }; }; diff --git a/packages/rangelink-vscode-extension/src/__tests__/helpers/createMockEligibleFile.ts b/packages/rangelink-vscode-extension/src/__tests__/helpers/createMockEligibleFile.ts index 12bcf9fd..1e21c6be 100644 --- a/packages/rangelink-vscode-extension/src/__tests__/helpers/createMockEligibleFile.ts +++ b/packages/rangelink-vscode-extension/src/__tests__/helpers/createMockEligibleFile.ts @@ -1,3 +1,5 @@ +import type * as vscode from 'vscode'; + import type { EligibleFile } from '../../types'; import { createMockUri } from './createMockUri'; @@ -9,7 +11,7 @@ export interface MockEligibleFileOptions { readonly isCurrentInGroup?: boolean; readonly isActiveEditor?: boolean; readonly boundState?: EligibleFile['boundState']; - readonly uri?: EligibleFile['uri']; + readonly uri?: vscode.Uri; } export const createMockEligibleFile = (options: MockEligibleFileOptions = {}): EligibleFile => { @@ -22,8 +24,9 @@ export const createMockEligibleFile = (options: MockEligibleFileOptions = {}): E boundState, uri, } = options; + const resolvedUri = uri ?? createMockUri(`/workspace/${filename}`); return { - uri: uri ?? createMockUri(`/workspace/${filename}`), + bindOptions: { kind: 'text-editor', uri: resolvedUri, viewColumn }, filename, displayPath: displayPath ?? `src/${filename}`, viewColumn, diff --git a/packages/rangelink-vscode-extension/src/__tests__/helpers/createMockEligibleTerminal.ts b/packages/rangelink-vscode-extension/src/__tests__/helpers/createMockEligibleTerminal.ts index bd784fd8..a0ac03a9 100644 --- a/packages/rangelink-vscode-extension/src/__tests__/helpers/createMockEligibleTerminal.ts +++ b/packages/rangelink-vscode-extension/src/__tests__/helpers/createMockEligibleTerminal.ts @@ -1,3 +1,5 @@ +import type * as vscode from 'vscode'; + import type { EligibleTerminal } from '../../types'; import { createMockTerminal } from './createMockTerminal'; @@ -7,15 +9,16 @@ export interface MockEligibleTerminalOptions { readonly isActive?: boolean; readonly processId?: number; readonly boundState?: EligibleTerminal['boundState']; - readonly terminal?: EligibleTerminal['terminal']; + readonly terminal?: vscode.Terminal; } export const createMockEligibleTerminal = ( options: MockEligibleTerminalOptions = {}, ): EligibleTerminal => { const { name = 'bash', isActive = false, processId, boundState, terminal } = options; + const resolvedTerminal = terminal ?? createMockTerminal({ name }); return { - terminal: terminal ?? createMockTerminal({ name }), + bindOptions: { kind: 'terminal', terminal: resolvedTerminal }, name, isActive, ...(processId !== undefined && { processId }), diff --git a/packages/rangelink-vscode-extension/src/__tests__/ide/vscode/VscodeAdapter.test.ts b/packages/rangelink-vscode-extension/src/__tests__/ide/vscode/VscodeAdapter.test.ts index 66414a53..5a8c79d4 100644 --- a/packages/rangelink-vscode-extension/src/__tests__/ide/vscode/VscodeAdapter.test.ts +++ b/packages/rangelink-vscode-extension/src/__tests__/ide/vscode/VscodeAdapter.test.ts @@ -1,7 +1,7 @@ import type { Logger } from 'barebone-logger'; import { createMockLogger } from 'barebone-logger-testing'; -import { VscodeAdapter } from '../../../ide/vscode/VscodeAdapter'; +import { projectTestStatusFields, VscodeAdapter } from '../../../ide/vscode/VscodeAdapter'; import { PathFormat } from '../../../types/PathFormat'; import { RelativePathFormat } from '../../../types/RelativePathFormat'; import { TerminalFocusType } from '../../../types/TerminalFocusType'; @@ -509,23 +509,14 @@ describe('VscodeAdapter', () => { ); }); - it('should log semantic fields: displayName, isActive, boundState, remainingCount', async () => { + it('should log base semantic fields without flat status fields when RANGELINK_CAPTURE_LOGS is unset', async () => { const items = [ { label: 'Terminal ("bash")', description: 'bound · active', itemKind: 'bindable' as const, displayName: 'Terminal ("bash")', - isActive: true, - boundState: 'bound' as const, - }, - { - label: 'Terminal ("zsh")', - description: 'active', - itemKind: 'bindable' as const, - displayName: 'Terminal ("zsh")', - isActive: true, - boundState: 'not-bound' as const, + terminalInfo: { name: 'bash', isActive: true, boundState: 'bound' as const }, }, { label: 'More terminals...', @@ -542,7 +533,7 @@ describe('VscodeAdapter', () => { expect(mockLogger.debug).toHaveBeenCalledWith( { fn: 'VscodeAdapter.showQuickPick', - itemCount: 4, + itemCount: 3, options: undefined, items: [ { @@ -550,16 +541,6 @@ describe('VscodeAdapter', () => { description: 'bound · active', itemKind: 'bindable', displayName: 'Terminal ("bash")', - isActive: true, - boundState: 'bound', - }, - { - label: 'Terminal ("zsh")', - description: 'active', - itemKind: 'bindable', - displayName: 'Terminal ("zsh")', - isActive: true, - boundState: 'not-bound', }, { label: 'More terminals...', @@ -574,27 +555,96 @@ describe('VscodeAdapter', () => { ); }); + it('should project flat isActive/boundState from terminalInfo/fileInfo when RANGELINK_CAPTURE_LOGS=true', async () => { + const originalEnv = process.env.RANGELINK_CAPTURE_LOGS; + process.env.RANGELINK_CAPTURE_LOGS = 'true'; + try { + jest.resetModules(); + const { VscodeAdapter: CapturingAdapter } = await import( + '../../../ide/vscode/VscodeAdapter' + ); + const capturingAdapter = new CapturingAdapter(mockVSCode, mockLogger); + + const items = [ + { + label: 'Terminal ("bash")', + description: 'bound · active', + itemKind: 'bindable' as const, + displayName: 'Terminal ("bash")', + terminalInfo: { name: 'bash', isActive: true, boundState: 'bound' as const }, + }, + { + label: 'app.ts', + itemKind: 'bindable' as const, + displayName: 'app.ts', + fileInfo: { filename: 'app.ts', boundState: 'not-bound' as const }, + }, + { label: 'No destinations available', itemKind: 'info' as const }, + ]; + (mockVSCode.window.showQuickPick as jest.Mock).mockResolvedValue(undefined); + + await capturingAdapter.showQuickPick(items); + + expect(mockLogger.debug).toHaveBeenCalledWith( + { + fn: 'VscodeAdapter.showQuickPick', + itemCount: 3, + options: undefined, + items: [ + { + label: 'Terminal ("bash")', + description: 'bound · active', + itemKind: 'bindable', + displayName: 'Terminal ("bash")', + isActive: true, + boundState: 'bound', + }, + { + label: 'app.ts', + itemKind: 'bindable', + displayName: 'app.ts', + boundState: 'not-bound', + }, + { label: 'No destinations available', itemKind: 'info' }, + ], + }, + 'Showing quick pick', + ); + } finally { + if (originalEnv === undefined) { + delete process.env.RANGELINK_CAPTURE_LOGS; + } else { + process.env.RANGELINK_CAPTURE_LOGS = originalEnv; + } + } + }); + it('should omit absent semantic fields from logged items (not set to undefined)', async () => { const items = [ { label: 'Plain item' }, { label: 'With displayName only', displayName: 'raw name' }, { - label: 'With boundState only', - boundState: 'not-bound' as const, - itemKind: 'bindable' as const, + label: 'With itemKind only', + itemKind: 'info' as const, }, ]; (mockVSCode.window.showQuickPick as jest.Mock).mockResolvedValue(undefined); await adapter.showQuickPick(items); - const loggedItems = (mockLogger.debug as jest.Mock).mock.calls.find( - (call) => call[0]?.fn === 'VscodeAdapter.showQuickPick', - )?.[0]?.items; - - expect(Object.keys(loggedItems[0])).toStrictEqual(['label']); - expect(Object.keys(loggedItems[1]).sort()).toStrictEqual(['displayName', 'label']); - expect(Object.keys(loggedItems[2]).sort()).toStrictEqual(['boundState', 'itemKind', 'label']); + expect(mockLogger.debug).toHaveBeenCalledWith( + { + fn: 'VscodeAdapter.showQuickPick', + itemCount: 3, + options: undefined, + items: [ + { label: 'Plain item' }, + { label: 'With displayName only', displayName: 'raw name' }, + { label: 'With itemKind only', itemKind: 'info' }, + ], + }, + 'Showing quick pick', + ); }); it('should log only description when detail, kind, and itemKind are absent', async () => { @@ -614,6 +664,53 @@ describe('VscodeAdapter', () => { ); }); + describe('projectTestStatusFields', () => { + it('extracts isActive and boundState from terminalInfo', () => { + const record = { + terminalInfo: { name: 'bash', isActive: true, boundState: 'bound' }, + }; + + expect(projectTestStatusFields(record)).toStrictEqual({ + isActive: true, + boundState: 'bound', + }); + }); + + it('extracts only boundState from fileInfo (no isActive)', () => { + const record = { + fileInfo: { filename: 'app.ts', boundState: 'not-bound' }, + }; + + expect(projectTestStatusFields(record)).toStrictEqual({ + boundState: 'not-bound', + }); + }); + + it('omits absent fields rather than emitting undefined', () => { + const record = { + terminalInfo: { name: 'bash', isActive: true }, + }; + + expect(projectTestStatusFields(record)).toStrictEqual({ isActive: true }); + }); + + it('returns empty object when neither terminalInfo nor fileInfo is present', () => { + expect(projectTestStatusFields({ label: 'foo' })).toStrictEqual({}); + }); + + it('prefers terminalInfo when both terminalInfo and fileInfo are present', () => { + const record = { + terminalInfo: { name: 'bash', isActive: true, boundState: 'bound' }, + fileInfo: { filename: 'app.ts', boundState: 'not-bound' }, + }; + + expect(projectTestStatusFields(record)).toStrictEqual({ + isActive: true, + boundState: 'bound', + }); + }); + }); + it('should log only detail when description, kind, and itemKind are absent', async () => { const items = [{ label: 'Go to Link', detail: 'Navigate to a code reference' }]; (mockVSCode.window.showQuickPick as jest.Mock).mockResolvedValue(undefined); diff --git a/packages/rangelink-vscode-extension/src/__tests__/statusBar/RangeLinkStatusBar.test.ts b/packages/rangelink-vscode-extension/src/__tests__/statusBar/RangeLinkStatusBar.test.ts index 5928bbcc..e8e33cd5 100644 --- a/packages/rangelink-vscode-extension/src/__tests__/statusBar/RangeLinkStatusBar.test.ts +++ b/packages/rangelink-vscode-extension/src/__tests__/statusBar/RangeLinkStatusBar.test.ts @@ -197,7 +197,11 @@ describe('RangeLinkStatusBar', () => { displayName: 'Terminal', itemKind: 'bindable', bindOptions: { kind: 'terminal', terminal: mockTerminal }, - terminalInfo: { terminal: mockTerminal, name: mockTerminal.name, isActive: false }, + terminalInfo: { + bindOptions: { kind: 'terminal', terminal: mockTerminal }, + name: mockTerminal.name, + isActive: false, + }, }, ], 'claude-code': [ @@ -238,7 +242,11 @@ describe('RangeLinkStatusBar', () => { itemKind: 'bindable', bindOptions: { kind: 'terminal', terminal: mockTerminal }, description: undefined, - terminalInfo: { terminal: mockTerminal, name: mockTerminal.name, isActive: false }, + terminalInfo: { + bindOptions: { kind: 'terminal', terminal: mockTerminal }, + name: mockTerminal.name, + isActive: false, + }, }, { label: '', kind: vscode.QuickPickItemKind.Separator }, { @@ -450,7 +458,11 @@ describe('RangeLinkStatusBar', () => { displayName: 'Terminal', itemKind: 'bindable', bindOptions: { kind: 'terminal', terminal: mockTerminal }, - terminalInfo: { terminal: mockTerminal, name: mockTerminal.name, isActive: false }, + terminalInfo: { + bindOptions: { kind: 'terminal', terminal: mockTerminal }, + name: mockTerminal.name, + isActive: false, + }, }, ], }); @@ -475,7 +487,11 @@ describe('RangeLinkStatusBar', () => { itemKind: 'bindable', bindOptions: { kind: 'terminal', terminal: mockTerminal }, description: undefined, - terminalInfo: { terminal: mockTerminal, name: mockTerminal.name, isActive: false }, + terminalInfo: { + bindOptions: { kind: 'terminal', terminal: mockTerminal }, + name: mockTerminal.name, + isActive: false, + }, }, { label: '', kind: vscode.QuickPickItemKind.Separator }, { @@ -864,11 +880,7 @@ describe('RangeLinkStatusBar', () => { expect(capturedPlaceholder).toBe('Select file to bind to'); expect(showFilePickerSpy).toHaveBeenCalled(); - expect(mockDestinationManager.bind).toHaveBeenCalledWith({ - kind: 'text-editor', - uri: eligibleFile.uri, - viewColumn: eligibleFile.viewColumn, - }); + expect(mockDestinationManager.bind).toHaveBeenCalledWith(eligibleFile.bindOptions); expect(mockDestinationManager.bindAndFocus).not.toHaveBeenCalled(); expect(mockLogger.debug).toHaveBeenCalledWith( { @@ -914,11 +926,7 @@ describe('RangeLinkStatusBar', () => { await statusBar.openMenu(); - expect(mockDestinationManager.bind).toHaveBeenCalledWith({ - kind: 'text-editor', - uri: eligibleFile.uri, - viewColumn: eligibleFile.viewColumn, - }); + expect(mockDestinationManager.bind).toHaveBeenCalledWith(eligibleFile.bindOptions); expect(mockLogger.error).toHaveBeenCalledWith( { fn: 'RangeLinkStatusBar.openMenu', diff --git a/packages/rangelink-vscode-extension/src/commands/BindToTerminalCommand.ts b/packages/rangelink-vscode-extension/src/commands/BindToTerminalCommand.ts index e440d785..5e930f69 100644 --- a/packages/rangelink-vscode-extension/src/commands/BindToTerminalCommand.ts +++ b/packages/rangelink-vscode-extension/src/commands/BindToTerminalCommand.ts @@ -92,12 +92,12 @@ export class BindToTerminalCommand { } if (terminalItems.length === 1) { - const { terminal } = terminalItems[0].terminalInfo; + const { terminalInfo } = terminalItems[0]; this.logger.debug( - { ...logCtx, terminalName: terminal.name }, + { ...logCtx, terminalName: terminalInfo.name }, 'Single terminal, auto-binding', ); - return this.mapBindResult(await this.destinationManager.bind({ kind: 'terminal', terminal })); + return this.mapBindResult(await this.destinationManager.bind(terminalInfo.bindOptions)); } const bindResult = await showTerminalPicker( @@ -110,7 +110,7 @@ export class BindToTerminalCommand { { ...logCtx, terminalName: eligible.name }, `Binding to terminal "${eligible.name}"`, ); - return this.destinationManager.bind({ kind: 'terminal', terminal: eligible.terminal }); + return this.destinationManager.bind(eligible.bindOptions); }, }, this.logger, diff --git a/packages/rangelink-vscode-extension/src/commands/BindToTextEditorCommand.ts b/packages/rangelink-vscode-extension/src/commands/BindToTextEditorCommand.ts index a8211a35..bae0b110 100644 --- a/packages/rangelink-vscode-extension/src/commands/BindToTextEditorCommand.ts +++ b/packages/rangelink-vscode-extension/src/commands/BindToTextEditorCommand.ts @@ -124,13 +124,7 @@ export class BindToTextEditorCommand { if (fileItems.length === 1) { const { fileInfo } = fileItems[0]; this.logger.debug({ ...logCtx, filename: fileInfo.filename }, 'Single file, auto-binding'); - return this.mapBindResult( - await this.destinationManager.bind({ - kind: 'text-editor', - uri: fileInfo.uri, - viewColumn: fileInfo.viewColumn, - }), - ); + return this.mapBindResult(await this.destinationManager.bind(fileInfo.bindOptions)); } const result = await showFilePicker( @@ -139,13 +133,7 @@ export class BindToTextEditorCommand { { getPlaceholder: () => formatMessage(MessageCode.FILE_PICKER_BIND_ONLY_PLACEHOLDER), onSelected: async (file) => - this.mapBindResult( - await this.destinationManager.bind({ - kind: 'text-editor', - uri: file.uri, - viewColumn: file.viewColumn, - }), - ), + this.mapBindResult(await this.destinationManager.bind(file.bindOptions)), }, this.logger, ); diff --git a/packages/rangelink-vscode-extension/src/constants/envVarNames.ts b/packages/rangelink-vscode-extension/src/constants/envVarNames.ts new file mode 100644 index 00000000..24b2824b --- /dev/null +++ b/packages/rangelink-vscode-extension/src/constants/envVarNames.ts @@ -0,0 +1,24 @@ +/** + * Centralized environment-variable names used to toggle test-only code paths + * in production modules. Each constant value is the literal env-var name — + * importers read or write `process.env[ENV_RANGELINK_*]` so the name lives in + * exactly one place. + * + * The `.vscode-test.base.mjs` runner config sets these before launching VS + * Code; integration tests then observe them. Production code reads them at + * module load (see `LogCapture` and `VscodeAdapter.showQuickPick`'s items + * projection) or per-call (see `testFixtureRegistry`). + */ + +/** + * When `'true'`, enables in-memory log capture in `LogCapture` and the + * test-only enrichment of `VscodeAdapter.showQuickPick` log entries with flat + * `isActive` / `boundState` fields sourced from `terminalInfo` / `fileInfo`. + */ +export const ENV_RANGELINK_CAPTURE_LOGS = 'RANGELINK_CAPTURE_LOGS'; + +/** + * When `'true'`, lets integration tests register a marker on a terminal so + * `classifyTerminalForBinding` treats it as a normal bindable terminal. + */ +export const ENV_RANGELINK_TEST_FIXTURES_ENABLED = 'RANGELINK_TEST_FIXTURES_ENABLED'; diff --git a/packages/rangelink-vscode-extension/src/constants/index.ts b/packages/rangelink-vscode-extension/src/constants/index.ts index f6fc639a..e6e1c7c3 100644 --- a/packages/rangelink-vscode-extension/src/constants/index.ts +++ b/packages/rangelink-vscode-extension/src/constants/index.ts @@ -1,6 +1,7 @@ export * from './aiAssistantPasteConstants'; export * from './commandIds'; export * from './contextKeys'; +export * from './envVarNames'; export * from './settingDefaults'; export * from './settingKeys'; export * from './vscodeCommandIds'; diff --git a/packages/rangelink-vscode-extension/src/destinations/DestinationAvailabilityService.ts b/packages/rangelink-vscode-extension/src/destinations/DestinationAvailabilityService.ts index 6c749d7d..d02decd0 100644 --- a/packages/rangelink-vscode-extension/src/destinations/DestinationAvailabilityService.ts +++ b/packages/rangelink-vscode-extension/src/destinations/DestinationAvailabilityService.ts @@ -328,14 +328,9 @@ export class DestinationAvailabilityService { label: eligibleFile.filename, displayName: eligibleFile.filename, description: buildFileDescription(eligibleFile, disambiguator), - bindOptions: { - kind: 'text-editor', - uri: eligibleFile.uri, - viewColumn: eligibleFile.viewColumn, - }, + bindOptions: eligibleFile.bindOptions, itemKind: 'bindable', fileInfo: eligibleFile, - boundState: eligibleFile.boundState, }; } @@ -373,16 +368,14 @@ export class DestinationAvailabilityService { private buildTerminalItem(eligibleTerminal: EligibleTerminal): TerminalBindableQuickPickItem { const displayName = formatMessage(MessageCode.DESTINATION_TERMINAL_DISPLAY_FORMAT, { - name: eligibleTerminal.terminal.name, + name: eligibleTerminal.name, }); return { label: displayName, displayName, - bindOptions: { kind: 'terminal', terminal: eligibleTerminal.terminal }, + bindOptions: eligibleTerminal.bindOptions, itemKind: 'bindable', - isActive: eligibleTerminal.isActive, - boundState: eligibleTerminal.boundState, terminalInfo: eligibleTerminal, }; } diff --git a/packages/rangelink-vscode-extension/src/destinations/DestinationPicker.ts b/packages/rangelink-vscode-extension/src/destinations/DestinationPicker.ts index c1904972..18cebfb3 100644 --- a/packages/rangelink-vscode-extension/src/destinations/DestinationPicker.ts +++ b/packages/rangelink-vscode-extension/src/destinations/DestinationPicker.ts @@ -170,12 +170,12 @@ export class DestinationPicker { { getPlaceholder: () => formatMessage(placeholderMessageCode), onSelected: (file) => ({ - outcome: 'selected' as const, - bindOptions: { kind: 'text-editor' as const, uri: file.uri, viewColumn: file.viewColumn }, + outcome: 'selected', + bindOptions: file.bindOptions, }), onDismissed: () => { this.logger.debug(logCtx, 'User returned from secondary file picker'); - return { outcome: 'returned-to-main-picker' as const }; + return { outcome: 'returned-to-main-picker' }; }, }, this.logger, @@ -200,12 +200,12 @@ export class DestinationPicker { { getPlaceholder: () => formatMessage(placeholderMessageCode), onSelected: (eligible) => ({ - outcome: 'selected' as const, - bindOptions: { kind: 'terminal' as const, terminal: eligible.terminal }, + outcome: 'selected', + bindOptions: eligible.bindOptions, }), onDismissed: () => { this.logger.debug(logCtx, 'User returned from secondary terminal picker'); - return { outcome: 'returned-to-main-picker' as const }; + return { outcome: 'returned-to-main-picker' }; }, }, this.logger, diff --git a/packages/rangelink-vscode-extension/src/destinations/utils/buildTerminalPickerItems.ts b/packages/rangelink-vscode-extension/src/destinations/utils/buildTerminalPickerItems.ts index 33b7e3eb..80915bdc 100644 --- a/packages/rangelink-vscode-extension/src/destinations/utils/buildTerminalPickerItems.ts +++ b/packages/rangelink-vscode-extension/src/destinations/utils/buildTerminalPickerItems.ts @@ -18,8 +18,6 @@ export const buildTerminalPickerItems = ( description: buildTerminalDescription(item.terminalInfo), displayName: item.terminalInfo.name, bindOptions: item.bindOptions, - itemKind: 'bindable' as const, - isActive: item.isActive, - boundState: item.boundState, + itemKind: 'bindable', terminalInfo: item.terminalInfo, })); diff --git a/packages/rangelink-vscode-extension/src/destinations/utils/getEligibleFiles.ts b/packages/rangelink-vscode-extension/src/destinations/utils/getEligibleFiles.ts index c42b3075..22e49625 100644 --- a/packages/rangelink-vscode-extension/src/destinations/utils/getEligibleFiles.ts +++ b/packages/rangelink-vscode-extension/src/destinations/utils/getEligibleFiles.ts @@ -33,7 +33,7 @@ export const getEligibleFiles = (ideAdapter: VscodeAdapter): EligibleFile[] => { } eligibleFiles.push({ - uri, + bindOptions: { kind: 'text-editor', uri, viewColumn: group.viewColumn }, filename: ideAdapter.getFilenameFromUri(uri), displayPath: ideAdapter.asRelativePath(uri, RelativePathFormat.PathOnly), viewColumn: group.viewColumn, diff --git a/packages/rangelink-vscode-extension/src/destinations/utils/getEligibleTerminals.ts b/packages/rangelink-vscode-extension/src/destinations/utils/getEligibleTerminals.ts index 0e79e7bb..a57f436e 100644 --- a/packages/rangelink-vscode-extension/src/destinations/utils/getEligibleTerminals.ts +++ b/packages/rangelink-vscode-extension/src/destinations/utils/getEligibleTerminals.ts @@ -39,7 +39,7 @@ export const getEligibleTerminals = async ( () => undefined, ); return { - terminal, + bindOptions: { kind: 'terminal', terminal }, name: terminal.name, isActive: terminal === activeTerminal, processId, diff --git a/packages/rangelink-vscode-extension/src/destinations/utils/markBoundFile.ts b/packages/rangelink-vscode-extension/src/destinations/utils/markBoundFile.ts index df50b7ba..cb7efa2f 100644 --- a/packages/rangelink-vscode-extension/src/destinations/utils/markBoundFile.ts +++ b/packages/rangelink-vscode-extension/src/destinations/utils/markBoundFile.ts @@ -26,8 +26,8 @@ export const markBoundFile = ( ...f, boundState: boundFileUriString !== undefined && - f.uri.toString() === boundFileUriString && + f.bindOptions.uri.toString() === boundFileUriString && (boundFileViewColumn === undefined || f.viewColumn === boundFileViewColumn) - ? ('bound' as const) - : ('not-bound' as const), + ? 'bound' + : 'not-bound', })); diff --git a/packages/rangelink-vscode-extension/src/destinations/utils/markBoundTerminal.ts b/packages/rangelink-vscode-extension/src/destinations/utils/markBoundTerminal.ts index b34874b2..e86f6691 100644 --- a/packages/rangelink-vscode-extension/src/destinations/utils/markBoundTerminal.ts +++ b/packages/rangelink-vscode-extension/src/destinations/utils/markBoundTerminal.ts @@ -21,6 +21,6 @@ export const markBoundTerminal = ( boundTerminalProcessId !== undefined && t.processId !== undefined && t.processId === boundTerminalProcessId - ? ('bound' as const) - : ('not-bound' as const), + ? 'bound' + : 'not-bound', })); diff --git a/packages/rangelink-vscode-extension/src/destinations/utils/testFixtureRegistry.ts b/packages/rangelink-vscode-extension/src/destinations/utils/testFixtureRegistry.ts index d31fb4cd..352b2289 100644 --- a/packages/rangelink-vscode-extension/src/destinations/utils/testFixtureRegistry.ts +++ b/packages/rangelink-vscode-extension/src/destinations/utils/testFixtureRegistry.ts @@ -1,5 +1,6 @@ import type * as vscode from 'vscode'; +import { ENV_RANGELINK_TEST_FIXTURES_ENABLED } from '../../constants'; import { RangeLinkExtensionError, RangeLinkExtensionErrorCodes } from '../../errors'; const MARKER = '__rangeLinkTestFixture'; @@ -11,14 +12,14 @@ const MARKER = '__rangeLinkTestFixture'; * Sets a marker property directly on the terminal object so the bundled * extension can read it even though it lives in a separate esbuild module graph. * - * Only callable when `process.env.RANGELINK_TEST_FIXTURES_ENABLED === 'true'`. + * Only callable when `process.env[ENV_RANGELINK_TEST_FIXTURES_ENABLED] === 'true'`. * Follows the same env-var gating pattern as `LogCapture.ts`. */ export const markRangeLinkTestFixture = (terminal: vscode.Terminal): void => { - if (process.env.RANGELINK_TEST_FIXTURES_ENABLED !== 'true') { + if (process.env[ENV_RANGELINK_TEST_FIXTURES_ENABLED] !== 'true') { throw new RangeLinkExtensionError({ code: RangeLinkExtensionErrorCodes.TEST_FIXTURE_REGISTRY_DISABLED, - message: 'markRangeLinkTestFixture requires RANGELINK_TEST_FIXTURES_ENABLED=true', + message: `markRangeLinkTestFixture requires ${ENV_RANGELINK_TEST_FIXTURES_ENABLED}=true`, functionName: 'markRangeLinkTestFixture', }); } @@ -30,11 +31,11 @@ export const markRangeLinkTestFixture = (terminal: vscode.Terminal): void => { * Returns `true` when a terminal was marked as a RangeLink test fixture. * * Returns `false` immediately (without touching the terminal object) when - * `RANGELINK_TEST_FIXTURES_ENABLED` is not `'true'`, so production code pays - * only a single env-var read. + * the env var is not `'true'`, so production code pays only a single env-var + * read. */ export const isRangeLinkTestFixture = (terminal: vscode.Terminal): boolean => { - if (process.env.RANGELINK_TEST_FIXTURES_ENABLED !== 'true') return false; + if (process.env[ENV_RANGELINK_TEST_FIXTURES_ENABLED] !== 'true') return false; // eslint-disable-next-line @typescript-eslint/no-explicit-any return (terminal as any)[MARKER] === true; }; diff --git a/packages/rangelink-vscode-extension/src/ide/vscode/VscodeAdapter.ts b/packages/rangelink-vscode-extension/src/ide/vscode/VscodeAdapter.ts index f9a2e216..ad0f76af 100644 --- a/packages/rangelink-vscode-extension/src/ide/vscode/VscodeAdapter.ts +++ b/packages/rangelink-vscode-extension/src/ide/vscode/VscodeAdapter.ts @@ -5,6 +5,7 @@ import { displayName } from '../../../package.json'; import { AI_ASSISTANT_PASTE_COMMANDS, CLIPBOARD_POST_PASTE_DELAY_MS, + ENV_RANGELINK_CAPTURE_LOGS, FOCUS_TO_PASTE_DELAY_MS, VSCODE_CMD_TERMINAL_PASTE, } from '../../constants'; @@ -34,6 +35,42 @@ const STATUS_BAR_SUCCESS_PREFIX = `✓ ${STATUS_BAR_PREFIX}`; const getUnknownFilename = (): string => formatMessage(MessageCode.UNKNOWN_FILENAME_FALLBACK); +/** + * Captured once at module load so the prod path of `showQuickPick`'s items + * projection pays a single `process.env` read. Matches `LogCapture`'s + * import-time-read pattern; toggling the env var mid-run is not supported + * (the integration-test runner sets it before spawning VS Code). + */ +const isLogCaptureEnabled = process.env[ENV_RANGELINK_CAPTURE_LOGS] === 'true'; + +const isObject = (value: unknown): value is Record => + typeof value === 'object' && value !== null; + +/** + * Test-only projection: pull the status fields integration tests destructure + * out of a picker item, sourced from `terminalInfo` / `fileInfo`. Returns + * a partial object so it can be spread into the base log entry. + * + * Production callers should never need this — gated by + * `ENV_RANGELINK_CAPTURE_LOGS` at the call site in `showQuickPick`. + * + * Exported for direct unit testing; not part of the adapter's public API. + */ +export const projectTestStatusFields = ( + record: Record, +): { isActive?: boolean; boundState?: string } => { + const fields: { isActive?: boolean; boundState?: string } = {}; + const terminalInfo = isObject(record.terminalInfo) ? record.terminalInfo : undefined; + const fileInfo = isObject(record.fileInfo) ? record.fileInfo : undefined; + if (terminalInfo !== undefined) { + if (typeof terminalInfo.isActive === 'boolean') fields.isActive = terminalInfo.isActive; + if (typeof terminalInfo.boundState === 'string') fields.boundState = terminalInfo.boundState; + } else if (fileInfo !== undefined) { + if (typeof fileInfo.boundState === 'string') fields.boundState = fileInfo.boundState; + } + return fields; +}; + /** * VSCode adapter for IDE-specific operations. * @@ -235,22 +272,16 @@ export class VscodeAdapter options, items: items.map((item) => { const record = item as Record; - return { + const base = { label: item.label, ...(item.description !== undefined ? { description: item.description } : {}), ...(item.detail !== undefined ? { detail: item.detail } : {}), ...(item.kind !== undefined ? { kind: item.kind } : {}), ...('itemKind' in item ? { itemKind: record.itemKind } : {}), ...('displayName' in item ? { displayName: record.displayName } : {}), - // TODO(#594): once TerminalBindableQuickPickItem and - // FileBindableQuickPickItem stop duplicating these fields at the - // top level, dig into record.terminalInfo / record.fileInfo here. - // Keep emitting them flat in the log so integration tests that - // destructure { isActive, boundState } need no changes. - ...('isActive' in item ? { isActive: record.isActive } : {}), - ...('boundState' in item ? { boundState: record.boundState } : {}), ...('remainingCount' in item ? { remainingCount: record.remainingCount } : {}), }; + return isLogCaptureEnabled ? { ...base, ...projectTestStatusFields(record) } : base; }), }, 'Showing quick pick', diff --git a/packages/rangelink-vscode-extension/src/statusBar/RangeLinkStatusBar.ts b/packages/rangelink-vscode-extension/src/statusBar/RangeLinkStatusBar.ts index 04c8a4b9..a8baeab1 100644 --- a/packages/rangelink-vscode-extension/src/statusBar/RangeLinkStatusBar.ts +++ b/packages/rangelink-vscode-extension/src/statusBar/RangeLinkStatusBar.ts @@ -203,10 +203,7 @@ export class RangeLinkStatusBar implements vscode.Disposable { { getPlaceholder: () => formatMessage(MessageCode.TERMINAL_PICKER_BIND_ONLY_PLACEHOLDER), onSelected: async (eligible) => { - const bindResult = await this.destinationManager.bind({ - kind: 'terminal', - terminal: eligible.terminal, - }); + const bindResult = await this.destinationManager.bind(eligible.bindOptions); if (!bindResult.success) { this.logger.error( { ...logCtx, error: bindResult.error }, @@ -252,11 +249,7 @@ export class RangeLinkStatusBar implements vscode.Disposable { { getPlaceholder: () => formatMessage(MessageCode.FILE_PICKER_BIND_ONLY_PLACEHOLDER), onSelected: async (file) => { - const bindResult = await this.destinationManager.bind({ - kind: 'text-editor', - uri: file.uri, - viewColumn: file.viewColumn, - }); + const bindResult = await this.destinationManager.bind(file.bindOptions); if (!bindResult.success) { this.logger.error( { ...logCtx, error: bindResult.error }, @@ -287,13 +280,13 @@ export class RangeLinkStatusBar implements vscode.Disposable { { label: '', kind: vscode.QuickPickItemKind.Separator }, { label: formatMessage(MessageCode.STATUS_BAR_MENU_ITEM_NAVIGATE_TO_LINK_LABEL), - itemKind: 'command' as const, + itemKind: 'command', command: CMD_GO_TO_RANGELINK, }, ...(this.bookmarkService.isVisible() ? this.buildBookmarksQuickPickItems() : []), // TODO: #366 remove isVisible() gate { label: formatMessage(MessageCode.STATUS_BAR_MENU_ITEM_VERSION_INFO_LABEL), - itemKind: 'command' as const, + itemKind: 'command', command: CMD_SHOW_VERSION, }, ]; @@ -314,12 +307,12 @@ export class RangeLinkStatusBar implements vscode.Disposable { { label: formatMessage(MessageCode.STATUS_BAR_MENU_ITEM_JUMP_ENABLED_LABEL), description: jumpDescription, - itemKind: 'command' as const, + itemKind: 'command', command: CMD_JUMP_TO_DESTINATION, }, { label: formatMessage(MessageCode.STATUS_BAR_MENU_ITEM_UNBIND_LABEL), - itemKind: 'command' as const, + itemKind: 'command', command: CMD_UNBIND_DESTINATION, }, ]; @@ -346,7 +339,7 @@ export class RangeLinkStatusBar implements vscode.Disposable { return [ { label: formatMessage(MessageCode.STATUS_BAR_MENU_DESTINATIONS_NONE_AVAILABLE), - itemKind: 'info' as const, + itemKind: 'info', }, ]; } @@ -354,7 +347,7 @@ export class RangeLinkStatusBar implements vscode.Disposable { return [ { label: formatMessage(MessageCode.STATUS_BAR_MENU_DESTINATIONS_CHOOSE_BELOW), - itemKind: 'info' as const, + itemKind: 'info', }, ...destinationItems, ]; @@ -373,12 +366,12 @@ export class RangeLinkStatusBar implements vscode.Disposable { ? [ { label: `${MENU_ITEM_INDENT}${formatMessage(MessageCode.BOOKMARK_LIST_EMPTY)}`, - itemKind: 'info' as const, + itemKind: 'info', }, ] : bookmarks.map((bookmark) => ({ label: `${MENU_ITEM_INDENT}$(bookmark) ${bookmark.label}`, - itemKind: 'bookmark' as const, + itemKind: 'bookmark', bookmarkId: bookmark.id, })); @@ -386,18 +379,18 @@ export class RangeLinkStatusBar implements vscode.Disposable { { label: '', kind: vscode.QuickPickItemKind.Separator }, { label: formatMessage(MessageCode.STATUS_BAR_MENU_BOOKMARKS_SECTION_LABEL), - itemKind: 'info' as const, + itemKind: 'info', }, ...bookmarkItems, { label: '', kind: vscode.QuickPickItemKind.Separator }, { label: `${MENU_ITEM_INDENT}${formatMessage(MessageCode.BOOKMARK_ACTION_ADD)}`, - itemKind: 'command' as const, + itemKind: 'command', command: CMD_BOOKMARK_ADD, }, { label: `${MENU_ITEM_INDENT}${formatMessage(MessageCode.BOOKMARK_ACTION_MANAGE)}`, - itemKind: 'command' as const, + itemKind: 'command', command: CMD_BOOKMARK_MANAGE, }, { label: '', kind: vscode.QuickPickItemKind.Separator }, diff --git a/packages/rangelink-vscode-extension/src/types/EligibleFile.ts b/packages/rangelink-vscode-extension/src/types/EligibleFile.ts index 582f5f88..ef673411 100644 --- a/packages/rangelink-vscode-extension/src/types/EligibleFile.ts +++ b/packages/rangelink-vscode-extension/src/types/EligibleFile.ts @@ -1,5 +1,4 @@ -import type * as vscode from 'vscode'; - +import type { TextEditorBindOptions } from './BindOptions'; import type { BoundState } from './BoundState'; /** @@ -9,7 +8,7 @@ import type { BoundState } from './BoundState'; * `ideAdapter.findVisibleEditorsByUri()`. */ export interface EligibleFile { - readonly uri: vscode.Uri; + readonly bindOptions: TextEditorBindOptions; readonly filename: string; readonly displayPath: string; readonly viewColumn: number; diff --git a/packages/rangelink-vscode-extension/src/types/EligibleTerminal.ts b/packages/rangelink-vscode-extension/src/types/EligibleTerminal.ts index 46f79749..38b1203b 100644 --- a/packages/rangelink-vscode-extension/src/types/EligibleTerminal.ts +++ b/packages/rangelink-vscode-extension/src/types/EligibleTerminal.ts @@ -1,5 +1,4 @@ -import type * as vscode from 'vscode'; - +import type { TerminalBindOptions } from './BindOptions'; import type { BoundState } from './BoundState'; /** @@ -9,7 +8,7 @@ import type { BoundState } from './BoundState'; * out upstream in `getEligibleTerminals` and never reach this shape. */ export interface EligibleTerminal { - readonly terminal: vscode.Terminal; + readonly bindOptions: TerminalBindOptions; readonly name: string; readonly isActive: boolean; readonly processId?: number; diff --git a/packages/rangelink-vscode-extension/src/types/QuickPickTypes.ts b/packages/rangelink-vscode-extension/src/types/QuickPickTypes.ts index 26197e34..8b0d00d4 100644 --- a/packages/rangelink-vscode-extension/src/types/QuickPickTypes.ts +++ b/packages/rangelink-vscode-extension/src/types/QuickPickTypes.ts @@ -1,7 +1,6 @@ import type * as vscode from 'vscode'; import type { BindOptions, TerminalBindOptions, TextEditorBindOptions } from './BindOptions'; -import type { BoundState } from './BoundState'; import type { EligibleFile } from './EligibleFile'; import type { EligibleTerminal } from './EligibleTerminal'; import type { WithDisplayName } from './WithDisplayName'; @@ -61,7 +60,6 @@ export interface WithBindOptions { */ export interface FileBindableQuickPickItem extends BindableQuickPickItem { readonly fileInfo: EligibleFile; - readonly boundState?: BoundState; } /** @@ -106,7 +104,6 @@ export interface BindableQuickPickItem WithBindOptions, WithDisplayName { readonly itemKind: Extract; - readonly isActive?: boolean; } /** @@ -114,14 +111,8 @@ export interface BindableQuickPickItem * Extends BindableQuickPickItem with terminal metadata, * so callers get both UI item and domain object from a single source. */ -// TODO(#594): top-level isActive (inherited) and boundState duplicate fields -// already present on terminalInfo. Same pattern applies to -// FileBindableQuickPickItem.boundState below. The duplication is preserved for -// now to keep VscodeAdapter.showQuickPick's log projection simple; #594 removes -// it by teaching the projection to dig into terminalInfo / fileInfo. export interface TerminalBindableQuickPickItem extends BindableQuickPickItem { readonly terminalInfo: EligibleTerminal; - readonly boundState?: BoundState; } // ============================================================================