Skip to content

Commit 618cee5

Browse files
authored
test(prefetch): cover the workspace-list seed, and fix two tests that could not fail (#6659)
The audit found the seed contract — the whole point of #6656 — had no test, and that one existing assertion was vacuous. - prefetchWorkspaceSidebar and seedWorkspaceList now have coverage: the empty list seeds nothing (so the client reaches the route's default-workspace creation path), a populated list seeds, a rejected read neither throws nor seeds, and a host context for another workspace seeds nothing at all. Verified falsifiable — removing the empty-list guard turns the first red. - The graceful-failure row for prefetchFilesBrowser asserted on the file-list key, which that function deliberately never writes, so it held no matter what the code did. It now asserts the folder key it owns, and the setup rejects the folder read. - The sidebar was the third hand-rolled copy of the folder prefetch the shared helper was extracted to remove; it now calls prefetchResourceFolders too. - prefetchKnowledgeBases returns early without a userId like every sibling, rather than reaching the authenticator and throwing per render. - Dropped two docblock claims about a `retry` default that no longer applies server-side.
1 parent c49751b commit 618cee5

3 files changed

Lines changed: 126 additions & 17 deletions

File tree

apps/sim/app/workspace/[workspaceId]/knowledge/prefetch.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export async function prefetchKnowledgeBases(
3636
workspaceId: string,
3737
userId: string | undefined
3838
): Promise<void> {
39+
if (!userId) return
40+
3941
await Promise.all([
4042
queryClient.prefetchQuery({
4143
queryKey: knowledgeKeys.list(workspaceId, 'active'),

apps/sim/app/workspace/[workspaceId]/lib/prefetch.test.ts

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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
}))
4656
vi.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
}))
4972
vi.mock('@/lib/table/service', () => ({
5073
listTables: mockListTables,
@@ -74,6 +97,7 @@ vi.mock('@sim/emcn', () => ({
7497

7598
import { prefetchFilesBrowser } from '@/app/workspace/[workspaceId]/files/prefetch'
7699
import { prefetchKnowledgeBases } from '@/app/workspace/[workspaceId]/knowledge/prefetch'
100+
import { prefetchWorkspaceSidebar } from '@/app/workspace/[workspaceId]/prefetch'
77101
import { prefetchTables } from '@/app/workspace/[workspaceId]/tables/prefetch'
78102
import { folderKeys } from '@/hooks/queries/utils/folder-keys'
79103
import { 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()

apps/sim/app/workspace/[workspaceId]/prefetch.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import type { QueryClient } from '@tanstack/react-query'
44
import { listWorkspacesContract, type WorkspaceHostContext } from '@/lib/api/contracts/workspaces'
55
import { listMothershipChats } from '@/lib/copilot/chat/list-mothership-chats'
66
import { isChatEnabled } from '@/lib/core/config/env-flags'
7-
import { listFoldersForWorkspace } from '@/lib/folders/queries'
87
import { getUserProfile } from '@/lib/users/queries'
98
import { listWorkflowsForUser } from '@/lib/workflows/queries'
109
import { listWorkspaceFilesWithShares } from '@/lib/workspace-files/queries'
1110
import { getWorkspaceHostContextForViewer } from '@/lib/workspaces/host-context'
1211
import { listWorkspacesForViewer } from '@/lib/workspaces/list'
1312
import { getWorkspacePermissionsForAuthorizedViewer } from '@/lib/workspaces/permissions/utils'
13+
import { prefetchResourceFolders } from '@/app/workspace/[workspaceId]/lib/prefetch-resource-folders'
1414
import {
1515
MOTHERSHIP_CHAT_LIST_STALE_TIME,
1616
mapChat,
@@ -21,7 +21,6 @@ import {
2121
USER_PROFILE_STALE_TIME,
2222
userProfileKeys,
2323
} from '@/hooks/queries/user-profile'
24-
import { FOLDER_LIST_STALE_TIME, folderKeys, mapFolder } from '@/hooks/queries/utils/folder-keys'
2524
import { workflowKeys } from '@/hooks/queries/utils/workflow-keys'
2625
import { mapWorkflow, WORKFLOW_LIST_STALE_TIME } from '@/hooks/queries/utils/workflow-list-query'
2726
import { normalizeWorkspacesResponse } from '@/hooks/queries/utils/workspace-list-query'
@@ -59,9 +58,8 @@ const logger = createLogger('WorkspacePrefetch')
5958
* Seeded rather than prefetched so the empty-list case can decline to create a
6059
* cache entry at all: the route's default-workspace creation path must run on
6160
* the client, and an entry — even an empty one — would suppress it. Expressing
62-
* that as an absent seed keeps a normal state out of the error channel, where
63-
* it previously cost a full second re-read (`retry: 1`) to re-derive an outcome
64-
* already known.
61+
* that as an absent seed also keeps a routine state out of the error channel,
62+
* where it read as a failure rather than as "nothing to seed".
6563
*/
6664
async function seedWorkspaceList(
6765
queryClient: QueryClient,
@@ -122,9 +120,7 @@ async function seedWorkspaceList(
122120
* to produce, without routing a normal state through the error channel. That
123121
* matters because only a settled query is dehydrated: an unawaited read would be
124122
* dropped from the payload entirely, so the switcher would waterfall on every
125-
* cold load rather than paint populated. Seeding also skips the `retry` default,
126-
* which previously ran the whole read a second time, a retry delay later, purely
127-
* to re-derive an outcome already known.
123+
* cold load rather than paint populated.
128124
*/
129125
export async function prefetchWorkspaceSidebar(
130126
queryClient: QueryClient,
@@ -156,14 +152,7 @@ export async function prefetchWorkspaceSidebar(
156152
}),
157153
]
158154
: []),
159-
queryClient.prefetchQuery({
160-
queryKey: folderKeys.list(workspaceId, 'active', 'workflow'),
161-
queryFn: async () => {
162-
const rows = await listFoldersForWorkspace(workspaceId, 'active', 'workflow')
163-
return rows.map(mapFolder)
164-
},
165-
staleTime: FOLDER_LIST_STALE_TIME,
166-
}),
155+
prefetchResourceFolders(queryClient, workspaceId, 'workflow', userId),
167156
/**
168157
* The sidebar reads the workspace's files for its search modal, on EVERY workspace route — so this
169158
* query is registered by sidebar chrome before any page renders. That ordering is why it has to be

0 commit comments

Comments
 (0)