From ac9a10ccbb9fe05125c54692c601b0d229201820 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 27 Jul 2026 12:54:49 -0500 Subject: [PATCH] test(desktop): cover the workspace-page reveal bug class Both layers, and the classification underneath them: a page route fronts the pane whether it carries a query or not, moving between two pages fronts it again even though `$workspaceIsPage` never changes, contributed routes count, and chat and overlay targets leave the tab alone. --- .../src/app/routes.workspace-reveal.test.ts | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 apps/desktop/src/app/routes.workspace-reveal.test.ts diff --git a/apps/desktop/src/app/routes.workspace-reveal.test.ts b/apps/desktop/src/app/routes.workspace-reveal.test.ts new file mode 100644 index 00000000000..7db75e610ae --- /dev/null +++ b/apps/desktop/src/app/routes.workspace-reveal.test.ts @@ -0,0 +1,189 @@ +/** + * A full page (Capabilities/Messaging/Artifacts/a contributed route) renders + * INSIDE the `workspace` pane, so navigating to one has to front that pane — + * otherwise a main zone parked on a session tile keeps the tile on screen and + * the click looks dead until the app restarts (#72602). + * + * Two layers, both covered here: `syncWorkspaceRoute` (the router location, + * every entry point) and `navigateToWorkspacePage` (the re-click, where the + * location doesn't change). + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { registry } from '@/contrib/registry' + +import { + $workspaceIsPage, + AGENTS_ROUTE, + appViewForPath, + ARTIFACTS_ROUTE, + CRON_ROUTE, + MESSAGING_ROUTE, + navigateToWorkspacePage, + NEW_CHAT_ROUTE, + routePathname, + ROUTES_AREA, + routeSessionId, + sessionRoute, + SETTINGS_ROUTE, + SKILLS_ROUTE, + syncWorkspaceRoute +} from './routes' + +vi.mock('@/components/pane-shell/tree/store', async importOriginal => ({ + ...(await importOriginal>()), + noteActiveTreeGroup: vi.fn(), + revealTreePane: vi.fn() +})) + +const { noteActiveTreeGroup, revealTreePane } = await import('@/components/pane-shell/tree/store') + +const CONTRIBUTED_ROUTE = '/kanban' + +function contributeRoute(): () => void { + return registry.register({ + area: ROUTES_AREA, + data: { path: CONTRIBUTED_ROUTE }, + id: 'test-route', + render: () => null + }) +} + +/** Did the workspace pane get fronted? Both calls, or the tab stays put. */ +const fronted = () => + vi.mocked(revealTreePane).mock.calls.some(([pane]) => pane === 'workspace') && + vi.mocked(noteActiveTreeGroup).mock.calls.some(([group]) => group === null) + +beforeEach(() => { + vi.mocked(revealTreePane).mockClear() + vi.mocked(noteActiveTreeGroup).mockClear() + $workspaceIsPage.set(false) +}) + +afterEach(() => { + $workspaceIsPage.set(false) +}) + +describe('routePathname', () => { + it('keeps a bare path and drops a query or hash', () => { + expect(routePathname(SKILLS_ROUTE)).toBe('/skills') + expect(routePathname('/skills?tab=mcp')).toBe('/skills') + expect(routePathname('/skills?tab=mcp&server=ctx7')).toBe('/skills') + expect(routePathname('/settings#keys')).toBe('/settings') + }) + + it('leaves an encoded session id alone', () => { + const route = sessionRoute('a?b#c') + + expect(routePathname(route)).toBe(route) + expect(routeSessionId(route)).toBe('a?b#c') + }) +}) + +describe('classification of targets carrying a query', () => { + // The palette navigates to every one of these (Capabilities tabs, MCP + // servers), and Settings redirects old /settings?tab=mcp deep links to the + // last one. Unstripped, they parsed as SESSION ids and read as 'chat'. + it.each([ + [`${SKILLS_ROUTE}?tab=skills`, 'skills'], + [`${SKILLS_ROUTE}?tab=toolsets`, 'skills'], + [`${SKILLS_ROUTE}?tab=mcp&server=ctx7`, 'skills'], + [`${SETTINGS_ROUTE}?tab=keys`, 'settings'] + ])('%s is not a session route', (to, view) => { + expect(routeSessionId(to)).toBeNull() + expect(appViewForPath(to)).toBe(view) + }) +}) + +describe('syncWorkspaceRoute', () => { + it('publishes and fronts on a page route', () => { + syncWorkspaceRoute(SKILLS_ROUTE) + + expect($workspaceIsPage.get()).toBe(true) + expect(fronted()).toBe(true) + }) + + it('fronts on a page route reached with a query', () => { + syncWorkspaceRoute(`${SKILLS_ROUTE}?tab=mcp`) + + expect($workspaceIsPage.get()).toBe(true) + expect(fronted()).toBe(true) + }) + + it('fronts when moving between two pages — the atom never changes, the tab must', () => { + syncWorkspaceRoute(ARTIFACTS_ROUTE) + vi.mocked(revealTreePane).mockClear() + vi.mocked(noteActiveTreeGroup).mockClear() + + syncWorkspaceRoute(MESSAGING_ROUTE) + + expect($workspaceIsPage.get()).toBe(true) + expect(fronted()).toBe(true) + }) + + it('fronts on a contributed page route', () => { + const dispose = contributeRoute() + + try { + syncWorkspaceRoute(CONTRIBUTED_ROUTE) + + expect(appViewForPath(CONTRIBUTED_ROUTE)).toBe('extension') + expect(fronted()).toBe(true) + } finally { + dispose() + } + }) + + it.each([ + ['a session route', sessionRoute('sess-a')], + ['the new-chat route', NEW_CHAT_ROUTE], + ['an overlay', SETTINGS_ROUTE], + ['an overlay with a query', `${SETTINGS_ROUTE}?tab=keys`], + ['another overlay', CRON_ROUTE], + ['yet another overlay', AGENTS_ROUTE] + ])('leaves the tab alone on %s', (_label, to) => { + syncWorkspaceRoute(to) + + expect($workspaceIsPage.get()).toBe(false) + expect(revealTreePane).not.toHaveBeenCalled() + }) +}) + +describe('navigateToWorkspacePage', () => { + it('navigates and fronts, so a re-click on the page you are already on still shows it', () => { + const navigate = vi.fn() + + navigateToWorkspacePage(navigate, SKILLS_ROUTE) + + expect(navigate).toHaveBeenCalledWith(SKILLS_ROUTE, undefined) + expect(fronted()).toBe(true) + }) + + it.each([`${SKILLS_ROUTE}?tab=skills`, `${SKILLS_ROUTE}?tab=toolsets`, `${SKILLS_ROUTE}?tab=mcp&server=ctx7`])( + 'fronts for the palette target %s', + to => { + navigateToWorkspacePage(vi.fn(), to) + + expect(fronted()).toBe(true) + } + ) + + it('passes navigation options through', () => { + const navigate = vi.fn() + + navigateToWorkspacePage(navigate, ARTIFACTS_ROUTE, { replace: true }) + + expect(navigate).toHaveBeenCalledWith(ARTIFACTS_ROUTE, { replace: true }) + }) + + it('navigates without fronting for chat and overlay targets', () => { + const navigate = vi.fn() + + navigateToWorkspacePage(navigate, sessionRoute('sess-a')) + navigateToWorkspacePage(navigate, SETTINGS_ROUTE) + + expect(navigate).toHaveBeenCalledTimes(2) + expect(revealTreePane).not.toHaveBeenCalled() + }) +})