diff --git a/apps/web/tests/cua-permissions.e2e.ts b/apps/web/tests/cua-permissions.e2e.ts index bbd3d3d437..d64821f905 100644 --- a/apps/web/tests/cua-permissions.e2e.ts +++ b/apps/web/tests/cua-permissions.e2e.ts @@ -7,7 +7,7 @@ import { ToolCallId } from '@deepseek-ai/dsh-llm' import type {} from '@deepseek-ai/dsh-user-approval' import type {} from '@deepseek-ai/dsh-agent-preset-registry' import { chromium, type Browser } from 'playwright' -import { expect, it, onTestFailed, onTestFinished } from 'vitest' +import { expect, it, onTestFailed, onTestFinished, vi } from 'vitest' import { captureStableAria, compareOrRefreshGolden, launchWebScaffold, webSnapshotMode, watchConsole, type WebScaffold } from './scaffold.ts' import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts' import { installNativeFixture, startFixtureHost } from '../../../packages/experimental/cua-macos/tests/native-fixture.ts' @@ -16,7 +16,7 @@ const bundle = fileURLToPath(new URL('../../../packages/experimental/cua-bundle' const expected = fileURLToPath(new URL('./expected/cua-permissions/missing.expected.md', import.meta.url)) // System-permission onboarding belongs to the macOS-only bundle; rendering and policy units remain portable. -it.skipIf(process.platform !== 'darwin').each([false, true])('keeps system requests pending until fresh observation and persists application revocation (read-only runtime: %s)', async (readOnlyRuntime) => { +it.skipIf(process.platform !== 'darwin')('keeps system requests pending until fresh observation and persists application revocation', async () => { const root = await mkdtemp(join(tmpdir(), 'dsh-cua-permissions-web-')) const resources: { scaffold?: WebScaffold; browser?: Browser; closeFixture?: () => Promise } = {} onTestFinished(async () => { @@ -38,11 +38,8 @@ it.skipIf(process.platform !== 'darwin').each([false, true])('keeps system reque await writeFile(join(fixtureBundle, 'package.json'), JSON.stringify({ name: 'cua-permission-fixture', version: '1.0.0', dsh: { bundle: { patch: 'cordis.patch.yml' } } })) await writeFile(join(fixtureBundle, 'cordis.patch.yml'), `- id: cua\n config: ${JSON.stringify({ helperPath: helper })}\n`) - const runtimePatch = join(root, 'readonly-runtime.yml') - await writeFile(runtimePatch, JSON.stringify([{ id: 'ptc-runtime', config: { executionMode: 'per-call' } }])) const scaffold = await launchWebScaffold({ profile: { packages: [{ dir: bundle, enabled: true }, { dir: fixtureBundle, enabled: true }] }, - ...(readOnlyRuntime ? { extraOverlayPath: runtimePatch } : {}), }) resources.scaffold = scaffold await scaffold.ctx.settings.mutate('cua', [{ op: 'set', path: ['allowedApps'], @@ -100,7 +97,7 @@ it.skipIf(process.platform !== 'darwin').each([false, true])('keeps system reque expect(await panel.getByRole('group', { name: 'Screen Recording', exact: true }).getByText('Visual Studio Code.app', { exact: true }).count()).toBe(1) await expect.poll(() => accessibility.locator('img').evaluate((image: HTMLImageElement) => image.naturalWidth)).toBe(1) await expect.poll(() => panel.getAttribute('aria-busy')).toBe('false') - if (!readOnlyRuntime) await compareOrRefreshGolden(expected, await captureStableAria(page, '[data-cua-settings]', scaffold.workspaceCwd), webSnapshotMode()) + await compareOrRefreshGolden(expected, await captureStableAria(page, '[data-cua-settings]', scaffold.workspaceCwd), webSnapshotMode()) try { for (const width of [1680, 390]) { await page.setViewportSize({ width, height: 1000 }) @@ -186,6 +183,8 @@ it.skipIf(process.platform !== 'darwin').each([false, true])('keeps system reque const toggle = page.locator('[data-plugin-panel]').getByRole('switch', { name: 'Enable Computer Use', exact: true }) for (const width of [1680, 390]) { await page.setViewportSize({ width, height: 1000 }) + if (width === 390) await page.locator('[data-sidebar-collapsed]').waitFor({ state: 'attached' }) + await page.locator('[data-animating]').waitFor({ state: 'hidden' }) await trace.evaluate((value) => { value.layouts.length = 0 }) for (let cycle = 0; cycle < 3; cycle++) { await toggle.click() @@ -205,5 +204,39 @@ it.skipIf(process.platform !== 'darwin').each([false, true])('keeps system reque await trace.evaluate((value) => { value.stop() }) await trace.dispose() } + await writeFile(state, JSON.stringify({ accessibility: 'unavailable', screenRecording: false })) + await page.evaluate(() => { window.dispatchEvent(new Event('focus')) }) + const diagnostic = panel.getByRole('alert') + await diagnostic.waitFor() + const diagnosticText = await diagnostic.innerText() + const height = await panel.evaluate(element => element.getBoundingClientRect().height) + const rowToggle = plugins.getByRole('switch', { name: 'Enable component @deepseek-ai/dsh-experimental-cua-macos', exact: true }) + const entered = Promise.withResolvers(), release = Promise.withResolvers() + const changeRow = scaffold.ctx.pluginManager.setPluginEnabled.bind(scaffold.ctx.pluginManager) + let changing: ReturnType | undefined + const hold = vi.spyOn(scaffold.ctx.pluginManager, 'setPluginEnabled').mockImplementationOnce((entryId, enabled) => { + entered.resolve(undefined) + changing = release.promise.then(() => changeRow(entryId, enabled)) + return changing + }) + try { + await rowToggle.click() + await entered.promise + await expect.poll(() => panel.getAttribute('aria-busy')).toBe('true') + expect(await diagnostic.innerText()).toBe(diagnosticText) + expect(await diagnostic.getByRole('button', { name: 'Retry', exact: true }).isEnabled()).toBe(false) + expect(await panel.evaluate(element => element.getBoundingClientRect().height)).toBe(height) + } finally { + release.resolve(undefined) + hold.mockRestore() + await changing + } + await expect.poll(() => rowToggle.getAttribute('aria-checked')).toBe('false') + await expect.poll(() => rowToggle.isEnabled()).toBe(true) + await writeFile(state, JSON.stringify({ accessibility: false, screenRecording: false, launchApplication })) + await rowToggle.click() + await expect.poll(() => rowToggle.getAttribute('aria-checked')).toBe('true') + await expect.poll(() => panel.getAttribute('aria-busy')).toBe('false') + await diagnostic.waitFor({ state: 'hidden' }) expect(tripwire.pageErrors).toEqual([]) }, 60_000) diff --git a/apps/web/tests/expected/cua-permissions/enabling.expected.md b/apps/web/tests/expected/cua-permissions/enabling.expected.md index 6059222749..b0e8e66529 100644 --- a/apps/web/tests/expected/cua-permissions/enabling.expected.md +++ b/apps/web/tests/expected/cua-permissions/enabling.expected.md @@ -1,5 +1,5 @@ - listitem: - button "查看 Computer Use": Computer Use - text: 实验性 目前仅支持 macOS,Windows 支持正在开发中。为已有及新建 PTC 会话加载 Computer Use。请切换到 PTC 模式来使用,并授予辅助功能和屏幕录制权限。 - - status "正在应用 Computer Use 的更改" + - status: 正在应用 Computer Use 的更改 - switch "启用 Computer Use" [disabled] diff --git a/apps/web/tests/expected/cua-permissions/missing.expected.md b/apps/web/tests/expected/cua-permissions/missing.expected.md index 4255b31e9f..8dfba492a2 100644 --- a/apps/web/tests/expected/cua-permissions/missing.expected.md +++ b/apps/web/tests/expected/cua-permissions/missing.expected.md @@ -16,7 +16,7 @@ - strong: Screen Recording - paragraph: In System Settings, allow Visual Studio Code.app to capture application windows. - button "Request permission" - - paragraph: Enable the application in System Settings. Follow the macOS prompt if it names another application. + - paragraph: Manage permissions in System Settings for the application named by macOS. - region "Manage applications Computer Use can access": - heading "Manage applications Computer Use can access" [level=3] - heading "Always-allowed applications" [level=4] diff --git a/apps/web/tests/expected/plugin-manager/exports-en.expected.md b/apps/web/tests/expected/plugin-manager/exports-en.expected.md index 9602e04da9..935d7ecb31 100644 --- a/apps/web/tests/expected/plugin-manager/exports-en.expected.md +++ b/apps/web/tests/expected/plugin-manager/exports-en.expected.md @@ -1,5 +1,6 @@ - button "Back to plugins": Plugins - button "Uninstall @fixture/bundle": Uninstall +- status - switch "Enable @fixture/bundle" - heading "@fixture/bundle" [level=3] - text: v0.0.1 diff --git a/apps/web/tests/expected/plugin-manager/exports.expected.md b/apps/web/tests/expected/plugin-manager/exports.expected.md index 702150c359..6cd9a63af2 100644 --- a/apps/web/tests/expected/plugin-manager/exports.expected.md +++ b/apps/web/tests/expected/plugin-manager/exports.expected.md @@ -1,5 +1,6 @@ - button "返回插件": 插件 - button "卸载 @fixture/bundle": 卸载 +- status - switch "启用 @fixture/bundle" - heading "@fixture/bundle" [level=3] - text: v0.0.1 diff --git a/apps/web/tests/expected/plugin-manager/live-enabled.expected.md b/apps/web/tests/expected/plugin-manager/live-enabled.expected.md index eab9dd48e1..2a8464f596 100644 --- a/apps/web/tests/expected/plugin-manager/live-enabled.expected.md +++ b/apps/web/tests/expected/plugin-manager/live-enabled.expected.md @@ -25,25 +25,31 @@ - listitem: - button "查看 智能体团队": 智能体团队 - text: 实验性 启用团队协作、团队工具、成员列表和共享任务看板。 + - status - switch "启用 智能体团队" - listitem: - button "查看 语音输入": 语音输入 - text: 实验性 在本机使用 SenseVoice 转写录音,首次使用需安装依赖。 + - status - switch "启用 语音输入" - listitem: - button "查看 自动授权审查": 自动授权审查 - text: 实验性 提供自动审查权限模式,由模型在每次工具调用前判断是否授权。 + - status - switch "启用 自动授权审查" - listitem: - button "查看 开发者工具": 开发者工具 - text: 实验性 查看调试会话原始数据、聊天消息分组数据,以及调试 NodeJS 后端 + - status - switch "启用 开发者工具" - heading "已安装插件" [level=3] - list: - listitem: - button "查看 @fixture/bundle": "@fixture/bundle" - text: Registry description for the fixture bundle. + - status - switch "启用 @fixture/bundle" [checked] - listitem: - button "查看 dsh-web-scaffold-defaults": dsh-web-scaffold-defaults + - status - switch "启用 dsh-web-scaffold-defaults" [checked] diff --git a/apps/web/tests/expected/plugin-manager/manager.expected.md b/apps/web/tests/expected/plugin-manager/manager.expected.md index f3dd4a7bc6..3d5cb8dfa7 100644 --- a/apps/web/tests/expected/plugin-manager/manager.expected.md +++ b/apps/web/tests/expected/plugin-manager/manager.expected.md @@ -25,25 +25,31 @@ - listitem: - button "查看 智能体团队": 智能体团队 - text: 实验性 启用团队协作、团队工具、成员列表和共享任务看板。 + - status - switch "启用 智能体团队" - listitem: - button "查看 语音输入": 语音输入 - text: 实验性 在本机使用 SenseVoice 转写录音,首次使用需安装依赖。 + - status - switch "启用 语音输入" - listitem: - button "查看 自动授权审查": 自动授权审查 - text: 实验性 提供自动审查权限模式,由模型在每次工具调用前判断是否授权。 + - status - switch "启用 自动授权审查" - listitem: - button "查看 开发者工具": 开发者工具 - text: 实验性 查看调试会话原始数据、聊天消息分组数据,以及调试 NodeJS 后端 + - status - switch "启用 开发者工具" - heading "已安装插件" [level=3] - list: - listitem: - button "查看 @fixture/bundle": "@fixture/bundle" - text: Registry description for the fixture bundle. + - status - switch "启用 @fixture/bundle" - listitem: - button "查看 dsh-web-scaffold-defaults": dsh-web-scaffold-defaults + - status - switch "启用 dsh-web-scaffold-defaults" [checked] diff --git a/apps/web/tests/expected/plugin-manager/missing-bundle.expected.md b/apps/web/tests/expected/plugin-manager/missing-bundle.expected.md index 268ad18e27..5d275c2323 100644 --- a/apps/web/tests/expected/plugin-manager/missing-bundle.expected.md +++ b/apps/web/tests/expected/plugin-manager/missing-bundle.expected.md @@ -1,5 +1,6 @@ - button "返回插件": 插件 - button "卸载 @fixture/missing-bundle": 卸载 +- status - switch "启用 @fixture/missing-bundle" [checked] - heading "@fixture/missing-bundle" [level=3] - text: 异常 未安装 diff --git a/apps/web/tests/expected/plugin-manager/preview.expected.md b/apps/web/tests/expected/plugin-manager/preview.expected.md index 9e44db86f7..d28b7ebbfe 100644 --- a/apps/web/tests/expected/plugin-manager/preview.expected.md +++ b/apps/web/tests/expected/plugin-manager/preview.expected.md @@ -1,4 +1,5 @@ - button "返回 实验性插件": 实验性插件 +- status - switch "启用 DSH 徽章技能" - heading "DSH 徽章技能" [level=3] - text: v{{version}} 实验性 diff --git a/packages/experimental/cua/src/client/PermissionPanel.module.css b/packages/experimental/cua/src/client/PermissionPanel.module.css index 200549ce64..2db3d674d2 100644 --- a/packages/experimental/cua/src/client/PermissionPanel.module.css +++ b/packages/experimental/cua/src/client/PermissionPanel.module.css @@ -42,7 +42,7 @@ background: var(--dsw-alias-bg-skeleton); } .buttonPlaceholder { height: 28px; } -.appPlaceholder { width: 160px; height: 20px; } -.appsPlaceholder { display: block; width: 240px; height: 1.6em; } +.appPlaceholder { width: 160px; height: 1.6em; } +.appsPlaceholder { display: block; color: transparent; } .iconPlaceholder { display: inline-block; width: 32px; height: 32px; flex-shrink: 0; } diff --git a/packages/experimental/cua/src/client/PermissionPanel.tsx b/packages/experimental/cua/src/client/PermissionPanel.tsx index 9329638812..0742fc3790 100644 --- a/packages/experimental/cua/src/client/PermissionPanel.tsx +++ b/packages/experimental/cua/src/client/PermissionPanel.tsx @@ -175,7 +175,7 @@ export function PermissionPanel(props: PermissionPanelProps) { {t(key === 'applications' ? 'applicationsFailed' : 'iconsFailed', { message: state[key].error })} )} - {apps === null ?
: <> + {apps === null ?

{t('noApps')}

: <> {apps.allowedApps.length === 0 && apps.blockedApps.length === 0 &&

{t('noApps')}

} {apps.allowedApps.length > 0 && } diff --git a/packages/experimental/cua/tests/panel.client.spec.tsx b/packages/experimental/cua/tests/panel.client.spec.tsx index 387038390c..c73f46a8eb 100644 --- a/packages/experimental/cua/tests/panel.client.spec.tsx +++ b/packages/experimental/cua/tests/panel.client.spec.tsx @@ -400,15 +400,19 @@ it('retains uncertainty and the diagnostic until a refresh succeeds', () => { it('retains a known read-only configuration and query failure throughout management and refresh', () => { const f = fixture() + f.preset.set({ ...f.preset.getSnapshot(), value: { default: 'standard' }, writable: false }) f.publish({ ...observe(value), runtime: { value: { persistent: false, writable: false }, busy: false, error: null }, applications: { value, busy: false, error: 'offline' } }) const view = render() const readonly = screen.getByText(en.persistentReadOnly) + const defaultReadonly = screen.getByText(en.defaultReadOnly) + act(() => { f.preset.set({ ...f.preset.getSnapshot(), status: 'loading' }) }) const query = screen.getByText(en.applicationsFailed.replace('{message}', 'offline')) f.publish({ changing: true, busy: true, runtime: { ...f.state.getSnapshot().runtime, busy: true }, applications: { ...f.state.getSnapshot().applications, busy: true } }) expect(screen.getByText(en.persistentReadOnly)).toBe(readonly) + expect(screen.getByText(en.defaultReadOnly)).toBe(defaultReadonly) expect(screen.getByText(en.applicationsFailed.replace('{message}', 'offline'))).toBe(query) view.unmount() expect(f.state.getSnapshot().changing).toBe(true)