From 33c881e89339bca9f86643da016b833eac5f161c Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Tue, 20 Oct 2026 08:43:33 +0000 Subject: [PATCH] refactor(web): use generated oRPC query keys (#44415) --- oxlint-suppressions.json | 10 - .../__tests__/hydration-boundary.spec.tsx | 5 +- .../__tests__/built-in-access-points.spec.tsx | 5 +- .../__tests__/cloud-analytics.spec.tsx | 5 +- .../__tests__/sidebar-focus.browser.spec.tsx | 3 +- .../__tests__/edit-pipeline-info.spec.tsx | 5 +- .../operations-dropdown.browser.spec.tsx | 5 +- .../web-apps-section.browser.spec.tsx | 29 ++- ...place-search-autocomplete.browser.spec.tsx | 14 +- .../__tests__/reflow.browser.spec.tsx | 5 +- .../comment/comment-movement.browser.spec.tsx | 3 +- .../comment/reply-deletion.browser.spec.tsx | 3 +- .../device/__tests__/page-terminal.spec.tsx | 16 +- .../use-agent-configure-sync.spec.tsx | 130 ++++++------ .../files/__tests__/index.spec.tsx | 95 +++++---- .../components/orchestrate/files/index.tsx | 42 ++-- .../skills/__tests__/index.spec.tsx | 186 ++++++++++-------- .../orchestrate/skills/use-skill-detail.ts | 66 +++---- .../working-directory-panel.spec.tsx | 143 +++++++------- .../__tests__/detail-page-clipboard.spec.tsx | 9 +- .../__tests__/detail-page-metadata.spec.tsx | 21 +- .../__tests__/detail-page-navigation.spec.tsx | 6 +- .../__tests__/detail-page-publishing.spec.tsx | 51 ++--- .../skills/__tests__/detail-page.fixture.tsx | 91 ++++----- .../system-features/__tests__/server.spec.ts | 28 ++- .../__tests__/use-app-access-control.spec.tsx | 20 +- web/test/console/account-profile.ts | 6 +- web/test/console/current-workspace.ts | 6 +- web/test/console/operation-hooks.ts | 22 ++- web/test/console/query-data.ts | 5 +- web/test/console/service-responses.ts | 35 ++++ web/test/console/workspace-permissions.ts | 8 +- 32 files changed, 551 insertions(+), 527 deletions(-) diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index cde7b176ea2..7cc95780240 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -1692,11 +1692,6 @@ "count": 1 } }, - "web/app/components/snippet-list/__tests__/reflow.browser.spec.tsx": { - "@tanstack/query/prefer-query-options": { - "count": 1 - } - }, "web/app/components/snippets/hooks/use-nodes-sync-draft.ts": { "dify/require-disable-directive-description": { "count": 1 @@ -3035,11 +3030,6 @@ "count": 1 } }, - "web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx": { - "@tanstack/query/prefer-query-options": { - "count": 1 - } - }, "web/features/agent-v2/agent-detail/configure/components/preview/working-directory-panel.tsx": { "@tanstack/query/prefer-query-options": { "count": 2 diff --git a/web/app/(commonLayout)/__tests__/hydration-boundary.spec.tsx b/web/app/(commonLayout)/__tests__/hydration-boundary.spec.tsx index abe52c22c14..4a581d5997f 100644 --- a/web/app/(commonLayout)/__tests__/hydration-boundary.spec.tsx +++ b/web/app/(commonLayout)/__tests__/hydration-boundary.spec.tsx @@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-quer import { render, screen } from '@testing-library/react' import { consoleQuery } from '@/service/console' import { httpErrorFromResponse } from '@/service/http-error' +import { consoleQueryKey } from '@/test/console/operation-hooks' const mocks = vi.hoisted(() => ({ queryClient: undefined as QueryClient | undefined, @@ -22,7 +23,7 @@ vi.mock('@/next/headers', () => ({ headers: () => mocks.headers() })) vi.mock('@/next/navigation', () => ({ redirect: (url: string) => mocks.redirect(url) })) vi.mock('@/features/account-profile/server', () => ({ serverUserProfileQueryOptions: () => ({ - queryKey: ['common', 'user-profile'], + queryKey: consoleQueryKey('account.profile.get'), queryFn: mocks.profile, retry: false, }), @@ -100,7 +101,7 @@ it('hydrates the profile, workspace, permissions and feature data', async () => render({element}) expect(screen.getByText('Common shell')).toBeInTheDocument() - expect(client.getQueryData(['common', 'user-profile'])).toMatchObject({ + expect(client.getQueryData(consoleQueryKey('account.profile.get'))).toMatchObject({ profile: { id: 'account-id' }, }) expect(client.getQueryData(keys.workspace)).toMatchObject({ id: 'workspace-id' }) diff --git a/web/app/components/app/access-point/__tests__/built-in-access-points.spec.tsx b/web/app/components/app/access-point/__tests__/built-in-access-points.spec.tsx index 3dad7524762..555c9367f82 100644 --- a/web/app/components/app/access-point/__tests__/built-in-access-points.spec.tsx +++ b/web/app/components/app/access-point/__tests__/built-in-access-points.spec.tsx @@ -3,6 +3,7 @@ import type { ReactElement } from 'react' import { screen } from '@testing-library/react' import { consoleQuery } from '@/service/console' import { createHttpError } from '@/service/http-error' +import { consoleQueryKey } from '@/test/console/operation-hooks' import { renderWithConsoleQuery as renderWithConsole } from '@/test/console/query-data' import { createAppDetailFixture } from '@/test/fixtures/app' import { createTestQueryClient } from '@/test/query-client' @@ -29,12 +30,12 @@ const mocks = vi.hoisted(() => ({ })) vi.mock('@/features/system-features/client', () => ({ - systemFeaturesQueryOptions: () => ({ queryKey: ['system-features'] }), + systemFeaturesQueryOptions: () => ({ queryKey: consoleQueryKey('systemFeatures.get') }), })) function render(ui: ReactElement) { const queryClient = createTestQueryClient() - queryClient.setQueryData(['system-features'], { + queryClient.setQueryData(consoleQueryKey('systemFeatures.get'), { webapp_auth: { enabled: true }, rbac_enabled: true, }) diff --git a/web/app/components/base/analytics-consent/__tests__/cloud-analytics.spec.tsx b/web/app/components/base/analytics-consent/__tests__/cloud-analytics.spec.tsx index 1c2ff7ccdd2..be4ef7fefd8 100644 --- a/web/app/components/base/analytics-consent/__tests__/cloud-analytics.spec.tsx +++ b/web/app/components/base/analytics-consent/__tests__/cloud-analytics.spec.tsx @@ -1,8 +1,10 @@ import { QueryClient } from '@tanstack/react-query' import { render } from '@testing-library/react' import { REGISTRATION_SUCCESS_STORAGE_KEY } from '@/app/components/base/amplitude/registration-session-state' +import { consoleQueryKey } from '@/test/console/operation-hooks' let queryClient: QueryClient +const systemFeaturesQueryKey = consoleQueryKey('systemFeatures.get') type ConfigState = { cookieYesSiteKey: string @@ -10,7 +12,7 @@ type ConfigState = { webPrefix: string | undefined } -const { configState, mockHeadersGet, mockBoundary, systemFeaturesQueryKey } = vi.hoisted(() => ({ +const { configState, mockHeadersGet, mockBoundary } = vi.hoisted(() => ({ configState: { cookieYesSiteKey: 'site-key', isProd: true, @@ -18,7 +20,6 @@ const { configState, mockHeadersGet, mockBoundary, systemFeaturesQueryKey } = vi } as ConfigState, mockHeadersGet: vi.fn(), mockBoundary: vi.fn(), - systemFeaturesQueryKey: ['console', 'system-features'] as const, })) vi.mock('@/features/system-features/server', () => ({ diff --git a/web/app/components/base/chat/chat-with-history/__tests__/sidebar-focus.browser.spec.tsx b/web/app/components/base/chat/chat-with-history/__tests__/sidebar-focus.browser.spec.tsx index 34605831b65..5059dbb28cf 100644 --- a/web/app/components/base/chat/chat-with-history/__tests__/sidebar-focus.browser.spec.tsx +++ b/web/app/components/base/chat/chat-with-history/__tests__/sidebar-focus.browser.spec.tsx @@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { useState } from 'react' import { userEvent } from 'vite-plus/test/browser' import { render } from 'vitest-browser-react' +import { consoleQueryKey } from '@/test/console/operation-hooks' import ChatWithHistory from '../index' vi.mock('@/hooks/use-breakpoints', () => ({ @@ -17,7 +18,7 @@ vi.mock('@/next/navigation', () => ({ vi.mock('@/context/web-app-context', () => ({ useWebAppStore: () => null })) vi.mock('@/features/system-features/client', () => ({ systemFeaturesQueryOptions: () => ({ - queryKey: ['sidebar-focus-system-features'], + queryKey: consoleQueryKey('systemFeatures.get'), initialData: { branding: { enabled: false } }, queryFn: async () => ({ branding: { enabled: false } }), staleTime: Infinity, diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx index 6e2a6563cf0..4f17fa5950f 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/__tests__/edit-pipeline-info.spec.tsx @@ -6,6 +6,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { ChunkingMode } from '@/models/datasets' +import { consoleQuery } from '@/service/console' import { mockEmojiData, renderWithEmoji as testingLibraryRender } from '@/test/emoji-picker' import EditPipelineInfo from '../edit-pipeline-info' @@ -143,7 +144,9 @@ describe('EditPipelineInfo', () => { await waitFor(() => { expect(QueryClient.prototype.invalidateQueries).toHaveBeenCalledWith({ - queryKey: expect.arrayContaining([['console', 'rag', 'pipeline', 'templates', 'get']]), + queryKey: consoleQuery.rag.pipeline.templates.get.queryKey({ + input: { query: { type: 'customized' } }, + }), }) }) }) diff --git a/web/app/components/datasets/list/dataset-card/components/__tests__/operations-dropdown.browser.spec.tsx b/web/app/components/datasets/list/dataset-card/components/__tests__/operations-dropdown.browser.spec.tsx index b990fbff8e4..110b74e5c24 100644 --- a/web/app/components/datasets/list/dataset-card/components/__tests__/operations-dropdown.browser.spec.tsx +++ b/web/app/components/datasets/list/dataset-card/components/__tests__/operations-dropdown.browser.spec.tsx @@ -3,6 +3,7 @@ import { userEvent } from 'vite-plus/test/browser' import { render } from 'vitest-browser-react' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' +import { consoleQueryKey } from '@/test/console/operation-hooks' import { createSystemFeaturesFixture } from '@/test/console/system-features' import { createDatasetFixture } from '@/test/fixtures/dataset' import { DatasetACLPermission } from '@/utils/permission' @@ -21,14 +22,14 @@ vi.mock('@/context/permission-state', async () => { vi.mock('@/features/account-profile/client', () => ({ userProfileQueryOptions: () => ({ - queryKey: ['account-profile'], + queryKey: consoleQueryKey('account.profile.get'), queryFn: async () => ({ profile: { id: 'user-1' } }), }), })) vi.mock('@/features/system-features/client', () => ({ systemFeaturesQueryOptions: () => ({ - queryKey: ['system-features'], + queryKey: consoleQueryKey('systemFeatures.get'), queryFn: async () => ({ rbac_enabled: true }), }), })) diff --git a/web/app/components/main-nav/components/__tests__/web-apps-section.browser.spec.tsx b/web/app/components/main-nav/components/__tests__/web-apps-section.browser.spec.tsx index b9d55b14d1b..88f907cfb1a 100644 --- a/web/app/components/main-nav/components/__tests__/web-apps-section.browser.spec.tsx +++ b/web/app/components/main-nav/components/__tests__/web-apps-section.browser.spec.tsx @@ -18,24 +18,17 @@ vi.mock('@/context/permission-state', async () => { const { atom } = await import('jotai') return { workspacePermissionKeysAtom: atom(['app_library.access']) } }) -vi.mock('@/service/console', () => ({ - consoleQuery: { - installedApps: { - get: { - infiniteOptions: (options: { - input: (cursor?: string) => { query: { name?: string } } - }) => ({ - ...options, - queryKey: ['installed-apps', options.input().query.name ?? ''], - queryFn: () => service.list(), - }), - }, - byInstalledAppId: { - patch: { mutationOptions: () => ({ mutationFn: service.pin }) }, - }, - }, - }, -})) +vi.mock('@/service/console', async (importOriginal) => { + const actual = await importOriginal() + const { withConsoleResponses } = await import('@/test/console/service-responses') + return { + ...actual, + consoleQuery: withConsoleResponses(actual.consoleQuery, { + 'installedApps.get': () => service.list(), + 'installedApps.byInstalledAppId.patch': () => service.pin(), + }), + } +}) function createApp(index: number): InstalledAppResponse { return { diff --git a/web/app/components/plugins/marketplace/home/__tests__/marketplace-search-autocomplete.browser.spec.tsx b/web/app/components/plugins/marketplace/home/__tests__/marketplace-search-autocomplete.browser.spec.tsx index 0f70d6a55dc..abb6a4d8211 100644 --- a/web/app/components/plugins/marketplace/home/__tests__/marketplace-search-autocomplete.browser.spec.tsx +++ b/web/app/components/plugins/marketplace/home/__tests__/marketplace-search-autocomplete.browser.spec.tsx @@ -43,15 +43,19 @@ vi.mock('@/service/marketplace', async (importOriginal) => { ...original, marketplaceQuery: { searchAdvanced: { - queryOptions: ({ input }: { input: unknown }) => ({ - queryKey: ['marketplace', 'plugins', input], + queryOptions: ( + options: Parameters[0], + ) => ({ + ...original.marketplaceQuery.searchAdvanced.queryOptions(options), queryFn: () => ({ data: { plugins: [], total: 0 } }), }), }, templateSearch: { - queryOptions: ({ input }: { input: unknown }) => ({ - queryKey: ['marketplace', 'templates', input], - queryFn: () => mockTemplateSearch(input), + queryOptions: ( + options: Parameters[0], + ) => ({ + ...original.marketplaceQuery.templateSearch.queryOptions(options), + queryFn: () => mockTemplateSearch(options.input), }), }, }, diff --git a/web/app/components/snippet-list/__tests__/reflow.browser.spec.tsx b/web/app/components/snippet-list/__tests__/reflow.browser.spec.tsx index 98da0fcf086..cd0896e9688 100644 --- a/web/app/components/snippet-list/__tests__/reflow.browser.spec.tsx +++ b/web/app/components/snippet-list/__tests__/reflow.browser.spec.tsx @@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { createStore, Provider } from 'jotai' import { page } from 'vite-plus/test/browser' import { render } from 'vitest-browser-react' +import { consoleQueryKey } from '@/test/console/operation-hooks' import { seedRegisteredConsoleStateFixture } from '@/test/console/state-fixture' import SnippetList from '..' @@ -46,7 +47,7 @@ vi.mock('@/context/permission-state', async () => { vi.mock('@/features/account-profile/client', () => ({ userProfileQueryOptions: () => ({ queryFn: async () => ({ profile: { id: 'creator-1' } }), - queryKey: ['account-profile'], + queryKey: consoleQueryKey('account.profile.get'), }), })) @@ -106,7 +107,7 @@ const renderSnippetList = async () => { queries: { retry: false }, }, }) - queryClient.setQueryData(['account-profile'], { profile: { id: 'creator-1' } }) + queryClient.setQueryData(consoleQueryKey('account.profile.get'), { profile: { id: 'creator-1' } }) const store = createStore() seedRegisteredConsoleStateFixture(store) diff --git a/web/app/components/workflow/comment/comment-movement.browser.spec.tsx b/web/app/components/workflow/comment/comment-movement.browser.spec.tsx index 60436afced7..60d2a15b820 100644 --- a/web/app/components/workflow/comment/comment-movement.browser.spec.tsx +++ b/web/app/components/workflow/comment/comment-movement.browser.spec.tsx @@ -4,13 +4,14 @@ import { Suspense, useState } from 'react' import ReactFlow, { ReactFlowProvider } from 'reactflow' import { page, userEvent } from 'vite-plus/test/browser' import { render } from 'vitest-browser-react' +import { consoleQueryKey } from '@/test/console/operation-hooks' import { CommentIcon } from './comment-icon' import { CommentInput } from './comment-input' import 'reactflow/dist/style.css' vi.mock('@/features/account-profile/client', () => ({ userProfileQueryOptions: () => ({ - queryKey: ['profile'], + queryKey: consoleQueryKey('account.profile.get'), queryFn: async () => ({ profile: { id: 'author', name: 'Alice', avatar_url: null } }), }), })) diff --git a/web/app/components/workflow/comment/reply-deletion.browser.spec.tsx b/web/app/components/workflow/comment/reply-deletion.browser.spec.tsx index 71d031ab9cc..3dc58be4845 100644 --- a/web/app/components/workflow/comment/reply-deletion.browser.spec.tsx +++ b/web/app/components/workflow/comment/reply-deletion.browser.spec.tsx @@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { Suspense, useState } from 'react' import { page, userEvent } from 'vite-plus/test/browser' import { render } from 'vitest-browser-react' +import { consoleQueryKey } from '@/test/console/operation-hooks' import { EditorResourceScope } from '../editor-resource/provider' import { CommentThread } from './thread' @@ -20,7 +21,7 @@ vi.mock('../store', () => ({ })) vi.mock('@/features/account-profile/client', () => ({ userProfileQueryOptions: () => ({ - queryKey: ['profile'], + queryKey: consoleQueryKey('account.profile.get'), queryFn: async () => ({ profile: { id: 'author', name: 'Alice', avatar_url: null } }), }), })) diff --git a/web/app/device/__tests__/page-terminal.spec.tsx b/web/app/device/__tests__/page-terminal.spec.tsx index 4a648c47ac6..2c08ecc11d4 100644 --- a/web/app/device/__tests__/page-terminal.spec.tsx +++ b/web/app/device/__tests__/page-terminal.spec.tsx @@ -2,6 +2,7 @@ import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { DeviceFlowError } from '@/service/device-flow' +import { consoleQueryKey } from '@/test/console/operation-hooks' import { renderWithConsoleQuery as render } from '@/test/console/query-data' import DevicePage from '../page' @@ -22,7 +23,10 @@ vi.mock('@/service/device-flow', async (importOriginal) => ({ })) vi.mock('@/features/account-profile/client', () => ({ - userProfileQueryOptions: () => ({ queryKey: ['profile'], queryFn: async () => null }), + userProfileQueryOptions: () => ({ + queryKey: consoleQueryKey('account.profile.get'), + queryFn: async () => null, + }), })) const VALID_CODE = 'ABCD-3456' @@ -40,7 +44,7 @@ beforeEach(() => { async function reachTerminal(rejectWith: unknown) { mockDeviceLookup.mockRejectedValue(rejectWith) - render() + render(, { accountProfile: null }) const user = userEvent.setup() await user.type(screen.getByRole('textbox'), VALID_CODE) await user.click(screen.getByRole('button', { name: /deviceFlow.codeEntry.continue/i })) @@ -80,7 +84,7 @@ describe('error_sso dedicated view', () => { it('renders the dedicated SSO error screen (not the code-entry page)', async () => { mockSearchParams = { sso_error: 'sso_failed', user_code: 'ABCD-3456' } - render() + render(, { accountProfile: null }) expect(await screen.findByText(TITLE)).toBeInTheDocument() expect(await screen.findByText(GENERIC)).toBeInTheDocument() expect(screen.queryByRole('textbox')).not.toBeInTheDocument() @@ -89,7 +93,7 @@ describe('error_sso dedicated view', () => { it('shows the email special-case copy', async () => { mockSearchParams = { sso_error: 'email_belongs_to_dify_account', user_code: 'ABCD-3456' } - render() + render(, { accountProfile: null }) expect(await screen.findByText(EMAIL_COPY)).toBeInTheDocument() expect(screen.queryByText('email_belongs_to_dify_account')).not.toBeInTheDocument() }) @@ -98,7 +102,7 @@ describe('error_sso dedicated view', () => { const user = userEvent.setup() mockSearchParams = { sso_error: 'sso_failed', user_code: 'ABCD-3456' } mockDeviceLookup.mockResolvedValue({ valid: true }) - render() + render(, { accountProfile: null }) await screen.findByText(TITLE) await user.click(screen.getByRole('button', { name: BACK_TO_LOGIN })) await screen.findByText('deviceFlow.chooser.subtitle') @@ -106,7 +110,7 @@ describe('error_sso dedicated view', () => { }) it('shows no SSO error screen when sso_error is absent', () => { - render() + render(, { accountProfile: null }) expect(screen.getByRole('textbox')).toBeInTheDocument() expect(screen.queryByText(TITLE)).not.toBeInTheDocument() expect(document.title).toBe('deviceFlow.codeEntry.title - Dify') diff --git a/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx b/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx index dcbcd1932cc..f5414afc7b5 100644 --- a/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx @@ -187,31 +187,14 @@ vi.mock('@/service/console', async () => { }, }, agent: { - get: { - key: () => ['agents'], - }, + get: consoleOperationStub('agent.get'), byAgentId: { - get: { - queryKey: ({ input }: { input: { params: { agent_id: string } } }) => [ - 'agent-detail', - input.params.agent_id, - ], - }, + get: consoleOperationStub('agent.byAgentId.get'), apiAccess: { - get: { - queryKey: ({ input }: { input: { params: { agent_id: string } } }) => [ - 'agent-api-access', - input.params.agent_id, - ], - }, + get: consoleOperationStub('agent.byAgentId.apiAccess.get'), }, composer: { - get: { - queryKey: ({ input }: { input: { params: { agent_id: string } } }) => [ - 'agent-composer', - input.params.agent_id, - ], - }, + get: consoleOperationStub('agent.byAgentId.composer.get'), put: { mutationOptions: composerPutMutationOptions, }, @@ -222,9 +205,7 @@ vi.mock('@/service/console', async () => { }, }, versions: { - get: { - key: () => ['agent-versions'], - }, + get: consoleOperationStub('agent.byAgentId.versions.get'), }, }, }, @@ -370,11 +351,10 @@ describe('useAgentConfigureSync', () => { it('should automatically save configure page changes to draft', async () => { const { queryClient, store } = renderUseAgentConfigureSync() - queryClient.setQueryData(['agent-detail', 'agent-1'], { - active_config_is_published: true, - name: 'Agent', - permission_keys: Object.values(AgentPermission), - }) + queryClient.setQueryData( + consoleQuery.agent.byAgentId.get.queryKey({ input: { params: { agent_id: 'agent-1' } } }), + createAgentFixture({ name: 'Agent', permission_keys: Object.values(AgentPermission) }), + ) act(() => { store.set(agentComposerDraftAtom, { @@ -383,11 +363,13 @@ describe('useAgentConfigureSync', () => { }) }) - expect(queryClient.getQueryData(['agent-detail', 'agent-1'])).toEqual({ - active_config_is_published: true, - name: 'Agent', - permission_keys: Object.values(AgentPermission), - }) + expect( + queryClient.getQueryData( + consoleQuery.agent.byAgentId.get.queryKey({ input: { params: { agent_id: 'agent-1' } } }), + ), + ).toEqual( + createAgentFixture({ name: 'Agent', permission_keys: Object.values(AgentPermission) }), + ) expect(composerPutMutationFn).not.toHaveBeenCalled() await act(async () => { @@ -410,11 +392,13 @@ describe('useAgentConfigureSync', () => { }), }), ) - expect(queryClient.getQueryData(['agent-detail', 'agent-1'])).toEqual({ - active_config_is_published: true, - name: 'Agent', - permission_keys: Object.values(AgentPermission), - }) + expect( + queryClient.getQueryData( + consoleQuery.agent.byAgentId.get.queryKey({ input: { params: { agent_id: 'agent-1' } } }), + ), + ).toEqual( + createAgentFixture({ name: 'Agent', permission_keys: Object.values(AgentPermission) }), + ) }) it('should autosave only the latest committed Agent configuration', async () => { @@ -507,11 +491,10 @@ describe('useAgentConfigureSync', () => { it('should cancel pending autosave when the draft returns to the saved baseline', async () => { const { queryClient, store } = renderUseAgentConfigureSync() - queryClient.setQueryData(['agent-detail', 'agent-1'], { - active_config_is_published: true, - name: 'Agent', - permission_keys: Object.values(AgentPermission), - }) + queryClient.setQueryData( + consoleQuery.agent.byAgentId.get.queryKey({ input: { params: { agent_id: 'agent-1' } } }), + createAgentFixture({ name: 'Agent', permission_keys: Object.values(AgentPermission) }), + ) act(() => { store.set(agentComposerDraftAtom, { @@ -528,11 +511,13 @@ describe('useAgentConfigureSync', () => { }) expect(composerPutMutationFn).not.toHaveBeenCalled() - expect(queryClient.getQueryData(['agent-detail', 'agent-1'])).toEqual({ - active_config_is_published: true, - name: 'Agent', - permission_keys: Object.values(AgentPermission), - }) + expect( + queryClient.getQueryData( + consoleQuery.agent.byAgentId.get.queryKey({ input: { params: { agent_id: 'agent-1' } } }), + ), + ).toEqual( + createAgentFixture({ name: 'Agent', permission_keys: Object.values(AgentPermission) }), + ) }) it('should save dirty draft once when the page is closing', async () => { @@ -1027,22 +1012,23 @@ describe('useAgentConfigureSync', () => { it('should not save the draft immediately when the composer draft is unchanged', async () => { const { queryClient, result } = renderUseAgentConfigureSync() - queryClient.setQueryData(['agent-detail', 'agent-1'], { - active_config_is_published: true, - name: 'Agent', - permission_keys: Object.values(AgentPermission), - }) + queryClient.setQueryData( + consoleQuery.agent.byAgentId.get.queryKey({ input: { params: { agent_id: 'agent-1' } } }), + createAgentFixture({ name: 'Agent', permission_keys: Object.values(AgentPermission) }), + ) await act(async () => { await result.current.saveDraft() }) expect(composerPutMutationFn).not.toHaveBeenCalled() - expect(queryClient.getQueryData(['agent-detail', 'agent-1'])).toEqual({ - active_config_is_published: true, - name: 'Agent', - permission_keys: Object.values(AgentPermission), - }) + expect( + queryClient.getQueryData( + consoleQuery.agent.byAgentId.get.queryKey({ input: { params: { agent_id: 'agent-1' } } }), + ), + ).toEqual( + createAgentFixture({ name: 'Agent', permission_keys: Object.values(AgentPermission) }), + ) }) it('saves a newly selected model from the draft without waiting for a render', async () => { @@ -1632,9 +1618,10 @@ describe('useAgentConfigureSync', () => { enabled: false, currentModel: configuredModel, }) - queryClient.setQueryData(['agent-detail', 'agent-1'], { - permission_keys: [AgentPermission.TestAndRun], - }) + queryClient.setQueryData( + consoleQuery.agent.byAgentId.get.queryKey({ input: { params: { agent_id: 'agent-1' } } }), + createAgentFixture({ permission_keys: [AgentPermission.TestAndRun] }), + ) act(() => { store.set(agentComposerPromptAtom, 'Unsaved local changes') }) @@ -1654,9 +1641,10 @@ describe('useAgentConfigureSync', () => { publishEnabled: true, currentModel: configuredModel, }) - queryClient.setQueryData(['agent-detail', 'agent-1'], { - permission_keys: [AgentPermission.ReleaseAndVersion], - }) + queryClient.setQueryData( + consoleQuery.agent.byAgentId.get.queryKey({ input: { params: { agent_id: 'agent-1' } } }), + createAgentFixture({ permission_keys: [AgentPermission.ReleaseAndVersion] }), + ) await act(async () => { await expect(result.current.publishDraft()).resolves.toMatchObject({ kind: 'update' }) }) @@ -1669,7 +1657,10 @@ describe('useAgentConfigureSync', () => { const { queryClient, store, unmount } = renderUseAgentConfigureSync() act(() => { store.set(agentComposerPromptAtom, 'Dirty before revocation') - queryClient.setQueryData(['agent-detail', 'agent-1'], { permission_keys: [] }) + queryClient.setQueryData( + consoleQuery.agent.byAgentId.get.queryKey({ input: { params: { agent_id: 'agent-1' } } }), + createAgentFixture({ permission_keys: [] }), + ) unmount() }) await act(async () => { @@ -1689,9 +1680,10 @@ describe('useAgentConfigureSync', () => { await act(async () => { await Promise.resolve() }) - queryClient.setQueryData(['agent-detail', 'agent-1'], { - permission_keys: [AgentPermission.Edit], - }) + queryClient.setQueryData( + consoleQuery.agent.byAgentId.get.queryKey({ input: { params: { agent_id: 'agent-1' } } }), + createAgentFixture({ permission_keys: [AgentPermission.Edit] }), + ) await act(async () => { saving.resolve({ agent_soul: {} }) await publishing diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx index 809eff49dc0..44c625a6208 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx @@ -1,6 +1,6 @@ import type { AgentConfigApiContext } from '../../config-context' import type { AgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state' -import { QueryClient } from '@tanstack/react-query' +import { QueryClient, skipToken } from '@tanstack/react-query' import { act, render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useAtomValue } from 'jotai' @@ -71,17 +71,37 @@ vi.mock('@/utils/download', () => ({ downloadUrl: mocks.downloadUrl, })) -vi.mock('@/service/console', async () => { +vi.mock('@/service/console', async (importOriginal) => { + const actual = await importOriginal() const { consoleOperationStub } = await import('@/test/console/operation-hooks') + const { withConsoleQueryOverrides } = await import('@/test/console/service-responses') + const query = withConsoleQueryOverrides(actual.consoleQuery, { + 'agent.byAgentId.config.files.byName.preview.get': { queryOptions: mocks.previewQueryOptions }, + 'agent.byAgentId.config.files.byName.download.get': { + queryOptions: mocks.downloadQueryOptions, + }, + 'apps.byAppId.agent.config.files.byName.preview.get': { + queryOptions: mocks.previewQueryOptions, + }, + 'apps.byAppId.agent.config.files.byName.download.get': { + queryOptions: mocks.downloadQueryOptions, + }, + 'trialApps.byAppId.agent.config.files.byName.preview.get': { + queryOptions: mocks.previewQueryOptions, + }, + 'trialApps.byAppId.agent.config.files.byName.download.get': { + queryOptions: mocks.downloadQueryOptions, + }, + }) + return { consoleQuery: { systemFeatures: { get: { - queryKey: () => ['console', 'systemFeatures', 'get'], + ...consoleOperationStub('systemFeatures.get'), queryOptions: (options?: Record) => ({ - queryKey: ['console', 'systemFeatures', 'get'], + ...consoleOperationStub('systemFeatures.get').queryOptions(options), queryFn: () => new Promise(() => {}), - ...options, }), }, }, @@ -97,14 +117,10 @@ vi.mock('@/service/console', async () => { mutationOptions: () => ({ mutationFn: mocks.deleteFileMutationFn }), }, preview: { - get: { - queryOptions: mocks.previewQueryOptions, - }, + get: query.agent.byAgentId.config.files.byName.preview.get, }, download: { - get: { - queryOptions: mocks.downloadQueryOptions, - }, + get: query.agent.byAgentId.config.files.byName.download.get, }, }, }, @@ -124,14 +140,10 @@ vi.mock('@/service/console', async () => { mutationOptions: () => ({ mutationFn: mocks.deleteFileMutationFn }), }, preview: { - get: { - queryOptions: mocks.previewQueryOptions, - }, + get: query.apps.byAppId.agent.config.files.byName.preview.get, }, download: { - get: { - queryOptions: mocks.downloadQueryOptions, - }, + get: query.apps.byAppId.agent.config.files.byName.download.get, }, }, }, @@ -145,8 +157,12 @@ vi.mock('@/service/console', async () => { config: { files: { byName: { - preview: { get: { queryOptions: mocks.previewQueryOptions } }, - download: { get: { queryOptions: mocks.downloadQueryOptions } }, + preview: { + get: query.trialApps.byAppId.agent.config.files.byName.preview.get, + }, + download: { + get: query.trialApps.byAppId.agent.config.files.byName.download.get, + }, }, }, }, @@ -217,7 +233,7 @@ function renderAgentFiles({ }, }) queryClient.setQueryData( - ['console', 'systemFeatures', 'get'], + consoleQuery.systemFeatures.get.queryKey(), createSystemFeaturesFixture({ deployment_edition: 'COMMUNITY' }), ) @@ -256,7 +272,6 @@ describe('AgentFiles', () => { workflow_file_upload_limit: 10, }) mocks.previewQueryOptions.mockImplementation(({ input }) => ({ - queryKey: ['preview-config-file', input], queryFn: async () => ({ name: input.params.name, binary: false, @@ -265,7 +280,6 @@ describe('AgentFiles', () => { }), })) mocks.downloadQueryOptions.mockImplementation(({ input }) => ({ - queryKey: ['download-config-file', input], queryFn: async () => ({ url: `https://example.com/${input.params.name}`, }), @@ -528,19 +542,17 @@ describe('AgentFiles', () => { resolveDownload = resolve }) const downloadQueryFn = vi.fn(() => downloadResponse) - mocks.downloadQueryOptions.mockImplementation(({ input }) => ({ - queryKey: ['download-config-file', input], + mocks.downloadQueryOptions.mockImplementation(() => ({ queryFn: downloadQueryFn, })) const { queryClient } = renderAgentFiles() queryClient.setQueryData( - [ - 'download-config-file', - { + consoleQuery.agent.byAgentId.config.files.byName.download.get.queryKey({ + input: { params: { agent_id: 'agent-1', name: 'diagram.png' }, query: { draft_type: 'draft', version_id: undefined }, }, - ], + }), { url: 'https://example.com/expired-diagram.png' }, ) @@ -563,13 +575,12 @@ describe('AgentFiles', () => { const user = userEvent.setup() const { queryClient } = renderAgentFiles() queryClient.setQueryData( - [ - 'download-config-file', - { + consoleQuery.agent.byAgentId.config.files.byName.download.get.queryKey({ + input: { params: { agent_id: 'agent-1', name: 'diagram.png' }, query: { draft_type: 'draft', version_id: undefined }, }, - ], + }), { url: 'https://example.com/stale-diagram.png' }, ) @@ -599,7 +610,7 @@ describe('AgentFiles', () => { it('should download the selected file from the preview header action', async () => { const user = userEvent.setup() - renderAgentFiles() + const { queryClient } = renderAgentFiles() await user.click(screen.getByRole('button', { name: 'diagram.png' })) const dialog = await screen.findByRole('dialog') @@ -626,6 +637,14 @@ describe('AgentFiles', () => { url: 'https://example.com/diagram.png', fileName: 'diagram.png', }) + const trialFile = consoleQuery.trialApps.byAppId.agent.config.files.byName + for (const operation of [trialFile.preview.get, trialFile.download.get]) { + expect(queryClient.getQueryState(operation.queryKey({ input: skipToken }))).toMatchObject({ + status: 'pending', + fetchStatus: 'idle', + error: null, + }) + } }) it('should show config note as a virtual build note file and preview its content locally', async () => { @@ -791,10 +810,12 @@ describe('AgentFiles', () => { await user.click(within(imageDialog).getByRole('button', { name: 'common.operation.close' })) queryClient.setQueryData( - [ - 'preview-config-file', - { params: { app_id: 'trial-1', name: 'brief.md' }, query: { version_id: undefined } }, - ], + consoleQuery.trialApps.byAppId.agent.config.files.byName.preview.get.queryKey({ + input: { + params: { app_id: 'trial-1', name: 'brief.md' }, + query: { version_id: undefined }, + }, + }), { name: 'brief.md', binary: true, truncated: false, text: null }, ) await user.click(screen.getByRole('button', { name: 'brief.md' })) diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/index.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/index.tsx index b36e16b3b8c..6bc5da3698f 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/index.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/index.tsx @@ -134,17 +134,15 @@ function AgentFileItem({ enabled: isPreviewOpen && !!previewFileId && !isVirtualPreviewFile && !!apiContext.workflow, }) const trialPreviewQuery = useQuery( - apiContext.trialAppId - ? { - ...consoleQuery.trialApps.byAppId.agent.config.files.byName.preview.get.queryOptions({ - input: { - params: { app_id: apiContext.trialAppId, name: previewFileId ?? '' }, - query: { version_id: apiContext.versionId }, - }, - }), - enabled: isPreviewOpen && !!previewFileId && !isVirtualPreviewFile, - } - : { queryKey: ['agent-v2', 'trial-file-preview-disabled'], queryFn: skipToken }, + consoleQuery.trialApps.byAppId.agent.config.files.byName.preview.get.queryOptions({ + input: apiContext.trialAppId + ? { + params: { app_id: apiContext.trialAppId, name: previewFileId ?? '' }, + query: { version_id: apiContext.versionId }, + } + : skipToken, + enabled: !!apiContext.trialAppId && isPreviewOpen && !!previewFileId && !isVirtualPreviewFile, + }), ) const previewQuery = apiContext.trialAppId ? trialPreviewQuery @@ -191,18 +189,16 @@ function AgentFileItem({ enabled: shouldDownloadPreviewFile && !!apiContext.workflow, }) const trialDownloadQuery = useQuery( - apiContext.trialAppId - ? { - ...consoleQuery.trialApps.byAppId.agent.config.files.byName.download.get.queryOptions({ - input: { - params: { app_id: apiContext.trialAppId, name: previewFileId ?? '' }, - query: { version_id: apiContext.versionId }, - }, - }), - staleTime: 0, - enabled: shouldDownloadPreviewFile, - } - : { queryKey: ['agent-v2', 'trial-file-download-disabled'], queryFn: skipToken }, + consoleQuery.trialApps.byAppId.agent.config.files.byName.download.get.queryOptions({ + input: apiContext.trialAppId + ? { + params: { app_id: apiContext.trialAppId, name: previewFileId ?? '' }, + query: { version_id: apiContext.versionId }, + } + : skipToken, + enabled: !!apiContext.trialAppId && shouldDownloadPreviewFile, + staleTime: 0, + }), ) const downloadQuery = apiContext.trialAppId ? trialDownloadQuery diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx index 6f586eb80ab..3e9b9b43411 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx @@ -1,7 +1,7 @@ import type { SkillResponse } from '@dify/contracts/api/console/workspaces/types.gen' import type { AgentConfigApiContext } from '../../config-context' import type { AgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { QueryClient, QueryClientProvider, skipToken } from '@tanstack/react-query' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useAtomValue } from 'jotai' @@ -50,7 +50,6 @@ type ConfigSkillDownloadQueryOptionsInput = { } const mocks = vi.hoisted(() => ({ - agentSkillBindingsKey: vi.fn((_options: unknown): unknown[] => ['workspace-agent-skills']), agentSkillBindingsQueryOptions: vi.fn((_options: unknown) => ({})), deleteSkillMutationFn: vi.fn(async (_input: unknown) => ({ removed_names: ['Tender Analyzer'], @@ -114,21 +113,66 @@ vi.mock('@/context/permission-state', async () => { }) vi.mock('@/service/console', async (importOriginal) => { + const actual = await importOriginal() const { consoleOperationStub } = await import('@/test/console/operation-hooks') + const { withConsoleQueryOverrides } = await import('@/test/console/service-responses') + const query = withConsoleQueryOverrides(actual.consoleQuery, { + 'tags.get': { queryOptions: mocks.workspaceSkillTagsQueryOptions }, + 'agent.byAgentId.config.skills.byName.download.get': { + queryOptions: mocks.skillDownloadQueryOptions, + }, + 'agent.byAgentId.config.skills.byName.inspect.get': { queryOptions: mocks.inspectQueryOptions }, + 'agent.byAgentId.config.skills.byName.files.preview.get': { + queryOptions: mocks.previewQueryOptions, + }, + 'agent.byAgentId.config.skills.byName.files.download.get': { + queryOptions: mocks.downloadQueryOptions, + }, + 'apps.byAppId.agent.config.skills.byName.download.get': { + queryOptions: mocks.skillDownloadQueryOptions, + }, + 'apps.byAppId.agent.config.skills.byName.inspect.get': { + queryOptions: mocks.inspectQueryOptions, + }, + 'apps.byAppId.agent.config.skills.byName.files.preview.get': { + queryOptions: mocks.previewQueryOptions, + }, + 'apps.byAppId.agent.config.skills.byName.files.download.get': { + queryOptions: mocks.downloadQueryOptions, + }, + 'trialApps.byAppId.agent.config.skills.byName.download.get': { + queryOptions: mocks.skillDownloadQueryOptions, + }, + 'trialApps.byAppId.agent.config.skills.byName.inspect.get': { + queryOptions: mocks.inspectQueryOptions, + }, + 'trialApps.byAppId.agent.config.skills.byName.files.preview.get': { + queryOptions: mocks.previewQueryOptions, + }, + 'trialApps.byAppId.agent.config.skills.byName.files.download.get': { + queryOptions: mocks.downloadQueryOptions, + }, + 'workspaces.current.agents.byAgentId.skills.get': { + queryOptions: mocks.agentSkillBindingsQueryOptions, + }, + 'workspaces.current.skills.get': { + queryOptions: mocks.workspaceSkillsQueryOptions, + infiniteOptions: mocks.workspaceSkillsInfiniteOptions, + }, + }) + return { consoleQuery: { files: { upload: { get: consoleOperationStub('files.upload.get') } }, features: (await importOriginal()).consoleQuery.features, tags: { - get: { - queryOptions: mocks.workspaceSkillTagsQueryOptions, - }, + get: query.tags.get, }, agent: { byAgentId: { composer: { get: { - key: vi.fn((_options: unknown): unknown[] => ['agent-composer']), + key: actual.consoleQuery.agent.byAgentId.composer.get.key, }, }, config: { @@ -143,25 +187,17 @@ vi.mock('@/service/console', async (importOriginal) => { mutationOptions: () => ({ mutationFn: mocks.deleteSkillMutationFn }), }, download: { - get: { - queryOptions: mocks.skillDownloadQueryOptions, - }, + get: query.agent.byAgentId.config.skills.byName.download.get, }, inspect: { - get: { - queryOptions: mocks.inspectQueryOptions, - }, + get: query.agent.byAgentId.config.skills.byName.inspect.get, }, files: { preview: { - get: { - queryOptions: mocks.previewQueryOptions, - }, + get: query.agent.byAgentId.config.skills.byName.files.preview.get, }, download: { - get: { - queryOptions: mocks.downloadQueryOptions, - }, + get: query.agent.byAgentId.config.skills.byName.files.download.get, }, }, }, @@ -184,25 +220,17 @@ vi.mock('@/service/console', async (importOriginal) => { mutationOptions: () => ({ mutationFn: mocks.deleteSkillMutationFn }), }, download: { - get: { - queryOptions: mocks.skillDownloadQueryOptions, - }, + get: query.apps.byAppId.agent.config.skills.byName.download.get, }, inspect: { - get: { - queryOptions: mocks.inspectQueryOptions, - }, + get: query.apps.byAppId.agent.config.skills.byName.inspect.get, }, files: { preview: { - get: { - queryOptions: mocks.previewQueryOptions, - }, + get: query.apps.byAppId.agent.config.skills.byName.files.preview.get, }, download: { - get: { - queryOptions: mocks.downloadQueryOptions, - }, + get: query.apps.byAppId.agent.config.skills.byName.files.download.get, }, }, }, @@ -217,11 +245,19 @@ vi.mock('@/service/console', async (importOriginal) => { config: { skills: { byName: { - download: { get: { queryOptions: mocks.skillDownloadQueryOptions } }, - inspect: { get: { queryOptions: mocks.inspectQueryOptions } }, + download: { + get: query.trialApps.byAppId.agent.config.skills.byName.download.get, + }, + inspect: { + get: query.trialApps.byAppId.agent.config.skills.byName.inspect.get, + }, files: { - preview: { get: { queryOptions: mocks.previewQueryOptions } }, - download: { get: { queryOptions: mocks.downloadQueryOptions } }, + preview: { + get: query.trialApps.byAppId.agent.config.skills.byName.files.preview.get, + }, + download: { + get: query.trialApps.byAppId.agent.config.skills.byName.files.download.get, + }, }, }, }, @@ -234,10 +270,7 @@ vi.mock('@/service/console', async (importOriginal) => { agents: { byAgentId: { skills: { - get: { - key: mocks.agentSkillBindingsKey, - queryOptions: mocks.agentSkillBindingsQueryOptions, - }, + get: query.workspaces.current.agents.byAgentId.skills.get, put: { mutationOptions: () => ({ mutationFn: mocks.replaceAgentSkillBindingsMutationFn, @@ -248,8 +281,8 @@ vi.mock('@/service/console', async (importOriginal) => { }, skills: { get: { - queryOptions: mocks.workspaceSkillsQueryOptions, - infiniteOptions: mocks.workspaceSkillsInfiniteOptions, + ...query.workspaces.current.skills.get, + infiniteOptions: query.workspaces.current.skills.get.infiniteOptions, }, }, }, @@ -401,15 +434,10 @@ describe('AgentSkills', () => { headers: { 'Content-Type': 'application/octet-stream' }, }), ) - mocks.agentSkillBindingsKey.mockImplementation((options) => { - const { input } = options as { input: { params: { agent_id: string } } } - return ['workspace-agent-skills', input] - }) mocks.agentSkillBindingsQueryOptions.mockImplementation((options) => { const { input } = options as { input: { params: { agent_id: string } } } return { - queryKey: ['workspace-agent-skills', input], queryFn: async () => ({ agent_id: input.params.agent_id, skill_ids: [], @@ -418,7 +446,6 @@ describe('AgentSkills', () => { } }) mocks.inspectQueryOptions.mockImplementation(({ input }) => ({ - queryKey: ['inspect-skill', input], queryFn: async () => ({ id: input.params.name, name: input.params.name, @@ -465,7 +492,6 @@ describe('AgentSkills', () => { }), })) mocks.previewQueryOptions.mockImplementation(({ input }) => ({ - queryKey: ['preview-skill-file', input], queryFn: async () => ({ path: input.query.path, binary: input.query.path.endsWith('.bin'), @@ -474,22 +500,17 @@ describe('AgentSkills', () => { }), })) mocks.downloadQueryOptions.mockImplementation(({ input }) => ({ - queryKey: ['download-skill-file', input], queryFn: async () => ({ url: `/console/api/agent/agent-1/config/skills/Tender%20Analyzer/files/content?path=${encodeURIComponent(input.query.path)}`, }), })) mocks.skillDownloadQueryOptions.mockImplementation(({ input }) => ({ - queryKey: ['download-skill', input], queryFn: async () => ({ url: `https://example.com/${input.params.name}.skill`, }), })) - mocks.workspaceSkillsQueryOptions.mockImplementation((options) => { - const { input } = options as { input: { query?: { keyword?: string } } } - + mocks.workspaceSkillsQueryOptions.mockImplementation(() => { return { - queryKey: ['workspace-skills', input], queryFn: async () => ({ data: [], }), @@ -505,7 +526,6 @@ describe('AgentSkills', () => { } return { - queryKey: ['workspace-skills', input(initialPageParam)], queryFn: async ({ pageParam = initialPageParam }: { pageParam?: number }) => ({ data: [], has_more: false, @@ -518,7 +538,6 @@ describe('AgentSkills', () => { } }) mocks.workspaceSkillTagsQueryOptions.mockImplementation(() => ({ - queryKey: ['workspace-skill-tags'], queryFn: async () => [ { id: 'tag-support', name: 'support', type: 'skill', binding_count: '1' }, { id: 'tag-sales', name: 'sales', type: 'skill', binding_count: '1' }, @@ -723,7 +742,7 @@ describe('AgentSkills', () => { it('should bind workspace skills without adding them to inline config skills', async () => { const user = userEvent.setup() mocks.workspaceSkillsInfiniteOptions.mockImplementation((options) => { - const { input, getNextPageParam, initialPageParam } = options as { + const { getNextPageParam, initialPageParam } = options as { input: (pageParam: number) => { query?: { keyword?: string; limit?: number; page?: number } } @@ -732,7 +751,6 @@ describe('AgentSkills', () => { } return { - queryKey: ['workspace-skills', input(initialPageParam)], queryFn: async ({ pageParam = initialPageParam }: { pageParam?: number }) => ({ data: [ { @@ -821,11 +839,8 @@ describe('AgentSkills', () => { let resolveBindings: | ((value: { agent_id: string; skill_ids: string[]; data: never[] }) => void) | undefined - mocks.agentSkillBindingsQueryOptions.mockImplementation((options) => { - const { input } = options as { input: { params: { agent_id: string } } } - + mocks.agentSkillBindingsQueryOptions.mockImplementation(() => { return { - queryKey: ['workspace-agent-skills', input], queryFn: () => new Promise<{ agent_id: string; skill_ids: string[]; data: never[] }>((resolve) => { resolveBindings = resolve @@ -840,7 +855,6 @@ describe('AgentSkills', () => { } return { - queryKey: ['workspace-skills', input(initialPageParam)], queryFn: async ({ pageParam = initialPageParam }: { pageParam?: number }) => ({ data: [createWorkspaceSkill()], has_more: false, @@ -900,7 +914,6 @@ describe('AgentSkills', () => { } return { - queryKey: ['workspace-skills', input(initialPageParam)], queryFn: async ({ pageParam = initialPageParam }: { pageParam?: number }) => ({ data: [createWorkspaceSkill()], has_more: false, @@ -937,7 +950,6 @@ describe('AgentSkills', () => { const { input } = options as { input: { params: { agent_id: string } } } return { - queryKey: ['workspace-agent-skills', input], queryFn: async () => ({ agent_id: input.params.agent_id, skill_ids: boundSkills.map((skill) => skill.id), @@ -959,7 +971,6 @@ describe('AgentSkills', () => { } return { - queryKey: ['workspace-skills', input(initialPageParam)], queryFn: async ({ pageParam = initialPageParam }: { pageParam?: number }) => ({ data: [createWorkspaceSkill({ id: 'candidate-skill' })], has_more: false, @@ -1004,7 +1015,6 @@ describe('AgentSkills', () => { } return { - queryKey: ['workspace-skills', input(initialPageParam)], queryFn: async ({ pageParam = initialPageParam }: { pageParam?: number }) => ({ data: [createWorkspaceSkill()], has_more: false, @@ -1027,7 +1037,7 @@ describe('AgentSkills', () => { it('should allow workflow agent nodes to bind workspace skills', async () => { const user = userEvent.setup() mocks.workspaceSkillsInfiniteOptions.mockImplementation((options) => { - const { input, getNextPageParam, initialPageParam } = options as { + const { getNextPageParam, initialPageParam } = options as { input: (pageParam: number) => { query?: { keyword?: string; limit?: number; page?: number } } @@ -1036,7 +1046,6 @@ describe('AgentSkills', () => { } return { - queryKey: ['workspace-skills', input(initialPageParam)], queryFn: async ({ pageParam = initialPageParam }: { pageParam?: number }) => ({ data: [ { @@ -1106,7 +1115,6 @@ describe('AgentSkills', () => { const { input } = options as { input: { params: { agent_id: string } } } return { - queryKey: ['workspace-agent-skills', input], queryFn: async () => ({ agent_id: input.params.agent_id, skill_ids: ['workspace-skill-1'], @@ -1123,7 +1131,7 @@ describe('AgentSkills', () => { } }) mocks.workspaceSkillsInfiniteOptions.mockImplementation((options) => { - const { input, getNextPageParam, initialPageParam } = options as { + const { getNextPageParam, initialPageParam } = options as { input: (pageParam: number) => { query?: { keyword?: string; limit?: number; page?: number } } @@ -1132,7 +1140,6 @@ describe('AgentSkills', () => { } return { - queryKey: ['workspace-skills', input(initialPageParam)], queryFn: async ({ pageParam = initialPageParam }: { pageParam?: number }) => ({ data: [ createWorkspaceSkill(), @@ -1177,7 +1184,7 @@ describe('AgentSkills', () => { it('should fetch the next workspace skill page when scrolling the selector', async () => { const user = userEvent.setup() mocks.workspaceSkillsInfiniteOptions.mockImplementation((options) => { - const { input, getNextPageParam, initialPageParam } = options as { + const { getNextPageParam, initialPageParam } = options as { input: (pageParam: number) => { query?: { keyword?: string; limit?: number; page?: number } } @@ -1186,7 +1193,6 @@ describe('AgentSkills', () => { } return { - queryKey: ['workspace-skills', input(initialPageParam)], queryFn: async ({ pageParam = initialPageParam }: { pageParam?: number }) => ({ data: pageParam === 1 @@ -1232,7 +1238,6 @@ describe('AgentSkills', () => { const { input } = options as { input: { params: { agent_id: string } } } return { - queryKey: ['workspace-agent-skills', input], queryFn: async () => ({ agent_id: input.params.agent_id, skill_ids: ['workspace-skill-1'], @@ -1285,7 +1290,6 @@ describe('AgentSkills', () => { const { input } = options as { input: { params: { agent_id: string } } } return { - queryKey: ['workspace-agent-skills', input], queryFn: async () => ({ agent_id: input.params.agent_id, skill_ids: ['workspace-skill-1'], @@ -1463,13 +1467,12 @@ describe('AgentSkills', () => { const user = userEvent.setup() const { queryClient } = renderAgentSkills() queryClient.setQueryData( - [ - 'download-skill', - { + consoleQuery.agent.byAgentId.config.skills.byName.download.get.queryKey({ + input: { params: { agent_id: 'agent-1', name: 'Tender Analyzer' }, query: { draft_type: 'draft', version_id: undefined }, }, - ], + }), { url: 'https://example.com/stale.skill' }, ) @@ -1662,7 +1665,7 @@ describe('AgentSkills', () => { }, query: { path: 'references/guide.md', - draft_type: 'draft', + draft_type: 'draft' as const, version_id: undefined, }, } @@ -1670,7 +1673,12 @@ describe('AgentSkills', () => { const currentContentUrl = '/console/api/agent/agent-1/config/skills/Tender%20Analyzer/files/content?path=references%2Fguide.md' const staleBlob = new Blob(['stale skill file']) - queryClient.setQueryData(['download-skill-file', downloadInput], { url: staleContentUrl }) + queryClient.setQueryData( + consoleQuery.agent.byAgentId.config.skills.byName.files.download.get.queryKey({ + input: downloadInput, + }), + { url: staleContentUrl }, + ) queryClient.setQueryData(['agent-v2', 'skill-file-content', staleContentUrl], staleBlob) queryClient.setQueryData(['agent-v2', 'skill-file-content', currentContentUrl], staleBlob) mocks.fetch.mockResolvedValueOnce( @@ -1816,6 +1824,21 @@ describe('AgentSkills', () => { }) }) + const trialSkill = consoleQuery.trialApps.byAppId.agent.config.skills.byName + for (const operation of [ + trialSkill.inspect.get, + trialSkill.files.preview.get, + trialSkill.files.download.get, + ]) { + expect( + view.queryClient.getQueryState(operation.queryKey({ input: skipToken })), + ).toMatchObject({ + status: 'pending', + fetchStatus: 'idle', + error: null, + }) + } + view.unmount() expect(revokeObjectURL).toHaveBeenCalledWith('blob:skill-image') }) @@ -1854,7 +1877,6 @@ describe('AgentSkills', () => { const { input } = options as { input: { params: { agent_id: string } } } return { - queryKey: ['workspace-agent-skills', input], queryFn: async () => ({ agent_id: input.params.agent_id, skill_ids: ['workspace-skill-1'], diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/use-skill-detail.ts b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/use-skill-detail.ts index 7abd66f6fef..63ca4833911 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/use-skill-detail.ts +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/use-skill-detail.ts @@ -217,17 +217,15 @@ export function useAgentSkillDetail({ enabled: isOpen && !!apiContext.workflow, }) const trialSkillInspectQuery = useQuery( - apiContext.trialAppId - ? { - ...consoleQuery.trialApps.byAppId.agent.config.skills.byName.inspect.get.queryOptions({ - input: { - params: { app_id: apiContext.trialAppId, name: skill.name }, - query: { version_id: apiContext.versionId }, - }, - }), - enabled: isOpen, - } - : { queryKey: ['agent-v2', 'trial-skill-inspect-disabled'], queryFn: skipToken }, + consoleQuery.trialApps.byAppId.agent.config.skills.byName.inspect.get.queryOptions({ + input: apiContext.trialAppId + ? { + params: { app_id: apiContext.trialAppId, name: skill.name }, + query: { version_id: apiContext.versionId }, + } + : skipToken, + enabled: !!apiContext.trialAppId && isOpen, + }), ) const inspectQuery = apiContext.trialAppId ? trialSkillInspectQuery @@ -284,19 +282,15 @@ export function useAgentSkillDetail({ enabled: isOpen && !!selectedPreviewPath && !!apiContext.workflow, }) const trialPreviewQuery = useQuery( - apiContext.trialAppId - ? { - ...consoleQuery.trialApps.byAppId.agent.config.skills.byName.files.preview.get.queryOptions( - { - input: { - params: { app_id: apiContext.trialAppId, name: skill.name }, - query: { path: selectedPreviewPath ?? '', version_id: apiContext.versionId }, - }, - }, - ), - enabled: isOpen && !!selectedPreviewPath, - } - : { queryKey: ['agent-v2', 'trial-skill-preview-disabled'], queryFn: skipToken }, + consoleQuery.trialApps.byAppId.agent.config.skills.byName.files.preview.get.queryOptions({ + input: apiContext.trialAppId + ? { + params: { app_id: apiContext.trialAppId, name: skill.name }, + query: { path: selectedPreviewPath ?? '', version_id: apiContext.versionId }, + } + : skipToken, + enabled: !!apiContext.trialAppId && isOpen && !!selectedPreviewPath, + }), ) const previewQuery = apiContext.trialAppId ? trialPreviewQuery @@ -341,20 +335,16 @@ export function useAgentSkillDetail({ enabled: shouldLoadImagePreview && !!apiContext.workflow, }) const trialDownloadQuery = useQuery( - apiContext.trialAppId - ? { - ...consoleQuery.trialApps.byAppId.agent.config.skills.byName.files.download.get.queryOptions( - { - input: { - params: { app_id: apiContext.trialAppId, name: skill.name }, - query: { path: selectedPreviewPath ?? '', version_id: apiContext.versionId }, - }, - }, - ), - staleTime: 0, - enabled: shouldLoadImagePreview, - } - : { queryKey: ['agent-v2', 'trial-skill-download-disabled'], queryFn: skipToken }, + consoleQuery.trialApps.byAppId.agent.config.skills.byName.files.download.get.queryOptions({ + input: apiContext.trialAppId + ? { + params: { app_id: apiContext.trialAppId, name: skill.name }, + query: { path: selectedPreviewPath ?? '', version_id: apiContext.versionId }, + } + : skipToken, + enabled: !!apiContext.trialAppId && shouldLoadImagePreview, + staleTime: 0, + }), ) const downloadQuery = apiContext.trialAppId ? trialDownloadQuery diff --git a/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/working-directory-panel.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/working-directory-panel.spec.tsx index fb395cf60f6..db410e50220 100644 --- a/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/working-directory-panel.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/preview/__tests__/working-directory-panel.spec.tsx @@ -86,29 +86,44 @@ const previewSourceCases = [ }, ] as const -vi.mock('@/service/console', () => ({ - consoleClient: { - agent: { - byAgentId: { - sandbox: { - files: { - download: { - post: mocks.sandboxFileDownloadClientPost, +vi.mock('@/service/console', async (importOriginal) => { + const actual = await importOriginal() + const { withConsoleQueryOverrides } = await import('@/test/console/service-responses') + const query = withConsoleQueryOverrides(actual.consoleQuery, { + 'agent.byAgentId.sandbox.get': { queryOptions: mocks.sandboxInfoQueryOptions }, + 'agent.byAgentId.sandbox.files.get': { queryOptions: mocks.sandboxFilesQueryOptions }, + 'agent.byAgentId.sandbox.files.read.get': { queryOptions: mocks.sandboxFileReadQueryOptions }, + 'apps.byAppId.workflowRuns.byWorkflowRunId.agentNodes.byNodeId.sandbox.files.get': { + queryOptions: mocks.workflowSandboxFilesQueryOptions, + }, + 'apps.byAppId.workflowRuns.byWorkflowRunId.agentNodes.byNodeId.sandbox.files.read.get': { + queryOptions: mocks.workflowSandboxFileReadQueryOptions, + }, + }) + return { + consoleClient: { + agent: { + byAgentId: { + sandbox: { + files: { + download: { + post: mocks.sandboxFileDownloadClientPost, + }, }, }, }, }, - }, - apps: { - byAppId: { - workflowRuns: { - byWorkflowRunId: { - agentNodes: { - byNodeId: { - sandbox: { - files: { - download: { - post: mocks.workflowSandboxFileDownloadClientPost, + apps: { + byAppId: { + workflowRuns: { + byWorkflowRunId: { + agentNodes: { + byNodeId: { + sandbox: { + files: { + download: { + post: mocks.workflowSandboxFileDownloadClientPost, + }, }, }, }, @@ -118,53 +133,57 @@ vi.mock('@/service/console', () => ({ }, }, }, - }, - consoleQuery: { - agent: { - byAgentId: { - sandbox: { - get: { - queryOptions: mocks.sandboxInfoQueryOptions, - }, - files: { + consoleQuery: { + agent: { + byAgentId: { + sandbox: { get: { - queryOptions: mocks.sandboxFilesQueryOptions, + queryOptions: query.agent.byAgentId.sandbox.get.queryOptions, }, - read: { + files: { get: { - queryOptions: mocks.sandboxFileReadQueryOptions, + queryOptions: query.agent.byAgentId.sandbox.files.get.queryOptions, }, - }, - download: { - post: { - mutationOptions: () => ({ mutationFn: mocks.sandboxFileDownloadMutationFn }), + read: { + get: { + queryOptions: query.agent.byAgentId.sandbox.files.read.get.queryOptions, + }, + }, + download: { + post: { + mutationOptions: () => ({ mutationFn: mocks.sandboxFileDownloadMutationFn }), + }, }, }, }, }, }, - }, - apps: { - byAppId: { - workflowRuns: { - byWorkflowRunId: { - agentNodes: { - byNodeId: { - sandbox: { - files: { - get: { - queryOptions: mocks.workflowSandboxFilesQueryOptions, - }, - read: { + apps: { + byAppId: { + workflowRuns: { + byWorkflowRunId: { + agentNodes: { + byNodeId: { + sandbox: { + files: { get: { - queryOptions: mocks.workflowSandboxFileReadQueryOptions, + queryOptions: + query.apps.byAppId.workflowRuns.byWorkflowRunId.agentNodes.byNodeId + .sandbox.files.get.queryOptions, }, - }, - download: { - post: { - mutationOptions: () => ({ - mutationFn: mocks.workflowSandboxFileDownloadMutationFn, - }), + read: { + get: { + queryOptions: + query.apps.byAppId.workflowRuns.byWorkflowRunId.agentNodes.byNodeId + .sandbox.files.read.get.queryOptions, + }, + }, + download: { + post: { + mutationOptions: () => ({ + mutationFn: mocks.workflowSandboxFileDownloadMutationFn, + }), + }, }, }, }, @@ -175,8 +194,8 @@ vi.mock('@/service/console', () => ({ }, }, }, - }, -})) + } +}) vi.mock('@/utils/download', () => ({ downloadUrl: mocks.downloadUrl, @@ -195,6 +214,7 @@ function createDeferred() { resolve = promiseResolve reject = promiseReject }) + return { promise, reject, resolve } } @@ -247,7 +267,6 @@ function mockFileListEntries( ? mocks.sandboxFilesQueryOptions : mocks.workflowSandboxFilesQueryOptions queryOptions.mockImplementation(({ input }: QueryOptionsInput) => ({ - queryKey: [`${source.type}-sandbox-files`, input, entries], queryFn: async () => ({ path: input.query?.path ?? '~', entries, @@ -261,7 +280,6 @@ function mockFileReadAsBinary(source: AgentWorkingDirectorySource) { ? mocks.sandboxFileReadQueryOptions : mocks.workflowSandboxFileReadQueryOptions queryOptions.mockImplementation(({ input }: QueryOptionsInput) => ({ - queryKey: [`${source.type}-sandbox-file-read`, input], queryFn: async () => ({ binary: true, path: input.query?.path ?? '', @@ -304,13 +322,11 @@ describe('AgentWorkingDirectoryPanel', () => { beforeEach(() => { vi.clearAllMocks() mocks.sandboxInfoQueryOptions.mockImplementation(() => ({ - queryKey: ['sandbox-info'], queryFn: async () => ({ workspace_cwd: 'workspace', }), })) mocks.sandboxFilesQueryOptions.mockImplementation(({ input }: QueryOptionsInput) => ({ - queryKey: ['sandbox-files', input], queryFn: async () => ({ path: input.query?.path ?? '~', entries: [ @@ -322,7 +338,6 @@ describe('AgentWorkingDirectoryPanel', () => { }), })) mocks.sandboxFileReadQueryOptions.mockImplementation(({ input }: QueryOptionsInput) => ({ - queryKey: ['sandbox-file-read', input], queryFn: async () => ({ binary: input.query?.path?.endsWith('model.bin') ?? false, path: input.query?.path ?? '', @@ -331,7 +346,6 @@ describe('AgentWorkingDirectoryPanel', () => { }), })) mocks.workflowSandboxFilesQueryOptions.mockImplementation(({ input }: QueryOptionsInput) => ({ - queryKey: ['workflow-sandbox-files', input], queryFn: async () => ({ path: input.query?.path ?? '.', entries: [{ name: 'chart.png', type: 'file' }], @@ -339,7 +353,6 @@ describe('AgentWorkingDirectoryPanel', () => { })) mocks.workflowSandboxFileReadQueryOptions.mockImplementation( ({ input }: QueryOptionsInput) => ({ - queryKey: ['workflow-sandbox-file-read', input], queryFn: async () => ({ binary: false, path: input.query?.path ?? '', @@ -355,7 +368,6 @@ describe('AgentWorkingDirectoryPanel', () => { mocks.sandboxFilesQueryOptions.mockImplementation(({ input }: QueryOptionsInput) => { const path = input.query?.path ?? '~' return { - queryKey: ['sandbox-files-by-folder', path], queryFn: async () => ({ path, entries: @@ -393,7 +405,6 @@ describe('AgentWorkingDirectoryPanel', () => { it('should separate persistent and temporary files by their sandbox path roots', async () => { const user = userEvent.setup() mocks.sandboxFilesQueryOptions.mockImplementation(({ input }: QueryOptionsInput) => ({ - queryKey: ['sandbox-files-by-root', input], queryFn: async () => ({ path: input.query?.path ?? '~', entries: diff --git a/web/features/skills/__tests__/detail-page-clipboard.spec.tsx b/web/features/skills/__tests__/detail-page-clipboard.spec.tsx index b030b272bee..b99116ce8a9 100644 --- a/web/features/skills/__tests__/detail-page-clipboard.spec.tsx +++ b/web/features/skills/__tests__/detail-page-clipboard.spec.tsx @@ -326,9 +326,8 @@ describe('SkillDetailPage clipboard', () => { }) return mocks.skillDetail }) - mocks.skillDetailKey.mockReturnValue(['skill-detail']) + mocks.skillDetailQueryOptions.mockImplementation(() => ({ - queryKey: ['skill-detail'], queryFn: async () => mocks.skillDetail, })) renderSkillDetailPage() @@ -390,9 +389,8 @@ describe('SkillDetailPage clipboard', () => { return copiedDetail }) mocks.skillDetailGetFn.mockResolvedValueOnce(latestDetail) - mocks.skillDetailKey.mockReturnValue(['skill-detail']) + mocks.skillDetailQueryOptions.mockImplementation(() => ({ - queryKey: ['skill-detail'], queryFn: async () => mocks.skillDetail, })) renderSkillDetailPage() @@ -472,9 +470,8 @@ describe('SkillDetailPage clipboard', () => { .mockRejectedValueOnce(conflict) .mockResolvedValueOnce(afterBetaCopy) mocks.skillDetailGetFn.mockResolvedValueOnce(refreshedDetail) - mocks.skillDetailKey.mockReturnValue(['skill-detail']) + mocks.skillDetailQueryOptions.mockImplementation(() => ({ - queryKey: ['skill-detail'], queryFn: async () => mocks.skillDetail, })) renderSkillDetailPage() diff --git a/web/features/skills/__tests__/detail-page-metadata.spec.tsx b/web/features/skills/__tests__/detail-page-metadata.spec.tsx index a812e7ca0f4..5a6e53cb789 100644 --- a/web/features/skills/__tests__/detail-page-metadata.spec.tsx +++ b/web/features/skills/__tests__/detail-page-metadata.spec.tsx @@ -152,9 +152,8 @@ describe('SkillDetailPage metadata', () => { it('keeps selected tags visible and prevents reopening while the metadata request is pending', async () => { const user = userEvent.setup() let resolveMutation!: (detail: SkillDetailResponse) => void - mocks.skillDetailKey.mockReturnValue(['skill-detail']) + mocks.skillDetailQueryOptions.mockImplementation(() => ({ - queryKey: ['skill-detail'], queryFn: async () => mocks.skillDetail, })) mocks.skillMetadataMutationFn.mockImplementation( @@ -207,9 +206,8 @@ describe('SkillDetailPage metadata', () => { it('restores persisted tags when saving the visible selection fails', async () => { const user = userEvent.setup() let rejectMutation!: (error: Error) => void - mocks.skillDetailKey.mockReturnValue(['skill-detail']) + mocks.skillDetailQueryOptions.mockImplementation(() => ({ - queryKey: ['skill-detail'], queryFn: async () => mocks.skillDetail, })) mocks.skillDetail = createSkillDetail({ tags: ['Search'] }) @@ -275,9 +273,8 @@ describe('SkillDetailPage metadata', () => { it('hides a removed tag only after the metadata request finishes', async () => { const user = userEvent.setup() let resolveMutation!: (detail: SkillDetailResponse) => void - mocks.skillDetailKey.mockReturnValue(['skill-detail']) + mocks.skillDetailQueryOptions.mockImplementation(() => ({ - queryKey: ['skill-detail'], queryFn: async () => mocks.skillDetail, })) mocks.skillDetail = createSkillDetail({ @@ -457,8 +454,7 @@ describe('SkillDetailPage metadata', () => { const detailPromise = new Promise((resolve) => { resolveDetail = resolve }) - mocks.skillDetailQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-detail', options], + mocks.skillDetailQueryOptions.mockImplementation(() => ({ queryFn: () => detailPromise, })) const { onUrlUpdate, queryClient } = renderSkillDetailPage({ @@ -521,8 +517,7 @@ describe('SkillDetailPage metadata', () => { const user = userEvent.setup() mocks.skillDetail = createSkillDetail({ reference_count: 1 }) preserveDraftFilesOnSave() - mocks.skillReferencesQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-references', options], + mocks.skillReferencesQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ data: [createAgentReference()] }), })) mocks.deleteSkillMutationFn.mockResolvedValue({}) @@ -568,8 +563,7 @@ describe('SkillDetailPage metadata', () => { let referenceRequestCount = 0 let shouldHangReferenceRequest = false mocks.skillDetail = createSkillDetail({ reference_count: 0 }) - mocks.skillReferencesQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-references-pending', options], + mocks.skillReferencesQueryOptions.mockImplementation(() => ({ queryFn: () => { referenceRequestCount += 1 if (!shouldHangReferenceRequest) return Promise.resolve({ data: [] }) @@ -611,8 +605,7 @@ describe('SkillDetailPage metadata', () => { }) it('does not expose mutable More actions while viewing a published version', async () => { - mocks.skillVersionsQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-versions', options], + mocks.skillVersionsQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ data: [createSkillVersion()] }), })) renderSkillDetailPage() diff --git a/web/features/skills/__tests__/detail-page-navigation.spec.tsx b/web/features/skills/__tests__/detail-page-navigation.spec.tsx index 352e17dc8a8..413a32a36f6 100644 --- a/web/features/skills/__tests__/detail-page-navigation.spec.tsx +++ b/web/features/skills/__tests__/detail-page-navigation.spec.tsx @@ -84,12 +84,10 @@ describe('SkillDetailPage navigation', () => { const { default: SkillDetailPageRoute } = await import('../detail-page') const user = userEvent.setup() const version = createSkillVersion({ id: 'original-version', version_name: 'Original version' }) - mocks.skillVersionsQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-versions', options], + mocks.skillVersionsQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ data: [version] }), })) - mocks.skillVersionDetailQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-version-detail', options], + mocks.skillVersionDetailQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ ...version, files: createSkillDetail().files, diff --git a/web/features/skills/__tests__/detail-page-publishing.spec.tsx b/web/features/skills/__tests__/detail-page-publishing.spec.tsx index ff20ea7f8b3..7bd065b24be 100644 --- a/web/features/skills/__tests__/detail-page-publishing.spec.tsx +++ b/web/features/skills/__tests__/detail-page-publishing.spec.tsx @@ -3,6 +3,7 @@ import { QueryClient } from '@tanstack/react-query' import { act, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { consoleQueryKey } from '@/test/console/operation-hooks' import { createAgentReference, createSkillDetail, @@ -52,8 +53,7 @@ describe('SkillDetailPage publishing', () => { it('shows a publish confirmation for referenced skills before publishing updates', async () => { const user = userEvent.setup() mocks.skillDetail = createSkillDetail({ reference_count: 1 }) - mocks.skillReferencesQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-references', options], + mocks.skillReferencesQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ data: [createAgentReference()], }), @@ -140,19 +140,18 @@ describe('SkillDetailPage publishing', () => { mutations: { retry: false }, }, }) - const referencesQueryKey = [ - 'skill-references', + const referencesQueryKey = consoleQueryKey( + 'workspaces.current.skills.bySkillId.references.get', { params: { skill_id: 'skill-1', }, }, - ] + ) let remoteReferences: SkillReferenceResponse[] = [] const referencesQueryFn = vi.fn(async () => ({ data: remoteReferences })) mocks.skillDetail = createSkillDetail({ reference_count: 0 }) - mocks.skillReferencesQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-references', (options as { input: unknown }).input], + mocks.skillReferencesQueryOptions.mockImplementation(() => ({ queryFn: referencesQueryFn, })) queryClient.setQueryData(referencesQueryKey, { data: [] }) @@ -204,8 +203,7 @@ describe('SkillDetailPage publishing', () => { return { data: [] } }) mocks.skillDetail = createSkillDetail({ reference_count: 0 }) - mocks.skillReferencesQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-references', (options as { input: unknown }).input], + mocks.skillReferencesQueryOptions.mockImplementation(() => ({ queryFn: referencesQueryFn, })) @@ -241,8 +239,7 @@ describe('SkillDetailPage publishing', () => { it('cancels publishing from the referenced skill confirmation dialog', async () => { const user = userEvent.setup() mocks.skillDetail = createSkillDetail({ reference_count: 1 }) - mocks.skillReferencesQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-references', options], + mocks.skillReferencesQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ data: [createAgentReference()], }), @@ -274,8 +271,7 @@ describe('SkillDetailPage publishing', () => { it('opens the sidebar references panel from the file tree footer', async () => { const user = userEvent.setup() mocks.skillDetail = createSkillDetail({ reference_count: 1 }) - mocks.skillReferencesQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-references', options], + mocks.skillReferencesQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ data: [createAgentReference({ display_name: 'Sidebar Agent', name: 'sidebar-agent' })], }), @@ -301,8 +297,7 @@ describe('SkillDetailPage publishing', () => { it('renders a non-interactive reference count when no agent uses the skill', async () => { mocks.skillDetail = createSkillDetail({ reference_count: 0 }) - mocks.skillReferencesQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-references', options], + mocks.skillReferencesQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ data: [] }), })) @@ -321,8 +316,7 @@ describe('SkillDetailPage publishing', () => { it('uses the references query count when the cached sidebar reference count is stale', async () => { mocks.skillDetail = createSkillDetail({ reference_count: 0 }) - mocks.skillReferencesQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-references', options], + mocks.skillReferencesQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ data: [createAgentReference({ display_name: 'Sidebar Agent', name: 'sidebar-agent' })], }), @@ -348,8 +342,7 @@ describe('SkillDetailPage publishing', () => { }), ) mocks.skillDetail = createSkillDetail({ reference_count: references.length }) - mocks.skillReferencesQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-references', options], + mocks.skillReferencesQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ data: references }), })) @@ -379,14 +372,12 @@ describe('SkillDetailPage publishing', () => { id: 'version-1', version_name: 'Rollback target', }) - mocks.skillVersionsQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-versions', options], + mocks.skillVersionsQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ data: [version], }), })) - mocks.skillVersionDetailQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-version-detail', options], + mocks.skillVersionDetailQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ ...version, files: [ @@ -447,14 +438,12 @@ describe('SkillDetailPage publishing', () => { version_name: '', publish_note: 'Release note only', }) - mocks.skillVersionsQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-versions', options], + mocks.skillVersionsQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ data: [version], }), })) - mocks.skillVersionDetailQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-version-detail', options], + mocks.skillVersionDetailQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ ...version, files: [ @@ -511,8 +500,7 @@ describe('SkillDetailPage publishing', () => { version_number: 2, version_name: '', }) - mocks.skillVersionsQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-versions', options], + mocks.skillVersionsQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ data: [namedVersion, unnamedVersion], }), @@ -553,8 +541,7 @@ describe('SkillDetailPage publishing', () => { publish_note: 'Initial note', version_name: 'Initial version', }) - mocks.skillVersionsQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-versions', options], + mocks.skillVersionsQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ data: [version], }), @@ -606,11 +593,9 @@ describe('SkillDetailPage publishing', () => { : 'skill.skillManagement.detail.restoreVersion' const version = createSkillVersion({ version_name: 'Target version' }) mocks.skillVersionsQueryOptions.mockImplementation(() => ({ - queryKey: ['skill-versions'], queryFn: async () => ({ data: [version] }), })) mocks.skillVersionDetailQueryOptions.mockImplementation(() => ({ - queryKey: ['skill-version-detail'], queryFn: async () => ({ ...version, files: createSkillDetail().files }), })) renderSkillDetailPage() diff --git a/web/features/skills/__tests__/detail-page.fixture.tsx b/web/features/skills/__tests__/detail-page.fixture.tsx index eaa48750929..3372b1506f7 100644 --- a/web/features/skills/__tests__/detail-page.fixture.tsx +++ b/web/features/skills/__tests__/detail-page.fixture.tsx @@ -1,10 +1,8 @@ import type { - GetWorkspacesCurrentModelsModelTypesByModelTypeData, SkillDetailResponse, SkillReferenceResponse, SkillVersionResponse, } from '@dify/contracts/api/console/workspaces/types.gen' -import type { OperationKey } from '@orpc/tanstack-query' import type userEvent from '@testing-library/user-event' import type { ReactNode } from 'react' import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations' @@ -41,18 +39,11 @@ const mocks = vi.hoisted(() => ({ | undefined, skillDetail: undefined as SkillDetailResponse | undefined, skillDetailGetFn: vi.fn(), - skillDetailKey: vi.fn((_options: { input?: unknown; type?: string }): unknown[] => [ - 'skill-detail', - ]), skillDetailQueryOptions: vi.fn((_options: unknown) => ({})), - agentSkillBindingsKey: vi.fn((_options: unknown): unknown[] => ['agent-skill-bindings']), - skillListKey: vi.fn((_options: unknown): unknown[] => ['skills']), skillTags: [] as { count: number; tag: string }[], skillMetadataMutationFn: vi.fn(), skillReferencesQueryOptions: vi.fn((_options: unknown) => ({})), - skillTagsKey: vi.fn((_options: unknown): unknown[] => ['skill-tags']), skillTagsQueryOptions: vi.fn((_options: unknown) => ({})), - skillVersionsKey: vi.fn((_options: unknown): unknown[] => ['skill-versions']), skillVersionsQueryOptions: vi.fn((_options: unknown) => ({})), skillVersionDetailQueryOptions: vi.fn((_options: unknown) => ({})), textGenerationModelList: [] as { @@ -195,6 +186,26 @@ vi.mock('@/utils/download', () => ({ vi.mock('@/service/console', async (importOriginal) => { const actual = await importOriginal() + const { withConsoleQueryOverrides, withConsoleResponses } = + await import('@/test/console/service-responses') + const modelQuery = withConsoleResponses(actual.consoleQuery, { + 'workspaces.current.models.modelTypes.byModelType.get': () => + mocks.textGenerationModelListGet(), + }) + const query = withConsoleQueryOverrides(actual.consoleQuery, { + 'workspaces.current.skills.tags.get': { queryOptions: mocks.skillTagsQueryOptions }, + 'workspaces.current.skills.bySkillId.get': { queryOptions: mocks.skillDetailQueryOptions }, + 'workspaces.current.skills.bySkillId.references.get': { + queryOptions: mocks.skillReferencesQueryOptions, + }, + 'workspaces.current.skills.bySkillId.versions.get': { + queryOptions: mocks.skillVersionsQueryOptions, + }, + 'workspaces.current.skills.bySkillId.versions.byVersionId.get': { + queryOptions: mocks.skillVersionDetailQueryOptions, + }, + }) + return { consoleClient: { workspaces: { @@ -210,25 +221,25 @@ vi.mock('@/service/console', async (importOriginal) => { consoleQuery: { workspaces: { current: { - models: actual.consoleQuery.workspaces.current.models, + models: modelQuery.workspaces.current.models, modelProviders: actual.consoleQuery.workspaces.current.modelProviders, agents: { byAgentId: { skills: { get: { - key: mocks.agentSkillBindingsKey, + key: actual.consoleQuery.workspaces.current.agents.byAgentId.skills.get.key, }, }, }, }, skills: { get: { - key: mocks.skillListKey, + key: actual.consoleQuery.workspaces.current.skills.get.key, }, tags: { get: { - key: mocks.skillTagsKey, - queryOptions: mocks.skillTagsQueryOptions, + key: actual.consoleQuery.workspaces.current.skills.tags.get.key, + queryOptions: query.workspaces.current.skills.tags.get.queryOptions, }, }, bySkillId: { @@ -241,8 +252,8 @@ vi.mock('@/service/console', async (importOriginal) => { }, }, get: { - key: mocks.skillDetailKey, - queryOptions: mocks.skillDetailQueryOptions, + key: actual.consoleQuery.workspaces.current.skills.bySkillId.get.key, + queryOptions: query.workspaces.current.skills.bySkillId.get.queryOptions, }, patch: { mutationOptions: () => ({ mutationFn: mocks.skillMetadataMutationFn }), @@ -257,7 +268,8 @@ vi.mock('@/service/console', async (importOriginal) => { }, references: { get: { - queryOptions: mocks.skillReferencesQueryOptions, + queryOptions: + query.workspaces.current.skills.bySkillId.references.get.queryOptions, }, }, restore: actual.consoleQuery.workspaces.current.skills.bySkillId.restore, @@ -273,12 +285,14 @@ vi.mock('@/service/console', async (importOriginal) => { }, versions: { get: { - key: mocks.skillVersionsKey, - queryOptions: mocks.skillVersionsQueryOptions, + key: actual.consoleQuery.workspaces.current.skills.bySkillId.versions.get.key, + queryOptions: query.workspaces.current.skills.bySkillId.versions.get.queryOptions, }, byVersionId: { get: { - queryOptions: mocks.skillVersionDetailQueryOptions, + queryOptions: + query.workspaces.current.skills.bySkillId.versions.byVersionId.get + .queryOptions, }, patch: { mutationOptions: () => ({ mutationFn: mocks.versionPatchMutationFn }), @@ -723,47 +737,38 @@ export function resetDetailPageFixture() { })) mocks.skillDetail = createSkillDetail() mocks.skillDetailGetFn.mockImplementation(async () => mocks.skillDetail) - mocks.skillDetailKey.mockImplementation(({ input }) => ['skill-detail', { input }]) - mocks.agentSkillBindingsKey.mockImplementation((options) => ['agent-skill-bindings', options]) - mocks.skillVersionsKey.mockImplementation((options) => ['skill-versions', options]) - mocks.skillListKey.mockImplementation((options) => ['skills', options]) + mocks.skillTags = [ { count: 3, tag: 'Search' }, { count: 2, tag: 'Productivity' }, { count: 1, tag: 'Utilities' }, ] - mocks.skillTagsKey.mockImplementation((options) => ['skill-tags', options]) + mocks.skillTagsQueryOptions.mockImplementation(() => ({ - queryKey: ['skill-tags'], queryFn: async () => ({ data: [], }), })) - mocks.skillDetailQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-detail', options], + mocks.skillDetailQueryOptions.mockImplementation(() => ({ queryFn: async () => mocks.skillDetail, })) - mocks.skillVersionsQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-versions', options], + mocks.skillVersionsQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ data: [], }), })) - mocks.skillVersionDetailQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-version-detail', options], + mocks.skillVersionDetailQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ ...mocks.skillDetail, files: [], }), })) - mocks.skillReferencesQueryOptions.mockImplementation((options) => ({ - queryKey: ['skill-references', options], + mocks.skillReferencesQueryOptions.mockImplementation(() => ({ queryFn: async () => ({ data: [], }), })) mocks.skillTagsQueryOptions.mockImplementation(() => ({ - queryKey: ['skill-tags'], queryFn: async () => ({ data: mocks.skillTags, }), @@ -873,19 +878,3 @@ export function resetDetailPageFixture() { afterEach(() => { vi.unstubAllGlobals() }) - -vi.mock('@tanstack/react-query', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - useQuery: (options: { - queryKey: OperationKey< - 'query', - { params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] } - > - }) => - options.queryKey[0].includes('modelTypes') - ? actual.useQuery({ ...options, queryFn: mocks.textGenerationModelListGet }) - : actual.useQuery(options), - } -}) diff --git a/web/features/system-features/__tests__/server.spec.ts b/web/features/system-features/__tests__/server.spec.ts index 5e23b6ff6c3..52e865f7611 100644 --- a/web/features/system-features/__tests__/server.spec.ts +++ b/web/features/system-features/__tests__/server.spec.ts @@ -1,12 +1,12 @@ // @vitest-environment node import { QueryClient } from '@tanstack/react-query' +import { consoleQuery } from '@/service/console' const mocks = vi.hoisted(() => ({ connection: vi.fn(async () => undefined), getQueryClient: vi.fn(), getSystemFeatures: vi.fn(), - queryKey: [['console', 'systemFeatures', 'get'], { type: 'query' }] as const, })) vi.mock('server-only', () => ({})) @@ -19,20 +19,16 @@ vi.mock('@/next/server', () => ({ connection: mocks.connection, })) -vi.mock('@/service/console', () => ({ - consoleQuery: { - systemFeatures: { - get: { - queryOptions: (options?: { staleTime?: 'static' }) => ({ - queryKey: mocks.queryKey, - queryFn: mocks.getSystemFeatures, - retry: false, - ...options, - }), - }, - }, - }, -})) +vi.mock('@/service/console', async (importOriginal) => { + const actual = await importOriginal() + const { withConsoleResponses } = await import('@/test/console/service-responses') + return { + ...actual, + consoleQuery: withConsoleResponses(actual.consoleQuery, { + 'systemFeatures.get': () => mocks.getSystemFeatures(), + }), + } +}) describe('System Features server requests', () => { beforeEach(() => { @@ -54,7 +50,7 @@ describe('System Features server requests', () => { expect(mocks.getSystemFeatures).toHaveBeenCalledOnce() expect(dehydrateSystemFeatures().queries).toEqual([ expect.objectContaining({ - queryKey: mocks.queryKey, + queryKey: consoleQuery.systemFeatures.get.queryKey(), state: expect.objectContaining({ data: { deployment_edition: 'CLOUD' }, status: 'success', diff --git a/web/service/access-control/__tests__/use-app-access-control.spec.tsx b/web/service/access-control/__tests__/use-app-access-control.spec.tsx index 0a3f6bae681..ae855e305b0 100644 --- a/web/service/access-control/__tests__/use-app-access-control.spec.tsx +++ b/web/service/access-control/__tests__/use-app-access-control.spec.tsx @@ -26,24 +26,12 @@ vi.mock('@/service/share', () => ({ vi.mock('@/service/console', async (importOriginal) => { const actual = await importOriginal() + const { withConsoleResponses } = await import('@/test/console/service-responses') return { ...actual, - consoleQuery: { - ...actual.consoleQuery, - systemFeatures: actual.consoleQuery.systemFeatures, - enterprise: { - ...actual.consoleQuery.enterprise, - webAppAuth: { - ...actual.consoleQuery.enterprise.webAppAuth, - getWebAppWhitelistSubjects: { - queryOptions: ({ input }: { input: { query: { appId?: string } } }) => ({ - queryKey: ['web-app-whitelist-subjects', input.query.appId], - queryFn: () => mockGetWebAppWhitelistSubjects(input), - }), - }, - }, - }, - }, + consoleQuery: withConsoleResponses(actual.consoleQuery, { + 'enterprise.webAppAuth.getWebAppWhitelistSubjects': mockGetWebAppWhitelistSubjects, + }), } }) diff --git a/web/test/console/account-profile.ts b/web/test/console/account-profile.ts index 73ea4698e7e..adfcb03ac69 100644 --- a/web/test/console/account-profile.ts +++ b/web/test/console/account-profile.ts @@ -4,15 +4,13 @@ import type { RenderOptions } from '@testing-library/react' import type { ReactElement } from 'react' import type { UserProfileWithMeta } from '@/features/account-profile/client' import { render } from '@testing-library/react' +import { consoleQueryKey } from '@/test/console/operation-hooks' import { createQueryClientWrapper } from '@/test/console/query-client' import { createTestQueryClient } from '@/test/query-client' type AccountProfileMeta = UserProfileWithMeta['meta'] -const accountProfileQueryKey = [ - ['console', 'account', 'profile', 'get'], - { type: 'query' }, -] as const +const accountProfileQueryKey = consoleQueryKey('account.profile.get') const createAccountProfileFixture = ( overrides: Partial = {}, diff --git a/web/test/console/current-workspace.ts b/web/test/console/current-workspace.ts index bcfdd56ec0d..c929604b113 100644 --- a/web/test/console/current-workspace.ts +++ b/web/test/console/current-workspace.ts @@ -1,10 +1,8 @@ import type { GetWorkspacesCurrentSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen' import type { QueryClient, QueryKey } from '@tanstack/react-query' +import { consoleQueryKey } from '@/test/console/operation-hooks' -export const currentWorkspaceQueryKey = [ - ['console', 'workspaces', 'current', 'summary', 'get'], - { type: 'query' }, -] as const +export const currentWorkspaceQueryKey = consoleQueryKey('workspaces.current.summary.get') const createCurrentWorkspaceFixture = ( overrides: Partial = {}, diff --git a/web/test/console/operation-hooks.ts b/web/test/console/operation-hooks.ts index 884b54811ef..38bb715dddc 100644 --- a/web/test/console/operation-hooks.ts +++ b/web/test/console/operation-hooks.ts @@ -1,12 +1,24 @@ +import type { OperationKeyOptions, OperationType } from '@orpc/tanstack-query' +import { generateOperationKey } from '@orpc/tanstack-query' + +// Keep the path prefix aligned with createConsoleQuery in service/console/query-policies.ts. +const consoleOperationKey = ( + path: string, + options: OperationKeyOptions = {}, +) => generateOperationKey(['console', ...path.split('.')], options) + +// Seed helpers also run with partial console mocks, so they cannot rely on consoleQuery. +export const consoleQueryKey = (path: string, input?: unknown) => + consoleOperationKey(path, { type: 'query', input }) + /** * Stands in for one console operation inside a fully mocked `@/service/console` - * module, producing the same key shape the real utilities use. + * module, producing keys with oRPC's own generator. */ export const consoleOperationStub = (path: string) => { - const queryKey = (options?: { input?: unknown }) => - [['console', ...path.split('.')], { type: 'query', input: options?.input }] as const + const queryKey = (options?: { input?: unknown }) => consoleQueryKey(path, options?.input) return { - key: () => [['console', ...path.split('.')]] as const, + key: () => consoleOperationKey(path), queryKey, queryOptions: (options: { input?: unknown } = {}) => ({ ...options, @@ -14,7 +26,7 @@ export const consoleOperationStub = (path: string) => { }), mutationOptions: (options: object = {}) => ({ ...options, - mutationKey: [['console', ...path.split('.')], { type: 'mutation' }], + mutationKey: consoleOperationKey(path, { type: 'mutation' }), }), } } diff --git a/web/test/console/query-data.ts b/web/test/console/query-data.ts index fbb53a6c53a..4178ea8de07 100644 --- a/web/test/console/query-data.ts +++ b/web/test/console/query-data.ts @@ -29,6 +29,7 @@ import { ensureCurrentWorkspaceQuery, seedCurrentWorkspaceQuery, } from '@/test/console/current-workspace' +import { consoleQueryKey } from '@/test/console/operation-hooks' import { createQueryClientWrapper } from '@/test/console/query-client' import { createSystemFeaturesFixture, @@ -58,8 +59,8 @@ type CurrentWorkspaceQueryProvider = { } } -const fallbackTrialModelsQueryKey = ['console', 'trialModels', 'get'] as const -const fallbackAppDslVersionQueryKey = ['console', 'appDslVersion', 'get'] as const +const fallbackTrialModelsQueryKey = consoleQueryKey('trialModels.get') +const fallbackAppDslVersionQueryKey = consoleQueryKey('appDslVersion.get') const getTrialModelsQueryKey = () => { const trialModelsQuery = (consoleQuery as { trialModels?: TrialModelsQueryProvider }).trialModels diff --git a/web/test/console/service-responses.ts b/web/test/console/service-responses.ts index 67e1286293e..23c41506a18 100644 --- a/web/test/console/service-responses.ts +++ b/web/test/console/service-responses.ts @@ -3,6 +3,41 @@ import { skipToken } from '@tanstack/react-query' type ResponseHandler = (input: never) => unknown +type QueryOverrides = Partial< + Record<'queryOptions' | 'infiniteOptions', (options: never) => object> +> + +/** Keep generated keys and policies when a scenario supplies custom query functions/options. */ +export function withConsoleQueryOverrides( + query: typeof consoleQuery, + overrides: Record, +): typeof consoleQuery { + const paths = Object.keys(overrides) + const wrap = (target: T, path: string): T => + new Proxy(target, { + get(current, property, receiver) { + const value = Reflect.get(current, property, receiver) + if (typeof property !== 'string') return value + const override = + property === 'queryOptions' || property === 'infiniteOptions' + ? overrides[path]?.[property] + : undefined + if (override) { + return (options: { input?: unknown } = {}) => { + const original = (value as (options: unknown) => { queryKey: unknown })(options) + if (options.input === skipToken) return original + return { ...original, ...override(options as never), queryKey: original.queryKey } + } + } + const nextPath = path ? `${path}.${property}` : property + if (paths.some((key) => key === nextPath || key.startsWith(`${nextPath}.`))) + return wrap(value as object, nextPath) + return value + }, + }) + return wrap(query, '') +} + /** Replace transport responses while retaining generated keys/options and real Query observers. */ export function withConsoleResponses( query: typeof consoleQuery, diff --git a/web/test/console/workspace-permissions.ts b/web/test/console/workspace-permissions.ts index f8e1c596d9b..3e1c59a4408 100644 --- a/web/test/console/workspace-permissions.ts +++ b/web/test/console/workspace-permissions.ts @@ -1,10 +1,10 @@ import type { MyPermissionsResponse } from '@dify/contracts/api/console/workspaces/types.gen' import type { QueryClient } from '@tanstack/react-query' +import { consoleQueryKey } from '@/test/console/operation-hooks' -const currentWorkspacePermissionsQueryKey = [ - ['console', 'workspaces', 'current', 'rbac', 'myPermissions', 'get'], - { type: 'query' }, -] as const +const currentWorkspacePermissionsQueryKey = consoleQueryKey( + 'workspaces.current.rbac.myPermissions.get', +) const createWorkspacePermissionsFixture = ( permissionKeys: readonly string[] = [],