Stop directory pickers creating typed prefixes (#6114)

This commit is contained in:
Ricardo Sawir 2026-06-23 09:09:45 +07:00 committed by GitHub
parent 9de696a357
commit cea8d97aec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 92 additions and 13 deletions

View File

@ -1,14 +1,24 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { handlers, appExitMock, appQuitMock, appRelaunchMock, execFileMock, destroySystemTrayMock } =
vi.hoisted(() => ({
handlers: new Map<string, (_event: unknown, args?: unknown) => unknown>(),
appExitMock: vi.fn(),
appQuitMock: vi.fn(),
appRelaunchMock: vi.fn(),
execFileMock: vi.fn(),
destroySystemTrayMock: vi.fn()
}))
const {
handlers,
appExitMock,
appQuitMock,
appRelaunchMock,
execFileMock,
destroySystemTrayMock,
showOpenDialogMock,
grantFloatingWorkspaceDirectoryMock
} = vi.hoisted(() => ({
handlers: new Map<string, (_event: unknown, args?: unknown) => unknown>(),
appExitMock: vi.fn(),
appQuitMock: vi.fn(),
appRelaunchMock: vi.fn(),
execFileMock: vi.fn(),
destroySystemTrayMock: vi.fn(),
showOpenDialogMock: vi.fn(),
grantFloatingWorkspaceDirectoryMock: vi.fn()
}))
vi.mock('node:child_process', () => ({
execFile: execFileMock
@ -26,7 +36,7 @@ vi.mock('electron', () => ({
fromWebContents: vi.fn(() => null)
},
dialog: {
showOpenDialog: vi.fn()
showOpenDialog: showOpenDialogMock
},
ipcMain: {
handle: vi.fn((channel: string, handler: (_event: unknown, args?: unknown) => unknown) => {
@ -43,6 +53,12 @@ vi.mock('../tray/system-tray', () => ({
destroySystemTray: destroySystemTrayMock
}))
vi.mock('./floating-workspace-directory', () => ({
ensureDefaultFloatingWorkspacePath: vi.fn(),
grantFloatingWorkspaceDirectory: grantFloatingWorkspaceDirectoryMock,
resolveFloatingTerminalCwd: vi.fn()
}))
import { registerAppHandlers } from './app'
describe('registerAppHandlers', () => {
@ -56,6 +72,8 @@ describe('registerAppHandlers', () => {
appRelaunchMock.mockReset()
execFileMock.mockReset()
destroySystemTrayMock.mockReset()
showOpenDialogMock.mockReset()
grantFloatingWorkspaceDirectoryMock.mockReset()
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true })
})
@ -177,4 +195,21 @@ describe('registerAppHandlers', () => {
await expect(resultPromise).resolves.toBeNull()
expect(killMock).toHaveBeenCalled()
})
it('picks an existing floating workspace directory without enabling native directory creation', async () => {
const store = {}
showOpenDialogMock.mockResolvedValue({
canceled: false,
filePaths: ['/Users/kaylee/notes']
})
registerAppHandlers(store as never)
await expect(
handlers.get('app:pickFloatingWorkspaceDirectory')?.({ sender: {} })
).resolves.toBe('/Users/kaylee/notes')
expect(showOpenDialogMock).toHaveBeenCalledWith({
properties: ['openDirectory']
})
expect(grantFloatingWorkspaceDirectoryMock).toHaveBeenCalledWith(store, '/Users/kaylee/notes')
})
})

View File

@ -57,7 +57,9 @@ async function pickFloatingWorkspaceDirectory(
): Promise<string | null> {
const parentWindow = BrowserWindow.fromWebContents(event.sender)
const options = {
properties: ['openDirectory', 'createDirectory']
// Why: this picker grants access to an existing workspace directory.
// Creation belongs to explicit file/write actions, not typeahead input.
properties: ['openDirectory']
} satisfies Electron.OpenDialogOptions
const result = parentWindow
? await dialog.showOpenDialog(parentWindow, options)

View File

@ -64,6 +64,14 @@ describe('repos folder pickers', () => {
return handler(null, undefined) as Promise<string[]>
}
const callPickDirectory = (): Promise<string | null> => {
const handler = handlers.get('repos:pickDirectory')
if (!handler) {
throw new Error('repos:pickDirectory handler was never registered')
}
return handler(null, undefined) as Promise<string | null>
}
beforeEach(() => {
handlers.clear()
handleMock.mockReset()
@ -101,4 +109,18 @@ describe('repos folder pickers', () => {
await expect(callPickFolders()).resolves.toEqual([])
})
it('picks an existing directory without enabling native directory creation', async () => {
const parentDir = join(sep, 'projects')
showOpenDialogMock.mockResolvedValue({
canceled: false,
filePaths: [parentDir]
})
await expect(callPickDirectory()).resolves.toBe(parentDir)
expect(showOpenDialogMock).toHaveBeenCalledWith(mockWindow, {
properties: ['openDirectory']
})
})
})

View File

@ -2070,7 +2070,9 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v
// destination directory that may not be a git repo yet.
ipcMain.handle('repos:pickDirectory', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory', 'createDirectory']
// Why: macOS can materialize typed partial paths when directory creation
// is enabled; clone/create actions already create the final path on submit.
properties: ['openDirectory']
})
if (result.canceled || result.filePaths.length === 0) {
return null

View File

@ -132,6 +132,22 @@ describe('registerShellHandlers', () => {
await expect(handler({})).resolves.toBeNull()
})
it('picks an existing directory without enabling native directory creation', async () => {
showOpenDialogMock.mockResolvedValue({
canceled: false,
filePaths: ['/Users/kaylee/projects']
})
const handler = getHandler('shell:pickDirectory')
await expect(handler({}, { defaultPath: '/Users/kaylee' })).resolves.toBe(
'/Users/kaylee/projects'
)
expect(showOpenDialogMock).toHaveBeenCalledWith({
defaultPath: '/Users/kaylee',
properties: ['openDirectory']
})
})
describe('shell:openPath', () => {
it('ignores relative paths', async () => {
const handler = getHandler('shell:openPath')

View File

@ -205,7 +205,9 @@ export function registerShellHandlers(): void {
async (_event, args: { defaultPath?: string }): Promise<string | null> => {
const result = await dialog.showOpenDialog({
defaultPath: args.defaultPath,
properties: ['openDirectory', 'createDirectory']
// Why: callers only need an existing folder grant; enabling native
// creation can leave typed prefix directories behind on macOS.
properties: ['openDirectory']
})
if (result.canceled || result.filePaths.length === 0) {
return null