File size: 5,988 Bytes
1f21206 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'bun:test'
import { mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
const originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR
let configDir: string | null = null
let computerUseApi: typeof import('../api/computer-use.js') | null = null
async function importComputerUseApi() {
if (!computerUseApi) throw new Error('Computer Use API module was not initialized')
return computerUseApi
}
function makeRequest(method: string, body?: unknown): Request {
return new Request('http://localhost/api/computer-use/authorized-apps', {
method,
body: body === undefined ? undefined : JSON.stringify(body),
})
}
async function callAuthorizedApps(method: string, body?: unknown): Promise<Response> {
const { handleComputerUseApi } = await importComputerUseApi()
return handleComputerUseApi(
makeRequest(method, body),
new URL('http://localhost/api/computer-use/authorized-apps'),
['api', 'computer-use', 'authorized-apps'],
)
}
beforeAll(async () => {
configDir = await mkdtemp(join(tmpdir(), 'cc-haha-computer-use-api-'))
process.env.CLAUDE_CONFIG_DIR = configDir
computerUseApi = await import('../api/computer-use.js')
})
beforeEach(async () => {
if (!configDir) throw new Error('configDir was not initialized')
process.env.CLAUDE_CONFIG_DIR = configDir
await rm(join(configDir, 'cc-haha'), { recursive: true, force: true })
await rm(join(configDir, '.runtime'), { recursive: true, force: true })
})
afterAll(async () => {
if (originalClaudeConfigDir === undefined) {
delete process.env.CLAUDE_CONFIG_DIR
} else {
process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir
}
if (configDir) {
await rm(configDir, { recursive: true, force: true })
configDir = null
}
})
describe('Computer Use API authorized app config', () => {
it('defaults Computer Use enabled for existing users without config', async () => {
const res = await callAuthorizedApps('GET')
expect(res.status).toBe(200)
expect(await res.json()).toMatchObject({
enabled: true,
authorizedApps: [],
})
})
it('persists the Computer Use enabled flag independently', async () => {
const putRes = await callAuthorizedApps('PUT', { enabled: false })
expect(putRes.status).toBe(200)
const getRes = await callAuthorizedApps('GET')
expect(await getRes.json()).toMatchObject({ enabled: false })
const raw = await readFile(
join(configDir!, 'cc-haha', 'computer-use-config.json'),
'utf8',
)
expect(JSON.parse(raw)).toMatchObject({ enabled: false })
})
it('persists and normalizes a custom Python interpreter path', async () => {
const pythonPath = ' C:\\Users\\me\\miniconda3\\envs\\cu\\python.exe '
const putRes = await callAuthorizedApps('PUT', { pythonPath })
expect(putRes.status).toBe(200)
const getRes = await callAuthorizedApps('GET')
expect(await getRes.json()).toMatchObject({
pythonPath: 'C:\\Users\\me\\miniconda3\\envs\\cu\\python.exe',
})
const resetRes = await callAuthorizedApps('PUT', { pythonPath: '' })
expect(resetRes.status).toBe(200)
const resetGetRes = await callAuthorizedApps('GET')
expect(await resetGetRes.json()).toMatchObject({ pythonPath: null })
})
})
describe('runPipInstallWithFallback', () => {
it('builds a clear unsupported Python version step for setup', async () => {
const { getUnsupportedPythonVersionStep } = await importComputerUseApi()
expect(getUnsupportedPythonVersionStep('3.8.18')).toEqual({
name: 'python_version',
ok: false,
message: 'Computer Use 需要 Python >= 3.9,当前版本为 3.8.18',
})
expect(getUnsupportedPythonVersionStep('3.9.19')).toBeNull()
})
it('installs setup dependencies by upgrading pip before requirements', async () => {
const { installSetupDependencies } = await importComputerUseApi()
const calls: string[] = []
const result = await installSetupDependencies(
'python',
'/tmp/requirements.txt',
async (cmd, args) => {
calls.push(`${cmd} ${args.join(' ')}`)
return { ok: true, stdout: args.includes('-r') ? 'deps' : 'pip', stderr: '', code: 0 }
},
)
expect(result.stdout).toBe('deps')
expect(calls).toEqual([
'python -m pip install --upgrade pip',
'python -m pip install -r /tmp/requirements.txt',
])
})
it('tries the mirror first and falls back to the default PyPI index', async () => {
const { runPipInstallWithFallback } = await importComputerUseApi()
const calls: string[] = []
const result = await runPipInstallWithFallback(
'python',
['-m', 'pip', 'install', '-r', 'requirements.txt'],
async (cmd, args) => {
calls.push(`${cmd} ${args.join(' ')}`)
if (args.includes('-i')) {
return { ok: false, stdout: '', stderr: 'mirror unavailable', code: 1 }
}
return { ok: true, stdout: 'installed', stderr: '', code: 0 }
},
)
expect(result.ok).toBe(true)
expect(result.stdout).toBe('installed')
expect(calls).toEqual([
'python -m pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/ --trusted-host pypi.tuna.tsinghua.edu.cn',
'python -m pip install -r requirements.txt',
])
})
it('returns the first failure when every pip index attempt fails', async () => {
const { runPipInstallWithFallback } = await importComputerUseApi()
const result = await runPipInstallWithFallback(
'python',
['-m', 'pip', 'install', '-r', 'requirements.txt'],
async (_cmd, args) => ({
ok: false,
stdout: '',
stderr: args.includes('-i') ? 'mirror failed' : 'default failed',
code: args.includes('-i') ? 1 : 2,
}),
)
expect(result).toEqual({ ok: false, stdout: '', stderr: 'mirror failed', code: 1 })
})
})
|