diff --git a/cli/src/api/apiMachine.test.ts b/cli/src/api/apiMachine.test.ts index 321990f0..4204c412 100644 --- a/cli/src/api/apiMachine.test.ts +++ b/cli/src/api/apiMachine.test.ts @@ -18,7 +18,7 @@ vi.mock('../modules/common/opencodeModels', () => ({ listOpencodeModelsForCwd: listOpencodeModelsForCwdMock })) -import { ApiMachineClient } from './apiMachine' +import { ApiMachineClient, normalizeWindowsDriveRoot } from './apiMachine' import type { Machine } from './types' function makeMachine(id: string): Machine { @@ -37,6 +37,18 @@ function makeMachine(id: string): Machine { } } +describe('normalizeWindowsDriveRoot', () => { + it('restores the trailing separator when Windows realpath returns a bare drive', () => { + expect(normalizeWindowsDriveRoot('C:')).toBe('C:\\') + expect(normalizeWindowsDriveRoot('D:')).toBe('D:\\') + }) + + it('leaves non-drive-root paths unchanged', () => { + expect(normalizeWindowsDriveRoot('C:\\Users')).toBe('C:\\Users') + expect(normalizeWindowsDriveRoot('/tmp/workspace')).toBe('/tmp/workspace') + }) +}) + async function callListOpencodeModels(client: ApiMachineClient, machineId: string, cwd: string): Promise { // Reach into the private rpc handler manager to dispatch a request. // Mirrors how the on-socket 'rpc-request' listener invokes handleRequest. @@ -137,7 +149,7 @@ describe('ApiMachineClient listOpencodeModelsForCwd handler', () => { }) // The handler realpaths the cwd (security: prevents symlink escape), // so on macOS /var/folders/... resolves to /private/var/folders/... - expect(listOpencodeModelsForCwdMock).toHaveBeenCalledWith(realpathSync(secondWorkspaceRoot)) + expect(listOpencodeModelsForCwdMock).toHaveBeenCalledWith(realpathSync.native(secondWorkspaceRoot)) } finally { rmSync(secondWorkspaceRoot, { recursive: true, force: true }) client.shutdown() diff --git a/cli/src/api/apiMachine.ts b/cli/src/api/apiMachine.ts index 4225ff6a..07e24153 100644 --- a/cli/src/api/apiMachine.ts +++ b/cli/src/api/apiMachine.ts @@ -41,6 +41,14 @@ interface ListMachineDirectoryRequest { path: string } +export function normalizeWindowsDriveRoot(path: string): string { + return /^[A-Za-z]:$/.test(path) ? `${path}\\` : path +} + +function canonicalRealpathSync(path: string): string { + return normalizeWindowsDriveRoot(realpathSync.native(path)) +} + function normalizeWorkspaceRoots(paths?: string[]): string[] | undefined { if (!paths?.length) { return undefined @@ -48,9 +56,9 @@ function normalizeWorkspaceRoots(paths?: string[]): string[] | undefined { const normalized = Array.from(new Set(paths.map((path) => { try { - return realpathSync(path) + return canonicalRealpathSync(path) } catch { - return resolvePath(path) + return normalizeWindowsDriveRoot(resolvePath(path)) } }))) @@ -232,7 +240,7 @@ export class ApiMachineClient { private async resolveForWorkspaceCheck(path: string): Promise { const absolute = resolvePath(path) try { - return await realpath(absolute) + return normalizeWindowsDriveRoot(await realpath(absolute)) } catch { const missing: string[] = [] let cursor = absolute @@ -240,12 +248,12 @@ export class ApiMachineClient { missing.unshift(basename(cursor)) cursor = dirname(cursor) try { - return join(await realpath(cursor), ...missing) + return join(normalizeWindowsDriveRoot(await realpath(cursor)), ...missing) } catch { // keep walking to the nearest existing parent } } - return absolute + return normalizeWindowsDriveRoot(absolute) } }