test(cua): cover wrapped loading and persistent diagnostics during row changes

This commit is contained in:
creatixchu committed 2026-10-20 15:59:56 +08:00
1 parent 8126594af5
commit 2bb84a5974
12 files changed
+64 -11

No files matched your search

+39 -6
View File
@@ -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<void> } = {}
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<undefined>(), release = Promise.withResolvers<undefined>()
const changeRow = scaffold.ctx.pluginManager.setPluginEnabled.bind(scaffold.ctx.pluginManager)
let changing: ReturnType<typeof changeRow> | 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)
@@ -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]
@@ -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]
@@ -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
@@ -1,5 +1,6 @@
- button "返回插件": 插件
- button "卸载 @fixture/bundle": 卸载
- status
- switch "启用 @fixture/bundle"
- heading "@fixture/bundle" [level=3]
- text: v0.0.1
@@ -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]
@@ -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]
@@ -1,5 +1,6 @@
- button "返回插件": 插件
- button "卸载 @fixture/missing-bundle": 卸载
- status
- switch "启用 @fixture/missing-bundle" [checked]
- heading "@fixture/missing-bundle" [level=3]
- text: 异常 未安装
@@ -1,4 +1,5 @@
- button "返回 实验性插件": 实验性插件
- status
- switch "启用 DSH 徽章技能"
- heading "DSH 徽章技能" [level=3]
- text: v{{version}} 实验性
@@ -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; }
@@ -175,7 +175,7 @@ export function PermissionPanel(props: PermissionPanelProps) {
<span className={css.error}>{t(key === 'applications' ? 'applicationsFailed' : 'iconsFailed', { message: state[key].error })}</span>
<Button size="sm" variant="outline" disabled={transitioning || state[key].busy} onClick={refresh}>{t('retry')}</Button>
</div>)}
{apps === null ? <div className={css.emptyState}><span className={css.appsPlaceholder} aria-hidden /></div> : <>
{apps === null ? <p className={css.emptyState}><span className={css.appsPlaceholder} aria-hidden>{t('noApps')}</span></p> : <>
{apps.allowedApps.length === 0 && apps.blockedApps.length === 0 && <p className={css.emptyState}>{t('noApps')}</p>}
{apps.allowedApps.length > 0 && <Applications title={t('allowedApps')} apps={apps.allowedApps} icons={state.icons.value}
onAction={props.revoke} label={t('revoke')} disabled={appsBusy || !apps.persistentAvailable} />}
@@ -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(<PermissionPanel {...f.props} />)
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)