fix(plugins): propagate refresh failures to their owning callers
This commit is contained in:
1 parent
b561b398b4
commit
ae99b884c6
6 files changed
+233
-29
No files matched your search
@@ -1,13 +1,15 @@
|
||||
import type { PluginsResponseData } from '@@/apis/plugins/type'
|
||||
import type { PluginAPI } from '@@/plugin-api'
|
||||
import * as pluginsApi from '@@/apis/plugins'
|
||||
// @vitest-environment happy-dom
|
||||
import { CanceledError, isCancel } from 'axios'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
|
||||
import { pinia } from '@/pinia'
|
||||
import { useSettingsStore } from '@/pinia/stores/settings'
|
||||
import { apiSuccess } from '../../../tests/api-response'
|
||||
import { allCommands, commandKeys } from '../utils/commands'
|
||||
import { pluginState, startPlugins, stopPlugins } from './host'
|
||||
import { pluginState, refreshPlugins, retryPlugin, startPlugins, stopPlugins } from './host'
|
||||
|
||||
const { settings } = useSettingsStore(pinia)
|
||||
|
||||
@@ -22,7 +24,7 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
it('命令失败保留原始异常并清理注册,正常插件列表读取不受影响', async () => {
|
||||
vi.spyOn(pluginsApi, 'getPluginsApi').mockResolvedValue(apiSuccess({ plugins: [{ id: 'fault', name: '故障插件', version: '1', apiVersion: 1, revision: 'rev', enabled: true }] }))
|
||||
const list = vi.spyOn(pluginsApi, 'getPluginsApi').mockResolvedValue(apiSuccess({ plugins: [{ id: 'fault', name: '故障插件', version: '1', apiVersion: 1, revision: 'rev', enabled: true }] }))
|
||||
const notices: string[] = []
|
||||
startPlugins({ notice: message => notices.push(message), getActiveNote: () => null, openNote: async () => {}, writeNote: async () => ({ rev: 'r1' }) })
|
||||
await vi.waitFor(() => expect(pluginState.status.fault?.phase).toBe('active'))
|
||||
@@ -36,6 +38,72 @@ it('命令失败保留原始异常并清理注册,正常插件列表读取不
|
||||
expect(pluginState.status.fault?.phase).toBe('error')
|
||||
expect(allCommands().some(command => command.id === 'plugin:fault:fail')).toBe(false)
|
||||
expect(notices.some(message => message.includes('命令失败'))).toBe(true)
|
||||
list.mockClear()
|
||||
await retryPlugin('fault')
|
||||
await vi.waitFor(() => expect(pluginState.status.fault?.phase).toBe('active'))
|
||||
expect(list).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('交互刷新失败保留原因并 reject,不从宿主重复通知', async () => {
|
||||
const list = vi.spyOn(pluginsApi, 'getPluginsApi').mockResolvedValue(apiSuccess({ plugins: [] }))
|
||||
const notice = vi.fn()
|
||||
startPlugins({ notice, getActiveNote: () => null, openNote: async () => {}, writeNote: async () => ({ rev: 'r1' }) })
|
||||
await vi.waitFor(() => expect(pluginState.error).toBe(''))
|
||||
const failure = new Error('插件磁盘不可读')
|
||||
list.mockRejectedValueOnce(failure)
|
||||
await expect(refreshPlugins()).rejects.toBe(failure)
|
||||
expect(pluginState.errorCause).toBe(failure)
|
||||
expect(pluginState.error).toContain(failure.message)
|
||||
expect(notice).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('启动失败由入口通知一次,取消刷新不产生故障状态', async () => {
|
||||
const list = vi.spyOn(pluginsApi, 'getPluginsApi').mockRejectedValueOnce(new Error('启动读取失败'))
|
||||
const notice = vi.fn()
|
||||
startPlugins({ notice, getActiveNote: () => null, openNote: async () => {}, writeNote: async () => ({ rev: 'r1' }) })
|
||||
await vi.waitFor(() => expect(notice).toHaveBeenCalledOnce())
|
||||
list.mockResolvedValueOnce(apiSuccess({ plugins: [] }))
|
||||
await refreshPlugins()
|
||||
list.mockRejectedValueOnce(new CanceledError('会话已变化'))
|
||||
await expect(refreshPlugins()).rejects.toSatisfy(isCancel)
|
||||
expect(pluginState.error).toBe('')
|
||||
expect(notice).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('旧宿主刷新即使最后到达,也必须按取消拒绝且不覆盖新宿主', async () => {
|
||||
let finish!: (response: PluginsResponseData) => void
|
||||
const list = vi.spyOn(pluginsApi, 'getPluginsApi').mockResolvedValue(apiSuccess({ plugins: [] }))
|
||||
const notice = vi.fn()
|
||||
const adapter = { notice, getActiveNote: () => null, openNote: async () => {}, writeNote: async () => ({ rev: 'r1' }) }
|
||||
startPlugins(adapter)
|
||||
await vi.waitFor(() => expect(pluginState.error).toBe(''))
|
||||
list.mockImplementationOnce(() => new Promise((resolve) => {
|
||||
finish = resolve
|
||||
}))
|
||||
const old = refreshPlugins()
|
||||
const canceled = expect(old).rejects.toSatisfy(isCancel)
|
||||
stopPlugins()
|
||||
startPlugins(adapter)
|
||||
await refreshPlugins()
|
||||
finish(apiSuccess({ plugins: [{ id: 'old', name: '旧插件', version: '1', apiVersion: 1, revision: 'rev', enabled: false }] }))
|
||||
await canceled
|
||||
expect(pluginState.installed).toEqual([])
|
||||
expect(notice).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('旧启动已失败但通知尚未执行时,切换宿主也不能向新页面提示', async () => {
|
||||
vi.spyOn(pluginsApi, 'getPluginsApi').mockResolvedValue(apiSuccess({ plugins: [] })).mockRejectedValueOnce(new Error('旧启动失败'))
|
||||
const oldNotice = vi.fn()
|
||||
const newNotice = vi.fn()
|
||||
const adapter = { getActiveNote: () => null, openNote: async () => {}, writeNote: async () => ({ rev: 'r1' }) }
|
||||
startPlugins({ ...adapter, notice: oldNotice })
|
||||
await Promise.resolve()
|
||||
expect(pluginState.error).toContain('旧启动失败')
|
||||
stopPlugins()
|
||||
startPlugins({ ...adapter, notice: newNotice })
|
||||
await vi.waitFor(() => expect(pluginState.error).toBe(''))
|
||||
expect(oldNotice).not.toHaveBeenCalled()
|
||||
expect(newNotice).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
vi.mock('@@/utils/resource-tickets', () => ({
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getTreeApi } from '@@/apis/tree'
|
||||
import { readNoteFile } from '@@/utils/note-files'
|
||||
import { pluginResourceURL } from '@@/utils/resource-tickets'
|
||||
|
||||
import { CanceledError, isCancel } from 'axios'
|
||||
import { reactive } from 'vue'
|
||||
import { allCommands, registerCommand } from '../utils/commands'
|
||||
import { editorExtensions, markdownSyntax, pluginEvents, pluginsPaused, registerCodeRenderer, registerEntry, settingTabs, sidebarViews } from './registry'
|
||||
@@ -255,12 +256,12 @@ async function pump() {
|
||||
}
|
||||
|
||||
export async function refreshPlugins() {
|
||||
if (!running) return
|
||||
if (!running) throw new CanceledError('插件宿主已停止')
|
||||
const currentSession = session
|
||||
const sequence = ++request
|
||||
try {
|
||||
const installed = (await getPluginsApi()).data.plugins
|
||||
if (!running || currentSession !== session || sequence !== request) return
|
||||
if (!running || currentSession !== session || sequence !== request) throw new CanceledError('插件刷新已过期')
|
||||
pluginState.installed = installed
|
||||
pluginState.error = ''
|
||||
pluginState.errorCause = undefined
|
||||
@@ -276,23 +277,26 @@ export async function refreshPlugins() {
|
||||
}
|
||||
void pump()
|
||||
} catch (cause) {
|
||||
if (!running || currentSession !== session || sequence !== request) return
|
||||
pluginState.error = '读取插件列表失败,请检查连接后重试'
|
||||
if (!running || currentSession !== session || sequence !== request) throw new CanceledError('插件刷新已过期')
|
||||
if (isCancel(cause)) throw cause
|
||||
pluginState.error = `读取插件列表失败:${cause instanceof Error ? cause.message : String(cause)}`
|
||||
pluginState.errorCause = cause
|
||||
adapter.notice(pluginState.error)
|
||||
for (const instance of instances.values()) {
|
||||
status(instance.info.id, 'paused')
|
||||
void stop(instance)
|
||||
}
|
||||
throw cause
|
||||
}
|
||||
}
|
||||
|
||||
export function startPlugins(value: Adapter) {
|
||||
adapter = value
|
||||
running = true
|
||||
session++
|
||||
const currentSession = ++session
|
||||
faults.clear()
|
||||
void refreshPlugins()
|
||||
void refreshPlugins().catch((cause) => {
|
||||
if (running && currentSession === session && !isCancel(cause)) value.notice(pluginState.error)
|
||||
})
|
||||
}
|
||||
|
||||
export function stopPlugins() {
|
||||
@@ -305,7 +309,7 @@ export function stopPlugins() {
|
||||
|
||||
export function retryPlugin(id: string) {
|
||||
faults.delete(id)
|
||||
void refreshPlugins()
|
||||
return refreshPlugins()
|
||||
}
|
||||
|
||||
export function pluginEditorFailure(error: unknown) {
|
||||
|
||||
Reference in new issue
Block a user