fix: preserve manager failures and support worker preview imports
This commit is contained in:
1 parent
8e327513ba
commit
5a9c159c27
20 files changed
+138
-23
No files matched your search
+2
-2
@@ -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-14-current-profile-plugin-management.md
|
||||
2026-09-14-current-profile-plugin-management.md: 316fb57a7826ae937f8f6225a2b2425563476bb1
|
||||
2026-09-14-current-profile-plugin-management.zh.md: c2b6a0c5420f8b028ff6e3db6839b09e4672a65a
|
||||
2026-09-14-current-profile-plugin-management.md: 1234007022bb0ac5e9247a0387d03ed9a4e94cdc
|
||||
2026-09-14-current-profile-plugin-management.zh.md: 0d6059089c51379bad3b99bbcbe533afa8ac4c26
|
||||
+1
-1
@@ -14,7 +14,7 @@ Web and agent controls need to change a running profile without creating an inde
|
||||
|
||||
Profile files remain the persisted state: entry toggles edit only `disabled` in the YAML document, and bundle toggles edit the ordered string list. Dependency updates do not reactivate retained disabled bundles. A service removal first applies the composition without the bundle and waits for old fibers to finish before deleting the dependency. Saved configuration, pnpm completion and runtime activation have separate outcomes; failure preserves the actual partial state and a diagnostic path.
|
||||
|
||||
This extends the [profile bundle composition decision](2026-08-05-profile-plugin-bundles.md). Startup profiles keep their process composition, and Desktop package management remains shell-owned. Web controls and explicitly enabled agent tools call the same service, whose batched durable notices inform live Agents without waking them. The agent tool is disabled by default in the base bundle and shipped presets.
|
||||
This extends the [profile bundle composition decision](2026-08-05-profile-plugin-bundles.md). Startup profiles keep their process composition, and Desktop package management remains shell-owned. Web controls and explicitly enabled agent tools call the same service, whose batched durable notices inform live Agents without waking them. The agent tool is disabled by default in the base bundle and shipped presets. The browser-only worker preview has no host package installer; its module-proxy table refuses `execa` calls explicitly while retaining the management module for inventory discovery.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ Web 和 Agent 控件需要修改运行中的 profile,同时避免另建包安
|
||||
|
||||
profile 文件保持为持久状态:条目开关只修改 YAML 文档中的 `disabled`,组合包开关修改有序字符串列表。更新依赖不会重新激活保留的已停用组合包。service 删除组合包时,先应用去掉该组合包的配置,等待旧 fiber 完成卸载后再删除依赖。已保存配置、pnpm 完成状态与运行时激活分别报告;失败保留实际的部分状态与诊断路径。
|
||||
|
||||
这扩展了[profile 组合包决策](2026-08-05-profile-plugin-bundles.zh.md)。startup profile 保留进程组合,Desktop 包管理仍由 shell 持有。Web 控件与显式启用的 Agent 工具调用同一 service;service 合并持久通知,告知存活 Agent 而不唤醒它们。base 组合包和内置预设默认禁用该 Agent 工具。
|
||||
这扩展了[profile 组合包决策](2026-08-05-profile-plugin-bundles.zh.md)。startup profile 保留进程组合,Desktop 包管理仍由 shell 持有。Web 控件与显式启用的 Agent 工具调用同一 service;service 合并持久通知,告知存活 Agent 而不唤醒它们。base 组合包和内置预设默认禁用该 Agent 工具。纯浏览器 worker 预览没有宿主包安装器;其模块代理表明确拒绝 `execa` 调用,同时保留管理模块用于清单发现。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
|
||||
@@ -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 docs/module-graph.md
|
||||
module-graph.md: 27646579259f09fca6cb0090a8bc201c4fae28ef
|
||||
module-graph.zh.md: 9d1c4ced2fea4bad5432ab2b2d7d0f618036fc4a
|
||||
module-graph.md: 3c6e6a81c741e4f3779cda709fe00d455a65ca95
|
||||
module-graph.zh.md: 5df66d6740712a4d617778c9279d3676266cb218
|
||||
@@ -126,6 +126,7 @@ flowchart TD
|
||||
subgraph group_boot["packages/boot"]
|
||||
pkg_app_boot["app-boot"]
|
||||
pkg_cmdline["cmdline"]
|
||||
pkg_hmr["hmr"]
|
||||
pkg_plugin_manager["plugin-manager"]
|
||||
end
|
||||
subgraph group_browser_use["packages/browser-use"]
|
||||
@@ -456,6 +457,7 @@ flowchart TD
|
||||
pkg_spill --> pkg_brand
|
||||
pkg_spill --> pkg_llm
|
||||
pkg_spill --> pkg_session
|
||||
pkg_app_boot --> pkg_hmr
|
||||
pkg_app_boot --> pkg_home_paths
|
||||
pkg_app_boot --> pkg_launch_environment
|
||||
pkg_app_boot --> pkg_system_prompt
|
||||
@@ -1082,6 +1084,7 @@ flowchart TD
|
||||
pkg_acp --> pkg_token_meter
|
||||
pkg_acp --> pkg_user_approval
|
||||
pkg_plugin_manager --> pkg_agent
|
||||
pkg_plugin_manager --> pkg_hmr
|
||||
pkg_plugin_manager --> pkg_host_plugin_inventory
|
||||
pkg_plugin_manager --> pkg_llm
|
||||
pkg_plugin_manager --> pkg_subprocess
|
||||
@@ -1300,6 +1303,7 @@ flowchart TD
|
||||
| [`api-gateway`](../packages/api/gateway) | `api` | — |
|
||||
| [`api-workspace-files`](../packages/api/workspace-files) | `api` | — |
|
||||
| [`cmdline`](../packages/boot/cmdline) | `boot` | — |
|
||||
| [`hmr`](../packages/boot/hmr) | `boot` | — |
|
||||
| [`acp-app`](../packages/bundle/acp-app) | `bundle` | — |
|
||||
| [`base`](../packages/bundle/base) | `bundle` | — |
|
||||
| [`sdk-app`](../packages/bundle/sdk-app) | `bundle` | — |
|
||||
@@ -1412,7 +1416,7 @@ flowchart TD
|
||||
| [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`skill`](../packages/skill/skill) |
|
||||
| [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
| [`app-boot`](../packages/boot/app-boot) | `boot` | [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`app-boot`](../packages/boot/app-boot) | `boot` | [`hmr`](../packages/boot/hmr), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`persona`](../packages/preset/persona) | `preset` | [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
| [`session-format-catalog`](../packages/session/session-format-catalog) | `session` | [`session`](../packages/core/session) |
|
||||
@@ -1538,7 +1542,7 @@ flowchart TD
|
||||
| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
|
||||
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`agent`](../packages/core/agent), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`agent`](../packages/core/agent), [`hmr`](../packages/boot/hmr), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |
|
||||
| [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
|
||||
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) |
|
||||
|
||||
@@ -128,6 +128,7 @@ flowchart TD
|
||||
subgraph group_boot["packages/boot"]
|
||||
pkg_app_boot["app-boot"]
|
||||
pkg_cmdline["cmdline"]
|
||||
pkg_hmr["hmr"]
|
||||
pkg_plugin_manager["plugin-manager"]
|
||||
end
|
||||
subgraph group_browser_use["packages/browser-use"]
|
||||
@@ -458,6 +459,7 @@ flowchart TD
|
||||
pkg_spill --> pkg_brand
|
||||
pkg_spill --> pkg_llm
|
||||
pkg_spill --> pkg_session
|
||||
pkg_app_boot --> pkg_hmr
|
||||
pkg_app_boot --> pkg_home_paths
|
||||
pkg_app_boot --> pkg_launch_environment
|
||||
pkg_app_boot --> pkg_system_prompt
|
||||
@@ -1084,6 +1086,7 @@ flowchart TD
|
||||
pkg_acp --> pkg_token_meter
|
||||
pkg_acp --> pkg_user_approval
|
||||
pkg_plugin_manager --> pkg_agent
|
||||
pkg_plugin_manager --> pkg_hmr
|
||||
pkg_plugin_manager --> pkg_host_plugin_inventory
|
||||
pkg_plugin_manager --> pkg_llm
|
||||
pkg_plugin_manager --> pkg_subprocess
|
||||
@@ -1302,6 +1305,7 @@ flowchart TD
|
||||
| [`api-gateway`](../packages/api/gateway) | `api` | — |
|
||||
| [`api-workspace-files`](../packages/api/workspace-files) | `api` | — |
|
||||
| [`cmdline`](../packages/boot/cmdline) | `boot` | — |
|
||||
| [`hmr`](../packages/boot/hmr) | `boot` | — |
|
||||
| [`acp-app`](../packages/bundle/acp-app) | `bundle` | — |
|
||||
| [`base`](../packages/bundle/base) | `bundle` | — |
|
||||
| [`sdk-app`](../packages/bundle/sdk-app) | `bundle` | — |
|
||||
@@ -1414,7 +1418,7 @@ flowchart TD
|
||||
| [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
|
||||
| [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`skill`](../packages/skill/skill) |
|
||||
| [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
| [`app-boot`](../packages/boot/app-boot) | `boot` | [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`app-boot`](../packages/boot/app-boot) | `boot` | [`hmr`](../packages/boot/hmr), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`persona`](../packages/preset/persona) | `preset` | [`system-prompt`](../packages/core/system-prompt) |
|
||||
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
|
||||
| [`session-format-catalog`](../packages/session/session-format-catalog) | `session` | [`session`](../packages/core/session) |
|
||||
@@ -1540,7 +1544,7 @@ flowchart TD
|
||||
| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
|
||||
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
|
||||
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`agent`](../packages/core/agent), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`agent`](../packages/core/agent), [`hmr`](../packages/boot/hmr), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
|
||||
| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) |
|
||||
| [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
|
||||
| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) |
|
||||
|
||||
@@ -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 packages/client/ui-settings-plugin-inventory/README.md
|
||||
README.md: 49092e4598cb42818238aa75827f07f44cc76dda
|
||||
README.zh.md: 2ae1a388078e003cbab6380c990d014296652219
|
||||
README.md: 54392425cb58028dafbfd1bf749323b93b10b56e
|
||||
README.zh.md: 4ce94dbeb11a5ceed6a346438c5c87a6a69c1071
|
||||
@@ -47,7 +47,7 @@ A failed read renders a generic failure state inside the tab; retrying re-runs t
|
||||
<details>
|
||||
<summary>Implementation internals — click to expand</summary>
|
||||
|
||||
The tab reads the Host inventory on first selection, without Remote calls during plugin activation. Hosts exposing [Plugin Manager](../../boot/plugin-manager/README.md) also provide bundle installation, removal and switches for uniquely addressable global entries. Operations refresh observed state and show failures, overrides and pending restarts; preset compositions remain read-only.
|
||||
The tab reads the Host inventory on first selection, without Remote calls during plugin activation. Hosts exposing [Plugin Manager](../../boot/plugin-manager/README.md) also provide bundle installation, removal and switches for uniquely addressable global entries. Operations refresh observed state and show failures, overrides and pending restarts; a successful inventory refresh preserves the last operation error; preset compositions remain read-only.
|
||||
|
||||
### Registration
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ kind: "package-reference"
|
||||
<details>
|
||||
<summary>实现细节——点击展开</summary>
|
||||
|
||||
该标签页首次选择时读取宿主清单,插件激活期间不执行 Remote 调用。提供[插件管理器](../../boot/plugin-manager/README.zh.md)的宿主还支持组合包安装、删除及可唯一定位的全局条目开关。操作后刷新实际状态,并显示失败、覆盖与待重启结果;预设组合保持只读。
|
||||
该标签页首次选择时读取宿主清单,插件激活期间不执行 Remote 调用。提供[插件管理器](../../boot/plugin-manager/README.zh.md)的宿主还支持组合包安装、删除及可唯一定位的全局条目开关。操作后刷新实际状态,并显示失败、覆盖与待重启结果;清单刷新成功时仍保留最近一次操作错误;预设组合保持只读。
|
||||
|
||||
### 注册
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ export function usePluginManagement(manager: PluginManagement | undefined, avail
|
||||
const submitting = useRef(false)
|
||||
const [result, setResult] = useState<ChangeResult>()
|
||||
const [error, setError] = useState<string>()
|
||||
const [readError, setReadError] = useState<string>()
|
||||
const [refresh, setRefresh] = useState(0)
|
||||
useEffect(() => {
|
||||
if (!available || manager === undefined) return
|
||||
@@ -35,8 +36,8 @@ export function usePluginManagement(manager: PluginManagement | undefined, avail
|
||||
if (!current) return
|
||||
setPlugins(plugins)
|
||||
setBundles(bundles)
|
||||
setError(undefined)
|
||||
}, (error: unknown) => { if (current) setError(error instanceof Error ? error.message : String(error)) })
|
||||
setReadError(undefined)
|
||||
}, (error: unknown) => { if (current) setReadError(error instanceof Error ? error.message : String(error)) })
|
||||
return () => { current = false }
|
||||
}, [manager, available, revision, refresh])
|
||||
const run = async (operation: () => Promise<ChangeResult>): Promise<void> => {
|
||||
@@ -48,7 +49,7 @@ export function usePluginManagement(manager: PluginManagement | undefined, avail
|
||||
catch (error) { setError(error instanceof Error ? error.message : String(error)) }
|
||||
finally { submitting.current = false; setBusy(false); setRefresh(value => value + 1) }
|
||||
}
|
||||
return { plugins, bundles, busy, result, error, run, refresh }
|
||||
return { plugins, bundles, busy, result, error: error ?? readError, run, refresh }
|
||||
}
|
||||
|
||||
/** Minimal bundle installation and enablement form. */
|
||||
|
||||
@@ -8,6 +8,7 @@ import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { apply, inject, NS } from '../src/client/index.ts'
|
||||
import { PluginInventorySettingsTab } from '../src/client/PluginInventorySettingsTab.tsx'
|
||||
import type { PluginEntryId } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type { PluginInventorySettingsTabInjected } from '../src/client/PluginInventorySettingsTab.tsx'
|
||||
import { apply as hostApply } from '../src/index.ts'
|
||||
|
||||
@@ -34,8 +35,15 @@ async function bench() {
|
||||
.mockResolvedValue({ ok: true, value: EMPTY })
|
||||
ctx.provide('remote.pluginInventory', { list })
|
||||
const listBundles = vi.fn().mockResolvedValue({ ok: true, value: [] })
|
||||
ctx.provide('remote.pluginManager', { listBundles })
|
||||
return { ctx, slots: ctx.get('slots') as SlotRegistry, locale, list, listBundles }
|
||||
const managed = {
|
||||
listBundles, listPlugins: vi.fn().mockResolvedValue({ ok: true, value: [] }),
|
||||
setPluginEnabled: vi.fn().mockResolvedValue({ ok: true, value: { changed: true, application: 'applied', message: 'plugin' } }),
|
||||
setBundleEnabled: vi.fn().mockResolvedValue({ ok: true, value: { changed: true, application: 'applied', message: 'bundle' } }),
|
||||
installBundle: vi.fn().mockResolvedValue({ ok: true, value: { changed: true, application: 'restart-required', message: 'install' } }),
|
||||
removeBundle: vi.fn().mockResolvedValue({ ok: true, value: { changed: true, application: 'applied', message: 'remove' } }),
|
||||
}
|
||||
ctx.provide('remote.pluginManager', managed)
|
||||
return { ctx, slots: ctx.get('slots') as SlotRegistry, locale, list, listBundles, managed }
|
||||
}
|
||||
|
||||
function declare(slots: SlotRegistry): () => void {
|
||||
@@ -71,6 +79,18 @@ describe('ui-settings-plugin-inventory browser plugin', () => {
|
||||
expect(b.list).toHaveBeenCalledOnce()
|
||||
await expect(injected.management?.listBundles()).resolves.toEqual([])
|
||||
expect(b.listBundles).toHaveBeenCalledOnce()
|
||||
const manager = injected.management!
|
||||
await expect(manager.listPlugins()).resolves.toEqual([])
|
||||
await expect(manager.setPluginEnabled('include:plugin' as PluginEntryId, false)).resolves.toMatchObject({ message: 'plugin' })
|
||||
expect(b.managed.setPluginEnabled).toHaveBeenCalledWith('include:plugin', false)
|
||||
await expect(manager.setBundleEnabled('extra', false)).resolves.toMatchObject({ message: 'bundle' })
|
||||
expect(b.managed.setBundleEnabled).toHaveBeenCalledWith('extra', false)
|
||||
await expect(manager.installBundle('extra@1', { enabled: false })).resolves.toMatchObject({ application: 'restart-required' })
|
||||
expect(b.managed.installBundle).toHaveBeenCalledWith('extra@1', { enabled: false })
|
||||
await expect(manager.removeBundle('extra')).resolves.toMatchObject({ message: 'remove' })
|
||||
expect(b.managed.removeBundle).toHaveBeenCalledWith('extra')
|
||||
b.listBundles.mockResolvedValueOnce({ ok: false, error: new Error('manager unavailable') })
|
||||
await expect(manager.listBundles()).rejects.toThrow('manager unavailable')
|
||||
b.list.mockResolvedValueOnce({ ok: false, error: { code: 'REMOTE_ERROR', message: 'unavailable' } })
|
||||
await expect(injected.list()).rejects.toThrow('pluginInventory.list failed: REMOTE_ERROR: unavailable')
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @vitest-environment jsdom
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { act, cleanup, fireEvent, render, renderHook, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginInventorySettingsTab } from '../src/client/PluginInventorySettingsTab.tsx'
|
||||
import type {
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
PluginInventorySettingsTabProps,
|
||||
} from '../src/client/PluginInventorySettingsTab.tsx'
|
||||
import { en, type PluginInventoryLocaleKey } from '../src/client/locales.ts'
|
||||
import { BundleManager, usePluginManagement, type PluginManagement } from '../src/client/management.tsx'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@@ -414,11 +415,83 @@ describe('persistent profile management', () => {
|
||||
await waitFor(() => { expect(management.setPluginEnabled).toHaveBeenCalledWith('managed', false) })
|
||||
await waitFor(() => { expect(screen.getByRole<HTMLInputElement>('switch', { name: 'Toggle plugin managed' }).checked).toBe(false) })
|
||||
fireEvent.change(screen.getByRole('textbox', { name: 'npm package name or local path' }), { target: { value: 'new-bundle' } })
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: 'Enable after installation' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Install' }))
|
||||
await waitFor(() => { expect(management.installBundle).toHaveBeenCalledWith('new-bundle', { enabled: true }) })
|
||||
await waitFor(() => { expect(management.installBundle).toHaveBeenCalledWith('new-bundle', { enabled: false }) })
|
||||
expect((await screen.findByRole('alert')).textContent).toContain('Registry unavailable.')
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Remove' }))
|
||||
await waitFor(() => { expect(management.removeBundle).toHaveBeenCalledWith('extra') })
|
||||
await waitFor(() => { expect(screen.queryByRole('switch', { name: 'Toggle bundle extra' })).toBeNull() })
|
||||
})
|
||||
})
|
||||
|
||||
function managementFixture() {
|
||||
const applied = { changed: true, application: 'applied' as const, message: 'saved' }
|
||||
return {
|
||||
listPlugins: vi.fn<PluginManagement['listPlugins']>(async () => []),
|
||||
listBundles: vi.fn<PluginManagement['listBundles']>(async () => []),
|
||||
setPluginEnabled: vi.fn(async () => applied), setBundleEnabled: vi.fn(async () => applied),
|
||||
installBundle: vi.fn(async () => applied), removeBundle: vi.fn(async () => applied),
|
||||
}
|
||||
}
|
||||
|
||||
it('ignores inventory completion after unmount and reports current read failures', async () => {
|
||||
for (const failure of [undefined, new Error('late failure')]) {
|
||||
const read = Promise.withResolvers<Awaited<ReturnType<PluginManagement['listBundles']>>>()
|
||||
const manager = managementFixture()
|
||||
vi.mocked(manager.listBundles).mockReturnValue(read.promise)
|
||||
const hook = renderHook(() => usePluginManagement(manager, true, 0))
|
||||
hook.unmount()
|
||||
await act(async () => {
|
||||
if (failure === undefined) read.resolve([])
|
||||
else read.reject(failure)
|
||||
await read.promise.catch(() => {})
|
||||
})
|
||||
}
|
||||
for (const failure of [new Error('inventory unavailable'), 'inventory rejected']) {
|
||||
const manager = managementFixture()
|
||||
vi.mocked(manager.listBundles).mockRejectedValue(failure)
|
||||
const hook = renderHook(() => usePluginManagement(manager, true, 0))
|
||||
await waitFor(() => { expect(hook.result.current.error).toBe(failure instanceof Error ? failure.message : failure) })
|
||||
hook.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
it('blocks duplicate submissions and refreshes after a rejected management request', async () => {
|
||||
const manager = managementFixture()
|
||||
const hook = renderHook(() => usePluginManagement(manager, true, 0))
|
||||
const pending = Promise.withResolvers<Awaited<ReturnType<PluginManagement['removeBundle']>>>()
|
||||
const operation = vi.fn(() => pending.promise)
|
||||
await act(async () => {
|
||||
const first = hook.result.current.run(operation)
|
||||
await hook.result.current.run(operation)
|
||||
pending.resolve({ changed: true, application: 'applied', message: 'removed' })
|
||||
await first
|
||||
})
|
||||
expect(operation).toHaveBeenCalledOnce()
|
||||
expect(hook.result.current.result?.message).toBe('removed')
|
||||
for (const failure of [new Error('request unavailable'), 'request rejected']) {
|
||||
await act(async () => { await hook.result.current.run(async () => { throw failure }) })
|
||||
expect(hook.result.current.error).toBe(failure instanceof Error ? failure.message : failure)
|
||||
expect(hook.result.current.busy).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('shows package diagnostics, read errors and protected or missing bundles', () => {
|
||||
const manager = managementFixture()
|
||||
const state: ReturnType<typeof usePluginManagement> = {
|
||||
plugins: [], bundles: [
|
||||
{ name: 'missing', enabled: false, removable: true, error: 'package files missing' },
|
||||
{ name: 'core', enabled: true, removable: false, readOnlyReason: 'required for management' },
|
||||
], busy: false, refresh: 0, error: 'inventory unavailable', run: async () => {},
|
||||
result: { changed: true, application: 'failed', message: 'installation failed',
|
||||
packageResult: { exitCode: 1, output: 'failed', truncated: false, logPath: '/profile/operation/pnpm.log' } },
|
||||
}
|
||||
render(<BundleManager manager={manager} state={state} t={t} />)
|
||||
expect(screen.getByText('inventory unavailable')).toBeDefined()
|
||||
expect(screen.getByText('/profile/operation/pnpm.log')).toBeDefined()
|
||||
expect(screen.getByText('package files missing')).toBeDefined()
|
||||
expect(screen.getByText('required for management')).toBeDefined()
|
||||
expect(screen.getByRole<HTMLInputElement>('switch', { name: 'Toggle bundle missing' }).disabled).toBe(true)
|
||||
expect(screen.getByRole<HTMLInputElement>('switch', { name: 'Toggle bundle core' }).disabled).toBe(true)
|
||||
})
|
||||
@@ -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 packages/experimental/webworker-runtime/README.md
|
||||
README.md: ab54ec689664adc1c0e01cc3387777c77f36bc66
|
||||
README.zh.md: d6593eeff22e642e2423fa9348f64d751d5de62f
|
||||
README.md: 724e47a004fac83fe0f19a588faf0c227d843232
|
||||
README.zh.md: 4e88b3cf10f98a423268c60b69269d2550d2c1f9
|
||||
@@ -53,6 +53,7 @@ None; this package neither assembles nor sends a provider request.
|
||||
|
||||
- **The worker composition writes plaintext session logs** (`compression: 'none'` boot patch): it carries no Zstandard codec, so exported logs are `.jsonl`, never `.jsonl.zstd`.
|
||||
- **`node:dns/promises`, `node:vm`, `node:net`, `node:sqlite`, `node:worker_threads` are structural stubs**: every call reports its refusal on the console and throws. Rows needing native DNS, a real process, or realm isolation cannot run here.
|
||||
- **Host package commands are unavailable**: `execa` reports an explicit worker-host refusal; the preview cannot run pnpm or install native dependencies.
|
||||
- **PTC Node programs are unavailable**: the process shim exposes `/dsh/bin/node` as its executable identity so the provider can activate, but the Worker has neither a Node executable nor `stripTypeScriptTypes`. Program execution fails before launching a child.
|
||||
- **Filesystem watchers observe only the mounted VFS**: image seeding is silent and the VFS has no symlinks or external writers. `persistent`, `ref()`, and `unref()` preserve the Node API but cannot control a dedicated Worker's lifetime because browsers expose no ref-counted event loop.
|
||||
- **Worker confinement is a VFS boundary, not kernel Landlock**: `read-only` and `workspace-write` run the unchanged `@deepseek-ai/node-addon-system/landlock-run` JavaScript and launcher argv, but the process layer implements the logical `landlock-run` executable and enforces its grants on every shell filesystem request. `full` therefore covers the Worker command table and mounted VFS only; it does not claim arbitrary native-process execution or Linux kernel isolation.
|
||||
|
||||
@@ -53,6 +53,7 @@ kind: "package-library"
|
||||
|
||||
- **worker 组合写明文会话日志**(`compression: 'none'` boot patch):不带 Zstandard 编解码器,导出日志是 `.jsonl`,不会是 `.jsonl.zstd`。
|
||||
- **`node:dns/promises`、`node:vm`、`node:net`、`node:sqlite`、`node:worker_threads` 是结构化 stub**:每次调用在 console 报告拒绝并抛出。需要原生 DNS、真进程或真 realm 隔离的行在此无法运行。
|
||||
- **宿主包管理命令不可用**:`execa` 明确报告 worker host 不支持该调用;预览无法运行 pnpm 或安装原生依赖。
|
||||
- **PTC Node 程序不可用**:process shim 用 `/dsh/bin/node` 表示可执行文件身份,使 provider 能够激活,但 Worker 既没有 Node 可执行文件,也没有 `stripTypeScriptTypes`。程序执行会在启动子进程前失败。
|
||||
- **文件 watcher 只能观察已挂载的 VFS**:镜像 seed 不产生事件,VFS 也没有符号链接或外部写入方。`persistent`、`ref()` 和 `unref()` 保留 Node API,但浏览器没有引用计数事件循环,因此这些接口不能控制 dedicated Worker 的生存期。
|
||||
- **Worker confinement 是 VFS 边界,不是内核 Landlock**:`read-only` 和 `workspace-write` 运行未经修改的 `@deepseek-ai/node-addon-system/landlock-run` JavaScript 与 launcher argv,进程层则实现逻辑 `landlock-run` 可执行文件,并在 shell 的每次文件系统请求上执行其授权。`full` 仅覆盖 Worker 命令表和已挂载 VFS,不表示能够执行任意 native 进程,也不表示 Linux 内核隔离。
|
||||
|
||||
@@ -69,6 +69,7 @@ export const MODULE_PROXIES: Record<string, string> = {
|
||||
'koffi': './node/external_packages/koffi.ts',
|
||||
'sharp': './node/external_packages/sharp.ts',
|
||||
'node-pty': './node/external_packages/node-pty.ts',
|
||||
'execa': './node/external_packages/execa.ts',
|
||||
'@vscode/ripgrep': './node/external_packages/ripgrep.ts',
|
||||
'@earendil-works/pi-ai': './node/external_packages/pi-ai.ts',
|
||||
// Constructible fakes whose methods are never reached.
|
||||
|
||||
@@ -48,6 +48,7 @@ import * as nodeWorkerThreads from './builtin_modules/mock/worker_threads.ts'
|
||||
import * as systemFlock from './external_packages/node-addon-system-flock.ts'
|
||||
import * as koffi from './external_packages/koffi.ts'
|
||||
import * as nodePty from './external_packages/node-pty.ts'
|
||||
import * as execa from './external_packages/execa.ts'
|
||||
import * as piAi from './external_packages/pi-ai.ts'
|
||||
import * as ripgrep from './external_packages/ripgrep.ts'
|
||||
import * as sharp from './external_packages/sharp.ts'
|
||||
@@ -90,6 +91,7 @@ const EXTERNALS: Record<string, StaticModuleFactory> = {
|
||||
'koffi': () => koffi,
|
||||
'sharp': () => sharp,
|
||||
'node-pty': () => nodePty,
|
||||
'execa': () => execa,
|
||||
'ws': () => ws,
|
||||
'@vscode/ripgrep': () => ripgrep,
|
||||
'@earendil-works/pi-ai': () => piAi,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/** Host package commands cannot install dependencies in the browser preview. */
|
||||
import { notImplementedFail } from '../notImplementedFail.ts'
|
||||
|
||||
/** Refuse external package commands in the worker host. */
|
||||
export const execa = notImplementedFail('execa', 'execa')
|
||||
+1
@@ -12,6 +12,7 @@ export const REPLACED_EXTERNAL_PACKAGES: readonly string[] = [
|
||||
'@deepseek-ai/node-addon-system/flock',
|
||||
'koffi',
|
||||
'node-pty',
|
||||
'execa',
|
||||
'sharp',
|
||||
'ws',
|
||||
]
|
||||
@@ -21,6 +21,7 @@ import * as stream from '../../src/node/builtin_modules/implemented/stream.ts'
|
||||
import * as vm from '../../src/node/builtin_modules/mock/vm.ts'
|
||||
import * as workerThreads from '../../src/node/builtin_modules/mock/worker_threads.ts'
|
||||
import * as nodePty from '../../src/node/external_packages/node-pty.ts'
|
||||
import * as execa from '../../src/node/external_packages/execa.ts'
|
||||
import * as piAi from '../../src/node/external_packages/pi-ai.ts'
|
||||
import * as ripgrep from '../../src/node/external_packages/ripgrep.ts'
|
||||
import * as ws from '../../src/node/external_packages/ws.ts'
|
||||
@@ -43,6 +44,7 @@ const CALLED: [string, Record<string, unknown>, readonly string[]][] = [
|
||||
// these three need a real process, so they stay refusals.
|
||||
['node:child_process', childProcess, ['execFileSync', 'execSync', 'fork']],
|
||||
['node-pty', nodePty, ['spawn', 'open']],
|
||||
['execa', execa, ['execa']],
|
||||
['@deepseek-ai/pi-ai', piAi, [
|
||||
'createProvider', 'createModels', 'openAICompletionsApi', 'openAIResponsesApi', 'anthropicMessagesApi',
|
||||
'isContextOverflow', 'getSupportedThinkingLevels',
|
||||
|
||||
Reference in new issue
Block a user