mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 18:58:35 +08:00
895 lines
26 KiB
TypeScript
895 lines
26 KiB
TypeScript
import type { PluginInstallationItemResponse } from '@dify/contracts/api/console/workspaces/types.gen'
|
|
import type { ReactNode } from 'react'
|
|
import type { Permissions, PluginTaskStart } from '@/app/components/plugins/types'
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
import { act, renderHook, waitFor } from '@testing-library/react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { FormTypeEnum } from '@/app/components/base/form/types'
|
|
import {
|
|
AUTO_UPDATE_MODE,
|
|
AUTO_UPDATE_STRATEGY,
|
|
} from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
|
|
import {
|
|
PermissionType,
|
|
PluginCategoryEnum,
|
|
PluginSource,
|
|
TaskStatus,
|
|
} from '@/app/components/plugins/types'
|
|
import {
|
|
normalizeInstalledPluginDetail,
|
|
useInstalledPluginList,
|
|
useMutationPluginAutoUpgradeSettings,
|
|
useMutationPluginPermissionSettings,
|
|
usePluginAutoUpgradeSettings,
|
|
usePluginTaskList,
|
|
} from '../use-plugins'
|
|
|
|
const { mockGet, mockPost, mockWorkspacePermissionKeysAtom } = vi.hoisted(() => ({
|
|
mockGet: vi.fn(),
|
|
mockPost: vi.fn(),
|
|
mockWorkspacePermissionKeysAtom: Symbol('workspacePermissionKeysAtom'),
|
|
}))
|
|
|
|
vi.mock('@/service/base', () => ({
|
|
get: mockGet,
|
|
getMarketplace: vi.fn(),
|
|
post: mockPost,
|
|
postMarketplace: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list', () => ({
|
|
default: () => ({
|
|
refreshPluginList: vi.fn(),
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/context/account-state', () => ({
|
|
workspacePermissionKeysAtom: mockWorkspacePermissionKeysAtom,
|
|
}))
|
|
vi.mock('@/context/workspace-state', () => ({
|
|
workspacePermissionKeysAtom: mockWorkspacePermissionKeysAtom,
|
|
}))
|
|
vi.mock('@/context/permission-state', () => ({
|
|
workspacePermissionKeysAtom: mockWorkspacePermissionKeysAtom,
|
|
}))
|
|
vi.mock('@/context/version-state', () => ({
|
|
workspacePermissionKeysAtom: mockWorkspacePermissionKeysAtom,
|
|
}))
|
|
vi.mock('@/context/system-features-state', () => ({
|
|
workspacePermissionKeysAtom: mockWorkspacePermissionKeysAtom,
|
|
}))
|
|
|
|
vi.mock('jotai', () => ({
|
|
useAtomValue: (atom: unknown) => {
|
|
if (atom === mockWorkspacePermissionKeysAtom) return ['plugin.install']
|
|
|
|
throw new Error('Unexpected atom')
|
|
},
|
|
}))
|
|
|
|
vi.mock('../use-tools', () => ({
|
|
useInvalidateAllBuiltInTools: () => vi.fn(),
|
|
}))
|
|
|
|
const createQueryClient = () =>
|
|
new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
retry: false,
|
|
},
|
|
mutations: {
|
|
retry: false,
|
|
},
|
|
},
|
|
})
|
|
|
|
const createWrapper = (queryClient: QueryClient) => {
|
|
return function Wrapper({ children }: { children: ReactNode }) {
|
|
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
}
|
|
}
|
|
|
|
const createPluginInstallation = (): PluginInstallationItemResponse => ({
|
|
id: 'installation-row-id',
|
|
created_at: '2026-06-01T00:00:00Z',
|
|
updated_at: '2026-06-02T00:00:00Z',
|
|
plugin_id: 'langgenius/full-declaration',
|
|
plugin_unique_identifier: 'langgenius/full-declaration:1.0.0@test',
|
|
tenant_id: 'tenant-id',
|
|
endpoints_setups: 1,
|
|
endpoints_active: 1,
|
|
version: '1.0.0',
|
|
source: PluginSource.marketplace,
|
|
runtime_type: 'local',
|
|
checksum: 'checksum',
|
|
meta: {
|
|
repo: 'langgenius/full-declaration',
|
|
version: '1.0.0',
|
|
package: 'full-declaration.difypkg',
|
|
},
|
|
declaration: {
|
|
version: '1.0.0',
|
|
author: 'Dify',
|
|
icon: 'icon.svg',
|
|
icon_dark: 'icon-dark.svg',
|
|
name: 'full-declaration',
|
|
category: PluginCategoryEnum.trigger,
|
|
label: { en_US: 'Full declaration', zh_Hans: 'Full declaration' },
|
|
description: { en_US: 'Full declaration plugin', zh_Hans: 'Full declaration plugin' },
|
|
created_at: '2026-06-01T00:00:00Z',
|
|
resource: {},
|
|
plugins: {},
|
|
verified: true,
|
|
tags: ['automation'],
|
|
meta: {
|
|
version: '1.0.0',
|
|
minimum_dify_version: '1.4.0',
|
|
},
|
|
tool: {
|
|
identity: {
|
|
author: 'Dify',
|
|
name: 'tool-provider',
|
|
description: { en_US: 'Tool provider' },
|
|
icon: 'tool.svg',
|
|
label: { en_US: 'Tool provider' },
|
|
tags: ['tool'],
|
|
},
|
|
credentials_schema: [
|
|
{
|
|
name: 'api_key',
|
|
label: { en_US: 'API key' },
|
|
type: 'secret-input',
|
|
required: true,
|
|
default: 'token',
|
|
},
|
|
],
|
|
},
|
|
datasource: {
|
|
identity: {
|
|
author: 'Dify',
|
|
name: 'datasource-provider',
|
|
description: { en_US: 'Datasource provider' },
|
|
icon: 'datasource.svg',
|
|
label: { en_US: 'Datasource provider' },
|
|
tags: ['datasource'],
|
|
},
|
|
credentials_schema: [],
|
|
},
|
|
endpoint: {
|
|
settings: [
|
|
{
|
|
name: 'endpoint_secret',
|
|
label: { en_US: 'Endpoint secret' },
|
|
type: 'secret-input',
|
|
required: true,
|
|
default: '',
|
|
},
|
|
],
|
|
endpoints: [
|
|
{
|
|
path: '/webhook',
|
|
method: 'POST',
|
|
hidden: false,
|
|
},
|
|
],
|
|
},
|
|
trigger: {
|
|
events: [
|
|
{
|
|
name: 'issue_created',
|
|
identity: {
|
|
author: 'Dify',
|
|
name: 'issue_created',
|
|
label: { en_US: 'Issue created' },
|
|
provider: 'github',
|
|
},
|
|
description: { en_US: 'Issue created event' },
|
|
parameters: [
|
|
{
|
|
name: 'retry_count',
|
|
label: { en_US: 'Retry count' },
|
|
type: 'number',
|
|
default: 0,
|
|
required: false,
|
|
multiple: false,
|
|
},
|
|
],
|
|
output_schema: {
|
|
type: 'object',
|
|
},
|
|
},
|
|
],
|
|
identity: {
|
|
author: 'Dify',
|
|
name: 'github-trigger',
|
|
label: { en_US: 'GitHub trigger' },
|
|
description: { en_US: 'GitHub trigger provider' },
|
|
icon: 'trigger.svg',
|
|
tags: ['trigger'],
|
|
},
|
|
subscription_constructor: {
|
|
credentials_schema: [],
|
|
oauth_schema: {
|
|
client_schema: [],
|
|
credentials_schema: [],
|
|
},
|
|
parameters: [
|
|
{
|
|
name: 'max_retries',
|
|
label: { en_US: 'Max retries' },
|
|
type: 'number',
|
|
default: 3,
|
|
required: false,
|
|
multiple: false,
|
|
},
|
|
],
|
|
},
|
|
subscription_schema: [
|
|
{
|
|
name: 'repository',
|
|
label: { en_US: 'Repository' },
|
|
type: 'text-input',
|
|
required: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
})
|
|
|
|
describe('normalizeInstalledPluginDetail', () => {
|
|
it('should preserve generated plugin declaration capabilities', () => {
|
|
const detail = normalizeInstalledPluginDetail(createPluginInstallation())
|
|
|
|
expect(detail.name).toBe('full-declaration')
|
|
expect(detail.installation_id).toBe('installation-row-id')
|
|
expect(detail.latest_version).toBe('1.0.0')
|
|
expect(detail.latest_unique_identifier).toBe('langgenius/full-declaration:1.0.0@test')
|
|
expect(detail.status).toBe('active')
|
|
expect(detail.declaration.tool?.identity.name).toBe('tool-provider')
|
|
expect(detail.declaration.tool?.credentials_schema[0]?.name).toBe('api_key')
|
|
expect(detail.declaration.datasource?.identity.name).toBe('datasource-provider')
|
|
expect(detail.declaration.endpoint?.endpoints?.[0]).toEqual({
|
|
path: '/webhook',
|
|
method: 'POST',
|
|
hidden: false,
|
|
})
|
|
expect(detail.declaration.trigger.identity.name).toBe('github-trigger')
|
|
expect(detail.declaration.trigger.events[0]?.parameters[0]?.default).toBe(0)
|
|
expect(detail.declaration.trigger.subscription_constructor.parameters[0]?.type).toBe(
|
|
FormTypeEnum.textNumber,
|
|
)
|
|
expect(detail.declaration.trigger.subscription_schema[0]?.name).toBe('repository')
|
|
})
|
|
})
|
|
|
|
describe('use-plugins mutations', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('optimistically updates category auto-upgrade cache before the request finishes', async () => {
|
|
const queryClient = createQueryClient()
|
|
const queryKey = ['plugins', 'referenceSettings', 'autoUpgrade', PluginCategoryEnum.model]
|
|
const previousAutoUpgrade = {
|
|
strategy_setting: AUTO_UPDATE_STRATEGY.latest,
|
|
upgrade_time_of_day: 0,
|
|
upgrade_mode: AUTO_UPDATE_MODE.exclude,
|
|
exclude_plugins: [],
|
|
include_plugins: [],
|
|
}
|
|
const nextAutoUpgrade = {
|
|
...previousAutoUpgrade,
|
|
upgrade_time_of_day: 3600,
|
|
}
|
|
let resolvePost: (value: unknown) => void = () => {}
|
|
mockPost.mockReturnValue(
|
|
new Promise((resolve) => {
|
|
resolvePost = resolve
|
|
}),
|
|
)
|
|
queryClient.setQueryData(queryKey, {
|
|
category: PluginCategoryEnum.model,
|
|
auto_upgrade: previousAutoUpgrade,
|
|
})
|
|
|
|
const { result } = renderHook(
|
|
() => useMutationPluginAutoUpgradeSettings({ category: PluginCategoryEnum.model }),
|
|
{ wrapper: createWrapper(queryClient) },
|
|
)
|
|
|
|
act(() => {
|
|
result.current.mutate(nextAutoUpgrade)
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(mockPost).toHaveBeenCalledWith('/workspaces/current/plugin/auto-upgrade/change', {
|
|
body: {
|
|
category: PluginCategoryEnum.model,
|
|
auto_upgrade: nextAutoUpgrade,
|
|
},
|
|
})
|
|
})
|
|
await waitFor(() => {
|
|
expect(queryClient.getQueryData(queryKey)).toEqual({
|
|
category: PluginCategoryEnum.model,
|
|
auto_upgrade: nextAutoUpgrade,
|
|
})
|
|
})
|
|
|
|
resolvePost({})
|
|
})
|
|
|
|
it('optimistically updates plugin permission cache before the request finishes', async () => {
|
|
const queryClient = createQueryClient()
|
|
const queryKey = ['plugins', 'referenceSettings', 'permission']
|
|
const previousPermission: Permissions = {
|
|
install_permission: PermissionType.admin,
|
|
debug_permission: PermissionType.admin,
|
|
}
|
|
const nextPermission: Permissions = {
|
|
install_permission: PermissionType.everyone,
|
|
debug_permission: PermissionType.admin,
|
|
}
|
|
let resolvePost: (value: unknown) => void = () => {}
|
|
mockPost.mockReturnValue(
|
|
new Promise((resolve) => {
|
|
resolvePost = resolve
|
|
}),
|
|
)
|
|
queryClient.setQueryData(queryKey, previousPermission)
|
|
|
|
const { result } = renderHook(() => useMutationPluginPermissionSettings(), {
|
|
wrapper: createWrapper(queryClient),
|
|
})
|
|
|
|
act(() => {
|
|
result.current.mutate(nextPermission)
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(mockPost).toHaveBeenCalledWith('/workspaces/current/plugin/permission/change', {
|
|
body: nextPermission,
|
|
})
|
|
})
|
|
await waitFor(() => {
|
|
expect(queryClient.getQueryData(queryKey)).toEqual(nextPermission)
|
|
})
|
|
|
|
resolvePost({})
|
|
})
|
|
|
|
it('rolls back category auto-upgrade cache when the request fails', async () => {
|
|
const queryClient = createQueryClient()
|
|
const queryKey = ['plugins', 'referenceSettings', 'autoUpgrade', PluginCategoryEnum.model]
|
|
const previousAutoUpgrade = {
|
|
strategy_setting: AUTO_UPDATE_STRATEGY.latest,
|
|
upgrade_time_of_day: 0,
|
|
upgrade_mode: AUTO_UPDATE_MODE.exclude,
|
|
exclude_plugins: [],
|
|
include_plugins: [],
|
|
}
|
|
const nextAutoUpgrade = {
|
|
...previousAutoUpgrade,
|
|
upgrade_time_of_day: 3600,
|
|
}
|
|
let rejectPost: (reason?: unknown) => void = () => {}
|
|
mockPost.mockReturnValue(
|
|
new Promise((_resolve, reject) => {
|
|
rejectPost = reject
|
|
}),
|
|
)
|
|
queryClient.setQueryData(queryKey, {
|
|
category: PluginCategoryEnum.model,
|
|
auto_upgrade: previousAutoUpgrade,
|
|
})
|
|
|
|
const { result } = renderHook(
|
|
() => useMutationPluginAutoUpgradeSettings({ category: PluginCategoryEnum.model }),
|
|
{ wrapper: createWrapper(queryClient) },
|
|
)
|
|
|
|
const mutation = result.current.mutateAsync(nextAutoUpgrade).catch(() => undefined)
|
|
|
|
await waitFor(() => {
|
|
expect(queryClient.getQueryData(queryKey)).toEqual({
|
|
category: PluginCategoryEnum.model,
|
|
auto_upgrade: nextAutoUpgrade,
|
|
})
|
|
})
|
|
|
|
rejectPost(new Error('auto-upgrade update failed'))
|
|
|
|
await waitFor(() => {
|
|
expect(queryClient.getQueryData(queryKey)).toEqual({
|
|
category: PluginCategoryEnum.model,
|
|
auto_upgrade: previousAutoUpgrade,
|
|
})
|
|
})
|
|
await mutation
|
|
})
|
|
|
|
it('clears optimistic category auto-upgrade cache when the request fails without previous cache', async () => {
|
|
const queryClient = createQueryClient()
|
|
const queryKey = ['plugins', 'referenceSettings', 'autoUpgrade', PluginCategoryEnum.model]
|
|
const nextAutoUpgrade = {
|
|
strategy_setting: AUTO_UPDATE_STRATEGY.latest,
|
|
upgrade_time_of_day: 3600,
|
|
upgrade_mode: AUTO_UPDATE_MODE.exclude,
|
|
exclude_plugins: [],
|
|
include_plugins: [],
|
|
}
|
|
let rejectPost: (reason?: unknown) => void = () => {}
|
|
mockPost.mockReturnValue(
|
|
new Promise((_resolve, reject) => {
|
|
rejectPost = reject
|
|
}),
|
|
)
|
|
|
|
const { result } = renderHook(
|
|
() => useMutationPluginAutoUpgradeSettings({ category: PluginCategoryEnum.model }),
|
|
{ wrapper: createWrapper(queryClient) },
|
|
)
|
|
|
|
const mutation = result.current.mutateAsync(nextAutoUpgrade).catch(() => undefined)
|
|
|
|
await waitFor(() => {
|
|
expect(queryClient.getQueryData(queryKey)).toEqual({
|
|
category: PluginCategoryEnum.model,
|
|
auto_upgrade: nextAutoUpgrade,
|
|
})
|
|
})
|
|
|
|
rejectPost(new Error('auto-upgrade update failed'))
|
|
|
|
await waitFor(() => {
|
|
expect(queryClient.getQueryData(queryKey)).toBeUndefined()
|
|
})
|
|
await mutation
|
|
})
|
|
|
|
it('rolls back plugin permission cache when the request fails', async () => {
|
|
const queryClient = createQueryClient()
|
|
const queryKey = ['plugins', 'referenceSettings', 'permission']
|
|
const previousPermission: Permissions = {
|
|
install_permission: PermissionType.admin,
|
|
debug_permission: PermissionType.admin,
|
|
}
|
|
const nextPermission: Permissions = {
|
|
install_permission: PermissionType.everyone,
|
|
debug_permission: PermissionType.admin,
|
|
}
|
|
let rejectPost: (reason?: unknown) => void = () => {}
|
|
mockPost.mockReturnValue(
|
|
new Promise((_resolve, reject) => {
|
|
rejectPost = reject
|
|
}),
|
|
)
|
|
queryClient.setQueryData(queryKey, previousPermission)
|
|
|
|
const { result } = renderHook(() => useMutationPluginPermissionSettings(), {
|
|
wrapper: createWrapper(queryClient),
|
|
})
|
|
|
|
const mutation = result.current.mutateAsync(nextPermission).catch(() => undefined)
|
|
|
|
await waitFor(() => {
|
|
expect(queryClient.getQueryData(queryKey)).toEqual(nextPermission)
|
|
})
|
|
|
|
rejectPost(new Error('permission update failed'))
|
|
|
|
await waitFor(() => {
|
|
expect(queryClient.getQueryData(queryKey)).toEqual(previousPermission)
|
|
})
|
|
await mutation
|
|
})
|
|
})
|
|
|
|
describe('useInstalledPluginList', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('fetches the default installed plugin list when no category is provided', async () => {
|
|
const queryClient = createQueryClient()
|
|
mockGet.mockResolvedValue({ plugins: [], total: 0 })
|
|
|
|
renderHook(() => useInstalledPluginList(false, 100), { wrapper: createWrapper(queryClient) })
|
|
|
|
await waitFor(() => {
|
|
expect(mockGet).toHaveBeenCalledWith('/workspaces/current/plugin/list?page=1&page_size=100')
|
|
})
|
|
})
|
|
|
|
it('fetches the scoped installed plugin category list when category is provided', async () => {
|
|
const queryClient = createQueryClient()
|
|
mockGet.mockResolvedValue({ plugins: [], has_more: false })
|
|
|
|
renderHook(() => useInstalledPluginList(false, 100, { category: PluginCategoryEnum.trigger }), {
|
|
wrapper: createWrapper(queryClient),
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(mockGet).toHaveBeenCalledWith(
|
|
'/workspaces/current/plugin/trigger/list?page=1&page_size=100',
|
|
)
|
|
})
|
|
})
|
|
|
|
it('keeps builtin tools from the scoped tool plugin category response', async () => {
|
|
const queryClient = createQueryClient()
|
|
const builtinTools = [
|
|
{
|
|
id: 'builtin-tool',
|
|
name: 'builtin-tool',
|
|
label: { en_US: 'Builtin Tool', zh_Hans: 'Builtin Tool' },
|
|
description: { en_US: 'Builtin Tool description', zh_Hans: 'Builtin Tool description' },
|
|
author: 'Dify',
|
|
icon: '',
|
|
type: 'builtin',
|
|
team_credentials: {},
|
|
is_team_authorization: false,
|
|
allow_delete: false,
|
|
labels: [],
|
|
},
|
|
]
|
|
mockGet.mockResolvedValue({ plugins: [], builtin_tools: builtinTools, has_more: false })
|
|
|
|
const { result } = renderHook(
|
|
() => useInstalledPluginList(false, 100, { category: PluginCategoryEnum.tool }),
|
|
{ wrapper: createWrapper(queryClient) },
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.data?.builtin_tools).toEqual(builtinTools)
|
|
})
|
|
})
|
|
|
|
it('uses has_more to load the next scoped plugin category page', async () => {
|
|
const queryClient = createQueryClient()
|
|
mockGet
|
|
.mockResolvedValueOnce({
|
|
plugins: [{ plugin_id: 'trigger-plugin-1' }],
|
|
has_more: true,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
plugins: [{ plugin_id: 'trigger-plugin-2' }],
|
|
has_more: false,
|
|
})
|
|
|
|
const { result } = renderHook(
|
|
() => useInstalledPluginList(false, 100, { category: PluginCategoryEnum.trigger }),
|
|
{ wrapper: createWrapper(queryClient) },
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLastPage).toBe(false)
|
|
})
|
|
|
|
act(() => {
|
|
result.current.loadNextPage()
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(mockGet).toHaveBeenCalledWith(
|
|
'/workspaces/current/plugin/trigger/list?page=2&page_size=100',
|
|
)
|
|
})
|
|
await waitFor(() => {
|
|
expect(result.current.isLastPage).toBe(true)
|
|
})
|
|
})
|
|
|
|
it('keeps builtin tools from the first scoped tool plugin page when loading more pages', async () => {
|
|
const queryClient = createQueryClient()
|
|
const builtinTools = [
|
|
{
|
|
id: 'builtin-tool',
|
|
name: 'builtin-tool',
|
|
label: { en_US: 'Builtin Tool', zh_Hans: 'Builtin Tool' },
|
|
description: { en_US: 'Builtin Tool description', zh_Hans: 'Builtin Tool description' },
|
|
author: 'Dify',
|
|
icon: '',
|
|
type: 'builtin',
|
|
team_credentials: {},
|
|
is_team_authorization: false,
|
|
allow_delete: false,
|
|
labels: [],
|
|
},
|
|
]
|
|
mockGet
|
|
.mockResolvedValueOnce({
|
|
plugins: [{ plugin_id: 'tool-plugin-1' }],
|
|
builtin_tools: builtinTools,
|
|
has_more: true,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
plugins: [{ plugin_id: 'tool-plugin-2' }],
|
|
builtin_tools: builtinTools,
|
|
has_more: false,
|
|
})
|
|
|
|
const { result } = renderHook(
|
|
() => useInstalledPluginList(false, 100, { category: PluginCategoryEnum.tool }),
|
|
{ wrapper: createWrapper(queryClient) },
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isLastPage).toBe(false)
|
|
})
|
|
|
|
act(() => {
|
|
result.current.loadNextPage()
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.data?.plugins).toEqual([
|
|
{ plugin_id: 'tool-plugin-1' },
|
|
{ plugin_id: 'tool-plugin-2' },
|
|
])
|
|
})
|
|
expect(result.current.data?.builtin_tools).toEqual(builtinTools)
|
|
})
|
|
})
|
|
|
|
describe('usePluginTaskList', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('adds the task from an install start response to the task list cache', async () => {
|
|
const queryClient = createQueryClient()
|
|
queryClient.setQueryData(['plugins', 'referenceSettings', 'permission'], {
|
|
install_permission: PermissionType.admin,
|
|
debug_permission: PermissionType.admin,
|
|
})
|
|
mockGet.mockResolvedValue({ tasks: [] })
|
|
|
|
const startedTask: PluginTaskStart = {
|
|
id: 'task-new',
|
|
created_at: '2026-06-05T03:34:59.578653Z',
|
|
updated_at: '2026-06-05T03:34:59.578653Z',
|
|
status: 'running',
|
|
total_plugins: 1,
|
|
completed_plugins: 0,
|
|
plugins: [
|
|
{
|
|
plugin_unique_identifier: 'langgenius/gitlab_datasource:0.3.11@test',
|
|
plugin_id: 'langgenius/gitlab_datasource',
|
|
status: TaskStatus.running,
|
|
message: '',
|
|
icon: 'gitlab.png',
|
|
labels: {
|
|
en_US: 'GitLab',
|
|
} as PluginTaskStart['plugins'][number]['labels'],
|
|
source: PluginSource.marketplace,
|
|
},
|
|
],
|
|
}
|
|
|
|
const { result } = renderHook(() => usePluginTaskList(PluginCategoryEnum.tool), {
|
|
wrapper: createWrapper(queryClient),
|
|
})
|
|
|
|
act(() => {
|
|
result.current.handleInstallTaskStart({
|
|
all_installed: false,
|
|
task_id: 'task-new',
|
|
plugin_unique_identifier: 'langgenius/gitlab_datasource:0.3.11@test',
|
|
task: startedTask,
|
|
})
|
|
})
|
|
|
|
expect(queryClient.getQueryData(['plugins', 'pluginTaskList'])).toEqual({
|
|
tasks: [
|
|
{
|
|
...startedTask,
|
|
plugins: [
|
|
{
|
|
...startedTask.plugins[0],
|
|
taskId: 'task-new',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
})
|
|
})
|
|
|
|
it('replaces an existing task with the latest start response task data', () => {
|
|
const queryClient = createQueryClient()
|
|
queryClient.setQueryData(['plugins', 'referenceSettings', 'permission'], {
|
|
install_permission: PermissionType.admin,
|
|
debug_permission: PermissionType.admin,
|
|
})
|
|
queryClient.setQueryData(['plugins', 'pluginTaskList'], {
|
|
tasks: [
|
|
{
|
|
id: 'task-new',
|
|
created_at: '2026-06-05T03:34:59.578653Z',
|
|
updated_at: '2026-06-05T03:34:59.578653Z',
|
|
status: 'running',
|
|
total_plugins: 1,
|
|
completed_plugins: 0,
|
|
plugins: [],
|
|
},
|
|
{
|
|
id: 'task-old',
|
|
created_at: '2026-06-04T03:34:59.578653Z',
|
|
updated_at: '2026-06-04T03:34:59.578653Z',
|
|
status: 'success',
|
|
total_plugins: 1,
|
|
completed_plugins: 1,
|
|
plugins: [],
|
|
},
|
|
],
|
|
})
|
|
mockGet.mockResolvedValue({ tasks: [] })
|
|
|
|
const { result } = renderHook(() => usePluginTaskList(PluginCategoryEnum.tool), {
|
|
wrapper: createWrapper(queryClient),
|
|
})
|
|
|
|
act(() => {
|
|
result.current.handleInstallTaskStart({
|
|
all_installed: false,
|
|
task_id: 'task-new',
|
|
plugin_unique_identifier: 'langgenius/gitlab_datasource:0.3.11@test',
|
|
task: {
|
|
id: 'task-new',
|
|
created_at: '2026-06-05T03:34:59.578653Z',
|
|
updated_at: '2026-06-05T03:35:59.578653Z',
|
|
status: 'success',
|
|
total_plugins: 1,
|
|
completed_plugins: 1,
|
|
plugins: [],
|
|
},
|
|
})
|
|
})
|
|
|
|
expect(queryClient.getQueryData(['plugins', 'pluginTaskList'])).toEqual({
|
|
tasks: [
|
|
{
|
|
id: 'task-new',
|
|
created_at: '2026-06-05T03:34:59.578653Z',
|
|
updated_at: '2026-06-05T03:35:59.578653Z',
|
|
status: 'success',
|
|
total_plugins: 1,
|
|
completed_plugins: 1,
|
|
plugins: [],
|
|
},
|
|
{
|
|
id: 'task-old',
|
|
created_at: '2026-06-04T03:34:59.578653Z',
|
|
updated_at: '2026-06-04T03:34:59.578653Z',
|
|
status: 'success',
|
|
total_plugins: 1,
|
|
completed_plugins: 1,
|
|
plugins: [],
|
|
},
|
|
],
|
|
})
|
|
})
|
|
|
|
it('keeps a locally started unfinished task when an immediate refetch returns a stale task list', async () => {
|
|
const queryClient = createQueryClient()
|
|
queryClient.setQueryData(['plugins', 'referenceSettings', 'permission'], {
|
|
install_permission: PermissionType.admin,
|
|
debug_permission: PermissionType.admin,
|
|
})
|
|
const staleTask = {
|
|
id: 'task-old',
|
|
created_at: '2026-06-04T03:34:59.578653Z',
|
|
updated_at: '2026-06-04T03:34:59.578653Z',
|
|
status: TaskStatus.success,
|
|
total_plugins: 1,
|
|
completed_plugins: 1,
|
|
plugins: [],
|
|
}
|
|
mockGet.mockResolvedValue({ tasks: [staleTask] })
|
|
const startedTask: PluginTaskStart = {
|
|
id: 'task-new',
|
|
created_at: '2026-06-05T03:34:59.578653Z',
|
|
updated_at: '2026-06-05T03:34:59.578653Z',
|
|
status: TaskStatus.running,
|
|
total_plugins: 1,
|
|
completed_plugins: 0,
|
|
plugins: [
|
|
{
|
|
plugin_unique_identifier: 'langgenius/gitlab_datasource:0.3.11@test',
|
|
plugin_id: 'langgenius/gitlab_datasource',
|
|
status: TaskStatus.pending,
|
|
message: '',
|
|
icon: 'gitlab.png',
|
|
labels: {
|
|
en_US: 'GitLab',
|
|
} as PluginTaskStart['plugins'][number]['labels'],
|
|
source: PluginSource.marketplace,
|
|
},
|
|
],
|
|
}
|
|
|
|
const { result } = renderHook(() => usePluginTaskList(PluginCategoryEnum.tool), {
|
|
wrapper: createWrapper(queryClient),
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(queryClient.getQueryData(['plugins', 'pluginTaskList'])).toEqual({
|
|
tasks: [staleTask],
|
|
})
|
|
})
|
|
|
|
act(() => {
|
|
result.current.handleInstallTaskStart({
|
|
all_installed: false,
|
|
task_id: 'task-new',
|
|
task: startedTask,
|
|
})
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(queryClient.getQueryData(['plugins', 'pluginTaskList'])).toEqual({
|
|
tasks: [
|
|
{
|
|
...startedTask,
|
|
plugins: [
|
|
{
|
|
...startedTask.plugins[0],
|
|
taskId: 'task-new',
|
|
},
|
|
],
|
|
},
|
|
staleTask,
|
|
],
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('usePluginAutoUpgradeSettings', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('does not expose frontend default settings before backend data resolves', () => {
|
|
const queryClient = createQueryClient()
|
|
mockGet.mockReturnValue(new Promise(() => {}))
|
|
|
|
const { result } = renderHook(() => usePluginAutoUpgradeSettings(PluginCategoryEnum.model), {
|
|
wrapper: createWrapper(queryClient),
|
|
})
|
|
|
|
expect(result.current.data).toBeUndefined()
|
|
expect(mockGet).toHaveBeenCalledWith('/workspaces/current/plugin/auto-upgrade/fetch', {
|
|
params: {
|
|
category: PluginCategoryEnum.model,
|
|
},
|
|
})
|
|
})
|
|
|
|
it('returns backend auto-upgrade settings when the request resolves', async () => {
|
|
const queryClient = createQueryClient()
|
|
const backendAutoUpgrade = {
|
|
strategy_setting: AUTO_UPDATE_STRATEGY.fixOnly,
|
|
upgrade_time_of_day: 0,
|
|
upgrade_mode: AUTO_UPDATE_MODE.exclude,
|
|
exclude_plugins: [],
|
|
include_plugins: [],
|
|
}
|
|
mockGet.mockResolvedValue({
|
|
category: PluginCategoryEnum.tool,
|
|
auto_upgrade: backendAutoUpgrade,
|
|
})
|
|
|
|
const { result } = renderHook(() => usePluginAutoUpgradeSettings(PluginCategoryEnum.tool), {
|
|
wrapper: createWrapper(queryClient),
|
|
})
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.data).toEqual({
|
|
category: PluginCategoryEnum.tool,
|
|
auto_upgrade: backendAutoUpgrade,
|
|
})
|
|
})
|
|
})
|
|
})
|