@@ -12,6 +12,11 @@ const {
1212 mockListFoldersForWorkspace,
1313 mockListInternalKnowledgeBases,
1414 mockListPinnedItemsForUser,
15+ mockListWorkflowsForUser,
16+ mockListWorkspacesForViewer,
17+ mockGetUserProfile,
18+ mockGetWorkspacePermissions,
19+ mockListMothershipChats,
1520 mockListTables,
1621 mockListWorkspaceFileFolders,
1722 mockListWorkspaceFilesWithShares,
@@ -23,6 +28,11 @@ const {
2328 mockListFoldersForWorkspace : vi . fn ( ) ,
2429 mockListInternalKnowledgeBases : vi . fn ( ) ,
2530 mockListPinnedItemsForUser : vi . fn ( ) ,
31+ mockListWorkflowsForUser : vi . fn ( ) ,
32+ mockListWorkspacesForViewer : vi . fn ( ) ,
33+ mockGetUserProfile : vi . fn ( ) ,
34+ mockGetWorkspacePermissions : vi . fn ( ) ,
35+ mockListMothershipChats : vi . fn ( ) ,
2636 mockListTables : vi . fn ( ) ,
2737 mockListWorkspaceFileFolders : vi . fn ( ) ,
2838 mockListWorkspaceFilesWithShares : vi . fn ( ) ,
@@ -45,6 +55,19 @@ vi.mock('@/lib/pinned-items/queries', () => ({
4555} ) )
4656vi . mock ( '@/lib/workspaces/permissions/utils' , ( ) => ( {
4757 getWorkspaceMemberProfiles : mockGetWorkspaceMemberProfiles ,
58+ getWorkspacePermissionsForAuthorizedViewer : mockGetWorkspacePermissions ,
59+ } ) )
60+ vi . mock ( '@/lib/workflows/queries' , ( ) => ( {
61+ listWorkflowsForUser : mockListWorkflowsForUser ,
62+ } ) )
63+ vi . mock ( '@/lib/workspaces/list' , ( ) => ( {
64+ listWorkspacesForViewer : mockListWorkspacesForViewer ,
65+ } ) )
66+ vi . mock ( '@/lib/users/queries' , ( ) => ( {
67+ getUserProfile : mockGetUserProfile ,
68+ } ) )
69+ vi . mock ( '@/lib/copilot/chat/list-mothership-chats' , ( ) => ( {
70+ listMothershipChats : mockListMothershipChats ,
4871} ) )
4972vi . mock ( '@/lib/table/service' , ( ) => ( {
5073 listTables : mockListTables ,
@@ -74,6 +97,7 @@ vi.mock('@sim/emcn', () => ({
7497
7598import { prefetchFilesBrowser } from '@/app/workspace/[workspaceId]/files/prefetch'
7699import { prefetchKnowledgeBases } from '@/app/workspace/[workspaceId]/knowledge/prefetch'
100+ import { prefetchWorkspaceSidebar } from '@/app/workspace/[workspaceId]/prefetch'
77101import { prefetchTables } from '@/app/workspace/[workspaceId]/tables/prefetch'
78102import { folderKeys } from '@/hooks/queries/utils/folder-keys'
79103import { knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys'
@@ -98,6 +122,16 @@ describe('workspace list prefetches', () => {
98122 mockListWorkspaceFilesWithShares . mockResolvedValue ( [ ] )
99123 mockListWorkspaceFileFolders . mockResolvedValue ( [ ] )
100124 mockListPinnedItemsForUser . mockResolvedValue ( [ ] )
125+ mockListWorkflowsForUser . mockResolvedValue ( [ ] )
126+ mockGetUserProfile . mockResolvedValue ( { id : USER_ID , name : 'Ada' , email : 'a@b.c' } )
127+ mockGetWorkspacePermissions . mockResolvedValue ( { users : [ ] } )
128+ mockListMothershipChats . mockResolvedValue ( [ ] )
129+ mockListWorkspacesForViewer . mockResolvedValue ( {
130+ workspaces : [ ] ,
131+ lastActiveWorkspaceId : null ,
132+ pinnedWorkspaceIds : [ ] ,
133+ creationPolicy : null ,
134+ } )
101135 mockGetWorkspaceMemberProfiles . mockResolvedValue ( [ ] )
102136 mockListTables . mockResolvedValue ( [ ] )
103137 mockAuthenticate . mockResolvedValue ( { kind : 'session' , userId : USER_ID , sessionId : 'sess-1' } )
@@ -366,6 +400,84 @@ describe('workspace list prefetches', () => {
366400 }
367401 } )
368402
403+ describe ( 'prefetchWorkspaceSidebar / seedWorkspaceList' , ( ) => {
404+ const HOST_CONTEXT = {
405+ workspace : { id : WORKSPACE_ID } ,
406+ viewer : { permission : 'admin' } ,
407+ } as never
408+
409+ const WORKSPACE_ROW = {
410+ id : WORKSPACE_ID ,
411+ name : 'GTM' ,
412+ ownerId : USER_ID ,
413+ organizationId : null ,
414+ workspaceMode : 'personal' ,
415+ permissions : 'admin' ,
416+ }
417+
418+ const LIST_PAYLOAD = {
419+ workspaces : [ WORKSPACE_ROW ] ,
420+ lastActiveWorkspaceId : null ,
421+ pinnedWorkspaceIds : [ ] ,
422+ creationPolicy : null ,
423+ }
424+
425+ /**
426+ * The load-bearing contract: an empty list must leave the key UNSET so the client
427+ * fetch reaches `GET /api/workspaces`' default-workspace creation path. Seeding an
428+ * empty array instead would suppress it and strand a brand-new viewer.
429+ */
430+ it ( 'seeds nothing when the viewer has no workspaces' , async ( ) => {
431+ mockListWorkspacesForViewer . mockResolvedValue ( { ...LIST_PAYLOAD , workspaces : [ ] } )
432+ const client = makeClient ( )
433+
434+ await prefetchWorkspaceSidebar ( client , WORKSPACE_ID , USER_ID , HOST_CONTEXT , null )
435+
436+ expect ( client . getQueryData ( workspaceKeys . list ( 'active' ) ) ) . toBeUndefined ( )
437+ } )
438+
439+ it ( 'seeds the workspace list when the viewer has one' , async ( ) => {
440+ mockListWorkspacesForViewer . mockResolvedValue ( LIST_PAYLOAD )
441+ const client = makeClient ( )
442+
443+ await prefetchWorkspaceSidebar ( client , WORKSPACE_ID , USER_ID , HOST_CONTEXT , null )
444+
445+ const cached = client . getQueryData ( workspaceKeys . list ( 'active' ) ) as
446+ | { workspaces : Array < { id : string } > }
447+ | undefined
448+ expect ( cached ) . toBeDefined ( )
449+ expect ( cached ?. workspaces . map ( ( w ) => w . id ) ) . toEqual ( [ WORKSPACE_ID ] )
450+ } )
451+
452+ /** A failed seed is an optimization loss, not a render failure. */
453+ it ( 'does not throw when the workspace read rejects, and seeds nothing' , async ( ) => {
454+ mockListWorkspacesForViewer . mockRejectedValue ( new Error ( '500' ) )
455+ const client = makeClient ( )
456+
457+ await expect (
458+ prefetchWorkspaceSidebar ( client , WORKSPACE_ID , USER_ID , HOST_CONTEXT , null )
459+ ) . resolves . toBeUndefined ( )
460+ expect ( client . getQueryData ( workspaceKeys . list ( 'active' ) ) ) . toBeUndefined ( )
461+ } )
462+
463+ /** Guards the mismatch check that keeps one workspace's data out of another's cache. */
464+ it ( 'seeds nothing when the host context is for a different workspace' , async ( ) => {
465+ mockListWorkspacesForViewer . mockResolvedValue ( LIST_PAYLOAD )
466+ const client = makeClient ( )
467+
468+ await prefetchWorkspaceSidebar (
469+ client ,
470+ WORKSPACE_ID ,
471+ USER_ID ,
472+ { workspace : { id : 'other-ws' } , viewer : { permission : 'admin' } } as never ,
473+ null
474+ )
475+
476+ expect ( client . getQueryCache ( ) . getAll ( ) ) . toHaveLength ( 0 )
477+ expect ( mockListWorkspacesForViewer ) . not . toHaveBeenCalled ( )
478+ } )
479+ } )
480+
369481 describe ( 'graceful failure' , ( ) => {
370482 it . each ( [
371483 [
@@ -379,9 +491,14 @@ describe('workspace list prefetches', () => {
379491 tableKeys . list ( WORKSPACE_ID , 'active' ) ,
380492 ] ,
381493 [
494+ /**
495+ * Asserted against the folder key, not the file list: `prefetchFilesBrowser`
496+ * deliberately never seeds `workspaceFilesKeys` (the layout owns it), so an
497+ * assertion on that key would hold no matter what this function did.
498+ */
382499 'prefetchFilesBrowser' ,
383500 ( client : QueryClient ) => prefetchFilesBrowser ( client , WORKSPACE_ID , USER_ID ) ,
384- workspaceFilesKeys . list ( WORKSPACE_ID , 'active' ) ,
501+ workspaceFileFolderKeys . list ( WORKSPACE_ID , 'active' ) ,
385502 ] ,
386503 ] as const ) (
387504 '%s does not throw when the fetcher rejects (page still renders, client refetches)' ,
@@ -393,6 +510,7 @@ describe('workspace list prefetches', () => {
393510 mockListInternalKnowledgeBases . mockRejectedValue ( boom )
394511 mockListPinnedItemsForUser . mockRejectedValue ( boom )
395512 mockGetWorkspaceMemberProfiles . mockRejectedValue ( boom )
513+ mockListWorkspaceFileFolders . mockRejectedValue ( boom )
396514 const client = makeClient ( )
397515
398516 await expect ( prefetch ( client ) ) . resolves . toBeUndefined ( )
0 commit comments