diff --git a/calm-plugins/vscode/src/extension/extension.clear-url.test.ts b/calm-plugins/vscode/src/extension/extension.clear-url.test.ts new file mode 100644 index 0000000000..8adca85be4 --- /dev/null +++ b/calm-plugins/vscode/src/extension/extension.clear-url.test.ts @@ -0,0 +1,113 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import * as vscode from 'vscode'; +import { activate, disconnectFromHub } from './extension'; + +/** + * Regression test for #3523: after "Clear URL" the saved `calm.hub.url` is + * gone, so the status bar must forget it too. Before the fix the tooltip kept + * saying "Disconnected from / Click to reconnect" while the click + * opened an empty URL box. + */ + +interface ConfigUpdate { + section: string; + key: string; + value: unknown; +} + +type CommandCallback = (...args: unknown[]) => unknown; + +const HUB_URL = 'https://calmhub.example.com'; + +const registeredCommands = new Map(); +const configUpdates: ConfigUpdate[] = []; +let statusBarItem: vscode.StatusBarItem; + +function context(): vscode.ExtensionContext { + return { + subscriptions: [], + extensionUri: vscode.Uri.file('/tmp/extension'), + secrets: { + // Reading the stored token fails, so connecting ends in the error + // state without any network access. + get: async () => { + throw new Error('secret storage unavailable'); + }, + store: async () => {}, + delete: async () => {}, + onDidChange: () => ({ dispose: () => {} }), + }, + } as unknown as vscode.ExtensionContext; +} + +beforeEach(() => { + registeredCommands.clear(); + configUpdates.length = 0; + + (vscode.commands as unknown as { registerCommand: unknown }).registerCommand = ( + command: string, + callback: CommandCallback + ) => { + registeredCommands.set(command, callback); + return { dispose: () => {} }; + }; + + (vscode.window as unknown as { createStatusBarItem: unknown }).createStatusBarItem = () => { + statusBarItem = { + text: '', + tooltip: undefined, + command: undefined, + backgroundColor: undefined, + show: () => {}, + hide: () => {}, + dispose: () => {}, + } as unknown as vscode.StatusBarItem; + return statusBarItem; + }; + + (vscode.workspace as unknown as { getConfiguration: unknown }).getConfiguration = ( + section?: string + ) => ({ + get: (key: string, defaultValue?: T): T | undefined => { + if (key === 'url') return HUB_URL as unknown as T; + if (key === 'autoConnect') return false as unknown as T; + return defaultValue; + }, + update: async (key: string, value: unknown) => { + configUpdates.push({ section: section ?? '', key, value }); + }, + }); + + activate(context()); +}); + +async function failToConnect(): Promise { + const connect = registeredCommands.get('calm.connectToHub'); + expect(connect).toBeDefined(); + await connect!(); + expect(statusBarItem.text).toBe('$(cloud) CALM Hub (error)'); + expect(statusBarItem.tooltip).toBe(`Failed to connect to ${HUB_URL}\nClick to retry`); +} + +describe('clearing the Hub URL after a failed connection', () => { + it('makes the status bar forget the URL along with the setting', async () => { + await failToConnect(); + + await disconnectFromHub({ clearUrl: true }); + + expect(configUpdates).toEqual([{ section: 'calm.hub', key: 'url', value: undefined }]); + expect(statusBarItem.text).toBe('$(cloud) CALM Hub'); + expect(statusBarItem.tooltip).toBe('Click to connect to CalmHub'); + }); + + it('keeps the URL on a plain disconnect so "Click to reconnect" still works', async () => { + await failToConnect(); + + const disconnect = registeredCommands.get('calm.disconnectFromHub'); + expect(disconnect).toBeDefined(); + await disconnect!(); + + expect(configUpdates).toEqual([]); + expect(statusBarItem.tooltip).toBe(`Disconnected from ${HUB_URL}\nClick to reconnect`); + }); +}); diff --git a/calm-plugins/vscode/src/extension/extension.ts b/calm-plugins/vscode/src/extension/extension.ts index f1fdb09606..46011a29ff 100644 --- a/calm-plugins/vscode/src/extension/extension.ts +++ b/calm-plugins/vscode/src/extension/extension.ts @@ -304,6 +304,7 @@ export async function disconnectFromHub( await vscode.workspace .getConfiguration('calm.hub') .update('url', undefined, vscode.ConfigurationTarget.Global); + hubStatusBar?.clearHubUrl(); } outputChannel.appendLine( options.clearUrl diff --git a/calm-plugins/vscode/src/extension/services/hub-status-bar.test.ts b/calm-plugins/vscode/src/extension/services/hub-status-bar.test.ts index bf217374be..84e85bc185 100644 --- a/calm-plugins/vscode/src/extension/services/hub-status-bar.test.ts +++ b/calm-plugins/vscode/src/extension/services/hub-status-bar.test.ts @@ -107,6 +107,39 @@ describe('HubStatusBar', () => { }); }); + describe('clearHubUrl', () => { + it('keeps the URL on a plain disconnect so the tooltip offers to reconnect', () => { + statusBar.setState('error', undefined, 'https://calmhub.example.com'); + expect(mockItem.tooltip).toBe( + 'Failed to connect to https://calmhub.example.com\nClick to retry' + ); + + statusBar.setState('disconnected'); + expect(mockItem.tooltip).toBe( + 'Disconnected from https://calmhub.example.com\nClick to reconnect' + ); + }); + + it('forgets the URL so the tooltip offers to connect', () => { + statusBar.setState('error', undefined, 'https://calmhub.example.com'); + statusBar.setState('disconnected'); + + statusBar.clearHubUrl(); + + expect(statusBar.state).toBe('disconnected'); + expect(mockItem.text).toBe('$(cloud) CALM Hub'); + expect(mockItem.tooltip).toBe('Click to connect to CalmHub'); + }); + + it('does not bring the URL back on the next state change', () => { + statusBar.setState('error', undefined, 'https://calmhub.example.com'); + statusBar.clearHubUrl(); + + statusBar.setState('connecting'); + expect(mockItem.tooltip).toBe('Connecting...'); + }); + }); + describe('dispose', () => { it('disposes the underlying status bar item', () => { statusBar.dispose(); diff --git a/calm-plugins/vscode/src/extension/services/hub-status-bar.ts b/calm-plugins/vscode/src/extension/services/hub-status-bar.ts index 2ad8df32c7..c23bc21568 100644 --- a/calm-plugins/vscode/src/extension/services/hub-status-bar.ts +++ b/calm-plugins/vscode/src/extension/services/hub-status-bar.ts @@ -36,6 +36,16 @@ export class HubStatusBar { this.update(); } + /** + * Forget the Hub URL. Called when the saved `calm.hub.url` is cleared, so + * the tooltip offers to connect instead of reconnecting to a URL that no + * longer exists (#3523). A plain disconnect keeps the URL (#3494). + */ + clearHubUrl(): void { + this.hubUrl = undefined; + this.update(); + } + setAvailableAndSelected(available: string[], selected: string[]): void { this.availableNamespaces = available; this.selectedNamespaces = selected;