mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33:35 +08:00
Desktop launches can inherit a sparse GUI environment, while user runtimes such as nvm and Homebrew are often initialized from shell startup files. Capture a bounded login+interactive shell environment once and reuse it for desktop CLI sessions, scheduled tasks, and MCP stdio startup while keeping explicit MCP PATH and provider-managed env overrides authoritative. Constraint: macOS app launches may omit user shell PATH and exported runtime variables Rejected: Patch only MCP stdio | leaves normal desktop sessions and scheduled tasks inconsistent Confidence: high Scope-risk: moderate Directive: Keep provider/OAuth scrubbing after shell env merging when changing child env builders Tested: bun test src/utils/terminalShellEnvironment.test.ts src/utils/mcpStdioEnvironment.test.ts src/server/services/mcpHostPreflight.test.ts src/server/__tests__/conversation-service.test.ts src/server/__tests__/cron-scheduler-launcher.test.ts src/server/__tests__/mcp.test.ts Tested: bun run check:server Tested: bun run check:coverage Tested: bun run verify Not-tested: Real desktop UI live-model smoke on an installed .app
158 lines
4.8 KiB
TypeScript
158 lines
4.8 KiB
TypeScript
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.',
|
|
})
|
|
})
|
|
})
|