mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
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
239 lines
6.2 KiB
TypeScript
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,
|
|
},
|
|
})
|
|
})
|
|
})
|