From de9f791797bc47b29d9b0ae60edc532efd67ce80 Mon Sep 17 00:00:00 2001 From: Charles Ouimet Date: Thu, 21 May 2026 11:08:47 -0400 Subject: [PATCH 1/2] [issues/594] Refactor QuickPick item shape: single source of truth via nested info objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Consolidates the `EligibleTerminal` / `EligibleFile` domain types so the live `vscode.Terminal` / `vscode.Uri` references live exclusively on `bindOptions`, and drops the top-level `isActive` / `boundState` duplication from `TerminalBindableQuickPickItem` / `FileBindableQuickPickItem`. The log enrichment that used to require those top-level fields moves behind a `RANGELINK_CAPTURE_LOGS=true` env gate, matching the existing `LogCapture` / `markRangeLinkTestFixture` pattern. Pure refactor: no behavior change, integration-test log contract preserved. ## Changes - `EligibleTerminal.terminal: vscode.Terminal` → `EligibleTerminal.bindOptions: TerminalBindOptions`. Same shape for `EligibleFile`: `uri: vscode.Uri` → `bindOptions: TextEditorBindOptions`. The live VS Code reference is now encapsulated as a bind intent from the moment a candidate is identified. - `BindableQuickPickItem` base no longer carries `readonly isActive?`. `TerminalBindableQuickPickItem` no longer carries `readonly boundState?`. `FileBindableQuickPickItem` no longer carries `readonly boundState?`. All three fields had zero production consumers — they existed solely to feed `VscodeAdapter.showQuickPick`'s log projection. - `VscodeAdapter.showQuickPick` items projection split into prod path (base fields only: `label`, `description`, `detail`, `kind`, `itemKind`, `displayName`, `remainingCount`) and a `process.env.RANGELINK_CAPTURE_LOGS === 'true'` gate that additionally projects `isActive` / `boundState` flat by digging into `terminalInfo` / `fileInfo`. Integration tests that destructure `{ isActive, boundState }` from the log line need no changes. - New `projectTestStatusFields` helper in `VscodeAdapter.ts`, exported for direct unit testing. Five new unit tests cover its terminalInfo / fileInfo / absent / both-present cases. Two additional `showQuickPick` log-projection tests cover both env-gated branches end-to-end: one for the unset/prod path (base fields only, even when `terminalInfo` is present on the item), and one for `RANGELINK_CAPTURE_LOGS='true'` using `jest.resetModules()` + `await import()` + `try/finally` env restoration to reload the adapter with the new env value. The env var name stays as a literal in the test per T003 — it's an external contract. - Call sites that used to construct `bindOptions` inline now read `eligible.bindOptions` — see `DestinationAvailabilityService.buildTerminalItem`, `buildFileItem`, `DestinationPicker.showSecondaryFilePicker`, `showSecondaryTerminalPicker`, `BindToTerminalCommand.execute`, `BindToTextEditorCommand.executeWithPicker`, `RangeLinkStatusBar.showSecondaryTerminalPicker`, and the file overflow picker. The picker item's top-level `bindOptions` is now the SAME object reference as `terminalInfo.bindOptions` — single source. - Both `TODO(#594)` comments removed: one in `QuickPickTypes.ts`, one in `VscodeAdapter.ts`. - New `src/constants/envVarNames.ts` centralizes the two production env-var names (`ENV_RANGELINK_CAPTURE_LOGS`, `ENV_RANGELINK_TEST_FIXTURES_ENABLED`). Production readers (`LogCapture.ts`, `VscodeAdapter.ts`, `testFixtureRegistry.ts`) import from the constant; error messages template-interpolate the name so a future rename stays in one place. The `classifyTerminalForBinding` test keeps the literal env-var name per T003 — the test acts as a contract tripwire: a rename in the constants file doesn't silently propagate to the test, so the prod-vs-test mismatch surfaces loudly. `.vscode-test.base.mjs` also keeps its literal (it runs at vscode-test config-load time, outside the TS module graph; touching it would entangle build order). - `test:release:automated` in `packages/rangelink-vscode-extension/package.json` now bakes in `--exclude-label requires-extensions --exclude-label cursor`, matching CI. The redundant flags removed from `.github/actions/run-integration-tests/action.yml`. Local devs no longer wheel-spin on tests CI was already skipping. - Removed 20 unnecessary `as const` assertions across files this PR touched: `getEligibleTerminals.ts`, `getEligibleFiles.ts`, `buildTerminalPickerItems.ts`, `markBoundFile.ts`, `markBoundTerminal.ts`, `DestinationPicker.ts`, `RangeLinkStatusBar.ts`. The annotated return types and surrounding contextual types already narrow the literals — the assertions were noise. Out-of-scope `as const` sites (`ListBookmarksCommand.ts`, `PasteDestinationManager.ts`, `FocusCapability.ts`) left for a follow-up. ## Key Discoveries - `EligibleFile.viewColumn` is intentionally kept even though it duplicates `bindOptions.viewColumn`. It's plain numeric metadata read by `sortEligibleFiles`, `buildFilePickerItems` (grouping), and `buildDestinationQuickPickItems` (group label) — forcing those three call sites through `bindOptions.viewColumn` for one number is uglier than the redundancy. The smell that motivates removing `terminal` / `uri` (live VS Code refs in domain types) does not apply to a plain integer. - The issue body listed `nonBindableReason` as a top-level field on `TerminalBindableQuickPickItem` to drop. It was never there — that field only lives in `classifyTerminalForBinding`'s discriminated-union return type. The "drop top-level `nonBindableReason`" cleanup was a no-op. - The two clipboard-preservation tests for Cursor AI (`clipboard-preservation-013`, `clipboard-preservation-014`) are pre-existing failures on `main` — verified by stashing and re-running. The CI workflow was already excluding them via `--exclude-label cursor`. This PR pulls that exclusion into the script entry so local runs match CI without manual flag-passing. ## Test Plan - [x] All 1947 unit tests pass (`pnpm test:fast`) - [x] All 142 automated integration tests pass (`pnpm test:release:automated`, ~7 minutes) - [x] `pnpm compile` clean - [x] `pnpm fix` clean - [x] Sweep grep confirmed zero stragglers — no remaining `.terminal` reads on a typed `EligibleTerminal`, no remaining `.uri` reads on a typed `EligibleFile`, no remaining `item.isActive` / `item.boundState` reads on a typed `BindableQuickPickItem`. - [x] Integration-test log contract preserved: the prod log no longer emits flat `isActive` / `boundState`, but the env-gated test mode does, sourced from `terminalInfo` / `fileInfo`. Integration tests destructuring `{ isActive, boundState }` from `VscodeAdapter.showQuickPick` log lines pass unchanged. ## Related - Closes https://github.com/couimet/rangeLink/issues/594 - Born from https://github.com/couimet/rangeLink/issues/592 (merged via https://github.com/couimet/rangeLink/pull/595, which left two `TODO(#594)` markers for this cleanup) --- .../actions/run-integration-tests/action.yml | 2 +- .../rangelink-vscode-extension/package.json | 2 +- .../src/LogCapture.ts | 12 +- .../commands/BindToTextEditorCommand.test.ts | 12 +- .../DestinationAvailabilityService.test.ts | 84 +++------- .../destinations/DestinationPicker.test.ts | 4 +- .../buildDestinationQuickPickItems.test.ts | 72 +++++---- .../utils/buildTerminalPickerItems.test.ts | 15 +- .../utils/getEligibleFiles.test.ts | 24 +-- .../utils/getEligibleTerminals.test.ts | 65 ++++++-- .../destinations/utils/markBoundFile.test.ts | 33 ++-- .../utils/markBoundTerminal.test.ts | 2 +- .../utils/showTerminalPicker.test.ts | 4 +- .../createMockBindableQuickPickItem.ts | 29 ++-- .../helpers/createMockEligibleFile.ts | 7 +- .../helpers/createMockEligibleTerminal.ts | 7 +- .../ide/vscode/VscodeAdapter.test.ts | 145 ++++++++++++++---- .../statusBar/RangeLinkStatusBar.test.ts | 36 +++-- .../src/commands/BindToTerminalCommand.ts | 8 +- .../src/commands/BindToTextEditorCommand.ts | 16 +- .../src/constants/envVarNames.ts | 24 +++ .../src/constants/index.ts | 1 + .../DestinationAvailabilityService.ts | 13 +- .../src/destinations/DestinationPicker.ts | 12 +- .../utils/buildTerminalPickerItems.ts | 4 +- .../destinations/utils/getEligibleFiles.ts | 2 +- .../utils/getEligibleTerminals.ts | 2 +- .../src/destinations/utils/markBoundFile.ts | 6 +- .../destinations/utils/markBoundTerminal.ts | 4 +- .../destinations/utils/testFixtureRegistry.ts | 13 +- .../src/ide/vscode/VscodeAdapter.ts | 47 +++++- .../src/statusBar/RangeLinkStatusBar.ts | 33 ++-- .../src/types/EligibleFile.ts | 5 +- .../src/types/EligibleTerminal.ts | 5 +- .../src/types/QuickPickTypes.ts | 9 -- 35 files changed, 450 insertions(+), 309 deletions(-) create mode 100644 packages/rangelink-vscode-extension/src/constants/envVarNames.ts diff --git a/.github/actions/run-integration-tests/action.yml b/.github/actions/run-integration-tests/action.yml index 3098632d4..f5f1fa9fe 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 3bc789af3..aec785da5 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 9d2526dd0..bea7ffd75 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 31ba7b130..c2442798d 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 45cb34c5f..d31686334 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 7b46f73ba..ca5078b07 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/utils/buildDestinationQuickPickItems.test.ts b/packages/rangelink-vscode-extension/src/__tests__/destinations/utils/buildDestinationQuickPickItems.test.ts index 1412da869..4b1407773 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 b735502cc..5380dee51 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 a28dcc1f9..a2997509a 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 186eee401..b8c28e99a 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 bc9f651ae..08ff9c7a5 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 e41369788..e7e661b4d 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 588881c5e..298d91042 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 79db66895..90d224762 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 12bcf9fd1..1e21c6bee 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 bd784fd86..a0ac03a9c 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 66414a533..3335c71aa 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,14 +555,77 @@ 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); @@ -594,7 +638,7 @@ describe('VscodeAdapter', () => { 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(Object.keys(loggedItems[2]).sort()).toStrictEqual(['itemKind', 'label']); }); it('should log only description when detail, kind, and itemKind are absent', async () => { @@ -614,6 +658,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 5928bbcc6..e8e33cd58 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 e440d7857..5e930f692 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 a8211a353..bae0b1102 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 000000000..24b2824b7 --- /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 f6fc639a8..e6e1c7c30 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 6c749d7d6..d02decd0d 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 c1904972b..18cebfb31 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 33b7e3eba..80915bdcb 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 c42b30750..22e496255 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 0e79e7bb9..a57f436e7 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 df50b7bac..cb7efa2f5 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 b34874b26..e86f6691d 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 d31fb4cd1..352b22897 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 f9a2e216c..ad0f76af1 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 04c8a4b9e..a8baeab11 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 582f5f88e..ef6734113 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 46f797495..38b1203bd 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 26197e34a..8b0d00d4d 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; } // ============================================================================ From 02ca184e13087ead918e20526fa246d72bb5c561 Mon Sep 17 00:00:00 2001 From: Charles Ouimet Date: Thu, 21 May 2026 16:41:30 -0400 Subject: [PATCH 2/2] [PR feedback] Use toHaveBeenCalledWith instead of .mock.calls (T006) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three test files used .mock.calls extraction to pull out mock arguments for assertion, violating T006. CodeRabbit flagged the VscodeAdapter.test.ts instance; the sweep caught matching patterns in destinationBuilders.test.ts and PasteDestinationManager.test.ts. Ignored Feedback: - destinationBuilders.test.ts (4 sites): .mock.calls[0][1] extracts callback functions for invocation, not for assertion — necessary when testing factory-returned callbacks - wireActiveTerminalBindabilityContext.test.ts:114: extracts onDidChangeActiveTerminal listener to fire it for event simulation - PasteDestinationManager.test.ts:196-199: extracts terminal/document close listeners for event simulation Ref: https://github.com/couimet/rangeLink/pull/596#pullrequestreview-4339759068 --- .../PasteDestinationManager.test.ts | 34 +++++++++---------- .../destinations/destinationBuilders.test.ts | 31 +++++++++-------- .../ide/vscode/VscodeAdapter.test.ts | 20 +++++++---- 3 files changed, 47 insertions(+), 38 deletions(-) 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 a4195f0dd..1c746c2c5 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 236ea4595..0d0cb1e16 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__/ide/vscode/VscodeAdapter.test.ts b/packages/rangelink-vscode-extension/src/__tests__/ide/vscode/VscodeAdapter.test.ts index 3335c71aa..5a8c79d4d 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 @@ -632,13 +632,19 @@ describe('VscodeAdapter', () => { 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(['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 () => {