cc-haha/desktop/src/pages/ComputerUseSettings.test.tsx
程序员阿江(Relakkes) b31be5161c feat: support custom Computer Use Python interpreters
Computer Use setup could fail on machines where PATH discovery misses a valid Python installation, especially Windows or conda-style environments. Store an optional interpreter path, prefer it during environment checks and venv creation, and expose a desktop settings control for selecting or clearing it.

Constraint: Python discovery is environment-specific and cannot always be inferred from PATH.
Rejected: Continue falling back to PATH after an invalid custom path | hides a user-selected broken interpreter and makes diagnosis ambiguous.
Confidence: high
Scope-risk: moderate
Directive: Preserve unknown Computer Use config fields and keep blank interpreter paths normalized to automatic detection.
Tested: bun test src/utils/computerUse/preauthorizedConfig.test.ts src/server/__tests__/computer-use-python.test.ts src/server/__tests__/computer-use-api.test.ts
Tested: cd desktop && bun run test src/pages/ComputerUseSettings.test.tsx
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Tested: Computer Use browser smoke saved /opt/homebrew/bin/python3 and backend persisted then reset pythonPath to null
Not-tested: Full bun run check:server; existing cron-scheduler-launcher test expects CLAUDE_CODE_ENTRYPOINT=sdk-cli but received undefined.
Related: https://github.com/NanmiCoder/cc-haha/issues/331
2026-05-09 21:46:11 +08:00

239 lines
6.2 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import '@testing-library/jest-dom'
import { ComputerUseSettings } from './ComputerUseSettings'
import { useSettingsStore } from '../stores/settingsStore'
const computerUseApiMock = vi.hoisted(() => ({
getStatus: vi.fn(),
getInstalledApps: vi.fn(),
getAuthorizedApps: vi.fn(),
setAuthorizedApps: vi.fn(),
runSetup: vi.fn(),
openSettings: vi.fn(),
}))
vi.mock('../api/computerUse', () => ({
computerUseApi: computerUseApiMock,
}))
const readyStatus = {
platform: 'darwin',
supported: true,
python: {
installed: true,
version: '3.12.0',
path: '/usr/bin/python3',
source: 'system',
error: null,
},
venv: {
created: false,
path: '/tmp/venv',
},
dependencies: {
installed: false,
requirementsFound: true,
},
permissions: {
accessibility: null,
screenRecording: null,
},
}
const enabledConfig = {
enabled: true,
authorizedApps: [],
grantFlags: {
clipboardRead: true,
clipboardWrite: true,
systemKeyCombos: true,
},
pythonPath: null,
}
function deferred<T>() {
let resolve!: (value: T) => void
const promise = new Promise<T>(res => {
resolve = res
})
return { promise, resolve }
}
describe('ComputerUseSettings', () => {
beforeEach(() => {
useSettingsStore.setState({ locale: 'en' })
computerUseApiMock.getStatus.mockReset()
computerUseApiMock.getInstalledApps.mockReset()
computerUseApiMock.getAuthorizedApps.mockReset()
computerUseApiMock.setAuthorizedApps.mockReset()
computerUseApiMock.runSetup.mockReset()
computerUseApiMock.openSettings.mockReset()
computerUseApiMock.getStatus.mockResolvedValue(readyStatus)
computerUseApiMock.getAuthorizedApps.mockResolvedValue(enabledConfig)
computerUseApiMock.setAuthorizedApps.mockResolvedValue({ ok: true })
})
it('renders the stored disabled state with the MCP exposure hint', async () => {
computerUseApiMock.getAuthorizedApps.mockResolvedValue({
...enabledConfig,
enabled: false,
})
render(<ComputerUseSettings />)
const toggle = await screen.findByLabelText('Enabled')
await waitFor(() => expect(toggle).not.toBeChecked())
expect(
screen.getByText(/will not inject the computer-use MCP server/i),
).toBeInTheDocument()
})
it('saves the Computer Use enablement toggle independently', async () => {
render(<ComputerUseSettings />)
const toggle = await screen.findByLabelText('Enabled')
await waitFor(() => expect(computerUseApiMock.getAuthorizedApps).toHaveBeenCalled())
await act(async () => {
fireEvent.click(toggle)
await Promise.resolve()
})
expect(computerUseApiMock.setAuthorizedApps).toHaveBeenCalledWith({
enabled: false,
})
})
it('saves a custom Python interpreter path and rechecks status', async () => {
render(<ComputerUseSettings />)
const input = await screen.findByLabelText('Python Interpreter Path')
await act(async () => {
fireEvent.change(input, {
target: { value: ' C:\\Users\\me\\miniconda3\\envs\\cu\\python.exe ' },
})
fireEvent.click(screen.getByText('Apply'))
await Promise.resolve()
})
expect(computerUseApiMock.setAuthorizedApps).toHaveBeenCalledWith({
pythonPath: 'C:\\Users\\me\\miniconda3\\envs\\cu\\python.exe',
})
expect(computerUseApiMock.getStatus).toHaveBeenCalledTimes(2)
})
it('keeps the user-selected enablement when a stale refresh resolves later', async () => {
const staleRefresh = deferred<typeof enabledConfig>()
computerUseApiMock.getStatus.mockResolvedValue({
...readyStatus,
venv: {
...readyStatus.venv,
created: true,
},
dependencies: {
...readyStatus.dependencies,
installed: true,
},
})
computerUseApiMock.getInstalledApps.mockResolvedValue({ apps: [] })
computerUseApiMock.getAuthorizedApps
.mockResolvedValueOnce({
...enabledConfig,
enabled: false,
})
.mockReturnValueOnce(staleRefresh.promise)
render(<ComputerUseSettings />)
const toggle = await screen.findByLabelText('Enabled')
await waitFor(() => expect(toggle).not.toBeChecked())
await waitFor(() => expect(computerUseApiMock.getInstalledApps).toHaveBeenCalled())
await act(async () => {
fireEvent.click(toggle)
await Promise.resolve()
})
expect(toggle).toBeChecked()
await act(async () => {
staleRefresh.resolve({
...enabledConfig,
enabled: false,
})
await staleRefresh.promise
})
expect(toggle).toBeChecked()
})
it('saves app and grant flag changes from the ready environment view', async () => {
computerUseApiMock.getStatus.mockResolvedValue({
...readyStatus,
venv: {
...readyStatus.venv,
created: true,
},
dependencies: {
...readyStatus.dependencies,
installed: true,
},
})
computerUseApiMock.getInstalledApps.mockResolvedValue({
apps: [
{
bundleId: 'com.example.Preview',
displayName: 'Preview',
path: '/Applications/Preview.app',
},
],
})
render(<ComputerUseSettings />)
await screen.findByText('Preview')
await act(async () => {
fireEvent.click(screen.getByText('Preview'))
await Promise.resolve()
})
expect(computerUseApiMock.setAuthorizedApps).toHaveBeenCalledWith({
authorizedApps: [
expect.objectContaining({
bundleId: 'com.example.Preview',
displayName: 'Preview',
}),
],
grantFlags: {
clipboardRead: true,
clipboardWrite: true,
systemKeyCombos: true,
},
})
await act(async () => {
fireEvent.click(screen.getByLabelText('Clipboard Access'))
await Promise.resolve()
})
expect(computerUseApiMock.setAuthorizedApps).toHaveBeenCalledWith({
authorizedApps: [
expect.objectContaining({
bundleId: 'com.example.Preview',
displayName: 'Preview',
}),
],
grantFlags: {
clipboardRead: false,
clipboardWrite: false,
systemKeyCombos: true,
},
})
})
})