mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
Validate WSL UNC worktrees inside the distro (#6536)
Opening a workspace whose worktree lives on a \wsl.localhost\<distro> UNC path failed with 'Working directory ... does not exist' even though it exists. validateWorkingDirectory relied on Win32 fs.statSync, which is unreliable against the WSL 9P share and can report ENOENT for directories that exist on the Linux side. Make working-directory validation WSL-aware: when the cwd is a WSL UNC path, ask the distro itself via 'wsl.exe -d <distro> -- test -d <linuxPath>' and only fall back to the fs check when wsl.exe is inconclusive. Native Windows path validation is unchanged. Fixes #6331 Co-authored-by: brennanb2025 <brennanb2025@users.noreply.github.com>
This commit is contained in:
co-authored by
brennanb2025
parent
f768e6b503
commit
9a7b2e1e2e
@@ -74,7 +74,10 @@ vi.mock('../wsl', () => ({
|
||||
toLinuxPath: (path: string) => path.replace(/^C:\\/i, '/mnt/c/').replace(/\\/g, '/'),
|
||||
toWindowsWslPath: (path: string, distro: string) =>
|
||||
`\\\\wsl.localhost\\${distro}${path.replace(/\//g, '\\')}`,
|
||||
isWslAvailable: () => true
|
||||
isWslAvailable: () => true,
|
||||
// Why: WSL worktree validation now asks the distro; these tests use WSL UNC
|
||||
// cwds that are meant to exist, so report them present without spawning wsl.exe.
|
||||
wslUncDirectoryExists: () => true
|
||||
}))
|
||||
|
||||
import { LocalPtyProvider } from './local-pty-provider'
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type * as fs from 'fs'
|
||||
import type { Stats } from 'fs'
|
||||
|
||||
const { existsSyncMock, statSyncMock, wslUncDirectoryExistsMock } = vi.hoisted(() => ({
|
||||
existsSyncMock: vi.fn(),
|
||||
statSyncMock: vi.fn(),
|
||||
wslUncDirectoryExistsMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('fs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof fs>()
|
||||
return {
|
||||
...actual,
|
||||
existsSync: existsSyncMock,
|
||||
statSync: statSyncMock
|
||||
}
|
||||
})
|
||||
|
||||
function dirStats(isDirectory: boolean): Stats {
|
||||
return { isDirectory: () => isDirectory } as Stats
|
||||
}
|
||||
|
||||
vi.mock('../wsl', () => ({
|
||||
wslUncDirectoryExists: wslUncDirectoryExistsMock
|
||||
}))
|
||||
|
||||
import { validateWorkingDirectory } from './local-pty-utils'
|
||||
|
||||
const WSL_UNC_DIR = '\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo'
|
||||
const NATIVE_DIR = 'C:\\Users\\jin\\repo'
|
||||
|
||||
describe('validateWorkingDirectory', () => {
|
||||
beforeEach(() => {
|
||||
existsSyncMock.mockReset()
|
||||
statSyncMock.mockReset()
|
||||
wslUncDirectoryExistsMock.mockReset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('accepts a WSL UNC worktree that exists inside the distro even when fs.statSync would fail', () => {
|
||||
// Why: the Win32 9P stat is the exact path that falsely reported ENOENT and
|
||||
// broke opening WSL worktrees. The distro answer must win.
|
||||
wslUncDirectoryExistsMock.mockReturnValue(true)
|
||||
existsSyncMock.mockReturnValue(false)
|
||||
|
||||
expect(() => validateWorkingDirectory(WSL_UNC_DIR)).not.toThrow()
|
||||
expect(wslUncDirectoryExistsMock).toHaveBeenCalledWith(WSL_UNC_DIR)
|
||||
// The fs fallback must not run when the distro confirmed existence.
|
||||
expect(existsSyncMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects a WSL UNC worktree that does not exist inside the distro', () => {
|
||||
wslUncDirectoryExistsMock.mockReturnValue(false)
|
||||
|
||||
expect(() => validateWorkingDirectory(WSL_UNC_DIR)).toThrow(/does not exist/)
|
||||
expect(existsSyncMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('falls back to the fs check when the distro answer is inconclusive', () => {
|
||||
wslUncDirectoryExistsMock.mockReturnValue(null)
|
||||
existsSyncMock.mockReturnValue(true)
|
||||
statSyncMock.mockReturnValue(dirStats(true))
|
||||
|
||||
expect(() => validateWorkingDirectory(WSL_UNC_DIR)).not.toThrow()
|
||||
expect(wslUncDirectoryExistsMock).toHaveBeenCalledWith(WSL_UNC_DIR)
|
||||
expect(existsSyncMock).toHaveBeenCalledWith(WSL_UNC_DIR)
|
||||
})
|
||||
|
||||
it('validates native Windows paths via fs without consulting the distro', () => {
|
||||
existsSyncMock.mockReturnValue(true)
|
||||
statSyncMock.mockReturnValue(dirStats(true))
|
||||
|
||||
expect(() => validateWorkingDirectory(NATIVE_DIR)).not.toThrow()
|
||||
expect(wslUncDirectoryExistsMock).not.toHaveBeenCalled()
|
||||
expect(existsSyncMock).toHaveBeenCalledWith(NATIVE_DIR)
|
||||
})
|
||||
|
||||
it('rejects a missing native Windows path', () => {
|
||||
existsSyncMock.mockReturnValue(false)
|
||||
|
||||
expect(() => validateWorkingDirectory(NATIVE_DIR)).toThrow(/does not exist/)
|
||||
expect(wslUncDirectoryExistsMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects a native Windows path that exists but is not a directory', () => {
|
||||
existsSyncMock.mockReturnValue(true)
|
||||
statSyncMock.mockReturnValue(dirStats(false))
|
||||
|
||||
expect(() => validateWorkingDirectory(NATIVE_DIR)).toThrow(/is not a directory/)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,8 @@
|
||||
import { basename, join } from 'path'
|
||||
import { existsSync, accessSync, statSync, chmodSync, constants as fsConstants } from 'fs'
|
||||
import type * as pty from 'node-pty'
|
||||
import { isWslUncPath } from '../../shared/wsl-paths'
|
||||
import { wslUncDirectoryExists } from '../wsl'
|
||||
|
||||
let didEnsureSpawnHelperExecutable = false
|
||||
|
||||
@@ -75,16 +77,33 @@ export function ensureNodePtySpawnHelperExecutable(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function throwMissingWorkingDirectory(cwd: string): never {
|
||||
throw new Error(
|
||||
`Working directory "${cwd}" does not exist. ` +
|
||||
`It may have been deleted or is on an unmounted volume.`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a working directory exists and is a directory.
|
||||
* Throws a descriptive Error if not.
|
||||
*/
|
||||
export function validateWorkingDirectory(cwd: string): void {
|
||||
// Why: Win32 fs.statSync against the WSL 9P share (\\wsl.localhost\...) can
|
||||
// falsely report ENOENT for directories that exist on the Linux side. Ask the
|
||||
// distro itself; only fall back to the fs check when wsl.exe is inconclusive.
|
||||
if (isWslUncPath(cwd)) {
|
||||
const existsInDistro = wslUncDirectoryExists(cwd)
|
||||
if (existsInDistro === false) {
|
||||
throwMissingWorkingDirectory(cwd)
|
||||
}
|
||||
if (existsInDistro === true) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!existsSync(cwd)) {
|
||||
throw new Error(
|
||||
`Working directory "${cwd}" does not exist. ` +
|
||||
`It may have been deleted or is on an unmounted volume.`
|
||||
)
|
||||
throwMissingWorkingDirectory(cwd)
|
||||
}
|
||||
if (!statSync(cwd).isDirectory()) {
|
||||
throw new Error(`Working directory "${cwd}" is not a directory.`)
|
||||
|
||||
+79
-2
@@ -1,5 +1,29 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { toLinuxPath, toWindowsWslPath, parseWslPath } from './wsl'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type * as childProcess from 'child_process'
|
||||
|
||||
const { execFileSyncMock } = vi.hoisted(() => ({
|
||||
execFileSyncMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('child_process', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof childProcess>()
|
||||
return {
|
||||
...actual,
|
||||
execFileSync: execFileSyncMock
|
||||
}
|
||||
})
|
||||
|
||||
import { toLinuxPath, toWindowsWslPath, parseWslPath, wslUncDirectoryExists } from './wsl'
|
||||
|
||||
function withPlatform<T>(value: NodeJS.Platform, fn: () => T): T {
|
||||
const original = process.platform
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value })
|
||||
try {
|
||||
return fn()
|
||||
} finally {
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: original })
|
||||
}
|
||||
}
|
||||
|
||||
describe('wsl path helpers', () => {
|
||||
it('parses WSL UNC paths on Windows', () => {
|
||||
@@ -32,3 +56,56 @@ describe('wsl path helpers', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('wslUncDirectoryExists', () => {
|
||||
afterEach(() => {
|
||||
execFileSyncMock.mockReset()
|
||||
})
|
||||
|
||||
it('returns true when the distro reports the directory exists', () => {
|
||||
execFileSyncMock.mockReturnValue('')
|
||||
const result = withPlatform('win32', () =>
|
||||
wslUncDirectoryExists('\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo')
|
||||
)
|
||||
expect(result).toBe(true)
|
||||
expect(execFileSyncMock).toHaveBeenCalledWith(
|
||||
'wsl.exe',
|
||||
['-d', 'Ubuntu', '--', 'test', '-d', '/home/jin/repo'],
|
||||
expect.objectContaining({ timeout: 5000 })
|
||||
)
|
||||
})
|
||||
|
||||
it('returns false when test -d exits non-zero (directory missing)', () => {
|
||||
execFileSyncMock.mockImplementation(() => {
|
||||
// Why: child_process surfaces a non-zero exit as an Error with `status`.
|
||||
const error = new Error('Command failed') as Error & { status: number }
|
||||
error.status = 1
|
||||
throw error
|
||||
})
|
||||
const result = withPlatform('win32', () =>
|
||||
wslUncDirectoryExists('\\\\wsl.localhost\\Ubuntu\\home\\jin\\missing')
|
||||
)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('returns null when wsl.exe is unavailable (inconclusive)', () => {
|
||||
execFileSyncMock.mockImplementation(() => {
|
||||
// No numeric `status` -> spawn failure (ENOENT), not a missing directory.
|
||||
const error = new Error('spawn wsl.exe ENOENT') as Error & { code: string }
|
||||
error.code = 'ENOENT'
|
||||
throw error
|
||||
})
|
||||
const result = withPlatform('win32', () =>
|
||||
wslUncDirectoryExists('\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo')
|
||||
)
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for non-WSL paths and off Windows', () => {
|
||||
expect(withPlatform('win32', () => wslUncDirectoryExists('C:\\Users\\jin\\repo'))).toBeNull()
|
||||
expect(
|
||||
withPlatform('linux', () => wslUncDirectoryExists('\\\\wsl.localhost\\Ubuntu\\home\\jin'))
|
||||
).toBeNull()
|
||||
expect(execFileSyncMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -27,6 +27,43 @@ export function isWslPath(path: string): boolean {
|
||||
return parseWslPath(path) !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a WSL UNC working directory exists by testing it inside the
|
||||
* distro itself, returning null when the answer can't be determined.
|
||||
*
|
||||
* Why: Win32 fs.statSync against the WSL 9P filesystem (\\wsl.localhost\...)
|
||||
* is unreliable for repos that live on the WSL side — it can report ENOENT for
|
||||
* directories that exist, which made opening a WSL worktree fail with
|
||||
* "Working directory ... does not exist". `wsl.exe -d <distro> test -d` asks
|
||||
* the distro directly, which is the authoritative answer. Returns null (rather
|
||||
* than false) when wsl.exe is unavailable or errors so callers can fall back to
|
||||
* the fs check instead of falsely rejecting a valid directory.
|
||||
*/
|
||||
export function wslUncDirectoryExists(uncPath: string): boolean | null {
|
||||
if (process.platform !== 'win32') {
|
||||
return null
|
||||
}
|
||||
const info = parseWslUncPath(uncPath)
|
||||
if (!info) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
execFileSync('wsl.exe', ['-d', info.distro, '--', 'test', '-d', info.linuxPath], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 5000
|
||||
})
|
||||
return true
|
||||
} catch (error) {
|
||||
// A non-zero exit (directory missing) surfaces as an error with a numeric
|
||||
// `status`; treat that as a definitive "does not exist". Any other failure
|
||||
// (wsl.exe missing, distro not running, timeout) is inconclusive -> null.
|
||||
if (typeof (error as { status?: unknown })?.status === 'number') {
|
||||
return false
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Windows path to a Linux path for commands that will execute inside WSL.
|
||||
* Returns the path unchanged if it is already POSIX-style.
|
||||
|
||||
Reference in New Issue
Block a user