refactor(web): use generated oRPC query keys (#44415)

This commit is contained in:
Stephen Zhou authored and GitHub committed 2026-10-20 08:43:33 +00:00
1 parent a30dd002ee
commit 33c881e893
32 files changed
+551 -527

No files matched your search

-10
View File
@@ -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
@@ -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(<QueryClientProvider client={client}>{element}</QueryClientProvider>)
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' })
@@ -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,
})
@@ -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', () => ({
@@ -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,
@@ -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' } },
}),
})
})
})
@@ -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 }),
}),
}))
@@ -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<typeof import('@/service/console')>()
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 {
@@ -43,15 +43,19 @@ vi.mock('@/service/marketplace', async (importOriginal) => {
...original,
marketplaceQuery: {
searchAdvanced: {
queryOptions: ({ input }: { input: unknown }) => ({
queryKey: ['marketplace', 'plugins', input],
queryOptions: (
options: Parameters<typeof original.marketplaceQuery.searchAdvanced.queryOptions>[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<typeof original.marketplaceQuery.templateSearch.queryOptions>[0],
) => ({
...original.marketplaceQuery.templateSearch.queryOptions(options),
queryFn: () => mockTemplateSearch(options.input),
}),
},
},
@@ -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)
@@ -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 } }),
}),
}))
@@ -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 } }),
}),
}))
@@ -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(<DevicePage />)
render(<DevicePage />, { 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(<DevicePage />)
render(<DevicePage />, { 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(<DevicePage />)
render(<DevicePage />, { 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(<DevicePage />)
render(<DevicePage />, { 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(<DevicePage />)
render(<DevicePage />, { accountProfile: null })
expect(screen.getByRole('textbox')).toBeInTheDocument()
expect(screen.queryByText(TITLE)).not.toBeInTheDocument()
expect(document.title).toBe('deviceFlow.codeEntry.title - Dify')
@@ -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
@@ -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<typeof import('@/service/console')>()
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<string, unknown>) => ({
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' }))
@@ -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
@@ -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<typeof import('@/service/console')>()
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<typeof import('@/service/console')>()).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'],
@@ -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
@@ -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<typeof import('@/service/console')>()
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<T>() {
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:
@@ -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()
@@ -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<SkillDetailResponse>((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()
@@ -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,
@@ -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()
@@ -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<typeof import('@/service/console')>()
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<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: (options: {
queryKey: OperationKey<
'query',
{ params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] }
>
}) =>
options.queryKey[0].includes('modelTypes')
? actual.useQuery({ ...options, queryFn: mocks.textGenerationModelListGet })
: actual.useQuery(options),
}
})
@@ -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<typeof import('@/service/console')>()
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',
@@ -26,24 +26,12 @@ vi.mock('@/service/share', () => ({
vi.mock('@/service/console', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/service/console')>()
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,
}),
}
})
+2 -4
View File
@@ -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<GetAccountProfileResponse> = {},
+2 -4
View File
@@ -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<GetWorkspacesCurrentSummaryResponse> = {},
+17 -5
View File
@@ -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 = <TType extends OperationType, TInput = unknown>(
path: string,
options: OperationKeyOptions<TType, TInput> = {},
) => 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' }),
}),
}
}
+3 -2
View File
@@ -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
+35
View File
@@ -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<string, QueryOverrides>,
): typeof consoleQuery {
const paths = Object.keys(overrides)
const wrap = <T extends object>(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,
+4 -4
View File
@@ -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[] = [],