Просмотр исходного кода

refactor(desktop): remove unused recovery IPC paths

07akioni 1 неделя назад
Родитель
Сommit
89a89408c6

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.md
-2026-09-15-desktop-native-fatal-recovery.md: 6922b65495656a52b449b94e23f24b036a0bf79b
-2026-09-15-desktop-native-fatal-recovery.zh.md: 32c35753a6346849b3da26c5492a245fee5ab991
+2026-09-15-desktop-native-fatal-recovery.md: b2a0dfff1a630d1eedaa898e4a4c8144ff7ae776
+2026-09-15-desktop-native-fatal-recovery.zh.md: 0fee729e3be791d10966050d21482c5132b14fe6

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.md

@@ -14,7 +14,7 @@ Electron owns one native fatal dialog per application process. Explicit main-win
 
 The first report claims presentation before awaiting the dialog. Later reports remain in logs. The Electron console retains the complete reported diagnostic. The dialog bounds the first diagnostic to its final eight lines and limits the complete detail to 1,200 UTF-16 code units, including truncation notice and reinstall advice, because native dialogs cannot scroll. The dialog offers exit, restart, or disabling third-party bundles followed by a whole-application restart. Disabling writes activation metadata under the existing profile transaction lock after Host shutdown, without requiring runtime initialization or deleting installed files. An explicit recovery-operation failure is presented separately and does not count as another automatic fatal report.
 
-The Web document stays in place. A carrier callback owns startup failure presentation while the shared boot page retains its spinner; ordinary browser boot still renders its own failure report. Only the primary application frame may report a Web boot failure. Desktop has no profile reset, plugin-window recovery controls, or emergency recovery document. A fatal backend failure requires one of the native recovery actions rather than an in-process retry.
+The Web document stays in place. A carrier callback owns startup failure presentation while the shared boot page retains its spinner; ordinary browser boot still renders its own failure report. Only the primary application frame may report a Web boot failure. The plugin window exposes package operations only; backend state remains in the main process, and native recovery directly owns disabling all third-party bundles. Desktop has no profile reset, plugin-window recovery controls, or emergency recovery document. A fatal backend failure requires one of the native recovery actions rather than an in-process retry.
 
 This supersedes recovery-page and reset behavior in the [immediate-window decision](2026-09-09-desktop-immediate-window-and-direct-start.md), whose immediate visibility and direct Host startup rationale remain active. The [in-place profile decision](2026-09-09-desktop-in-place-profile.md) still owns package transactions and partial changes.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-15-desktop-native-fatal-recovery.zh.md

@@ -14,7 +14,7 @@ Electron 在每个应用进程中提供一次原生致命错误对话框。明
 
 首次报告在等待对话框前取得展示权。后续报告保留在日志中。Electron 控制台保留完整的已报告诊断。原生对话框无法滚动,因此仅显示首次诊断末尾八行,并将包含截断提示和重装建议的完整详情限制为 1,200 个 UTF-16 代码单元。对话框提供退出、重启或禁用第三方 bundle 后重启整个应用。禁用操作等待 Host 关闭后,在已有 profile 事务锁内写入启用元数据,不要求运行时初始化,也不删除安装文件。显式恢复操作失败会单独展示,不计为另一次自动致命报告。
 
-Web 文档保留在原位。宿主回调负责启动失败展示,共享启动页保留加载动画;普通浏览器启动仍显示自身的失败报告。只有主应用框架可以上报 Web 启动失败。Desktop 不提供 profile 重置、插件窗口恢复控件或应急恢复文档。后端致命故障必须通过原生恢复操作处理,不在当前进程中重试。
+Web 文档保留在原位。宿主回调负责启动失败展示,共享启动页保留加载动画;普通浏览器启动仍显示自身的失败报告。只有主应用框架可以上报 Web 启动失败。插件窗口只暴露包操作;后端状态保留在主进程中,原生恢复直接负责禁用全部第三方 bundle。Desktop 不提供 profile 重置、插件窗口恢复控件或应急恢复文档。后端致命故障必须通过原生恢复操作处理,不在当前进程中重试。
 
 这取代了[立即显示窗口决策](2026-09-09-desktop-immediate-window-and-direct-start.zh.md)中的恢复页和重置行为;该决策关于立即可见性和直接启动 Host 的理由仍然有效。[原位 profile 决策](2026-09-09-desktop-in-place-profile.zh.md)仍负责包事务和部分变更。
 

+ 2 - 2
apps/desktop/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write apps/desktop/README.md
-README.md: 70493cf0ba7badb72419e68470960a8a8ad33f58
-README.zh.md: c2aba1304571d6095f4a487dc1df795e64391b03
+README.md: 9075e86b0817853b28b28aa1cee45f35904a38d0
+README.zh.md: c25cc17a89ff65a7deedceb78778651b2d4a5c1f

+ 1 - 1
apps/desktop/README.md

@@ -62,7 +62,7 @@ Fatal main-window creation, main-document loading, preload, renderer, Web initia
 
 Native dialog details include at most 1,200 UTF-16 code units and eight diagnostic lines; the complete reported error is written to the Electron console. Host error diagnostics retain only the last 64 Ki characters written to stderr. Earlier output is discarded so a long-running Host does not grow the shell’s diagnostic buffer indefinitely.
 
-Recovery waits for Host shutdown before changing plugin activation. Disabling third-party bundles writes the profile under its transaction lock without loading runtime metadata or deleting files. Invalid profile data or write failures are reported as recovery-operation errors; Desktop does not restart as though disabling succeeded. Desktop has no profile-reset action or emergency HTML document.
+Recovery waits for Host shutdown before changing plugin activation. The native recovery action disables third-party bundles by writing the profile under its transaction lock without loading runtime metadata or deleting files. Invalid profile data or write failures are reported as recovery-operation errors; Desktop does not restart as though disabling succeeded. Desktop has no profile-reset action or emergency HTML document.
 
 Package transactions hold `$DSH_HOME/profiles/desktop/lock` exclusively through pnpm process exit. Before pnpm runs, the shared module-fallback helper removes only its owned links and preserves pnpm-managed directories; development Host startup restores needed links. Link cleanup preserves target directories. Native builds follow pnpm’s configured build policy; release preparation owns its separate build-time allowlist.
 

+ 1 - 1
apps/desktop/README.zh.md

@@ -62,7 +62,7 @@ CLI 与 Desktop 共用已安装依赖清单及 bundle 列表协调逻辑。bundl
 
 原生弹窗详情最多包含 1,200 个 UTF-16 代码单元和八行诊断;完整的已报告错误写入 Electron 控制台。Host 错误诊断仅保留 stderr 输出的最后 64 Ki 个字符。更早的输出会被丢弃,避免长期运行的 Host 使壳的诊断缓冲区无限增长。
 
-恢复操作等待 Host 关闭后才修改插件启用状态。禁用第三方 bundle 时持有事务锁写入 profile,不加载运行时元数据,也不删除文件。profile 数据无效或写入失败会作为恢复操作错误报告;Desktop 不会假装禁用成功后重启。Desktop 不提供 profile 重置操作或应急 HTML 文档。
+恢复操作等待 Host 关闭后才修改插件启用状态。原生恢复操作禁用第三方 bundle 时持有事务锁写入 profile,不加载运行时元数据,也不删除文件。profile 数据无效或写入失败会作为恢复操作错误报告;Desktop 不会假装禁用成功后重启。Desktop 不提供 profile 重置操作或应急 HTML 文档。
 
 包事务独占 `$DSH_HOME/profiles/desktop/lock`,直到 pnpm 进程退出。pnpm 运行前,共享模块回退辅助函数只删除其拥有的链接,保留 pnpm 管理的目录;开发 Host 在启动时重建所需链接。链接清理保留目标目录。原生构建遵循 pnpm 配置的构建策略;发布准备使用独立的构建期允许列表。
 

+ 0 - 9
apps/desktop/src/ipc.ts

@@ -2,7 +2,6 @@
 
 import type { DesktopPluginRecord } from './project-manager.ts'
 import type { DesktopLocale } from './locale.ts'
-import type { DesktopBackendState } from './backend-controller.ts'
 
 /** IPC channel names kept private to the desktop application bundle. */
 export const DESKTOP_IPC = {
@@ -14,9 +13,6 @@ export const DESKTOP_IPC = {
   pluginsRemove: 'dsh-desktop:plugins-remove',
   pluginsUpdate: 'dsh-desktop:plugins-update',
   pluginsToggle: 'dsh-desktop:plugins-toggle',
-  pluginsDisableAll: 'dsh-desktop:plugins-disable-all',
-  backendStatus: 'dsh-desktop:backend-status',
-  backendState: 'dsh-desktop:backend-state',
   updatesCheck: 'dsh-desktop:updates-check',
   updatesInstall: 'dsh-desktop:updates-install',
   updatesState: 'dsh-desktop:updates-state',
@@ -40,11 +36,6 @@ export interface DshDesktopApi {
     remove(name: string): Promise<void>
     update(name: string, version: string): Promise<void>
     toggle(name: string, enabled: boolean): Promise<void>
-    disableAll(): Promise<void>
-  }
-  readonly backend: {
-    status(): Promise<DesktopBackendState>
-    subscribe(listener: (state: DesktopBackendState) => void): () => void
   }
   readonly updates: {
     check(): Promise<DesktopUpdateState>

+ 1 - 12
apps/desktop/src/main.ts

@@ -20,7 +20,7 @@ import { resolveDesktopPaths } from './paths.ts'
 import { DesktopProjectManager, type DesktopProjectHooks } from './project-manager.ts'
 import { DesktopHostProcess } from './host-process.ts'
 import { desktopNodeEnvironment } from './node-environment.ts'
-import { DesktopBackendController, type DesktopBackendState } from './backend-controller.ts'
+import { DesktopBackendController } from './backend-controller.ts'
 import { DESKTOP_IPC, type DesktopUpdateState } from './ipc.ts'
 import { formatDesktopMessage, resolveDesktopLocale } from './locale.ts'
 import { claimDesktopSingleInstance } from './single-instance.ts'
@@ -223,11 +223,6 @@ async function main(): Promise<void> {
     navigation = next
     return next.promise
   }
-  const publishBackend = (state: DesktopBackendState): void => {
-    for (const window of BrowserWindow.getAllWindows()) {
-      window.webContents.send(DESKTOP_IPC.backendState, state)
-    }
-  }
   const backend = new DesktopBackendController((onFailure) => {
     const hostInspectPort = developmentHostInspectPort(development)
     const host = new DesktopHostProcess(resources.node, resources.dsh, activeProject,
@@ -247,7 +242,6 @@ async function main(): Promise<void> {
     }
   }, (state) => {
     if (state.phase === 'error') reportFatal(new Error(state.message))
-    publishBackend(state)
   })
 
   const publishUpdate = (state: DesktopUpdateState): DesktopUpdateState => {
@@ -377,11 +371,6 @@ async function main(): Promise<void> {
     if (typeof name !== 'string' || typeof enabled !== 'boolean') throw new Error('dsh desktop: invalid plugin activation request')
     return mutate(event, { type: 'plugin-toggle', name, enabled })
   })
-  ipcMain.handle(DESKTOP_IPC.pluginsDisableAll, event => mutate(event, { type: 'plugins-disable-all' }))
-  ipcMain.handle(DESKTOP_IPC.backendStatus, (event) => {
-    assertDesktopSender(event, ['shell'])
-    return backend.state
-  })
   ipcMain.handle(DESKTOP_IPC.updatesCheck, async (event) => {
     assertDesktopSender(event, ['shell'])
     return updates.check()

+ 0 - 10
apps/desktop/src/preload.ts

@@ -3,7 +3,6 @@
 import { contextBridge, ipcRenderer } from 'electron'
 import { DESKTOP_IPC, type DshDesktopApi, type DesktopUpdateState } from './ipc.ts'
 import { markDocumentPlatform } from './preload-platform.ts'
-import type { DesktopBackendState } from './backend-controller.ts'
 
 const api: DshDesktopApi = {
   protocolVersion: 1,
@@ -13,17 +12,8 @@ const api: DshDesktopApi = {
     add: spec => ipcRenderer.invoke(DESKTOP_IPC.pluginsAdd, spec) as Promise<void>,
     remove: name => ipcRenderer.invoke(DESKTOP_IPC.pluginsRemove, name) as Promise<void>,
     toggle: (name, enabled) => ipcRenderer.invoke(DESKTOP_IPC.pluginsToggle, name, enabled) as Promise<void>,
-    disableAll: () => ipcRenderer.invoke(DESKTOP_IPC.pluginsDisableAll) as Promise<void>,
     update: (name, version) => ipcRenderer.invoke(DESKTOP_IPC.pluginsUpdate, name, version) as Promise<void>,
   },
-  backend: {
-    status: () => ipcRenderer.invoke(DESKTOP_IPC.backendStatus) as ReturnType<DshDesktopApi['backend']['status']>,
-    subscribe(listener) {
-      const handle = (_event: Electron.IpcRendererEvent, state: DesktopBackendState): void => { listener(state) }
-      ipcRenderer.on(DESKTOP_IPC.backendState, handle)
-      return () => { ipcRenderer.off(DESKTOP_IPC.backendState, handle) }
-    },
-  },
   updates: {
     check: () => ipcRenderer.invoke(DESKTOP_IPC.updatesCheck) as Promise<DesktopUpdateState>,
     install: () => ipcRenderer.invoke(DESKTOP_IPC.updatesInstall) as Promise<void>,

+ 2 - 7
apps/desktop/src/project-manager.ts

@@ -61,7 +61,6 @@ export type DesktopProjectMutation =
   | { readonly type: 'plugin-remove'; readonly name: string }
   | { readonly type: 'plugin-update'; readonly name: string; readonly version: string }
   | { readonly type: 'plugin-toggle'; readonly name: string; readonly enabled: boolean }
-  | { readonly type: 'plugins-disable-all' }
 
 const PROJECT_NAME = '@deepseek-ai/dsh-desktop-runtime'
 const DSH_PACKAGE = '@deepseek-ai/dsh'
@@ -173,11 +172,7 @@ export class DesktopProjectManager {
       if (!existsSync(this.paths.profile)) throw new Error('desktop project: active profile is not installed')
       await hooks.beforeChange()
       try {
-        if (mutation.type === 'plugins-disable-all') {
-          writeProfileBundles(this.paths.profile, readProfileManifest('dsh', this.paths.profile), WEB_PROFILE.bundles)
-        } else {
-          await this.applyMutation(this.paths.profile, mutation)
-        }
+        await this.applyMutation(this.paths.profile, mutation)
       } catch (error) {
         try { await hooks.afterChange() } catch (restartError) {
           throw new AggregateError([error, restartError], 'Desktop package operation and backend restart failed')
@@ -188,7 +183,7 @@ export class DesktopProjectManager {
     })
   }
 
-  private async applyMutation(projectDir: string, mutation: Exclude<DesktopProjectMutation, { type: 'plugins-disable-all' }>): Promise<void> {
+  private async applyMutation(projectDir: string, mutation: DesktopProjectMutation): Promise<void> {
     const location = this.pluginLocation(projectDir)
     const before = readProfilePlugins(location)
     const bundles = before.manifest.dsh?.profile?.bundles ?? []

+ 14 - 18
apps/desktop/tests/main-startup.spec.ts

@@ -16,13 +16,13 @@ const harness = await vi.hoisted(async () => {
   const windows: FakeWindow[] = []
   let windowFailure: Error | undefined
   const hosts: FakeHost[] = []
-  const handlers = new Map<string, (event: { senderFrame: { url: string } }) => unknown>()
+  const handlers = new Map<string, (event: { senderFrame: { url: string } }, ...args: unknown[]) => unknown>()
   let pluginsEnabled = false
   let preparing = deferred()
   let prepared = deferred()
   let hostStarted = deferred()
   let navigated = deferred()
-  let errorPublished = deferred()
+  let dialogShown = deferred()
   let quitCompleted = deferred()
   class FakeWindow extends EventEmitter {
     destroyed = false
@@ -32,9 +32,7 @@ const harness = await vi.hoisted(async () => {
       setWindowOpenHandler: vi.fn(),
       openDevTools: vi.fn(),
       getURL: () => this.urls.at(-1) ?? '',
-      send: vi.fn((channel: string, state: { phase?: string }) => {
-        if (channel === 'dsh-desktop:backend-state' && state.phase === 'error') errorPublished.resolve()
-      }),
+      send: vi.fn(),
     })
     readonly show = vi.fn()
     readonly focus = vi.fn()
@@ -96,7 +94,7 @@ const harness = await vi.hoisted(async () => {
     disableAllPlugins: vi.fn(async () => { pluginsEnabled = false }),
     get preparing() { return preparing }, get prepared() { return prepared },
     get hostStarted() { return hostStarted }, get navigated() { return navigated },
-    get errorPublished() { return errorPublished }, get quitCompleted() { return quitCompleted },
+    get dialogShown() { return dialogShown }, get quitCompleted() { return quitCompleted },
     nextNavigation() { navigated = deferred(); return navigated.promise },
     nextHostStart() { hostStarted = deferred(); return hostStarted.promise },
     get pluginsEnabled() { return pluginsEnabled },
@@ -107,7 +105,7 @@ const harness = await vi.hoisted(async () => {
       windowFailure = undefined
       pluginsEnabled = false
       preparing = deferred(); prepared = deferred(); hostStarted = deferred()
-      navigated = deferred(); errorPublished = deferred(); quitCompleted = deferred()
+      navigated = deferred(); dialogShown = deferred(); quitCompleted = deferred()
     },
   }
 })
@@ -150,10 +148,10 @@ vi.mock('../src/project-manager.ts', () => ({
 vi.mock('../src/host-process.ts', () => ({ DesktopHostProcess: harness.FakeHost }))
 vi.mock('../src/update-coordinator.ts', () => ({ DesktopUpdateCoordinator: vi.fn() }))
 
-function invoke(channel: string): unknown {
+function invoke(channel: string, ...args: unknown[]): unknown {
   const handler = harness.handlers.get(channel)
   if (handler === undefined) throw new Error(`missing handler ${channel}`)
-  return handler({ senderFrame: { url: channel === DESKTOP_IPC.boot ? 'dsh-app://app/' : 'dsh-app://shell/plugin-manager.html' } })
+  return handler({ senderFrame: { url: channel === DESKTOP_IPC.boot ? 'dsh-app://app/' : 'dsh-app://shell/plugin-manager.html' } }, ...args)
 }
 
 beforeEach(() => {
@@ -161,7 +159,7 @@ beforeEach(() => {
   vi.clearAllMocks()
   vi.useFakeTimers()
   harness.reset()
-  harness.dialog.showMessageBox.mockImplementation(() => new Promise(() => {}))
+  harness.dialog.showMessageBox.mockImplementation(() => { harness.dialogShown.resolve(); return new Promise(() => {}) })
   vi.spyOn(console, 'error').mockImplementation(() => {})
   vi.stubEnv('DSH_DESKTOP_PNPM_ENTRY', 'test-pnpm')
   vi.stubEnv('DSH_DESKTOP_DSH_DIR', 'test-runtime')
@@ -339,11 +337,10 @@ describe('desktop main startup', () => {
     harness.mutateFailure.mockImplementationOnce(() => { throw new Error('package write failed') })
     host.exited.resolve()
     const nextStarted = harness.nextHostStart()
-    const failure = expect(invoke(DESKTOP_IPC.pluginsDisableAll)).rejects.toThrow('package write failed')
+    const failure = expect(invoke(DESKTOP_IPC.pluginsAdd, 'example-plugin')).rejects.toThrow('package write failed')
     await nextStarted
     harness.hosts[1]!.ready.resolve()
     await failure
-    expect(invoke(DESKTOP_IPC.backendStatus)).toEqual({ phase: 'ready' })
     expect(harness.dialog.showMessageBox).not.toHaveBeenCalled()
     expect(harness.windows[0]!.urls).toEqual(['dsh-app://app/', 'dsh-app://app/'])
   })
@@ -358,7 +355,7 @@ describe('desktop main startup', () => {
     await Promise.resolve(invoke(DESKTOP_IPC.boot))
     host.exited.resolve()
     const nextStarted = harness.nextHostStart()
-    const failure = expect(invoke(DESKTOP_IPC.pluginsDisableAll)).rejects.toThrow('new Host failed')
+    const failure = expect(invoke(DESKTOP_IPC.pluginsAdd, 'example-plugin')).rejects.toThrow('new Host failed')
     await nextStarted
     const replacement = harness.hosts[1]!
     replacement.exited.resolve()
@@ -373,7 +370,7 @@ describe('desktop main startup', () => {
     await import('../src/main.ts')
     await harness.preparing.promise
     harness.prepared.reject(new Error('runtime resources missing'))
-    await harness.errorPublished.promise
+    await harness.dialogShown.promise
     expect(harness.dialog.showMessageBox.mock.calls[0]![0].detail).toContain('runtime resources missing')
     expect(harness.dialog.showMessageBox.mock.calls[0]![0].buttons).toEqual(['Exit', 'Restart', 'Disable all third-party plugins and restart'])
     expect(harness.windows[0]!.urls).toEqual(['dsh-app://app/'])
@@ -387,7 +384,7 @@ describe('desktop main startup', () => {
     else window.webContents.emit('render-process-gone', {}, { reason: 'crashed' })
     window.webContents.emit('preload-error', {}, 'preload-app.cjs', new Error('secondary failure'))
     harness.prepared.reject(new Error('backend also failed'))
-    await harness.errorPublished.promise
+    await harness.dialogShown.promise
     expect(harness.dialog.showMessageBox).toHaveBeenCalledOnce()
     expect(harness.dialog.showMessageBox.mock.calls[0]![0].detail).not.toContain('secondary failure')
     expect(window.urls).toEqual(['dsh-app://app/'])
@@ -462,7 +459,6 @@ describe('desktop main startup', () => {
     expect(harness.hosts[0]!.start).toHaveBeenCalledTimes(1)
     expect(harness.windows).toHaveLength(1)
     expect(window.urls).toEqual(['dsh-app://app/'])
-    expect(invoke(DESKTOP_IPC.backendStatus)).toEqual({ phase: 'ready' })
   })
 
   it('prepares an independent plugin profile for the unpackaged Host', async () => {
@@ -488,11 +484,11 @@ describe('desktop main startup', () => {
     const first = harness.hosts[0]!
     first.exited.resolve()
     first.ready.reject(new Error('plugin composition failed'))
-    await harness.errorPublished.promise
+    await harness.dialogShown.promise
     expect(harness.windows[0]!.urls).toEqual(['dsh-app://app/'])
     expect(harness.dialog.showMessageBox).toHaveBeenCalledOnce()
     expect(harness.handlers.has('dsh-desktop:backend-retry')).toBe(false)
-    await expect(invoke(DESKTOP_IPC.pluginsDisableAll)).rejects.toThrow('The application could not start or stopped unexpectedly.')
+    await expect(invoke(DESKTOP_IPC.pluginsAdd, 'example-plugin')).rejects.toThrow('The application could not start or stopped unexpectedly.')
     expect(harness.hosts).toHaveLength(1)
   })
 

+ 0 - 1
apps/desktop/tests/plugin-manager.spec.ts

@@ -31,7 +31,6 @@ it.each(['en', 'zh-CN'])('updates through the inline form, including the install
   const update = vi.fn(async () => {})
   Object.defineProperty(dom.window, 'dshDesktop', { value: {
     locale: async () => locale,
-    backend: { status: async () => ({ phase: 'ready' }) },
     plugins: { list: async () => [{ name: 'example-plugin', version: '1.0.0', enabled: true }], update },
   } })
   try {

+ 1 - 1
apps/desktop/tests/plugin-pnpm.spec.ts

@@ -108,7 +108,7 @@ it.each(['directory', 'file', 'tarball', 'plain'] as const)('installs a %s sourc
     await manager.mutate({ type: 'plugin-add', spec }, hooks)
     expect(manager.listPlugins()).toEqual([{ name: 'local-plugin', version: '1.0.0', enabled: source !== 'plain' }])
     expect(existsSync(join(manager.paths.profile, 'node_modules/local-plugin/missing.yml'))).toBe(false)
-    await manager.mutate({ type: 'plugins-disable-all' }, hooks)
+    await manager.disableAllPlugins()
     expect(manager.listPlugins()[0]?.enabled).toBe(false)
     await manager.mutate({ type: 'plugin-remove', name: 'local-plugin' }, hooks)
     expect(manager.listPlugins()).toEqual([])

+ 5 - 9
apps/desktop/tests/project-manager.spec.ts

@@ -89,7 +89,7 @@ describe('desktop external plugin profile', () => {
     const manifest = join(manager.paths.profile, 'node_modules/plugin/package.json')
     writeFileSync(manifest, '{broken')
     await expect(manager.applyRelease()).resolves.toBeUndefined()
-    await manager.mutate({ type: 'plugins-disable-all' }, hooks())
+    await manager.disableAllPlugins()
     await expect(manager.applyRelease()).resolves.toBeUndefined()
     expect(readFileSync(manifest, 'utf8')).toBe('{broken')
   })
@@ -100,11 +100,7 @@ describe('desktop external plugin profile', () => {
     await manager.mutate({ type: 'plugin-add', spec: 'plugin@1.0.0' }, hooks())
     const patch = join(manager.paths.profile, 'node_modules/plugin/bundle.yml')
     unlinkSync(patch)
-    await manager.mutate({ type: 'plugins-disable-all' }, hooks({ afterChange: async () => {
-      expect((JSON.parse(readFileSync(join(manager.paths.profile, 'package.json'), 'utf8')) as {
-        dsh: { profile: { bundles: string[] } }
-      }).dsh.profile.bundles).not.toContain('plugin')
-    } }))
+    await manager.disableAllPlugins()
     expect((JSON.parse(readFileSync(join(manager.paths.profile, 'package.json'), 'utf8')) as {
       dsh: { profile: { bundles: string[] } }
     }).dsh.profile.bundles).not.toContain('plugin')
@@ -291,7 +287,7 @@ describe('desktop external plugin profile', () => {
     const { root, manager } = setup()
     await manager.applyRelease()
     await manager.mutate({ type: 'plugin-add', spec: 'plugin@1.0.0' }, hooks())
-    await manager.mutate({ type: 'plugins-disable-all' }, hooks())
+    await manager.disableAllPlugins()
     expect(calls(root)).toHaveLength(1)
     expect(manager.listPlugins()).toEqual([{ name: 'plugin', version: '1.0.0', enabled: false }])
     await manager.mutate({ type: 'plugin-update', name: 'plugin', version: '1.1.0' }, hooks())
@@ -339,7 +335,7 @@ describe('desktop external plugin profile', () => {
     const next = new DesktopProjectManager(manager.paths, { ...manager.runtime, dsh })
     await expect(next.applyRelease()).resolves.toBeUndefined()
     expect(next.dshVersion()).toBe('2.0.0')
-    await next.mutate({ type: 'plugins-disable-all' }, hooks())
+    await next.disableAllPlugins()
     expect(next.dshVersion()).toBe('2.0.0')
     expect(next.listPlugins()).toEqual([{ name: 'plugin', version: '1.0.0', enabled: false }])
   })
@@ -375,7 +371,7 @@ describe('desktop external plugin profile', () => {
     expect(existsSync(manager.paths.lock)).toBe(false)
     writeFileSync(join(manager.paths.profile, 'desktop-packages-pending'), '')
     await manager.applyRelease()
-    await manager.mutate({ type: 'plugins-disable-all' }, hooks({ afterChange: async () => { starts++ } }))
+    await manager.mutate({ type: 'plugin-toggle', name: 'plugin', enabled: false }, hooks({ afterChange: async () => { starts++ } }))
     expect(starts).toBe(2)
     await manager.mutate({ type: 'plugin-remove', name: 'plugin' }, hooks())
     expect(manager.listPlugins()).toEqual([])