File size: 4,890 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 | import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { resetMcpStdioEnvironmentCacheForTests } from '../../utils/mcpStdioEnvironment.js'
import { inspectMcpHostCommand } from './mcpHostPreflight.js'
let tmpDir: string
let originalEnv: {
HOME?: string
PATH?: string
SHELL?: string
ZDOTDIR?: string
CC_HAHA_DISABLE_TERMINAL_SHELL_ENV?: string
}
async function writeExecutable(filePath: string, content: string) {
await writeFile(filePath, content, { mode: 0o755 })
}
async function writeFakeZsh(filePath: string) {
await writeExecutable(
filePath,
[
'#!/bin/sh',
'command=',
'while [ "$#" -gt 0 ]; do',
' if [ "$1" = "-c" ]; then',
' shift',
' command="$1"',
' break',
' fi',
' shift',
'done',
'if [ -f "$HOME/.zshrc" ]; then',
' . "$HOME/.zshrc" </dev/null >/dev/null 2>/dev/null || true',
'fi',
'exec /bin/sh -c "$command"',
'',
].join('\n'),
)
}
describe('MCP host preflight', () => {
beforeEach(async () => {
tmpDir = await mkdtemp(path.join(tmpdir(), 'mcp-host-preflight-test-'))
originalEnv = {
HOME: process.env.HOME,
PATH: process.env.PATH,
SHELL: process.env.SHELL,
ZDOTDIR: process.env.ZDOTDIR,
CC_HAHA_DISABLE_TERMINAL_SHELL_ENV:
process.env.CC_HAHA_DISABLE_TERMINAL_SHELL_ENV,
}
delete process.env.CC_HAHA_DISABLE_TERMINAL_SHELL_ENV
resetMcpStdioEnvironmentCacheForTests()
})
afterEach(async () => {
for (const [key, value] of Object.entries(originalEnv)) {
if (value === undefined) {
delete process.env[key]
} else {
process.env[key] = value
}
}
resetMcpStdioEnvironmentCacheForTests()
await rm(tmpDir, { recursive: true, force: true })
})
it('finds npx from user shell PATH when the desktop process PATH is minimal', async () => {
const shellPath = path.join(tmpDir, 'zsh')
const nodeBin = path.join(tmpDir, 'node-bin')
const npxPath = path.join(nodeBin, 'npx')
await mkdir(nodeBin, { recursive: true })
await writeFakeZsh(shellPath)
await writeExecutable(npxPath, '#!/bin/sh\nexit 0\n')
await writeFile(
path.join(tmpDir, '.zshrc'),
`export PATH="${nodeBin}:$PATH"\n`,
)
process.env.HOME = tmpDir
process.env.SHELL = shellPath
process.env.PATH = '/usr/bin:/bin'
delete process.env.ZDOTDIR
await expect(inspectMcpHostCommand('npx', tmpDir, {})).resolves.toEqual({
ok: true,
resolvedCommand: npxPath,
})
})
it('finds commands from the desktop process PATH before shell fallback', async () => {
const processBin = path.join(tmpDir, 'process-bin')
const toolPath = path.join(processBin, 'mcp-tool')
await mkdir(processBin, { recursive: true })
await writeExecutable(toolPath, '#!/bin/sh\nexit 0\n')
process.env.HOME = tmpDir
process.env.SHELL = '/bin/zsh'
process.env.PATH = processBin
await expect(
inspectMcpHostCommand('mcp-tool', tmpDir, {}),
).resolves.toEqual({
ok: true,
resolvedCommand: toolPath,
})
})
it('uses explicit MCP PATH to resolve host commands', async () => {
const configuredBin = path.join(tmpDir, 'configured-bin')
const toolPath = path.join(configuredBin, 'mcp-tool')
await mkdir(configuredBin, { recursive: true })
await writeExecutable(toolPath, '#!/bin/sh\nexit 0\n')
process.env.HOME = tmpDir
process.env.SHELL = '/bin/zsh'
process.env.PATH = '/usr/bin:/bin'
await expect(
inspectMcpHostCommand('mcp-tool', tmpDir, { PATH: configuredBin }),
).resolves.toEqual({
ok: true,
resolvedCommand: toolPath,
})
})
it('does not fall back to shell PATH when MCP PATH is explicit', async () => {
const shellPath = path.join(tmpDir, 'zsh')
const nodeBin = path.join(tmpDir, 'node-bin')
const emptyBin = path.join(tmpDir, 'empty-bin')
const npxPath = path.join(nodeBin, 'npx')
await mkdir(nodeBin, { recursive: true })
await mkdir(emptyBin, { recursive: true })
await writeFakeZsh(shellPath)
await writeExecutable(npxPath, '#!/bin/sh\nexit 0\n')
await writeFile(
path.join(tmpDir, '.zshrc'),
`export PATH="${nodeBin}:$PATH"\n`,
)
process.env.HOME = tmpDir
process.env.SHELL = shellPath
process.env.PATH = '/usr/bin:/bin'
delete process.env.ZDOTDIR
await expect(
inspectMcpHostCommand('npx', tmpDir, { PATH: emptyBin }),
).resolves.toEqual({
ok: false,
message:
'Host command "npx" is not available in PATH. This STDIO MCP runs on the host machine. Install Node.js on this machine so "npx" is available in PATH, then retry.',
})
})
})
|