diff --git a/packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx b/packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx index f86d9fcdde7..90e3c163c20 100644 --- a/packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx +++ b/packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx @@ -137,6 +137,7 @@ beforeEach(() => { mocks.embeddedProps.current = null; mocks.connectionError.current = undefined; mocks.errorNotifications.current = 0; + sdkMocks.listWorkspaceSessionsPage.mockResolvedValue({ sessions: [] }); }); afterEach(() => { @@ -552,3 +553,92 @@ describe('EmbeddedApp host wiring', () => { } }); }); + +describe('session switch overlay', () => { + async function selectPastSession(): Promise<{ + container: HTMLElement; + historyButton: HTMLButtonElement; + }> { + sdkMocks.listWorkspaceSessionsPage.mockResolvedValueOnce({ + sessions: [ + { + sessionId: 'session-2', + workspaceCwd: '/workspace', + displayName: 'Old conversation', + updatedAt: Date.now() - 3 * 24 * 60 * 60 * 1000, + }, + ], + }); + await renderApp(); + const { container } = mounted[mounted.length - 1]; + const historyButton = container.querySelector( + 'button[aria-label="Past conversations"]', + ); + expect(historyButton).not.toBeNull(); + await act(async () => { + historyButton!.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await Promise.resolve(); + }); + const row = container.querySelector( + '[data-session-id="session-2"]', + ); + expect(row).not.toBeNull(); + await act(async () => { + row!.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await Promise.resolve(); + }); + return { container, historyButton: historyButton! }; + } + + it('unlocks the panel when the daemon never confirms a session switch', async () => { + // https://github.com/QwenLM/qwen-code/issues/10405 — with the daemon + // unreachable the embedded shell can never confirm the switch, so the + // "switching session" overlay would lock the panel until a webview + // reload. The host must lift the lock on its own so the user can retry. + vi.useFakeTimers(); + try { + const { container, historyButton } = await selectPastSession(); + expect(container.textContent).toContain('Loading conversation…'); + + await act(async () => { + vi.advanceTimersByTime(60_000); + await Promise.resolve(); + }); + + expect(container.textContent).not.toContain('Loading conversation…'); + expect(historyButton.disabled).toBe(false); + } finally { + vi.useRealTimers(); + } + }); + + it('still clears the overlay at once when the shell confirms the switch', async () => { + vi.useFakeTimers(); + try { + const { container } = await selectPastSession(); + expect(container.textContent).toContain('Loading conversation…'); + + await act(async () => { + callback<(sessionId: string | undefined) => void>( + mocks.embeddedProps.current as CapturedProps, + 'onSessionIdChange', + )('session-2'); + await Promise.resolve(); + }); + + expect(container.textContent).not.toContain('Loading conversation…'); + + // A confirmed switch must also cancel the failure timer, or it would + // raise a bogus switch-failed notice after the fact. + await act(async () => { + vi.advanceTimersByTime(60_000); + await Promise.resolve(); + }); + expect(container.textContent).not.toContain( + 'The conversation switch timed out.', + ); + } finally { + vi.useRealTimers(); + } + }); +});