Files
orca/src/main/cli/cli-installer.test.ts
T
Jinjing 6729f683f6 Fix Orca CLI path resolution and improve registration UI (#6484)
* fix Orca CLI path resolution and improve registration UI

- Hydrate the shell PATH before running installer actions on macOS to
  match the user's terminal instead of launchd.
- Resolve the active macOS command path dynamically against shell PATH
  candidates, ignoring broken symlinks and non-executable files.
- Detect and report conflicts with unmanaged executables shadowing the
  configured command path.
- Add "Preparing..." and "Registering..." loading states to the settings
  and setup panels during preflight checks.

* Fix Orca CLI detection and installation path conflict checks

- Allow detecting a managed Orca command located later in PATH when the
  default path entry is missing or non-executable.
- Avoid raising installation conflicts for unmanaged commands located
  after the default install path, as they will be safely shadowed.
- Translate terminal setup and Orca CLI registration strings for
  Spanish, Japanese, Korean, and Chinese locales.

* Only open terminal when pre-install checks succeed

Prevent the terminal from opening and setting the terminal command if
onBeforeOpenTerminal or refreshPreInstallNotice throws an error.
2026-06-27 02:05:23 -07:00

1193 lines
47 KiB
TypeScript

/* eslint-disable max-lines -- Why: cli-installer covers darwin/linux/win32 install, remove, fallback, and privileged-runner paths; each platform combination requires its own fixture and assertions to catch regressions. */
import {
chmod,
lstat,
mkdtemp,
mkdir,
readFile,
readlink,
symlink,
writeFile
} from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const execFileMock = vi.hoisted(() => vi.fn())
vi.mock('electron', () => ({
app: {
isPackaged: false,
getPath: () => tmpdir(),
getAppPath: () => tmpdir()
}
}))
vi.mock('node:child_process', () => ({
execFile: execFileMock
}))
import { CliInstaller } from './cli-installer'
import { buildAppImageCliWrapper } from './appimage-cli-wrapper'
async function makeFixture(): Promise<{
root: string
userDataPath: string
appPath: string
}> {
const root = await mkdtemp(join(tmpdir(), 'orca-cli-installer-'))
const userDataPath = join(root, 'userData')
const appPath = join(root, 'app')
const cliEntryPath = join(appPath, 'out', 'cli', 'index.js')
await mkdir(join(appPath, 'out', 'cli'), { recursive: true })
await writeFile(cliEntryPath, 'console.log("orca")\n', 'utf8')
return { root, userDataPath, appPath }
}
async function createPackagedMacLauncher(root: string): Promise<string> {
const resourcesPath = join(root, 'resources')
await mkdir(join(resourcesPath, 'bin'), { recursive: true })
await writeFile(join(resourcesPath, 'bin', 'orca'), '#!/usr/bin/env bash\necho orca\n', {
encoding: 'utf8',
mode: 0o755
})
return resourcesPath
}
describe('CliInstaller', () => {
beforeEach(() => {
execFileMock.mockReset()
})
afterEach(() => {
vi.useRealTimers()
vi.restoreAllMocks()
})
// Why: this test creates Unix symlinks and shell scripts that only apply on macOS.
it.skipIf(process.platform === 'win32')(
'creates a dev launcher and installs a macOS symlink in the requested path',
async () => {
const fixture = await makeFixture()
const installPath = join(fixture.root, 'bin', 'orca')
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: false,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
commandPathOverride: installPath,
processPathEnv: join(fixture.root, 'bin')
})
const initial = await installer.getStatus()
expect(initial.state).toBe('not_installed')
expect(initial.launcherPath).toContain(join('userData', 'cli', 'bin', 'orca'))
const installed = await installer.install()
expect(installed.state).toBe('installed')
expect(installed.pathConfigured).toBe(true)
const launcherContent = await readFile(installed.launcherPath as string, 'utf8')
expect(launcherContent).toContain('ELECTRON_RUN_AS_NODE=1')
expect(launcherContent).toContain(`export ORCA_USER_DATA_PATH='${fixture.userDataPath}'`)
expect(launcherContent).toContain('export ORCA_APP_EXECUTABLE="$ELECTRON"')
expect(launcherContent).toContain(join(fixture.appPath, 'out', 'cli', 'index.js'))
const removed = await installer.remove()
expect(removed.state).toBe('not_installed')
}
)
// Why: this test creates Unix symlinks and shell scripts that only apply on Linux.
it.skipIf(process.platform === 'win32')(
'creates a linux symlink under the requested path and warns when PATH is missing',
async () => {
const fixture = await makeFixture()
const installPath = join(fixture.root, '.local', 'bin', 'orca-ide')
const installer = new CliInstaller({
platform: 'linux',
isPackaged: false,
userDataPath: fixture.userDataPath,
execPath: '/opt/Orca/orca-ide',
appPath: fixture.appPath,
commandPathOverride: installPath,
processPathEnv: '/usr/bin'
})
const installed = await installer.install()
expect(installed.state).toBe('installed')
expect(installed.commandName).toBe('orca-ide')
expect(installed.pathConfigured).toBe(false)
expect(installed.detail).toContain('.local')
const launcherContent = await readFile(installed.launcherPath as string, 'utf8')
expect(launcherContent).toContain('ELECTRON_RUN_AS_NODE=1')
expect(launcherContent).toContain(`export ORCA_USER_DATA_PATH='${fixture.userDataPath}'`)
const removed = await installer.remove()
expect(removed.state).toBe('not_installed')
}
)
// Why: dev installs are useful for validation, but they must not replace the
// packaged `orca` / `orca-ide` commands developers rely on day to day.
it.skipIf(process.platform === 'win32')(
'uses a separate orca-dev command for default development installs',
async () => {
const fixture = await makeFixture()
const homePath = join(fixture.root, 'home')
const commandDir = join(homePath, '.local', 'bin')
const installer = new CliInstaller({
platform: 'linux',
isPackaged: false,
userDataPath: fixture.userDataPath,
execPath: '/opt/Orca/orca-ide',
appPath: fixture.appPath,
homePath,
processPathEnv: commandDir
})
const installed = await installer.install()
expect(installed.state).toBe('installed')
expect(installed.commandName).toBe('orca-dev')
expect(installed.commandPath).toBe(join(commandDir, 'orca-dev'))
expect(installed.launcherPath).toBe(join(fixture.userDataPath, 'cli', 'bin', 'orca-dev'))
await expect(readlink(installed.commandPath as string)).resolves.toBe(installed.launcherPath)
await expect(
readFile(join(fixture.userDataPath, 'cli', 'bin', 'orca'), 'utf8')
).resolves.toBe(await readFile(installed.launcherPath as string, 'utf8'))
}
)
// Why: AppImage resources live under a per-launch FUSE mount, so the
// installed shell command must be a stable wrapper rather than a symlink.
it.skipIf(process.platform === 'win32')(
'creates an AppImage wrapper under the linux command path',
async () => {
const fixture = await makeFixture()
const commandDir = join(fixture.root, '.local', 'bin')
const installPath = join(commandDir, 'orca-ide')
const appImagePath = join(fixture.root, 'Orca.AppImage')
await writeFile(appImagePath, '#!/usr/bin/env bash\n', {
encoding: 'utf8',
mode: 0o755
})
const installer = new CliInstaller({
platform: 'linux',
isPackaged: true,
appImagePath,
commandPathOverride: installPath,
processPathEnv: commandDir
})
const initial = await installer.getStatus()
expect(initial).toMatchObject({
state: 'not_installed',
installMethod: 'wrapper',
launcherPath: appImagePath
})
const installed = await installer.install()
expect(installed).toMatchObject({
state: 'installed',
commandName: 'orca-ide',
installMethod: 'wrapper',
launcherPath: appImagePath,
currentTarget: appImagePath,
pathConfigured: true
})
const commandStats = await lstat(installPath)
expect(commandStats.isFile()).toBe(true)
expect(commandStats.mode & 0o111).not.toBe(0)
await expect(readlink(installPath)).rejects.toMatchObject({ code: 'EINVAL' })
await expect(readFile(installPath, 'utf8')).resolves.toBe(
buildAppImageCliWrapper(appImagePath)
)
const removed = await installer.remove()
expect(removed.state).toBe('not_installed')
}
)
it.skipIf(process.platform === 'win32')(
'reports a stale AppImage wrapper when the AppImage path changes',
async () => {
const fixture = await makeFixture()
const commandDir = join(fixture.root, '.local', 'bin')
const installPath = join(commandDir, 'orca-ide')
const oldAppImagePath = join(fixture.root, 'Old-Orca.AppImage')
const newAppImagePath = join(fixture.root, 'Orca.AppImage')
await mkdir(commandDir, { recursive: true })
await writeFile(installPath, buildAppImageCliWrapper(oldAppImagePath), {
encoding: 'utf8',
mode: 0o755
})
await writeFile(newAppImagePath, '#!/usr/bin/env bash\n', {
encoding: 'utf8',
mode: 0o755
})
const installer = new CliInstaller({
platform: 'linux',
isPackaged: true,
appImagePath: newAppImagePath,
commandPathOverride: installPath,
processPathEnv: commandDir
})
await expect(installer.getStatus()).resolves.toMatchObject({
state: 'stale',
installMethod: 'wrapper',
currentTarget: newAppImagePath
})
await expect(installer.install()).resolves.toMatchObject({ state: 'installed' })
await expect(readFile(installPath, 'utf8')).resolves.toBe(
buildAppImageCliWrapper(newAppImagePath)
)
}
)
// Why: Linux renamed the public command to avoid shadowing GNOME Orca, so
// upgrading must clean up only the old symlink owned by prior Orca installs.
it.skipIf(process.platform === 'win32')(
'removes the old managed linux orca symlink when installing orca-ide',
async () => {
const fixture = await makeFixture()
const homePath = join(fixture.root, 'home')
const commandDir = join(homePath, '.local', 'bin')
const resourcesPath = join(fixture.root, 'resources')
const launcherPath = join(resourcesPath, 'bin', 'orca-ide')
const oldLauncherPath = join(resourcesPath, 'bin', 'orca')
const legacyCommandPath = join(commandDir, 'orca')
await mkdir(commandDir, { recursive: true })
await mkdir(join(resourcesPath, 'bin'), { recursive: true })
await writeFile(launcherPath, '#!/usr/bin/env bash\n', 'utf8')
await writeFile(oldLauncherPath, '#!/usr/bin/env bash\n', 'utf8')
await symlink(oldLauncherPath, legacyCommandPath)
const installer = new CliInstaller({
platform: 'linux',
isPackaged: true,
resourcesPath,
homePath,
processPathEnv: commandDir
})
const installed = await installer.install()
expect(installed.commandPath).toBe(join(commandDir, 'orca-ide'))
await expect(lstat(legacyCommandPath)).rejects.toMatchObject({ code: 'ENOENT' })
}
)
it.skipIf(process.platform === 'win32')(
'removes a legacy linux orca symlink when installing an AppImage wrapper',
async () => {
const fixture = await makeFixture()
const homePath = join(fixture.root, 'home')
const commandDir = join(homePath, '.local', 'bin')
const legacyCommandPath = join(commandDir, 'orca')
const appImagePath = join(fixture.root, 'Orca.AppImage')
await mkdir(commandDir, { recursive: true })
await writeFile(appImagePath, '#!/usr/bin/env bash\n', {
encoding: 'utf8',
mode: 0o755
})
await symlink(join('/tmp', '.mount_Orca1234', 'resources', 'bin', 'orca'), legacyCommandPath)
const installer = new CliInstaller({
platform: 'linux',
isPackaged: true,
appImagePath,
homePath,
processPathEnv: commandDir
})
const installed = await installer.install()
expect(installed.commandPath).toBe(join(commandDir, 'orca-ide'))
await expect(lstat(legacyCommandPath)).rejects.toMatchObject({ code: 'ENOENT' })
}
)
it('creates a windows wrapper and updates the user PATH', async () => {
const fixture = await makeFixture()
const installPath = join(fixture.root, 'Programs', 'Orca', 'bin', 'orca.cmd')
let userPath = 'C:\\Windows\\System32'
const installer = new CliInstaller({
platform: 'win32',
isPackaged: false,
userDataPath: fixture.userDataPath,
execPath: 'C:\\Users\\me\\AppData\\Local\\Orca\\Orca.exe',
appPath: fixture.appPath,
commandPathOverride: installPath,
userPathReader: async () => userPath,
userPathWriter: async (value) => {
userPath = value
}
})
const installed = await installer.install()
expect(installed.state).toBe('installed')
expect(installed.pathConfigured).toBe(true)
expect(userPath).toContain(join(fixture.root, 'Programs', 'Orca', 'bin'))
const wrapperContent = await readFile(installPath, 'utf8')
expect(wrapperContent).toContain('ORCA_LAUNCHER=')
expect(wrapperContent).toContain('orca.cmd')
const launcherContent = await readFile(installed.launcherPath as string, 'utf8')
expect(launcherContent).toContain(`set "ORCA_USER_DATA_PATH=${fixture.userDataPath}"`)
expect(launcherContent).toContain('set "ORCA_APP_EXECUTABLE=%ELECTRON%"')
const removed = await installer.remove()
expect(removed.state).toBe('not_installed')
expect(userPath).not.toContain(join(fixture.root, 'Programs', 'Orca', 'bin'))
})
it('settles when the Windows PATH query hangs', async () => {
vi.useFakeTimers()
const fixture = await makeFixture()
const installPath = join(fixture.root, 'Programs', 'Orca', 'bin', 'orca.cmd')
const killMock = vi.fn()
execFileMock.mockImplementation(() => ({ kill: killMock }))
const installer = new CliInstaller({
platform: 'win32',
isPackaged: false,
userDataPath: fixture.userDataPath,
execPath: 'C:\\Users\\me\\AppData\\Local\\Orca\\Orca.exe',
appPath: fixture.appPath,
commandPathOverride: installPath
})
const promise = installer.getStatus()
let settled = false
void promise
.catch(() => undefined)
.finally(() => {
settled = true
})
await vi.waitFor(() => expect(execFileMock).toHaveBeenCalled())
await vi.advanceTimersByTimeAsync(5_000)
await Promise.resolve()
expect(settled).toBe(true)
await expect(promise).rejects.toThrow('Windows PATH command timed out')
expect(killMock).toHaveBeenCalled()
})
// Why: this test creates a Unix symlink to /tmp/not-orca, which only applies on macOS/Linux.
it.skipIf(process.platform === 'win32')(
'refuses to replace an unknown symlink at the command path',
async () => {
const fixture = await makeFixture()
const installPath = join(fixture.root, 'bin', 'orca')
const existingTarget = '/tmp/not-orca'
await mkdir(join(fixture.root, 'bin'), { recursive: true })
await symlink(existingTarget, installPath)
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: false,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
commandPathOverride: installPath
})
await expect(installer.getStatus()).resolves.toMatchObject({
state: 'conflict',
supported: true
})
await expect(installer.install()).rejects.toThrow('Refusing to replace non-Orca command')
await expect(readlink(installPath)).resolves.toBe(existingTarget)
}
)
// Why: packaged app moves can leave a symlink to an older Orca-owned launcher;
// those are safe to refresh, unlike arbitrary user symlinks.
it.skipIf(process.platform === 'win32')(
'replaces stale packaged Orca launcher symlinks',
async () => {
const fixture = await makeFixture()
const commandDir = join(fixture.root, 'bin')
const installPath = join(commandDir, 'orca')
const resourcesPath = join(fixture.root, 'Current.app', 'Contents', 'Resources')
const launcherPath = join(resourcesPath, 'bin', 'orca')
const oldLauncherPath = join(fixture.root, 'Old.app', 'Contents', 'Resources', 'bin', 'orca')
await mkdir(commandDir, { recursive: true })
await mkdir(join(resourcesPath, 'bin'), { recursive: true })
await writeFile(launcherPath, '#!/usr/bin/env bash\n', 'utf8')
await symlink(oldLauncherPath, installPath)
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: true,
resourcesPath,
commandPathOverride: installPath,
processPathEnv: commandDir
})
await expect(installer.getStatus()).resolves.toMatchObject({
state: 'stale',
currentTarget: oldLauncherPath
})
await expect(installer.install()).resolves.toMatchObject({ state: 'installed' })
await expect(readlink(installPath)).resolves.toBe(launcherPath)
}
)
// Why: old dev/package experiments wrote a generated Orca launcher file
// directly into /usr/local/bin/orca. That broke profiling because Settings
// treated the regular file as a hard conflict and would not self-heal it.
it.skipIf(process.platform === 'win32')(
'replaces stale generated Unix launcher files',
async () => {
const fixture = await makeFixture()
const commandDir = join(fixture.root, 'bin')
const installPath = join(commandDir, 'orca')
const resourcesPath = join(fixture.root, 'Current.app', 'Contents', 'Resources')
const launcherPath = join(resourcesPath, 'bin', 'orca')
const oldCliPath = join(fixture.root, 'OldWorktree', 'out', 'cli', 'index.js')
await mkdir(commandDir, { recursive: true })
await mkdir(join(resourcesPath, 'bin'), { recursive: true })
await writeFile(launcherPath, '#!/usr/bin/env bash\n', 'utf8')
await writeFile(
installPath,
[
'#!/usr/bin/env bash',
'set -euo pipefail',
"ELECTRON='/tmp/Old.app/Contents/MacOS/Electron'",
`CLI='${oldCliPath}'`,
'export ORCA_NODE_OPTIONS="${NODE_OPTIONS-}"',
'export ORCA_NODE_REPL_EXTERNAL_MODULE="${NODE_REPL_EXTERNAL_MODULE-}"',
'unset NODE_OPTIONS',
'unset NODE_REPL_EXTERNAL_MODULE',
'ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" "$@"',
''
].join('\n'),
'utf8'
)
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: true,
resourcesPath,
commandPathOverride: installPath,
processPathEnv: commandDir
})
await expect(installer.getStatus()).resolves.toMatchObject({
state: 'stale',
currentTarget: oldCliPath
})
await expect(installer.install()).resolves.toMatchObject({ state: 'installed' })
await expect(readlink(installPath)).resolves.toBe(launcherPath)
}
)
it.skipIf(process.platform === 'win32')(
'keeps arbitrary regular files at the command path as conflicts',
async () => {
const fixture = await makeFixture()
const commandDir = join(fixture.root, 'bin')
const installPath = join(commandDir, 'orca')
const resourcesPath = await createPackagedMacLauncher(fixture.root)
await mkdir(commandDir, { recursive: true })
await writeFile(
installPath,
'#!/usr/bin/env bash\nELECTRON_RUN_AS_NODE=1 /tmp/not-orca "$@"\n',
'utf8'
)
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: true,
resourcesPath,
commandPathOverride: installPath,
processPathEnv: commandDir
})
await expect(installer.getStatus()).resolves.toMatchObject({
state: 'conflict',
currentTarget: null
})
await expect(installer.install()).rejects.toThrow('Refusing to replace non-Orca command')
await expect(readFile(installPath, 'utf8')).resolves.toContain('/tmp/not-orca')
}
)
// Why: a dev build can temporarily own the public command on developer
// machines; packaged Orca should treat that as stale, not a hard conflict.
it.skipIf(process.platform === 'win32')(
'replaces stale sibling dev launcher symlinks from packaged installs',
async () => {
const fixture = await makeFixture()
for (const devLauncherName of ['orca', 'orca-dev']) {
const caseRoot = join(fixture.root, devLauncherName)
const commandDir = join(caseRoot, 'bin')
const installPath = join(commandDir, 'orca')
const userDataPath = join(caseRoot, 'orca')
const resourcesPath = join(caseRoot, 'Current.app', 'Contents', 'Resources')
const launcherPath = join(resourcesPath, 'bin', 'orca')
const devLauncherPath = join(`${userDataPath}-dev`, 'cli', 'bin', devLauncherName)
await mkdir(commandDir, { recursive: true })
await mkdir(join(resourcesPath, 'bin'), { recursive: true })
await mkdir(join(`${userDataPath}-dev`, 'cli', 'bin'), { recursive: true })
await writeFile(launcherPath, '#!/usr/bin/env bash\n', 'utf8')
await writeFile(devLauncherPath, '#!/usr/bin/env bash\n', 'utf8')
await symlink(devLauncherPath, installPath)
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: true,
userDataPath,
resourcesPath,
commandPathOverride: installPath,
processPathEnv: commandDir
})
await expect(installer.getStatus()).resolves.toMatchObject({
state: 'stale',
currentTarget: devLauncherPath
})
await expect(installer.install()).resolves.toMatchObject({ state: 'installed' })
await expect(readlink(installPath)).resolves.toBe(launcherPath)
}
}
)
// Why: on Apple Silicon, /usr/local/bin does not exist by default. The installer
// must fall back to ~/.local/bin (user-writable, no sudo) rather than failing
// silently when the parent directory is absent.
it.skipIf(process.platform === 'win32')(
'falls back to ~/.local/bin/orca on macOS when /usr/local/bin does not exist',
async () => {
const fixture = await makeFixture()
const homePath = join(fixture.root, 'home')
const resourcesPath = await createPackagedMacLauncher(fixture.root)
// Simulate arm64: point defaultMacCommandPath at a dir that does not exist
// in the fixture so existsSync(dirname(...)) returns false.
const absentUsrLocalBin = join(fixture.root, 'usr', 'local', 'bin', 'orca')
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: true,
resourcesPath,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
homePath,
defaultMacCommandPath: absentUsrLocalBin,
processPathEnv: join(homePath, '.local', 'bin')
})
const status = await installer.getStatus()
expect(status.commandPath).toBe(join(homePath, '.local', 'bin', 'orca'))
expect(status.state).toBe('not_installed')
expect(status.supported).toBe(true)
const installed = await installer.install()
expect(installed.state).toBe('installed')
expect(installed.commandPath).toBe(join(homePath, '.local', 'bin', 'orca'))
expect(installed.pathConfigured).toBe(true)
}
)
// Why: on Intel Macs /usr/local/bin exists, so the installer must keep using
// it as the canonical path and not regress to ~/.local/bin.
it.skipIf(process.platform === 'win32')(
'uses /usr/local/bin/orca on macOS when /usr/local/bin exists',
async () => {
const fixture = await makeFixture()
const resourcesPath = await createPackagedMacLauncher(fixture.root)
const usrLocalBin = join(fixture.root, 'usr', 'local', 'bin')
await mkdir(usrLocalBin, { recursive: true })
const installPath = join(usrLocalBin, 'orca')
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: true,
resourcesPath,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
defaultMacCommandPath: installPath,
processPathEnv: usrLocalBin
})
const installed = await installer.install()
expect(installed.state).toBe('installed')
expect(installed.commandPath).toBe(installPath)
expect(installed.pathConfigured).toBe(true)
}
)
// Why: users can have a managed Orca command in ~/.local/bin even when
// /usr/local/bin exists; Settings must follow the shell-visible command.
it.skipIf(process.platform === 'win32')(
'uses an existing managed macOS orca command from the shell PATH before /usr/local/bin',
async () => {
const fixture = await makeFixture()
const homePath = join(fixture.root, 'home')
const resourcesPath = await createPackagedMacLauncher(fixture.root)
const usrLocalBin = join(fixture.root, 'usr', 'local', 'bin')
const userLocalBin = join(homePath, '.local', 'bin')
const defaultInstallPath = join(usrLocalBin, 'orca')
const userInstallPath = join(userLocalBin, 'orca')
const launcherPath = join(resourcesPath, 'bin', 'orca')
await mkdir(usrLocalBin, { recursive: true })
await mkdir(userLocalBin, { recursive: true })
await symlink(launcherPath, userInstallPath)
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: true,
resourcesPath,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
homePath,
defaultMacCommandPath: defaultInstallPath,
processPathEnv: `${userLocalBin}:${usrLocalBin}`
})
const status = await installer.getStatus()
expect(status.state).toBe('installed')
expect(status.commandPath).toBe(userInstallPath)
expect(status.pathConfigured).toBe(true)
const installed = await installer.install()
expect(installed.commandPath).toBe(userInstallPath)
await expect(readlink(userInstallPath)).resolves.toBe(launcherPath)
await expect(lstat(defaultInstallPath)).rejects.toMatchObject({ code: 'ENOENT' })
}
)
// Why: POSIX command lookup skips broken symlinks and keeps searching PATH,
// so a stale earlier artifact must not steal status from the install path.
it.skipIf(process.platform === 'win32')(
'skips a broken managed macOS orca symlink before /usr/local/bin',
async () => {
const fixture = await makeFixture()
const homePath = join(fixture.root, 'home')
const resourcesPath = await createPackagedMacLauncher(fixture.root)
const usrLocalBin = join(fixture.root, 'usr', 'local', 'bin')
const userLocalBin = join(homePath, '.local', 'bin')
const defaultInstallPath = join(usrLocalBin, 'orca')
const userInstallPath = join(userLocalBin, 'orca')
const launcherPath = join(resourcesPath, 'bin', 'orca')
const oldLauncherPath = join(fixture.root, 'Old.app', 'Contents', 'Resources', 'bin', 'orca')
await mkdir(usrLocalBin, { recursive: true })
await mkdir(userLocalBin, { recursive: true })
await symlink(oldLauncherPath, userInstallPath)
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: true,
resourcesPath,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
homePath,
defaultMacCommandPath: defaultInstallPath,
processPathEnv: `${userLocalBin}:${usrLocalBin}`
})
const status = await installer.getStatus()
expect(status).toMatchObject({
commandPath: defaultInstallPath,
state: 'not_installed',
currentTarget: null
})
const installed = await installer.install()
expect(installed.commandPath).toBe(defaultInstallPath)
expect(installed.state).toBe('installed')
await expect(readlink(defaultInstallPath)).resolves.toBe(launcherPath)
await expect(readlink(userInstallPath)).resolves.toBe(oldLauncherPath)
}
)
// Why: PATH lookup stops at the first existing command; a later managed
// ~/.local/bin/orca must not steal status from /usr/local/bin/orca.
it.skipIf(process.platform === 'win32')(
'keeps the default macOS command when a managed orca appears later on PATH',
async () => {
const fixture = await makeFixture()
const homePath = join(fixture.root, 'home')
const resourcesPath = await createPackagedMacLauncher(fixture.root)
const usrLocalBin = join(fixture.root, 'usr', 'local', 'bin')
const userLocalBin = join(homePath, '.local', 'bin')
const defaultInstallPath = join(usrLocalBin, 'orca')
const userInstallPath = join(userLocalBin, 'orca')
const launcherPath = join(resourcesPath, 'bin', 'orca')
await mkdir(usrLocalBin, { recursive: true })
await mkdir(userLocalBin, { recursive: true })
await symlink(launcherPath, defaultInstallPath)
await symlink(launcherPath, userInstallPath)
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: true,
resourcesPath,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
homePath,
defaultMacCommandPath: defaultInstallPath,
processPathEnv: `${usrLocalBin}:${userLocalBin}`
})
const status = await installer.getStatus()
expect(status.commandPath).toBe(defaultInstallPath)
expect(status.state).toBe('installed')
}
)
// Why: shells skip missing PATH entries, so a managed command later in PATH
// is still the shell-visible Orca command until the default path is installed.
it.skipIf(process.platform === 'win32')(
'uses a later managed macOS orca command when the default command is missing',
async () => {
const fixture = await makeFixture()
const homePath = join(fixture.root, 'home')
const resourcesPath = await createPackagedMacLauncher(fixture.root)
const usrLocalBin = join(fixture.root, 'usr', 'local', 'bin')
const userLocalBin = join(homePath, '.local', 'bin')
const defaultInstallPath = join(usrLocalBin, 'orca')
const userInstallPath = join(userLocalBin, 'orca')
const launcherPath = join(resourcesPath, 'bin', 'orca')
await mkdir(usrLocalBin, { recursive: true })
await mkdir(userLocalBin, { recursive: true })
await symlink(launcherPath, userInstallPath)
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: true,
resourcesPath,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
homePath,
defaultMacCommandPath: defaultInstallPath,
processPathEnv: `${usrLocalBin}:${userLocalBin}`
})
const status = await installer.getStatus()
expect(status.commandPath).toBe(userInstallPath)
expect(status.state).toBe('installed')
const installed = await installer.install()
expect(installed.commandPath).toBe(userInstallPath)
await expect(lstat(defaultInstallPath)).rejects.toMatchObject({ code: 'ENOENT' })
}
)
// Why: bash/zsh skip non-executable PATH entries even at Orca's configured
// install slot, then keep looking for a runnable command later in PATH.
it.skipIf(process.platform === 'win32')(
'uses a later managed macOS orca command when the default command is not executable',
async () => {
const fixture = await makeFixture()
const homePath = join(fixture.root, 'home')
const resourcesPath = await createPackagedMacLauncher(fixture.root)
const usrLocalBin = join(fixture.root, 'usr', 'local', 'bin')
const userLocalBin = join(homePath, '.local', 'bin')
const defaultInstallPath = join(usrLocalBin, 'orca')
const userInstallPath = join(userLocalBin, 'orca')
const launcherPath = join(resourcesPath, 'bin', 'orca')
await mkdir(usrLocalBin, { recursive: true })
await mkdir(userLocalBin, { recursive: true })
await writeFile(defaultInstallPath, '#!/usr/bin/env bash\necho other-orca\n', 'utf8')
await symlink(launcherPath, userInstallPath)
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: true,
resourcesPath,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
homePath,
defaultMacCommandPath: defaultInstallPath,
processPathEnv: `${usrLocalBin}:${userLocalBin}`
})
const status = await installer.getStatus()
expect(status.commandPath).toBe(userInstallPath)
expect(status.state).toBe('installed')
const installed = await installer.install()
expect(installed.commandPath).toBe(userInstallPath)
await expect(readFile(defaultInstallPath, 'utf8')).resolves.toContain('other-orca')
}
)
// Why: a non-Orca command after an empty default install slot can be shadowed
// by installing the default path without replacing the user's command.
it.skipIf(process.platform === 'win32')(
'installs the default macOS command instead of replacing an unmanaged later command',
async () => {
const fixture = await makeFixture()
const homePath = join(fixture.root, 'home')
const resourcesPath = await createPackagedMacLauncher(fixture.root)
const usrLocalBin = join(fixture.root, 'usr', 'local', 'bin')
const userLocalBin = join(homePath, '.local', 'bin')
const defaultInstallPath = join(usrLocalBin, 'orca')
const userInstallPath = join(userLocalBin, 'orca')
const launcherPath = join(resourcesPath, 'bin', 'orca')
await mkdir(usrLocalBin, { recursive: true })
await mkdir(userLocalBin, { recursive: true })
await writeFile(userInstallPath, '#!/usr/bin/env bash\necho other-orca\n', {
encoding: 'utf8',
mode: 0o755
})
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: true,
resourcesPath,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
homePath,
defaultMacCommandPath: defaultInstallPath,
processPathEnv: `${usrLocalBin}:${userLocalBin}`
})
const status = await installer.getStatus()
expect(status.commandPath).toBe(defaultInstallPath)
expect(status.state).toBe('not_installed')
const installed = await installer.install()
expect(installed.commandPath).toBe(defaultInstallPath)
expect(installed.state).toBe('installed')
await expect(readlink(defaultInstallPath)).resolves.toBe(launcherPath)
await expect(readFile(userInstallPath, 'utf8')).resolves.toContain('other-orca')
}
)
// Why: an off-PATH ~/.local/bin/orca must not hijack CLI registration and
// leave the shell-visible /usr/local/bin command missing.
it.skipIf(process.platform === 'win32')(
'ignores managed macOS orca commands that are not visible on the shell PATH',
async () => {
const fixture = await makeFixture()
const homePath = join(fixture.root, 'home')
const resourcesPath = await createPackagedMacLauncher(fixture.root)
const usrLocalBin = join(fixture.root, 'usr', 'local', 'bin')
const userLocalBin = join(homePath, '.local', 'bin')
const defaultInstallPath = join(usrLocalBin, 'orca')
const userInstallPath = join(userLocalBin, 'orca')
const launcherPath = join(resourcesPath, 'bin', 'orca')
await mkdir(usrLocalBin, { recursive: true })
await mkdir(userLocalBin, { recursive: true })
await symlink(launcherPath, userInstallPath)
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: true,
resourcesPath,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
homePath,
defaultMacCommandPath: defaultInstallPath,
processPathEnv: usrLocalBin
})
const status = await installer.getStatus()
expect(status.commandPath).toBe(defaultInstallPath)
expect(status.pathConfigured).toBe(true)
expect(status.state).toBe('not_installed')
const installed = await installer.install()
expect(installed.commandPath).toBe(defaultInstallPath)
expect(installed.state).toBe('installed')
await expect(readlink(defaultInstallPath)).resolves.toBe(launcherPath)
await expect(readlink(userInstallPath)).resolves.toBe(launcherPath)
}
)
it.skipIf(process.platform === 'win32')(
'reports a conflict for an unmanaged macOS orca that shadows the install path',
async () => {
const fixture = await makeFixture()
const homePath = join(fixture.root, 'home')
const resourcesPath = await createPackagedMacLauncher(fixture.root)
const usrLocalBin = join(fixture.root, 'usr', 'local', 'bin')
const userLocalBin = join(homePath, '.local', 'bin')
const defaultInstallPath = join(usrLocalBin, 'orca')
const userInstallPath = join(userLocalBin, 'orca')
await mkdir(usrLocalBin, { recursive: true })
await mkdir(userLocalBin, { recursive: true })
await writeFile(userInstallPath, '#!/usr/bin/env bash\necho other-orca\n', {
encoding: 'utf8',
mode: 0o755
})
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: true,
resourcesPath,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
homePath,
defaultMacCommandPath: defaultInstallPath,
processPathEnv: `${userLocalBin}:${usrLocalBin}`
})
const status = await installer.getStatus()
expect(status.commandPath).toBe(userInstallPath)
expect(status.state).toBe('conflict')
await expect(installer.install()).rejects.toThrow('Refusing to replace non-Orca command')
await expect(lstat(defaultInstallPath)).rejects.toMatchObject({ code: 'ENOENT' })
await expect(readFile(userInstallPath, 'utf8')).resolves.toContain('other-orca')
}
)
// Why: bash/zsh skip non-executable PATH entries, so reporting them as a
// conflict would block a valid later install path the shell would use.
it.skipIf(process.platform === 'win32')(
'skips a non-executable unmanaged macOS orca before the install path',
async () => {
const fixture = await makeFixture()
const homePath = join(fixture.root, 'home')
const resourcesPath = await createPackagedMacLauncher(fixture.root)
const usrLocalBin = join(fixture.root, 'usr', 'local', 'bin')
const userLocalBin = join(homePath, '.local', 'bin')
const defaultInstallPath = join(usrLocalBin, 'orca')
const userInstallPath = join(userLocalBin, 'orca')
const launcherPath = join(resourcesPath, 'bin', 'orca')
await mkdir(usrLocalBin, { recursive: true })
await mkdir(userLocalBin, { recursive: true })
await writeFile(userInstallPath, '#!/usr/bin/env bash\necho other-orca\n', 'utf8')
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: true,
resourcesPath,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
homePath,
defaultMacCommandPath: defaultInstallPath,
processPathEnv: `${userLocalBin}:${usrLocalBin}`
})
const status = await installer.getStatus()
expect(status.commandPath).toBe(defaultInstallPath)
expect(status.state).toBe('not_installed')
const installed = await installer.install()
expect(installed.commandPath).toBe(defaultInstallPath)
expect(installed.state).toBe('installed')
await expect(readlink(defaultInstallPath)).resolves.toBe(launcherPath)
await expect(readFile(userInstallPath, 'utf8')).resolves.toContain('other-orca')
}
)
// Why: when macCommandPath falls back to ~/.local/bin/orca on arm64, commandName
// must still be 'orca' (not 'orca-ide' which is Linux-only).
it.skipIf(process.platform === 'win32')(
'reports commandName as orca (not orca-ide) when falling back to ~/.local/bin on macOS',
async () => {
const fixture = await makeFixture()
const homePath = join(fixture.root, 'home')
const resourcesPath = await createPackagedMacLauncher(fixture.root)
const absentUsrLocalBin = join(fixture.root, 'usr', 'local', 'bin', 'orca')
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: true,
resourcesPath,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
homePath,
defaultMacCommandPath: absentUsrLocalBin,
processPathEnv: join(homePath, '.local', 'bin')
})
const status = await installer.getStatus()
expect(status.commandName).toBe('orca')
}
)
// Why: the privilegedRunner is injectable so the EACCES→osascript path can be
// exercised in integration without spawning osascript in unit tests.
it.skipIf(process.platform === 'win32' || process.getuid?.() === 0)(
'invokes the injected privilegedRunner when install falls back to elevated permissions',
async () => {
const fixture = await makeFixture()
const protectedDir = join(fixture.root, 'protected')
await mkdir(protectedDir)
await chmod(protectedDir, 0o500)
const installPath = join(protectedDir, 'bin', 'orca')
const privilegedCommands: string[] = []
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: false,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
commandPathOverride: installPath,
privilegedRunner: async (command: string) => {
privilegedCommands.push(command)
await chmod(protectedDir, 0o700)
const launcherPath = (await installer.getStatus()).launcherPath as string
await mkdir(dirname(installPath), { recursive: true })
await symlink(launcherPath, installPath)
},
processPathEnv: dirname(installPath)
})
try {
const installed = await installer.install()
expect(installed.state).toBe('installed')
expect(installed.pathConfigured).toBe(true)
expect(privilegedCommands).toHaveLength(1)
expect(privilegedCommands[0]).toContain('mkdir -p')
expect(privilegedCommands[0]).toContain('ln -sfn')
await expect(readlink(installPath)).resolves.toBe(installed.launcherPath)
} finally {
await chmod(protectedDir, 0o700).catch(() => undefined)
}
}
)
// Why: macCommandPath is resolved at construction — getStatus() must return the
// same commandPath on repeated calls without re-running existsSync.
it.skipIf(process.platform === 'win32')(
'resolves macCommandPath once at construction — commandPath stable across repeated getStatus()',
async () => {
const fixture = await makeFixture()
const homePath = join(fixture.root, 'home')
const resourcesPath = await createPackagedMacLauncher(fixture.root)
const absentUsrLocalBin = join(fixture.root, 'usr', 'local', 'bin', 'orca')
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: true,
resourcesPath,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
homePath,
defaultMacCommandPath: absentUsrLocalBin,
processPathEnv: join(homePath, '.local', 'bin')
})
const s1 = await installer.getStatus()
await mkdir(dirname(absentUsrLocalBin), { recursive: true })
const s2 = await installer.getStatus()
const s3 = await installer.getStatus()
expect(s1.commandPath).toBe(s2.commandPath)
expect(s2.commandPath).toBe(s3.commandPath)
expect(s1.commandPath).toBe(join(homePath, '.local', 'bin', 'orca'))
}
)
it('resolves packaged Windows command path to resources/bin/orca.cmd', async () => {
const fixture = await makeFixture()
const localAppDataPath = fixture.root
const resourcesPath = join(fixture.root, 'resources')
await mkdir(join(resourcesPath, 'bin'), { recursive: true })
await writeFile(join(resourcesPath, 'bin', 'orca.cmd'), '@echo off\n', 'utf8')
const installer = new CliInstaller({
platform: 'win32',
isPackaged: true,
resourcesPath,
localAppDataPath,
userDataPath: fixture.userDataPath,
execPath: join(localAppDataPath, 'Programs', 'Orca', 'Orca.exe'),
appPath: fixture.appPath,
userPathReader: async () => null,
userPathWriter: async () => {}
})
const status = await installer.getStatus()
expect(status.commandPath).toBe(
join(localAppDataPath, 'Programs', 'Orca', 'resources', 'bin', 'orca.cmd')
)
})
it('does not overwrite the packaged Windows launcher while registering PATH', async () => {
const fixture = await makeFixture()
const localAppDataPath = fixture.root
const resourcesPath = join(localAppDataPath, 'Programs', 'Orca', 'resources')
const bundledLauncher = join(resourcesPath, 'bin', 'orca.cmd')
const bundledContent = '@echo off\r\necho bundled-orca %*\r\n'
await mkdir(dirname(bundledLauncher), { recursive: true })
await writeFile(bundledLauncher, bundledContent, 'utf8')
let userPath: string | null = null
const installer = new CliInstaller({
platform: 'win32',
isPackaged: true,
resourcesPath,
localAppDataPath,
userDataPath: fixture.userDataPath,
execPath: join(localAppDataPath, 'Programs', 'Orca', 'Orca.exe'),
appPath: fixture.appPath,
userPathReader: async () => userPath,
userPathWriter: async (value) => {
userPath = value
}
})
const installed = await installer.install()
expect(installed.state).toBe('installed')
expect(installed.pathConfigured).toBe(true)
expect(installed.commandPath).toBe(bundledLauncher)
expect(userPath).toBe(dirname(bundledLauncher))
await expect(readFile(bundledLauncher, 'utf8')).resolves.toBe(bundledContent)
const removed = await installer.remove()
expect(removed.state).toBe('not_installed')
expect(removed.pathConfigured).toBe(false)
expect(userPath).toBe('')
await expect(readFile(bundledLauncher, 'utf8')).resolves.toBe(bundledContent)
})
// Why: the arm64 fallback must apply for packaged builds, not just dev launchers.
it.skipIf(process.platform === 'win32')(
'resolves to ~/.local/bin/orca on arm64 even when isPackaged is true',
async () => {
const fixture = await makeFixture()
const homePath = join(fixture.root, 'home')
const absentUsrLocalBin = join(fixture.root, 'usr', 'local', 'bin', 'orca')
const resourcesPath = join(fixture.root, 'resources')
const bundledLauncher = join(resourcesPath, 'bin', 'orca')
await mkdir(join(resourcesPath, 'bin'), { recursive: true })
await writeFile(bundledLauncher, '#!/usr/bin/env bash\necho orca\n', {
encoding: 'utf8',
mode: 0o755
})
const installer = new CliInstaller({
platform: 'darwin',
isPackaged: true,
resourcesPath,
userDataPath: fixture.userDataPath,
execPath: '/Applications/Orca.app/Contents/MacOS/Orca',
appPath: fixture.appPath,
homePath,
defaultMacCommandPath: absentUsrLocalBin,
processPathEnv: join(homePath, '.local', 'bin')
})
const status = await installer.getStatus()
expect(status.commandPath).toBe(join(homePath, '.local', 'bin', 'orca'))
expect(status.supported).toBe(true)
}
)
})