feat(picker): open the Win32 folder dialog in-process over koffi

The modern IFileOpenDialog becomes the primary win32 tier: a koffi-driven
COM conversation on a worker_threads worker (the modal Show never blocks
the host event loop), per-monitor-v2 DPI via SetThreadDpiAwarenessContext,
and abort service by re-posting WM_CLOSE to the dialog thread's windows,
with terminate+unref as the last resort (Node cannot interrupt a thread
blocked in native code, and such a worker must never hold the process open).

The PowerShell chain stays as the fallback tier with its trigger widened
from ENOENT to any pwsh failure, closing the review-flagged PowerShell 6
regression (no WinForms: exit 1, not ENOENT, so 5.1 never ran).

Layering keeps per-file coverage honest on every host: pure sequencing and
the driver test against fakes anywhere; the bindings run against a mocked
koffi COM world (the session-persistence-jsonl technique); POSIX hosts
drive the real spawn plumbing to its koffi-load rejection; win32 hosts run
a real open-and-abort-close smoke. The smoke joins processBoundTests: a
worker blocked in a native modal wedges the threads pool's teardown, while
a fork contains it. The worker bundles as its own CJS tsdown entry
(workflow-workerthread's pattern; no TLA), and the host module is imported
statically so the node-half bundle stays chunk-free.

Built-plane and real-COM behavior verified on native Windows: standalone
probes for the source worker, the built CJS worker, and the driver's abort
path all open and close the real dialog.

Agent Notes: new implemented/feature/2026-08-02-win32-in-process-folder-dialog
(bilingual) owns the decision; the DPI note is re-scoped to the fallback tier
it now describes and its AutoUpgradeEnabled attribution corrected (.NET Core
3.0 rewrote FolderBrowserDialog; the opt-out arrived in .NET 6).
This commit is contained in:
Huanqi Cao
2026-08-03 00:06:47 +08:00
parent da1b1ff87d
commit 089f4dfad8
23 changed files with 1174 additions and 41 deletions

View File

@@ -23,6 +23,9 @@ function failure(code: string | number, stderr = ''): Error {
const signal = () => new AbortController().signal
/** The PowerShell chain is reachable only when the in-process dialog fails. */
const noDialog = async (): Promise<string | null> => { throw new Error('dialog unavailable') }
describe('native directory picker', () => {
it('uses the macOS folder chooser and maps user cancellation to null', async () => {
const run = vi.fn<DirectoryPickerRunner>(async () => ({ stdout: '/Users/test/project/\n', stderr: '' }))
@@ -46,9 +49,18 @@ describe('native directory picker', () => {
await expect(pickNativeDirectory(signal(), { platform: 'darwin', run })).rejects.toBe(reason)
})
it('prefers pwsh for the Windows folder dialog and maps empty output to cancellation', async () => {
it('prefers the in-process Win32 dialog and never spawns PowerShell when it answers', async () => {
const run = vi.fn<DirectoryPickerRunner>()
const pickWin32Dialog = vi.fn(async (): Promise<string | null> => 'C:\\work\\selected')
await expect(pickNativeDirectory(signal(), { platform: 'win32', run, pickWin32Dialog })).resolves.toBe('C:\\work\\selected')
pickWin32Dialog.mockResolvedValueOnce(null)
await expect(pickNativeDirectory(signal(), { platform: 'win32', run, pickWin32Dialog })).resolves.toBeNull()
expect(run).not.toHaveBeenCalled()
})
it('falls back to pwsh when the dialog is unavailable and maps empty output to cancellation', async () => {
const run = vi.fn<DirectoryPickerRunner>(async () => ({ stdout: 'C:\\work\\project\r\n', stderr: '' }))
await expect(pickNativeDirectory(signal(), { platform: 'win32', run })).resolves.toBe('C:\\work\\project')
await expect(pickNativeDirectory(signal(), { platform: 'win32', run, pickWin32Dialog: noDialog })).resolves.toBe('C:\\work\\project')
expect(run).toHaveBeenCalledWith(
'pwsh.exe',
expect.arrayContaining(['-NoProfile', '-STA', '-Command']),
@@ -60,48 +72,70 @@ describe('native directory picker', () => {
// Description renders as a bottom strip (modern) / unthemed box (classic); never set it.
expect(script).not.toContain('Description')
run.mockResolvedValueOnce({ stdout: '', stderr: '' })
await expect(pickNativeDirectory(signal(), { platform: 'win32', run })).resolves.toBeNull()
run.mockRejectedValueOnce(failure(1, 'Add-Type failed'))
await expect(pickNativeDirectory(signal(), { platform: 'win32', run })).rejects.toThrow('command failed')
await expect(pickNativeDirectory(signal(), { platform: 'win32', run, pickWin32Dialog: noDialog })).resolves.toBeNull()
})
it('falls back to Windows PowerShell 5.1 only when pwsh is missing', async () => {
it('falls back to Windows PowerShell 5.1 whenever pwsh cannot deliver the dialog', async () => {
const run = vi.fn<DirectoryPickerRunner>()
.mockRejectedValueOnce(failure('ENOENT'))
.mockResolvedValueOnce({ stdout: 'C:\\work\\fallback\r\n', stderr: '' })
await expect(pickNativeDirectory(signal(), { platform: 'win32', run })).resolves.toBe('C:\\work\\fallback')
await expect(pickNativeDirectory(signal(), { platform: 'win32', run, pickWin32Dialog: noDialog })).resolves.toBe('C:\\work\\fallback')
expect(run.mock.calls.map(call => call[0])).toEqual(['pwsh.exe', 'powershell.exe'])
// Both runtimes execute the identical script, so DPI awareness holds either way.
expect(run.mock.calls[0]?.[1].at(-1)).toBe(run.mock.calls[1]?.[1].at(-1))
// A resolvable pwsh that cannot deliver the dialog (PowerShell 6: no
// WinForms, Add-Type exits 1 - not ENOENT) reaches 5.1 all the same.
const pwsh6 = vi.fn<DirectoryPickerRunner>()
.mockRejectedValueOnce(failure(1, "Cannot load assembly 'System.Windows.Forms'"))
.mockResolvedValueOnce({ stdout: 'C:\\work\\legacy\r\n', stderr: '' })
await expect(pickNativeDirectory(signal(), { platform: 'win32', run: pwsh6, pickWin32Dialog: noDialog })).resolves.toBe('C:\\work\\legacy')
expect(pwsh6.mock.calls.map(call => call[0])).toEqual(['pwsh.exe', 'powershell.exe'])
const cancelled = vi.fn<DirectoryPickerRunner>()
.mockRejectedValueOnce(failure('ENOENT'))
.mockResolvedValueOnce({ stdout: '', stderr: '' })
await expect(pickNativeDirectory(signal(), { platform: 'win32', run: cancelled })).resolves.toBeNull()
await expect(pickNativeDirectory(signal(), { platform: 'win32', run: cancelled, pickWin32Dialog: noDialog })).resolves.toBeNull()
const failed = vi.fn<DirectoryPickerRunner>()
.mockRejectedValueOnce(failure('ENOENT'))
.mockRejectedValueOnce(failure(2))
await expect(pickNativeDirectory(signal(), { platform: 'win32', run: failed })).rejects.toThrow('command failed')
const brokenPwsh = vi.fn<DirectoryPickerRunner>(async () => { throw failure(7) })
await expect(pickNativeDirectory(signal(), { platform: 'win32', run: brokenPwsh })).rejects.toThrow('command failed')
expect(brokenPwsh).toHaveBeenCalledOnce()
await expect(pickNativeDirectory(signal(), { platform: 'win32', run: failed, pickWin32Dialog: noDialog })).rejects.toThrow('command failed')
})
it('does not fall back when the caller aborted the pwsh spawn', async () => {
it('wires the real Win32 dialog as the default tier', async () => {
// A pre-aborted signal makes the DEFAULT dialog deterministic on every
// host: pickWin32Directory throws before spawning any worker or window.
const abort = new AbortController()
abort.abort()
const run = vi.fn<DirectoryPickerRunner>()
await expect(pickNativeDirectory(abort.signal, { platform: 'win32', run }))
.rejects.toThrow('native directory picker aborted')
expect(run).not.toHaveBeenCalled()
})
it('does not fall back when the caller aborted the dialog or the pwsh spawn', async () => {
const abort = new AbortController()
abort.abort(new Error('closed'))
const run = vi.fn<DirectoryPickerRunner>(async () => { throw failure('ENOENT') })
await expect(pickNativeDirectory(abort.signal, { platform: 'win32', run })).rejects.toThrow('command failed')
expect(run).toHaveBeenCalledOnce()
const run = vi.fn<DirectoryPickerRunner>()
await expect(pickNativeDirectory(abort.signal, { platform: 'win32', run, pickWin32Dialog: noDialog })).rejects.toThrow('dialog unavailable')
expect(run).not.toHaveBeenCalled()
const liveThenAborted = new AbortController()
const abortingRun = vi.fn<DirectoryPickerRunner>(async () => {
liveThenAborted.abort(new Error('closed'))
throw failure('ENOENT')
})
await expect(pickNativeDirectory(liveThenAborted.signal, { platform: 'win32', run: abortingRun, pickWin32Dialog: noDialog }))
.rejects.toThrow('command failed')
expect(abortingRun).toHaveBeenCalledOnce()
})
it('runs the default command adapter without a shell and preserves command failures', async () => {
execFileMock.mockImplementationOnce((_command, _args, _options, callback) => {
callback(null, 'C:\\work\\default\r\n', '')
})
await expect(pickNativeDirectory(signal(), { platform: 'win32' })).resolves.toBe('C:\\work\\default')
await expect(pickNativeDirectory(signal(), { platform: 'win32', pickWin32Dialog: noDialog })).resolves.toBe('C:\\work\\default')
const [command, args, options] = execFileMock.mock.calls[0]!
expect(command).toBe('pwsh.exe')
expect(args).toEqual(expect.arrayContaining(['-NoProfile', '-STA', '-Command']))
@@ -109,19 +143,30 @@ describe('native directory picker', () => {
expect(options.windowsHide).toBe(true)
expect(options.signal).toBeInstanceOf(AbortSignal)
// Both chain tiers fail: pwsh's code-7 failure now reaches 5.1, whose
// failure is the one the caller sees.
const pwshError = Object.assign(new Error('pwsh failed'), { code: 7 })
const commandError = Object.assign(new Error('powershell failed'), { code: 7 })
execFileMock.mockImplementationOnce((_command, _args, _options, callback) => {
callback(pwshError, '', 'no WinForms')
})
execFileMock.mockImplementationOnce((_command, _args, _options, callback) => {
callback(commandError, 'partial output', 'failure details')
})
await expect(pickNativeDirectory(signal(), { platform: 'win32' })).rejects.toMatchObject({
await expect(pickNativeDirectory(signal(), { platform: 'win32', pickWin32Dialog: noDialog })).rejects.toMatchObject({
message: 'powershell failed', cause: commandError, code: 7,
stdout: 'partial output', stderr: 'failure details',
})
expect(execFileMock.mock.calls.map(call => call[0])).toEqual(['pwsh.exe', 'pwsh.exe', 'powershell.exe'])
})
it('uses the current process platform when no platform override is supplied', async () => {
// Deterministic on every host: the win32 tier answers from the dialog,
// the POSIX tiers from the command runner.
const run = vi.fn<DirectoryPickerRunner>(async () => ({ stdout: '/default/platform\n', stderr: '' }))
await expect(pickNativeDirectory(signal(), { run })).resolves.toBe('/default/platform')
const pickWin32Dialog = async (): Promise<string | null> => 'C:\\default\\platform'
const expected = process.platform === 'win32' ? 'C:\\default\\platform' : '/default/platform'
await expect(pickNativeDirectory(signal(), { run, pickWin32Dialog })).resolves.toBe(expected)
})
it('uses Zenity on Linux and falls back to KDialog only when Zenity is missing', async () => {