From 5bd66bac8bfec4d9891846050114685aae1e257e Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:42:20 -0700 Subject: [PATCH] fix(cli): resolve a WSL worktree by the Linux path its own shell prints (#16628) (#17440) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/cli/selectors.ts | 61 ++++- .../worktree-selector-wsl-posix-path.test.ts | 120 ++++++++++ src/main/runtime/orca-runtime.ts | 2 + .../worktree-path-selector-wsl-posix.test.ts | 226 ++++++++++++++++++ src/shared/cross-platform-path.test.ts | 49 ++++ src/shared/cross-platform-path.ts | 25 ++ 6 files changed, 478 insertions(+), 5 deletions(-) create mode 100644 src/cli/worktree-selector-wsl-posix-path.test.ts create mode 100644 src/main/runtime/worktree-path-selector-wsl-posix.test.ts diff --git a/src/cli/selectors.ts b/src/cli/selectors.ts index cfc2dd68749..86ed2157ea2 100644 --- a/src/cli/selectors.ts +++ b/src/cli/selectors.ts @@ -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 { + // 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('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 { + 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) } } diff --git a/src/cli/worktree-selector-wsl-posix-path.test.ts b/src/cli/worktree-selector-wsl-posix-path.test.ts new file mode 100644 index 00000000000..10d71c8e9ed --- /dev/null +++ b/src/cli/worktree-selector-wsl-posix-path.test.ts @@ -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() + }) +}) diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 1808ef30136..c6fa5b474ec 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -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)) ) diff --git a/src/main/runtime/worktree-path-selector-wsl-posix.test.ts b/src/main/runtime/worktree-path-selector-wsl-posix.test.ts new file mode 100644 index 00000000000..2346afa500b --- /dev/null +++ b/src/main/runtime/worktree-path-selector-wsl-posix.test.ts @@ -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>()), + 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 { + 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 + }) + }) +}) diff --git a/src/shared/cross-platform-path.test.ts b/src/shared/cross-platform-path.test.ts index b817459301e..f23892d98e3 100644 --- a/src/shared/cross-platform-path.test.ts +++ b/src/shared/cross-platform-path.test.ts @@ -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) diff --git a/src/shared/cross-platform-path.ts b/src/shared/cross-platform-path.ts index aad0981c3a1..679ee9e57ba 100644 --- a/src/shared/cross-platform-path.ts +++ b/src/shared/cross-platform-path.ts @@ -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//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)