mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 18:47:40 +00:00
fix(wsl-host): chat and text generation never run a Windows-side agent CLI on a WSL host (#34590)
* fix(wsl-host): chat and text generation never run a Windows-side agent CLI on a WSL host * fix(wsl-host): a Windows-only claude or codex reads as not installed to every CLI caller * fix(wsl-host): say a Windows-only claude or codex is installed only on Windows at sign-in * test(wsl): move the lane's chat envelope out of the contract to break an import cycle * test(native-chat): inject guest mounts into the stock lookup the WSL guest-path tests exercise
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { chmodSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { spawnProcess } from '@orca/process-host'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { probeRecentClaudeCliVersion } from '../claude/claude-hook-event-versions'
|
||||
import { resolveGuestNativeAgentCommand, WINDOWS_ONLY_AGENT_CLI_DIR } from './command'
|
||||
|
||||
let root: string
|
||||
let native: string
|
||||
let drive: string
|
||||
|
||||
function install(dir: string, name: string): string {
|
||||
mkdirSync(dir, { recursive: true })
|
||||
const program = join(dir, name)
|
||||
writeFileSync(program, '#!/bin/sh\necho "9.9.9 (Claude Code)"\n')
|
||||
chmodSync(program, 0o755)
|
||||
return program
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
root = mkdtempSync(join(tmpdir(), 'orca-agent-cli-guest-'))
|
||||
native = join(root, 'home', '.local', 'bin')
|
||||
// Stands in for a Windows drive mount such as /mnt/c.
|
||||
drive = join(root, 'c')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe.skipIf(process.platform === 'win32')('agent CLI resolution on a WSL host', () => {
|
||||
const options = () => ({ pathEnv: `${native}:${drive}/npm`, homePath: join(root, 'home') })
|
||||
|
||||
it.each(['claude', 'codex'])('takes the WSL install of %s over a Windows copy', (name) => {
|
||||
const own = install(native, name)
|
||||
install(join(drive, 'npm'), name)
|
||||
expect(resolveGuestNativeAgentCommand(name, options(), [drive])).toBe(own)
|
||||
})
|
||||
|
||||
it.each(['claude', 'codex'])(
|
||||
'reads a Windows-only %s as not installed: never a bare name the spawn would find',
|
||||
(name) => {
|
||||
install(join(drive, 'npm'), name)
|
||||
expect(resolveGuestNativeAgentCommand(name, options(), [drive])).toBe(
|
||||
`${WINDOWS_ONLY_AGENT_CLI_DIR}/${name}`
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it('leaves a CLI missing everywhere as its bare name, as before', () => {
|
||||
expect(resolveGuestNativeAgentCommand('claude', options(), [drive])).toBe('claude')
|
||||
})
|
||||
|
||||
it('resolves exactly as before off a WSL host', () => {
|
||||
const windowsCopy = install(join(drive, 'npm'), 'claude')
|
||||
expect(resolveGuestNativeAgentCommand('claude', options(), [])).toBe(windowsCopy)
|
||||
})
|
||||
})
|
||||
|
||||
describe.skipIf(process.platform === 'win32')('callers given a Windows-only CLI', () => {
|
||||
const missing = `${WINDOWS_ONLY_AGENT_CLI_DIR}/claude`
|
||||
|
||||
it('see nothing on disk (hook fingerprints, presence checks)', () => {
|
||||
expect(existsSync(missing)).toBe(false)
|
||||
})
|
||||
|
||||
it('get ENOENT from a spawn (login, rate limits, app-server probes)', async () => {
|
||||
const child = spawnProcess({ program: missing, args: ['--version'] })
|
||||
const error = await new Promise<NodeJS.ErrnoException>((resolve) => child.on('error', resolve))
|
||||
expect(error.code).toBe('ENOENT')
|
||||
})
|
||||
|
||||
it('read no version (hook-event and diff-panel version probes)', async () => {
|
||||
await expect(probeRecentClaudeCliVersion(missing)).resolves.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1 +1,38 @@
|
||||
export * from '../../shared/node-cli-command-resolution'
|
||||
import { resolveCliCommand } from '../../shared/node-cli-command-resolution'
|
||||
import { resolveGuestNativeProgram } from '../ipc/wsl-guest-windows-drive-path'
|
||||
import { WINDOWS_ONLY_AGENT_CLI_DIR } from './windows-only-agent-cli'
|
||||
|
||||
export {
|
||||
getVersionManagerBinPaths,
|
||||
resolveCliCommand,
|
||||
resolveCliCommands,
|
||||
resolveExecutableCommand,
|
||||
withCliRuntimeOnPath
|
||||
} from '../../shared/node-cli-command-resolution'
|
||||
export { WINDOWS_ONLY_AGENT_CLI_DIR } from './windows-only-agent-cli'
|
||||
|
||||
type ResolveCommandOptions = NonNullable<Parameters<typeof resolveCliCommand>[1]>
|
||||
|
||||
/**
|
||||
* On a WSL-host orcad, a Windows-only copy reads as not installed: a path every spawn (ENOENT) and
|
||||
* stat reports missing, never a bare name the spawn's own PATH search would take to the Windows
|
||||
* shim. Unchanged wherever there are no Windows drive mounts.
|
||||
*/
|
||||
export function resolveGuestNativeAgentCommand(
|
||||
name: string,
|
||||
options: ResolveCommandOptions = {},
|
||||
mounts?: readonly string[]
|
||||
): string {
|
||||
return (
|
||||
resolveGuestNativeProgram(name, options, resolveCliCommand, mounts) ??
|
||||
`${WINDOWS_ONLY_AGENT_CLI_DIR}/${name}`
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveCodexCommand(options: ResolveCommandOptions = {}): string {
|
||||
return resolveGuestNativeAgentCommand('codex', options)
|
||||
}
|
||||
|
||||
export function resolveClaudeCommand(options: ResolveCommandOptions = {}): string {
|
||||
return resolveGuestNativeAgentCommand('claude', options)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { codexSpawnEnoent, WINDOWS_ONLY_AGENT_CLI_DIR } from './windows-only-agent-cli'
|
||||
|
||||
it('words a Codex spawn ENOENT by what was resolved', () => {
|
||||
expect(codexSpawnEnoent(`${WINDOWS_ONLY_AGENT_CLI_DIR}/codex`)).toEqual({
|
||||
message:
|
||||
'Codex is installed only on Windows, not in this WSL distro. Install it inside the distro.',
|
||||
missing: true
|
||||
})
|
||||
expect(codexSpawnEnoent('codex')).toEqual({ message: 'Codex CLI not found.', missing: true })
|
||||
expect(codexSpawnEnoent('/usr/lib/node_modules/codex/bin/codex.js').missing).toBe(false)
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
// Kept apart from ./command, which many tests mock whole, so error paths always see these.
|
||||
/** procfs takes no new entries, so nothing ever exists here. */
|
||||
export const WINDOWS_ONLY_AGENT_CLI_DIR = '/proc/orca-agent-cli-installed-only-on-windows'
|
||||
|
||||
export function isWindowsOnlyAgentCommand(command: string): boolean {
|
||||
return command.startsWith(`${WINDOWS_ONLY_AGENT_CLI_DIR}/`)
|
||||
}
|
||||
|
||||
/** What sign-in and status surfaces say when the only copy is the Windows one; null otherwise. */
|
||||
export function windowsOnlyCliMessage(command: string, agent: string): string | null {
|
||||
return isWindowsOnlyAgentCommand(command)
|
||||
? `${agent} is installed only on Windows, not in this WSL distro. Install it inside the distro.`
|
||||
: null
|
||||
}
|
||||
|
||||
/** Codex's spawn ENOENT: missing (bare name, or only a Windows copy), or a found CLI without its Node. */
|
||||
export function codexSpawnEnoent(command: string): { message: string; missing: boolean } {
|
||||
const windowsOnly = windowsOnlyCliMessage(command, 'Codex')
|
||||
if (windowsOnly) {
|
||||
return { message: windowsOnly, missing: true }
|
||||
}
|
||||
return command === 'codex'
|
||||
? { message: 'Codex CLI not found.', missing: true }
|
||||
: {
|
||||
message: 'Codex CLI found but could not run — Node.js may not be in your PATH.',
|
||||
missing: false
|
||||
}
|
||||
}
|
||||
|
||||
export function isSpawnEnoent(error: Error): boolean {
|
||||
return 'code' in error && error.code === 'ENOENT'
|
||||
}
|
||||
Reference in New Issue
Block a user