From d10e0ec7c72b848fe0f97005185faa67ac3d22d8 Mon Sep 17 00:00:00 2001 From: deven Date: Tue, 27 Oct 2026 14:20:50 +0530 Subject: [PATCH] fix(vscode): forget the Hub URL in the status bar when it is cleared "Clear URL" removes calm.hub.url, but HubStatusBar kept the old URL because setState only changes hubUrl when it gets a value. The tooltip said "Disconnected from / Click to reconnect" while the click opened an empty URL box. Add HubStatusBar.clearHubUrl() and call it from disconnectFromHub() next to the line that clears the setting, so the status bar and the setting agree. A plain disconnect still keeps the URL (#3494). Closes #3523 --- .../src/extension/extension.clear-url.test.ts | 113 ++++++++++++++++++ .../vscode/src/extension/extension.ts | 1 + .../extension/services/hub-status-bar.test.ts | 33 +++++ .../src/extension/services/hub-status-bar.ts | 10 ++ 4 files changed, 157 insertions(+) create mode 100644 calm-plugins/vscode/src/extension/extension.clear-url.test.ts 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 56fb8bddd1..d839f3ac4f 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 @@ -98,6 +98,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 afaf492aab..1a8f79080f 100644 --- a/calm-plugins/vscode/src/extension/services/hub-status-bar.ts +++ b/calm-plugins/vscode/src/extension/services/hub-status-bar.ts @@ -35,6 +35,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;