cc-haha/src/server/__tests__/desktop-cli-launcher.test.ts
程序员阿江(Relakkes) 5fea7033e3 Expose the bundled desktop CLI outside the app
The desktop app already shipped a bundled sidecar, but only desktop-managed
sessions could see it. This change installs a `claude-haha` launcher into the
user bin directory, wires PATH setup so new terminals can resolve it, and keeps
desktop installer sessions aligned on the same bundled sidecar resolution path.
The desktop install surface now also reports whether the launcher is ready or
still waiting on a terminal restart.

Constraint: The worktree already contains unrelated icon, docs, and UI changes, so this commit stages only the bundled CLI launcher slice
Rejected: Tell users to install the official Claude CLI separately | it breaks the desktop out-of-box install story
Rejected: Keep the bundled CLI reachable only inside desktop-managed shells | system terminals would still be unable to call the packaged runtime
Rejected: Symlink directly into the app bundle instead of copying to user bin | moving or replacing the app bundle would leave a stale launcher behind
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Proxy-backed non-Anthropic providers still depend on the desktop server; do not assume this launcher makes every provider fully standalone
Tested: bun test src/server/__tests__/desktop-cli-launcher.test.ts src/server/__tests__/settings.test.ts; bun test src/server/__tests__/conversation-service.test.ts; bun test src/utils/shell/bashProvider.test.ts; cd desktop && bun x vitest run sidecars/launcherRouting.test.ts src/components/settings/InstallCenter.test.tsx; cd desktop && bun run lint; cd desktop && bun run build; cargo check --manifest-path desktop/src-tauri/Cargo.toml
Not-tested: Manual packaged desktop app install plus real Terminal/iTerm/PowerShell invocation on fresh macOS and Windows machines
2026-04-23 12:12:49 +08:00

101 lines
3.3 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { ensureDesktopCliLauncherInstalled } from '../services/desktopCliLauncherService.js'
const isWindows = process.platform === 'win32'
const unixOnly = isWindows ? it.skip : it
const ORIGINAL_HOME = process.env.HOME
const ORIGINAL_USERPROFILE = process.env.USERPROFILE
const ORIGINAL_SHELL = process.env.SHELL
const ORIGINAL_PATH = process.env.PATH
const ORIGINAL_CLAUDE_CLI_PATH = process.env.CLAUDE_CLI_PATH
describe('ensureDesktopCliLauncherInstalled', () => {
let tempHome = ''
let tempSourceDir = ''
beforeEach(async () => {
tempHome = await mkdtemp(join(tmpdir(), 'desktop-cli-home-'))
tempSourceDir = await mkdtemp(join(tmpdir(), 'desktop-cli-source-'))
process.env.HOME = tempHome
process.env.USERPROFILE = tempHome
process.env.SHELL = '/bin/zsh'
process.env.PATH = ''
})
afterEach(async () => {
if (ORIGINAL_HOME === undefined) {
delete process.env.HOME
} else {
process.env.HOME = ORIGINAL_HOME
}
if (ORIGINAL_USERPROFILE === undefined) {
delete process.env.USERPROFILE
} else {
process.env.USERPROFILE = ORIGINAL_USERPROFILE
}
if (ORIGINAL_SHELL === undefined) {
delete process.env.SHELL
} else {
process.env.SHELL = ORIGINAL_SHELL
}
if (ORIGINAL_PATH === undefined) {
delete process.env.PATH
} else {
process.env.PATH = ORIGINAL_PATH
}
if (ORIGINAL_CLAUDE_CLI_PATH === undefined) {
delete process.env.CLAUDE_CLI_PATH
} else {
process.env.CLAUDE_CLI_PATH = ORIGINAL_CLAUDE_CLI_PATH
}
await rm(tempHome, { recursive: true, force: true })
await rm(tempSourceDir, { recursive: true, force: true })
})
unixOnly('copies the bundled sidecar into the user bin dir and configures PATH', async () => {
const sourcePath = join(tempSourceDir, 'claude-sidecar')
await writeFile(sourcePath, '#!/bin/sh\necho desktop-sidecar\n', 'utf8')
await chmod(sourcePath, 0o755)
process.env.CLAUDE_CLI_PATH = sourcePath
const status = await ensureDesktopCliLauncherInstalled()
const launcherPath = join(tempHome, '.local', 'bin', 'claude-haha')
const shellConfigPath = join(tempHome, '.zshrc')
expect(status.supported).toBe(true)
expect(status.installed).toBe(true)
expect(status.command).toBe('claude-haha')
expect(status.launcherPath).toBe(launcherPath)
expect(status.availableInNewTerminals).toBe(true)
expect(status.needsTerminalRestart).toBe(true)
expect(status.configTarget).toBe(shellConfigPath)
expect(await readFile(launcherPath, 'utf8')).toContain('desktop-sidecar')
expect(await readFile(shellConfigPath, 'utf8')).toContain(
'export PATH="$HOME/.local/bin:$PATH"',
)
})
it('reports unsupported status when the current launcher is not a bundled sidecar', async () => {
const sourcePath = join(tempSourceDir, 'claude')
await writeFile(sourcePath, '#!/bin/sh\necho plain-cli\n', 'utf8')
process.env.CLAUDE_CLI_PATH = sourcePath
const status = await ensureDesktopCliLauncherInstalled()
expect(status.supported).toBe(false)
expect(status.installed).toBe(false)
expect(status.command).toBe('claude-haha')
})
})