mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 00:02:56 +00:00
On a Windows host the runtime stores a WSL worktree as the UNC path Windows sees, but a user inside the distro types the Linux spelling, so every `path:` selector missed: `worktree show`, `terminal list --worktree` and `worktree rm --worktree` all reported selector_not_found for a directory Orca manages. Translate once in the CLI, which is the only side that can prove which distro the typed path belongs to — from its own UNC cwd, never from WSL_DISTRO_NAME, which a Linux-native CLI also sets. The runtime's `path:` branch stays exact-spelling-only for the same reason: this resolver feeds delete, so a tail-only match would remove another distro's copy.
This commit is contained in:
+56
-5
@@ -4,7 +4,8 @@ import type {
|
||||
RuntimeWorktreeListResult,
|
||||
RuntimeWorktreeRecord
|
||||
} from '../shared/runtime-types'
|
||||
import { isPathInsideOrEqual } from '../shared/cross-platform-path'
|
||||
import { isPathInsideOrEqual, isWslUncPathForCallerLinuxPath } from '../shared/cross-platform-path'
|
||||
import { parseWslUncPath } from '../shared/wsl-paths'
|
||||
import type { RuntimeClient } from './runtime-client'
|
||||
import { RuntimeClientError } from './runtime/types'
|
||||
import { getOptionalStringFlag, getRequiredStringFlag } from './flags'
|
||||
@@ -31,6 +32,56 @@ export function normalizeWorktreeSelector(selector: string, cwd: string): string
|
||||
return selector
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites the Linux path a WSL shell prints into the UNC path the runtime stored (#16628).
|
||||
*
|
||||
* Why here and not in the runtime: only the CLI sits in the distro, so only it can
|
||||
* prove which distro the typed path belongs to. Translating once at this chokepoint
|
||||
* covers `worktree show`, `terminal list --worktree` and `worktree rm --worktree`.
|
||||
*/
|
||||
export async function resolveCallerDistroPathSelector(
|
||||
selector: string,
|
||||
cwd: string,
|
||||
client: RuntimeClient
|
||||
): Promise<string> {
|
||||
// Why not WSL_DISTRO_NAME: it is also set for a Linux-native CLI whose runtime stores
|
||||
// POSIX paths, so it would name a distro for a caller that has none. ORCA_CLI_CWD, which
|
||||
// the WSL launcher always sets and which arrives here as the invocation cwd, proves it.
|
||||
const callerDistro = parseWslUncPath(cwd)?.distro
|
||||
const linuxPath = selector.startsWith('path:') ? selector.slice(5) : ''
|
||||
if (
|
||||
!callerDistro ||
|
||||
client.isRemote ||
|
||||
!linuxPath.startsWith('/') ||
|
||||
linuxPath.startsWith('//') ||
|
||||
// Backslash is a legal Linux filename character but a separator once a path reads as UNC.
|
||||
linuxPath.includes('\\')
|
||||
) {
|
||||
return selector
|
||||
}
|
||||
const worktrees = await client.call<RuntimeWorktreeListResult>('worktree.list', {
|
||||
limit: 10_000
|
||||
})
|
||||
const match = worktrees.result.worktrees.find((worktree) =>
|
||||
isWslUncPathForCallerLinuxPath(worktree.path, linuxPath, callerDistro)
|
||||
)
|
||||
// Why the stored spelling rather than a synthesized UNC path: an unmatched selector must
|
||||
// reach the runtime verbatim and fail as the caller typed it, never as a guessed distro.
|
||||
return match ? `path:${match.path}` : selector
|
||||
}
|
||||
|
||||
export async function normalizeWorktreeSelectorForCaller(
|
||||
selector: string,
|
||||
cwd: string,
|
||||
client: RuntimeClient
|
||||
): Promise<string> {
|
||||
return await resolveCallerDistroPathSelector(
|
||||
normalizeWorktreeSelector(selector, cwd),
|
||||
cwd,
|
||||
client
|
||||
)
|
||||
}
|
||||
|
||||
function assertLocalCwdWorktreeSelector(selector: string, client: RuntimeClient): void {
|
||||
if (!client.isRemote) {
|
||||
return
|
||||
@@ -95,7 +146,7 @@ export async function getOptionalWorktreeSelector(
|
||||
assertLocalCwdWorktreeSelector(value, client)
|
||||
return await resolveCurrentWorktreeSelector(cwd, client)
|
||||
}
|
||||
return normalizeWorktreeSelector(value, cwd)
|
||||
return await normalizeWorktreeSelectorForCaller(value, cwd, client)
|
||||
}
|
||||
|
||||
export async function getRequiredWorktreeSelector(
|
||||
@@ -109,7 +160,7 @@ export async function getRequiredWorktreeSelector(
|
||||
assertLocalCwdWorktreeSelector(value, client)
|
||||
return await resolveCurrentWorktreeSelector(cwd, client)
|
||||
}
|
||||
return normalizeWorktreeSelector(value, cwd)
|
||||
return await normalizeWorktreeSelectorForCaller(value, cwd, client)
|
||||
}
|
||||
|
||||
// Why: local browser commands default to the current worktree by auto-resolving
|
||||
@@ -128,7 +179,7 @@ export async function getBrowserWorktreeSelector(
|
||||
assertLocalCwdWorktreeSelector(value, client)
|
||||
return await resolveCurrentWorktreeSelector(cwd, client)
|
||||
}
|
||||
return normalizeWorktreeSelector(value, cwd)
|
||||
return await normalizeWorktreeSelectorForCaller(value, cwd, client)
|
||||
}
|
||||
if (client.isRemote) {
|
||||
return undefined
|
||||
@@ -184,7 +235,7 @@ export async function getBrowserCommandTarget(
|
||||
}
|
||||
return {
|
||||
page,
|
||||
worktree: normalizeWorktreeSelector(explicitWorktree, cwd)
|
||||
worktree: await normalizeWorktreeSelectorForCaller(explicitWorktree, cwd, client)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* The CLI half of #16628: inside WSL the user types `/home/neil/qa-repo`, the runtime stored the
|
||||
* UNC path Windows sees. Only this process sits in the distro, so only it may translate — and the
|
||||
* same selector reaches `worktree rm`, so an unprovable distro must leave the path alone.
|
||||
*/
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { RuntimeWorktreeRecord } from '../shared/runtime-types'
|
||||
import type { RuntimeClient } from './runtime-client'
|
||||
import { normalizeWorktreeSelectorForCaller } from './selectors'
|
||||
|
||||
const UBUNTU = 'Ubuntu-24.04'
|
||||
const DEBIAN = 'Debian'
|
||||
const LINUX_PATH = '/home/neil/qa-repo'
|
||||
|
||||
function uncPath(distro: string, linuxPath: string): string {
|
||||
return `\\\\wsl.localhost\\${distro}${linuxPath.replace(/\//g, '\\')}`
|
||||
}
|
||||
|
||||
function worktreeRecord(path: string): RuntimeWorktreeRecord {
|
||||
return { id: `repo::${path}`, path } as RuntimeWorktreeRecord
|
||||
}
|
||||
|
||||
function makeClient(paths: readonly string[], isRemote = false) {
|
||||
const call = vi.fn(async (method: string) => {
|
||||
if (method !== 'worktree.list') {
|
||||
throw new Error(`unexpected method ${method}`)
|
||||
}
|
||||
return {
|
||||
result: { worktrees: paths.map(worktreeRecord), totalCount: paths.length, truncated: false }
|
||||
}
|
||||
})
|
||||
return { client: { isRemote, call } as unknown as RuntimeClient, call }
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
|
||||
describe('normalizeWorktreeSelectorForCaller in a WSL shell (#16628)', () => {
|
||||
it.each([
|
||||
['a backslash UNC registration', uncPath(UBUNTU, LINUX_PATH)],
|
||||
['a forward-slash UNC registration', `//wsl.localhost/${UBUNTU}${LINUX_PATH}`],
|
||||
['the wsl$ alias', `\\\\wsl$\\${UBUNTU}${LINUX_PATH.replace(/\//g, '\\')}`]
|
||||
])('resolves the typed Linux path to %s', async (_label, storedPath) => {
|
||||
const { client } = makeClient([storedPath])
|
||||
|
||||
await expect(
|
||||
normalizeWorktreeSelectorForCaller(
|
||||
`path:${LINUX_PATH}`,
|
||||
uncPath(UBUNTU, '/home/neil'),
|
||||
client
|
||||
)
|
||||
).resolves.toBe(`path:${storedPath}`)
|
||||
})
|
||||
|
||||
it('leaves the path alone when only another distro spells it', async () => {
|
||||
const { client } = makeClient([uncPath(UBUNTU, LINUX_PATH)])
|
||||
// Interop forwards WSL_DISTRO_NAME, so it must not outvote the cwd that proves the distro.
|
||||
vi.stubEnv('WSL_DISTRO_NAME', UBUNTU)
|
||||
|
||||
await expect(
|
||||
normalizeWorktreeSelectorForCaller(
|
||||
`path:${LINUX_PATH}`,
|
||||
uncPath(DEBIAN, '/home/neil'),
|
||||
client
|
||||
)
|
||||
).resolves.toBe(`path:${LINUX_PATH}`)
|
||||
})
|
||||
|
||||
it('picks the caller-distro worktree when two distros spell the same Linux path', async () => {
|
||||
const { client } = makeClient([uncPath(UBUNTU, LINUX_PATH), uncPath(DEBIAN, LINUX_PATH)])
|
||||
|
||||
await expect(
|
||||
normalizeWorktreeSelectorForCaller(
|
||||
`path:${LINUX_PATH}`,
|
||||
uncPath(DEBIAN, '/home/neil'),
|
||||
client
|
||||
)
|
||||
).resolves.toBe(`path:${uncPath(DEBIAN, LINUX_PATH)}`)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['a Linux-native cwd', '/home/neil', `path:${LINUX_PATH}`],
|
||||
['a Windows drive cwd', 'C:\\Users\\neil', `path:${LINUX_PATH}`]
|
||||
])('never lists worktrees for %s', async (_label, cwd, selector) => {
|
||||
const { client, call } = makeClient([uncPath(UBUNTU, LINUX_PATH)])
|
||||
vi.stubEnv('WSL_DISTRO_NAME', UBUNTU)
|
||||
|
||||
await expect(normalizeWorktreeSelectorForCaller(selector, cwd, client)).resolves.toBe(selector)
|
||||
expect(call).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['a branch selector', 'branch:qa'],
|
||||
['a relative path', 'path:qa-repo'],
|
||||
// A UNC path is already the runtime's spelling; a backslash inside a Linux path has no UNC form.
|
||||
['a UNC path selector', `path:${uncPath(UBUNTU, LINUX_PATH)}`],
|
||||
['a Linux path containing a backslash', 'path:/home/neil/qa\\repo']
|
||||
])('leaves %s untranslated', async (_label, selector) => {
|
||||
const { client, call } = makeClient([uncPath(UBUNTU, LINUX_PATH)])
|
||||
|
||||
await expect(
|
||||
normalizeWorktreeSelectorForCaller(selector, uncPath(UBUNTU, '/home/neil'), client)
|
||||
).resolves.toBe(selector)
|
||||
expect(call).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('leaves a remote runtime alone, whose worktrees the caller cwd cannot name', async () => {
|
||||
const { client, call } = makeClient([uncPath(UBUNTU, LINUX_PATH)], true)
|
||||
|
||||
await expect(
|
||||
normalizeWorktreeSelectorForCaller(
|
||||
`path:${LINUX_PATH}`,
|
||||
uncPath(UBUNTU, '/home/neil'),
|
||||
client
|
||||
)
|
||||
).resolves.toBe(`path:${LINUX_PATH}`)
|
||||
expect(call).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -33860,6 +33860,8 @@ export class OrcaRuntimeService {
|
||||
}
|
||||
}
|
||||
} else if (selector.startsWith('path:')) {
|
||||
// Why exact-spelling-only (#16628): a Linux path names a directory in *some* WSL distro, and
|
||||
// only the CLI sits in one and can prove which. Guessing here would delete a stranger's.
|
||||
candidates = worktrees.filter((worktree) =>
|
||||
runtimePathsEqual(worktree.path, selector.slice(5))
|
||||
)
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* A WSL shell prints `/home/neil/qa-repo`; the runtime stored the same directory as the UNC path
|
||||
* Windows sees (#16628). The CLI translates, proving the caller's distro from its own UNC cwd —
|
||||
* this resolver also feeds `worktree rm`, so a tail-only match would delete another distro's copy.
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const electronMocks = vi.hoisted(() => {
|
||||
const ipcMain = {
|
||||
on: vi.fn(() => ipcMain),
|
||||
removeListener: vi.fn(() => ipcMain),
|
||||
emit: vi.fn(() => true)
|
||||
}
|
||||
return {
|
||||
BrowserWindow: { fromId: vi.fn((): unknown => null) },
|
||||
webContents: { fromId: vi.fn((): unknown => null) },
|
||||
ipcMain,
|
||||
app: { getPath: vi.fn(() => '/tmp'), isPackaged: false }
|
||||
}
|
||||
})
|
||||
vi.mock('electron', () => electronMocks)
|
||||
|
||||
const getSshGitProviderMock = vi.hoisted(() => vi.fn())
|
||||
vi.mock('../providers/ssh-git-dispatch', () => ({
|
||||
getSshGitProvider: getSshGitProviderMock,
|
||||
getSshGitProviderGeneration: vi.fn(() => 0),
|
||||
SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE: 'unavailable',
|
||||
requireSshGitProvider: (connectionId: string) => getSshGitProviderMock(connectionId)
|
||||
}))
|
||||
|
||||
const listWorktreesStrictMock = vi.hoisted(() => vi.fn())
|
||||
vi.mock('../git/worktree', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
listWorktreesStrict: listWorktreesStrictMock
|
||||
}))
|
||||
|
||||
import { isWslUncPathForCallerLinuxPath } from '../../shared/cross-platform-path'
|
||||
import { parseWslUncPath } from '../../shared/wsl-paths'
|
||||
import { OrcaRuntimeService } from './orca-runtime'
|
||||
|
||||
const UBUNTU = 'Ubuntu-24.04'
|
||||
const DEBIAN = 'Debian'
|
||||
const LINUX_REPO_PATH = '/home/neil/repo'
|
||||
const LINUX_WORKTREE_PATH = '/home/neil/qa-repo'
|
||||
|
||||
/** The cwd the WSL launcher hands the CLI: the caller's directory, already in UNC form. */
|
||||
function uncPath(distro: string, linuxPath: string): string {
|
||||
return `\\\\wsl.localhost\\${distro}${linuxPath.replace(/\//g, '\\')}`
|
||||
}
|
||||
|
||||
type Registration = { distro: string; repoPath: string; worktreePath: string }
|
||||
|
||||
function registration(distro: string, spelling: (linuxPath: string) => string): Registration {
|
||||
return {
|
||||
distro,
|
||||
repoPath: spelling(LINUX_REPO_PATH),
|
||||
worktreePath: spelling(LINUX_WORKTREE_PATH)
|
||||
}
|
||||
}
|
||||
|
||||
/** One repo per distro, so an unwanted cross-distro match shows up as the wrong resolved path. */
|
||||
function makeStore(registrations: readonly Registration[]) {
|
||||
const repos = registrations.map((entry) => ({
|
||||
id: `repo-${entry.distro.toLowerCase()}`,
|
||||
path: entry.repoPath,
|
||||
displayName: entry.distro,
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1
|
||||
}))
|
||||
const store = {
|
||||
getRepo: (id: string) => repos.find((repo) => repo.id === id),
|
||||
getRepos: () => repos,
|
||||
getAllWorktreeMeta: vi.fn(() => ({})),
|
||||
getWorktreeMeta: () => undefined,
|
||||
setWorktreeMeta: vi.fn(),
|
||||
removeWorktreeMeta: () => {},
|
||||
getAllWorktreeLineage: () => ({}),
|
||||
getAllWorkspaceLineage: () => ({}),
|
||||
removeWorktreeLineage: vi.fn(),
|
||||
removeWorkspaceLineage: vi.fn(),
|
||||
getGitHubCache: () => undefined as never,
|
||||
getSettings: () => ({
|
||||
workspaceDir: '/tmp/workspaces',
|
||||
nestWorkspaces: false,
|
||||
refreshLocalBaseRefOnWorktreeCreate: false,
|
||||
branchPrefix: 'none',
|
||||
branchPrefixCustom: ''
|
||||
}),
|
||||
getProjects: () => []
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
/** `git worktree list` for each registered repo: the main checkout plus one workspace. */
|
||||
function scanReports(registrations: readonly Registration[]): void {
|
||||
listWorktreesStrictMock.mockImplementation(async (repoPath: string) => {
|
||||
const entry = registrations.find((candidate) => candidate.repoPath === repoPath)
|
||||
if (!entry) {
|
||||
return []
|
||||
}
|
||||
return [
|
||||
{ path: entry.repoPath, head: 'abc', branch: 'main', isBare: false, isMainWorktree: true },
|
||||
{
|
||||
path: entry.worktreePath,
|
||||
head: 'def',
|
||||
branch: 'qa',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
function makeRuntime(registrations: readonly Registration[]): OrcaRuntimeService {
|
||||
scanReports(registrations)
|
||||
return new OrcaRuntimeService(makeStore(registrations) as never)
|
||||
}
|
||||
|
||||
/**
|
||||
* What `resolveCallerDistroPathSelector` sends: the caller's distro comes from its own UNC cwd,
|
||||
* and the stored spelling of the one worktree that distro can mean replaces the typed path.
|
||||
* Mirrored rather than imported — `src/cli` is outside this file's tsconfig project.
|
||||
*/
|
||||
async function selectorTheCliWouldSend(
|
||||
runtime: OrcaRuntimeService,
|
||||
callerCwd: string,
|
||||
typedPath: string
|
||||
): Promise<string> {
|
||||
const callerDistro = parseWslUncPath(callerCwd)?.distro
|
||||
if (!callerDistro) {
|
||||
return `path:${typedPath}`
|
||||
}
|
||||
const listed = await runtime.listManagedWorktrees()
|
||||
const match = listed.worktrees.find((worktree) =>
|
||||
isWslUncPathForCallerLinuxPath(worktree.path, typedPath, callerDistro)
|
||||
)
|
||||
return match ? `path:${match.path}` : `path:${typedPath}`
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
getSshGitProviderMock.mockReset()
|
||||
listWorktreesStrictMock.mockReset()
|
||||
})
|
||||
|
||||
describe('a WSL caller typing the Linux path of a UNC-stored worktree (#16628)', () => {
|
||||
it('resolves the Linux path a WSL shell prints to the UNC-stored worktree', async () => {
|
||||
const registrations = [registration(UBUNTU, (linuxPath) => uncPath(UBUNTU, linuxPath))]
|
||||
const runtime = makeRuntime(registrations)
|
||||
|
||||
// The live bug: the spelling the runtime stored resolves, the one the user types does not.
|
||||
await expect(
|
||||
runtime.showManagedWorktree(`path:${uncPath(UBUNTU, LINUX_WORKTREE_PATH)}`)
|
||||
).resolves.toMatchObject({ path: uncPath(UBUNTU, LINUX_WORKTREE_PATH) })
|
||||
await expect(runtime.showManagedWorktree(`path:${LINUX_WORKTREE_PATH}`)).rejects.toThrow(
|
||||
'selector_not_found'
|
||||
)
|
||||
|
||||
const selector = await selectorTheCliWouldSend(
|
||||
runtime,
|
||||
uncPath(UBUNTU, '/home/neil'),
|
||||
LINUX_WORKTREE_PATH
|
||||
)
|
||||
|
||||
await expect(runtime.showManagedWorktree(selector)).resolves.toMatchObject({
|
||||
path: uncPath(UBUNTU, LINUX_WORKTREE_PATH)
|
||||
})
|
||||
})
|
||||
|
||||
it('resolves a forward-slash UNC registration from the same Linux path', async () => {
|
||||
// Windows records the same share both ways; the caller's spelling never changes.
|
||||
const forwardSlash = (linuxPath: string) => `//wsl.localhost/${UBUNTU}${linuxPath}`
|
||||
const runtime = makeRuntime([registration(UBUNTU, forwardSlash)])
|
||||
|
||||
const selector = await selectorTheCliWouldSend(
|
||||
runtime,
|
||||
uncPath(UBUNTU, '/home/neil'),
|
||||
LINUX_WORKTREE_PATH
|
||||
)
|
||||
|
||||
await expect(runtime.showManagedWorktree(selector)).resolves.toMatchObject({
|
||||
path: forwardSlash(LINUX_WORKTREE_PATH)
|
||||
})
|
||||
})
|
||||
|
||||
it('refuses a Linux path another distro spells, instead of resolving that distro', async () => {
|
||||
const runtime = makeRuntime([registration(UBUNTU, (linuxPath) => uncPath(UBUNTU, linuxPath))])
|
||||
const selector = await selectorTheCliWouldSend(
|
||||
runtime,
|
||||
uncPath(DEBIAN, '/home/neil'),
|
||||
LINUX_WORKTREE_PATH
|
||||
)
|
||||
|
||||
// Untranslated, so the runtime refuses rather than deleting Ubuntu's copy on a Debian `rm`.
|
||||
expect(selector).toBe(`path:${LINUX_WORKTREE_PATH}`)
|
||||
await expect(runtime.showManagedWorktree(selector)).rejects.toThrow('selector_not_found')
|
||||
})
|
||||
|
||||
it('picks the caller-distro worktree when two distros spell the same Linux path', async () => {
|
||||
const runtime = makeRuntime([
|
||||
registration(UBUNTU, (linuxPath) => uncPath(UBUNTU, linuxPath)),
|
||||
registration(DEBIAN, (linuxPath) => uncPath(DEBIAN, linuxPath))
|
||||
])
|
||||
|
||||
const selector = await selectorTheCliWouldSend(
|
||||
runtime,
|
||||
uncPath(DEBIAN, '/home/neil'),
|
||||
LINUX_WORKTREE_PATH
|
||||
)
|
||||
|
||||
await expect(runtime.showManagedWorktree(selector)).resolves.toMatchObject({
|
||||
path: uncPath(DEBIAN, LINUX_WORKTREE_PATH)
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves a Linux-native CLI, whose runtime stores POSIX paths, untouched', async () => {
|
||||
const posix = (linuxPath: string) => linuxPath
|
||||
const runtime = makeRuntime([registration(UBUNTU, posix)])
|
||||
|
||||
const selector = await selectorTheCliWouldSend(runtime, '/home/neil', LINUX_WORKTREE_PATH)
|
||||
|
||||
expect(selector).toBe(`path:${LINUX_WORKTREE_PATH}`)
|
||||
await expect(runtime.showManagedWorktree(selector)).resolves.toMatchObject({
|
||||
path: LINUX_WORKTREE_PATH
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
isCaseInsensitiveRuntimeRoot,
|
||||
isPathInsideOrEqual,
|
||||
isRuntimePathAbsolute,
|
||||
isWslUncPathForCallerLinuxPath,
|
||||
normalizeRuntimePathForComparison,
|
||||
relativePathInsideRoot,
|
||||
resolveRuntimePath
|
||||
@@ -50,6 +51,54 @@ describe('isCaseInsensitiveRuntimeRoot', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('isWslUncPathForCallerLinuxPath', () => {
|
||||
const UBUNTU = 'Ubuntu-24.04'
|
||||
|
||||
it('matches the Linux path a WSL shell prints against both UNC spellings of its own distro', () => {
|
||||
expect(
|
||||
isWslUncPathForCallerLinuxPath(
|
||||
'\\\\wsl.localhost\\Ubuntu-24.04\\home\\neil\\qa-repo',
|
||||
'/home/neil/qa-repo',
|
||||
UBUNTU
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
isWslUncPathForCallerLinuxPath(
|
||||
'//wsl$/ubuntu-24.04/home/neil/qa-repo',
|
||||
'/home/neil/qa-repo',
|
||||
UBUNTU
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
// The destructive case: this feeds `worktree rm`, so a tail-only match deletes the wrong distro.
|
||||
it('refuses a Linux path another distro spells identically', () => {
|
||||
expect(
|
||||
isWslUncPathForCallerLinuxPath(
|
||||
'\\\\wsl.localhost\\Debian\\home\\neil\\qa-repo',
|
||||
'/home/neil/qa-repo',
|
||||
UBUNTU
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps the Linux tail case-sensitive and refuses a non-WSL path', () => {
|
||||
expect(
|
||||
isWslUncPathForCallerLinuxPath(
|
||||
'\\\\wsl.localhost\\Ubuntu-24.04\\home\\Neil\\qa-repo',
|
||||
'/home/neil/qa-repo',
|
||||
UBUNTU
|
||||
)
|
||||
).toBe(false)
|
||||
expect(isWslUncPathForCallerLinuxPath('/home/neil/qa-repo', '/home/neil/qa-repo', UBUNTU)).toBe(
|
||||
false
|
||||
)
|
||||
expect(isWslUncPathForCallerLinuxPath('C:\\repos\\qa-repo', '/home/neil/qa-repo', UBUNTU)).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('cross-platform path containment', () => {
|
||||
it('keeps POSIX sibling prefixes outside the root', () => {
|
||||
expect(isPathInsideOrEqual('/repo/app', '/repo/app')).toBe(true)
|
||||
|
||||
@@ -56,6 +56,31 @@ export function normalizeRuntimePathForComparison(rawValue: string): string {
|
||||
return isWindowsPath ? normalized.toLowerCase() : normalized
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `uncPath` is the WSL UNC spelling of `linuxPath` as the caller's own distro sees it.
|
||||
*
|
||||
* Why the distro must match and not just the Linux tail: every distro spells
|
||||
* `/home/<user>/repo`, so a tail-only match lets a Debian caller resolve — and
|
||||
* `worktree rm` then delete — an Ubuntu directory. The distro is proven by the
|
||||
* caller's UNC cwd, never guessed.
|
||||
*/
|
||||
export function isWslUncPathForCallerLinuxPath(
|
||||
uncPath: string,
|
||||
linuxPath: string,
|
||||
callerDistro: string
|
||||
): boolean {
|
||||
const parsed = parseWslUncPath(uncPath)
|
||||
if (!parsed) {
|
||||
return false
|
||||
}
|
||||
// Why the case split: Windows folds the distro name, the Linux tail it fronts is case-sensitive.
|
||||
return (
|
||||
parsed.distro.toLowerCase() === callerDistro.toLowerCase() &&
|
||||
normalizeRuntimePathForComparison(parsed.linuxPath) ===
|
||||
normalizeRuntimePathForComparison(linuxPath)
|
||||
)
|
||||
}
|
||||
|
||||
export function areLocalWindowsWslPathAliases(left: string, right: string): boolean {
|
||||
const leftIdentity = getLocalWindowsWslPathIdentity(left)
|
||||
const rightIdentity = getLocalWindowsWslPathIdentity(right)
|
||||
|
||||
Reference in New Issue
Block a user