diff --git a/calm-plugins/vscode/src/extension/extension.test.ts b/calm-plugins/vscode/src/extension/extension.test.ts new file mode 100644 index 0000000000..8ac1c6487c --- /dev/null +++ b/calm-plugins/vscode/src/extension/extension.test.ts @@ -0,0 +1,123 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import * as vscode from 'vscode'; +import { activate, disconnectFromHub } from './extension'; + +vi.mock('./services/hub-auth-service', () => ({ + HubAuthService: class { + async initialize(): Promise {} + async discoverAndAuthenticate(): Promise { + return false; + } + }, +})); + +type CommandCallback = (...args: unknown[]) => unknown; + +const commands = new Map(); +const configUpdates: Array<{ key: string; value: unknown }> = []; +let hubUrl: string | undefined; +let statusBarItem: vscode.StatusBarItem; +let inputValue: string | undefined; + +function context(): vscode.ExtensionContext { + return { + subscriptions: [], + extensionUri: vscode.Uri.file('/tmp/extension'), + secrets: { + get: async () => undefined, + store: async () => {}, + delete: async () => {}, + onDidChange: () => ({ dispose: () => {} }), + }, + } as unknown as vscode.ExtensionContext; +} + +beforeEach(() => { + commands.clear(); + configUpdates.length = 0; + hubUrl = 'https://calmhub.example.com'; + inputValue = undefined; + + (vscode.commands as unknown as { registerCommand: unknown }).registerCommand = ( + command: string, + callback: CommandCallback + ) => { + commands.set(command, callback); + return { dispose: () => {} }; + }; + + (vscode.workspace as unknown as { getConfiguration: unknown }).getConfiguration = ( + section?: string + ) => ({ + get: (key: string, defaultValue?: T): T | undefined => { + if (section === 'calm.hub' && key === 'url') return hubUrl as T | undefined; + if (section === 'calm.hub' && key === 'autoConnect') return false as T; + return defaultValue; + }, + update: async (key: string, value: unknown) => { + configUpdates.push({ key, value }); + if (section === 'calm.hub' && key === 'url') { + hubUrl = value as string | undefined; + } + }, + }); + + const window = vscode.window as unknown as { + createStatusBarItem: () => vscode.StatusBarItem; + showQuickPick: (items: Array<{ label: string }>) => Promise; + showInputBox: (options: { value?: string }) => Promise; + }; + window.createStatusBarItem = () => { + statusBarItem = { + text: '', + tooltip: undefined, + command: undefined, + backgroundColor: undefined, + show: () => {}, + hide: () => {}, + dispose: () => {}, + }; + return statusBarItem; + }; + window.showQuickPick = async (items) => items.find((item) => item.label.includes('Clear URL')); + window.showInputBox = async (options) => { + inputValue = options.value; + return undefined; + }; + + activate(context()); +}); + +describe('CALM Hub Clear URL', () => { + it('forgets the status bar URL and opens an empty reconnect prompt', async () => { + const connect = commands.get('calm.connectToHub'); + expect(connect).toBeDefined(); + + await connect!(); + expect(statusBarItem.tooltip).toContain('Failed to connect to https://calmhub.example.com'); + + await connect!(); + + expect(hubUrl).toBeUndefined(); + expect(configUpdates).toEqual([{ key: 'url', value: undefined }]); + expect(statusBarItem.tooltip).toBe('Click to connect to CalmHub'); + + await connect!(); + + expect(inputValue).toBe(''); + }); + + it('keeps the saved URL when Disconnect runs', async () => { + const connect = commands.get('calm.connectToHub'); + expect(connect).toBeDefined(); + + await connect!(); + await disconnectFromHub(); + + expect(hubUrl).toBe('https://calmhub.example.com'); + expect(configUpdates).toEqual([]); + expect(statusBarItem.tooltip).toBe( + 'Disconnected from https://calmhub.example.com\nClick to reconnect' + ); + }); +}); diff --git a/calm-plugins/vscode/src/extension/extension.ts b/calm-plugins/vscode/src/extension/extension.ts index a57a175d96..0cb5ccad27 100644 --- a/calm-plugins/vscode/src/extension/extension.ts +++ b/calm-plugins/vscode/src/extension/extension.ts @@ -157,7 +157,7 @@ async function connectToHub( : [ { label: '$(pencil) Change Hub URL', action: 'change' }, { label: '$(debug-restart) Retry Connection', action: 'retry' }, - { label: '$(sign-out) Clear URL', action: 'disconnect' }, + { label: '$(sign-out) Clear URL', action: 'clearUrl' }, ]; const choice = await vscode.window.showQuickPick(items, { @@ -167,6 +167,7 @@ async function connectToHub( }); if (!choice) return; if (choice.action === 'disconnect') { await disconnectFromHub(); return; } + if (choice.action === 'clearUrl') { await disconnectFromHub({ clearUrl: true }); return; } if (choice.action === 'namespaces') { await selectNamespaces(); return; } if (choice.action === 'refresh' || choice.action === 'retry') { // Fall through to reconnect with existing URL @@ -252,15 +253,24 @@ async function connectToHub( } } -async function disconnectFromHub(): Promise { +export interface DisconnectOptions { + clearUrl?: boolean; +} + +export async function disconnectFromHub(options: DisconnectOptions = {}): Promise { if (hubAuthService) { await hubAuthService.signOut(); } hubClient = undefined; hubAuthService = undefined; + if (options.clearUrl) { + hubStatusBar?.clearHubUrl(); + } hubStatusBar?.setState('disconnected'); void canvasPanel?.setHubConnection(undefined); - await vscode.workspace.getConfiguration('calm.hub').update('url', undefined, vscode.ConfigurationTarget.Global); + if (options.clearUrl) { + await vscode.workspace.getConfiguration('calm.hub').update('url', undefined, vscode.ConfigurationTarget.Global); + } outputChannel.appendLine('[INFO] Disconnected from CalmHub'); } 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..66230f5ece 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 @@ -88,6 +88,20 @@ describe('HubStatusBar', () => { expect(mockItem.text).toBe('$(cloud) CALM Hub (7)'); }); + it('forgets a cached URL only when it is cleared', () => { + statusBar.setState('connecting', undefined, 'https://calmhub.example.com'); + statusBar.setState('error'); + statusBar.setState('disconnected'); + + expect(mockItem.tooltip).toBe( + 'Disconnected from https://calmhub.example.com\nClick to reconnect' + ); + + statusBar.clearHubUrl(); + + expect(mockItem.tooltip).toBe('Click to connect to CalmHub'); + }); + it('shows selected vs available with setAvailableAndSelected', () => { statusBar.setState('connected', 3); statusBar.setAvailableAndSelected(['ns1', 'ns2', 'ns3'], ['ns1', 'ns3']); 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..975aace113 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,11 @@ export class HubStatusBar { this.update(); } + clearHubUrl(): void { + this.hubUrl = undefined; + this.update(); + } + setAvailableAndSelected(available: string[], selected: string[]): void { this.availableNamespaces = available; this.selectedNamespaces = selected;