Files
deepseek-harness/apps/web/tests/cua-permissions.e2e.ts
T

257 lines
17 KiB
TypeScript

/** Built permission UI talks to the real provider; the isolated helper substitutes only macOS privilege observations. */
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
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, vi } from 'vitest'
import { captureStableAria, compareOrRefreshGolden, launchWebScaffold, webSnapshotMode, watchConsole, type WebScaffold } from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage, PLUGIN_TOGGLE_SETTLE_MS, saveFailureShot } from './support.ts'
import { installNativeFixture, startFixtureHost } from '../../../packages/experimental/cua-macos/tests/native-fixture.ts'
const bundle = fileURLToPath(new URL('../../../packages/experimental/cua-bundle', import.meta.url))
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')('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 () => {
try { await resources.browser?.close() } finally {
try { await resources.scaffold?.close() } finally {
try { await resources.closeFixture?.() } finally { await rm(root, { recursive: true, force: true }) }
}
}
})
const helper = await installNativeFixture(root)
resources.closeFixture = await startFixtureHost(helper)
const state = join(root, 'permissions.json'), audit = join(root, 'helper-events.jsonl')
const launchApplication = { name: 'DSH Computer Use.app', path: '/Applications/DSH Computer Use.app',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAADElEQVQImWNgZGIGAAAOAAeCcsnOAAAAAElFTkSuQmCC' }
await writeFile(state, JSON.stringify({ accessibility: false, screenRecording: false, launchApplication }))
await writeFile(join(root, 'observe-helper'), '')
const fixturePatch = join(root, 'native-fixture.yml')
// Keep the external helper override above bundle order changes when CUA is re-enabled.
await writeFile(fixturePatch, JSON.stringify([{ id: 'cua', config: { helperPath: helper } }]))
const scaffold = await launchWebScaffold({
profile: { packages: [{ dir: bundle, enabled: true }] },
extraOverlayPath: fixturePatch,
})
resources.scaffold = scaffold
expect((await scaffold.ctx.pluginManager.listBundles()).find(bundle => bundle.name === '@deepseek-ai/dsh-cua-bundle'))
.toMatchObject({ listedByDefault: false, enabled: true, installed: true })
const browser = await chromium.launch()
resources.browser = browser
const page = await newEnglishPage(browser), tripwire = watchConsole(page)
onTestFailed(() => saveFailureShot(page, 'web-e2e-cua-permissions'))
await page.goto(scaffold.authenticatedUrl)
await connectFreshWorkspace(page, scaffold.workspaceCwd)
const agent = scaffold.ctx.agents.list()[0]!
await scaffold.ctx.agentPresets.select(agent, 'ptc')
scaffold.ctx.approval.setPolicy(agent, 'never')
// Application consent remains independent of the session's tool-approval policy.
const grant = scaffold.ctx.tools.execute({ agent, signal: new AbortController().signal,
calls: [{ type: 'tool-call', id: ToolCallId('cua-app-approval'), name: 'run_code',
arguments: JSON.stringify({ code: 'return await tools.cua({ method: "getApp", params: { selector: "Fixture" } })',
description: 'Request application access' }) }] })
const approval = page.locator('[data-approval-key]')
await Promise.race([approval.waitFor(), grant.then((result) => {
throw new Error(`Application selection completed before consent: ${JSON.stringify(result)}`)
})])
expect(await approval.getByText('Fixture', { exact: true }).count()).toBe(1)
expect(await approval.getByRole('textbox').count()).toBe(0)
expect(await approval.locator('[data-approval-scroll]').evaluate(element => getComputedStyle(element).textAlign)).toBe('left')
expect(await approval.getByRole('button', { name: 'Always allow', exact: true })
.evaluate(button => getComputedStyle(button.parentElement!).flexWrap)).toBe('wrap')
await expect.poll(() => approval.locator('img').evaluate((image: HTMLImageElement) => image.naturalWidth)).toBe(1)
expect(await approval.getByText('/Applications/Fixture.app', { exact: true }).count()).toBe(0)
await compareOrRefreshGolden(fileURLToPath(new URL('./expected/cua-permissions/application-approval.expected.md', import.meta.url)),
await captureStableAria(page, '[data-approval-key]', scaffold.workspaceCwd), webSnapshotMode())
await approval.getByRole('button', { name: 'Always allow', exact: true }).click()
expect((await grant).results).toMatchObject([{ result: { isError: false } }])
await approval.waitFor({ state: 'hidden' })
await page.getByRole('button', { name: 'Plugins', exact: true }).click()
const card = page.locator('[data-plugin-package="@deepseek-ai/dsh-cua-bundle"]')
expect(await card.getByRole('button', { name: /Accessibility permission|Screen Recording permission/ }).count()).toBe(0)
await page.setViewportSize({ width: 1680, height: 600 })
const plugins = page.locator('[data-plugin-panel]')
const openDetails = page.getByRole('button', { name: 'View Computer use', exact: true })
await openDetails.scrollIntoViewIfNeeded()
expect(await plugins.locator('[data-plugin-body]').evaluate(element => element.scrollTop)).toBeGreaterThan(0)
await openDetails.click()
const panel = page.locator('[data-cua-settings]')
const accessibility = panel.getByRole('group', { name: 'Accessibility', exact: true })
await accessibility.getByRole('button', { name: 'Request permission', exact: true }).waitFor()
expect(await panel.getByRole('region', { name: 'Authorize computer use', exact: true }).getByRole('group').count()).toBe(4)
expect(await panel.getByRole('region', { name: 'Manage applications computer use can access', exact: true }).count()).toBe(1)
await expect.poll(() => panel.locator('li img').evaluate((image: HTMLImageElement) => image.naturalWidth)).toBe(1)
expect(await panel.getByText('/fixture/app', { exact: true }).count()).toBe(0)
expect(await accessibility.getByText('DSH Computer Use.app', { exact: true }).count()).toBe(1)
expect(await accessibility.textContent()).toContain('In System Settings, allow')
expect(await accessibility.textContent()).toContain('to read interface controls, click, and type.')
expect(await panel.getByRole('group', { name: 'Screen Recording', exact: true }).getByText('DSH Computer Use.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')
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 })
for (const colorScheme of ['light', 'dark'] as const) {
await page.emulateMedia({ colorScheme })
await expect.poll(() => page.evaluate(() => document.body.hasAttribute('data-ds-dark-theme'))).toBe(colorScheme === 'dark')
const measure = () => panel.evaluate(element => ({
clientWidth: element.clientWidth, scrollWidth: element.scrollWidth,
overflowing: [...element.querySelectorAll('*')].filter(child =>
child.getBoundingClientRect().right > element.getBoundingClientRect().right + 1,
).map(child => ({ tag: child.tagName, className: child.className, text: child.textContent?.slice(0, 80) })),
}))
await expect.poll(async () => {
const layout = await measure()
return layout.scrollWidth <= layout.clientWidth ? true : { width, colorScheme, ...layout }
}).toBe(true)
if (webSnapshotMode() === 'refresh') {
const directory = fileURLToPath(new URL('../../../.artifacts', import.meta.url))
await mkdir(directory, { recursive: true })
const path = join(directory, `cua-permission-design-${process.pid}-${width}-${colorScheme}.png`)
await panel.screenshot({ path, animations: 'disabled' })
console.log(`CUA permission design: ${path}`)
}
}
}
} finally {
await page.emulateMedia({ colorScheme: null })
await page.setViewportSize({ width: 1680, height: 1000 })
}
await accessibility.getByRole('button', { name: 'Request permission', exact: true }).click()
await expect.poll(() => panel.getAttribute('aria-busy')).toBe('false')
expect(await accessibility.getByRole('button', { name: 'Request permission', exact: true }).count()).toBe(1)
await writeFile(state, JSON.stringify({ accessibility: true, screenRecording: false, launchApplication }))
// Headless pages retain document focus; model returning from System Settings at the window-event boundary.
await page.evaluate(() => { window.dispatchEvent(new Event('focus')) })
await accessibility.getByText('Allowed', { exact: true }).waitFor()
const screen = panel.getByRole('group', { name: 'Screen Recording', exact: true })
expect(await accessibility.getByRole('button').count()).toBe(1)
await accessibility.getByRole('button', { name: 'Allowed', exact: true }).click()
expect(await screen.getByRole('button').count()).toBe(1)
await expect.poll(() => panel.getAttribute('aria-busy')).toBe('false')
await panel.getByRole('button', { name: 'Revoke', exact: true }).click()
await panel.getByRole('heading', { name: 'Always-allowed applications', exact: true }).waitFor({ state: 'hidden' })
await panel.getByRole('heading', { name: 'Applications not allowed', exact: true }).waitFor()
await panel.getByRole('button', { name: 'Allow asking again', exact: true }).click()
await panel.getByText('Fixture', { exact: true }).waitFor({ state: 'hidden' })
const saved = await readFile(join(scaffold.harnessHome, 'profiles/scaffold/cordis.patch.yml'), 'utf8')
expect(saved).not.toContain('allowedApps:')
const files = await readdir(join(scaffold.workspaceCwd, '.dsh-storages'))
expect(files.some(file => file.startsWith('cua_permissions_') && file.endsWith('.json'))).toBe(true)
expect((await scaffold.ctx.cuaAccess.applications()).allowedApps).toEqual([])
const events = (await readFile(audit, 'utf8')).trim().split('\n').map(line => JSON.parse(line) as { event: string; method: string; params: object })
.filter(record => record.event === 'call').map(({ method, params }) => ({ method, params }))
expect(events).toContainEqual({ method: 'requestPermission', params: { permission: 'accessibility' } })
expect(events.filter(event => event.method === 'openPermissionSettings')).toEqual([
{ method: 'openPermissionSettings', params: { permission: 'accessibility' } },
])
expect(events.every(event => ['permissionStatus', 'requestPermission', 'openPermissionSettings', 'applicationIcons', 'getApp'].includes(event.method))).toBe(true)
const preview = plugins.locator('[data-detail-document] img')
await preview.waitFor()
await preview.evaluate(async (image: HTMLImageElement) => { await image.decode() })
const trace = await page.evaluateHandle(() => {
const errors: string[] = []
const stable = [...document.querySelectorAll('[data-cua-settings] [role="alert"]')].map(node => node.textContent?.trim())
const layouts: { height: number; sections: number[]; groups: number[] }[] = []
const documents: number[] = []
let frame = 0
const measure = () => {
const documentHeight = document.querySelector('[data-plugin-detail] [data-detail-document]')?.getBoundingClientRect().height ?? 0
if (documents.at(-1) !== documentHeight) documents.push(documentHeight)
const settings = document.querySelector('[data-cua-settings]')
if (settings) {
const height = settings.getBoundingClientRect().height
if (layouts.at(-1)?.height !== height) layouts.push({ height,
sections: [...settings.querySelectorAll('section')].map(node => node.getBoundingClientRect().height),
groups: [...settings.querySelectorAll('[role="group"]')].map(node => node.getBoundingClientRect().height),
})
}
frame = requestAnimationFrame(measure)
}
measure()
const observer = new MutationObserver(() => {
for (const node of document.querySelectorAll('[data-cua-settings] [role="alert"]')) {
const text = node.textContent?.trim()
if (text && !stable.includes(text) && errors.at(-1) !== text) errors.push(text)
}
})
observer.observe(document.body, { childList: true, subtree: true, characterData: true })
return { errors, layouts, documents, stop: () => { observer.disconnect(); cancelAnimationFrame(frame) } }
})
try {
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; value.documents.length = 0 })
for (let cycle = 0; cycle < 3; cycle++) {
await toggle.click()
await expect.poll(async () => (await scaffold.ctx.pluginManager.listBundles())
.find(bundle => bundle.name === '@deepseek-ai/dsh-cua-bundle'))
.toMatchObject({ enabled: false, installed: true })
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('false')
await expect.poll(() => toggle.isEnabled(), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe(true)
await toggle.click()
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('true')
const setup = page.getByRole('dialog', { name: 'Set up computer use before use', exact: true })
await setup.getByRole('button', { name: 'Later', exact: true }).click()
await expect.poll(() => panel.getAttribute('aria-busy'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('false')
expect(await trace.evaluate(value => value.errors)).toEqual([])
}
const layouts = await trace.evaluate(value => value.layouts)
expect(layouts.length).toBeGreaterThan(0)
const documentHeights = await trace.evaluate(value => value.documents)
expect(documentHeights.length).toBeGreaterThan(0)
expect(Math.max(...documentHeights) - Math.min(...documentHeights)).toBeLessThanOrEqual(1)
const heights = layouts.map(layout => layout.height)
expect(Math.max(...heights) - Math.min(...heights), JSON.stringify(layouts)).toBeLessThanOrEqual(1)
}
} finally {
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 macOS Computer Use provider', exact: true })
const 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) => {
changing = release.promise.then(() => changeRow(entryId, enabled))
return changing
})
try {
await rowToggle.click()
await expect.poll(() => hold.mock.calls.length, { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe(1)
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'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('false')
await expect.poll(() => rowToggle.isEnabled(), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe(true)
await writeFile(state, JSON.stringify({ accessibility: false, screenRecording: false, launchApplication }))
await rowToggle.click()
await expect.poll(() => rowToggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('true')
await expect.poll(() => panel.getAttribute('aria-busy'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('false')
await diagnostic.waitFor({ state: 'hidden' })
expect(tripwire.pageErrors).toEqual([])
})