fix(plugin): preserve credentials during local replacement (#39764)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
zyssyz123
2026-07-29 12:39:06 +00:00
committed by GitHub
co-authored by autofix-ci[bot]
parent 81fd1da3ff
commit 72c20daa61
15 changed files with 297 additions and 61 deletions
@@ -372,7 +372,7 @@ describe('Install', () => {
})
})
it('should uninstall existing plugin before installing new version', async () => {
it('should preserve credentials when replacing an installed plugin', async () => {
mockUseCheckInstalled.mockReturnValue({
installedInfo: {
'test-author/Test Plugin': {
@@ -394,7 +394,9 @@ describe('Install', () => {
fireEvent.click(screen.getByRole('button', { name: 'plugin.installModal.install' }))
await waitFor(() => {
expect(mockUninstallPlugin).toHaveBeenCalledWith('installed-id-to-uninstall')
expect(mockUninstallPlugin).toHaveBeenCalledWith('installed-id-to-uninstall', {
preserveCredentials: true,
})
})
await waitFor(() => {
@@ -69,7 +69,9 @@ const Installed: FC<Props> = ({
onStartToInstall?.()
try {
if (hasInstalled) await uninstallPlugin(installedInfoPayload.installedId)
if (hasInstalled) {
await uninstallPlugin(installedInfoPayload.installedId, { preserveCredentials: true })
}
const response = await installPackageFromLocal(uniqueIdentifier)
const { all_installed, task_id } = response
-4
View File
@@ -452,10 +452,6 @@ export type InstalledPluginCategoryListResponse = {
has_more: boolean
}
export type UninstallPluginResponse = {
success: boolean
}
export type GitHubItemAndMarketPlaceDependency = {
type: 'github' | 'marketplace' | 'package'
value: {
+56 -1
View File
@@ -19,15 +19,17 @@ import { renderHook } from '@/test/console/render'
import {
normalizeInstalledPluginDetail,
useInstalledPluginList,
useInstallOrUpdate,
useMutationPluginAutoUpgradeSettings,
useMutationPluginPermissionSettings,
usePluginAutoUpgradeSettings,
usePluginTaskList,
} from '../use-plugins'
const { mockGet, mockPost } = vi.hoisted(() => ({
const { mockGet, mockPost, mockUninstallPlugin } = vi.hoisted(() => ({
mockGet: vi.fn(),
mockPost: vi.fn(),
mockUninstallPlugin: vi.fn(),
}))
vi.mock('@/service/base', () => ({
@@ -37,6 +39,11 @@ vi.mock('@/service/base', () => ({
postMarketplace: vi.fn(),
}))
vi.mock('@/service/plugins', () => ({
fetchPluginInfoFromMarketPlace: vi.fn(),
uninstallPlugin: mockUninstallPlugin,
}))
vi.mock('@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list', () => ({
default: () => ({
refreshPluginList: vi.fn(),
@@ -303,6 +310,54 @@ describe('use-plugins mutations', () => {
resolvePost({})
})
it('preserves credentials when replacing an installed bundle package', async () => {
const queryClient = createQueryClient()
mockUninstallPlugin.mockResolvedValue({ success: true })
mockPost.mockResolvedValue({ all_installed: true, task_id: '' })
const payload = [
{
type: 'package' as const,
value: {
unique_identifier: 'langgenius/openai:0.0.2@new',
manifest: {},
},
},
]
const plugin = [
{
org: 'langgenius',
name: 'openai',
},
]
const installedInfo = {
'langgenius/openai': {
installedId: 'installation-id',
installedVersion: '0.0.1',
uniqueIdentifier: 'langgenius/openai:0.0.1@old',
},
}
const { result } = renderHook(() => useInstallOrUpdate({}), {
wrapper: createWrapper(queryClient),
})
await act(async () => {
await result.current.mutateAsync({
payload: payload as Parameters<typeof result.current.mutateAsync>[0]['payload'],
plugin: plugin as Parameters<typeof result.current.mutateAsync>[0]['plugin'],
installedInfo,
})
})
expect(mockUninstallPlugin).toHaveBeenCalledWith('installation-id', {
preserveCredentials: true,
})
expect(mockPost).toHaveBeenCalledWith('/workspaces/current/plugin/install/pkg', {
body: {
plugin_unique_identifiers: ['langgenius/openai:0.0.2@new'],
},
})
})
it('optimistically updates plugin permission cache before the request finishes', async () => {
const queryClient = createQueryClient()
const queryKey = ['plugins', 'referenceSettings', 'permission']
+56
View File
@@ -0,0 +1,56 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { uninstallPlugin } from './plugins'
const { mockUninstallPost } = vi.hoisted(() => ({
mockUninstallPost: vi.fn(),
}))
vi.mock('./base', () => ({
get: vi.fn(),
getMarketplace: vi.fn(),
post: vi.fn(),
upload: vi.fn(),
}))
vi.mock('./client', () => ({
consoleClient: {
workspaces: {
current: {
plugin: {
uninstall: {
post: mockUninstallPost,
},
},
},
},
},
}))
describe('uninstallPlugin', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUninstallPost.mockResolvedValue({ success: true })
})
it('should delete credentials for an ordinary uninstall by default', async () => {
await uninstallPlugin('installation-id')
expect(mockUninstallPost).toHaveBeenCalledWith({
body: {
plugin_installation_id: 'installation-id',
preserve_credentials: false,
},
})
})
it('should preserve credentials when requested for a replacement', async () => {
await uninstallPlugin('installation-id', { preserveCredentials: true })
expect(mockUninstallPost).toHaveBeenCalledWith({
body: {
plugin_installation_id: 'installation-id',
preserve_credentials: true,
},
})
})
})
+10 -4
View File
@@ -4,11 +4,11 @@ import type {
InstallPackageResponse,
PluginManifestInMarket,
TaskStatusResponse,
UninstallPluginResponse,
updatePackageResponse,
uploadGitHubResponse,
} from '@/app/components/plugins/types'
import { get, getMarketplace, post, upload } from './base'
import { consoleClient } from './client'
export const uploadFile = async (file: File, isBundle: boolean) => {
const formData = new FormData()
@@ -87,8 +87,14 @@ export const checkTaskStatus = async (taskId: string) => {
return get<TaskStatusResponse>(`/workspaces/current/plugin/tasks/${taskId}`)
}
export const uninstallPlugin = async (pluginId: string) => {
return post<UninstallPluginResponse>('/workspaces/current/plugin/uninstall', {
body: { plugin_installation_id: pluginId },
export const uninstallPlugin = async (
pluginId: string,
options: { preserveCredentials?: boolean } = {},
) => {
return consoleClient.workspaces.current.plugin.uninstall.post({
body: {
plugin_installation_id: pluginId,
preserve_credentials: options.preserveCredentials ?? false,
},
})
}
+1 -1
View File
@@ -919,7 +919,7 @@ export const useInstallOrUpdate = ({
}
if (isInstalled) {
if (item.type === 'package') {
await uninstallPlugin(installedPayload.installedId)
await uninstallPlugin(installedPayload.installedId, { preserveCredentials: true })
const response = await post<InstallPackageResponse>(
'/workspaces/current/plugin/install/pkg',
{