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 () => {

View File

@@ -0,0 +1,284 @@
/**
* The koffi-backed bindings against a mocked `koffi` module (the same
* technique as dsh-session-persistence-jsonl's win32 suite): a small in-memory
* COM world stands in for ole32/user32/kernel32, keeping the vtable dispatch,
* result extraction, memory hygiene, and the WM_CLOSE poster covered on every
* host. The worker entry is exercised the same way with a mocked
* `node:worker_threads`. Real-COM behavior is pinned by the win32-only smoke
* in win32-dialog.spec.ts.
*/
import { afterEach, describe, expect, it, vi } from 'vitest'
import { HRESULT_CANCELLED, runFolderDialog } from '../src/win32-dialog-logic.ts'
const E_FAIL = 0x80004005 | 0
const WM_CLOSE = 0x10
interface ComWorld {
coInitHr: number
coCreateHr: number
showHr: number
getResultHr: number
getDisplayNameHr: number
hasThreadDpi: boolean
enumThrows: boolean
path: string
titles: string[]
options: number[]
dpiContexts: unknown[]
freed: unknown[]
released: string[]
posted: { hwnd: unknown; message: number }[]
registered: number
unregistered: number
}
function comWorld(overrides: Partial<ComWorld> = {}): ComWorld {
return {
coInitHr: 0, coCreateHr: 0, showHr: 0, getResultHr: 0, getDisplayNameHr: 0,
hasThreadDpi: true, enumThrows: false,
path: 'C:\\选中\\directory',
titles: [], options: [], dpiContexts: [], freed: [], released: [], posted: [],
registered: 0, unregistered: 0,
...overrides,
}
}
/** Sentinel pointer objects standing in for native addresses. */
interface FakePtr { kind: string; [key: string]: unknown }
function installFakeKoffi(world: ComWorld): void {
const dialogPtr: FakePtr = { kind: 'dialog' }
const itemPtr: FakePtr = { kind: 'item' }
const namePtr: FakePtr = { kind: 'name', text: world.path }
const outBuffers = new Map<unknown, FakePtr>()
const dispatch = (self: FakePtr, slot: number, args: unknown[]): number => {
if (self.kind === 'dialog') {
switch (slot) {
case 9: world.options.push(args[0] as number); return 0
case 17: world.titles.push(args[0] as string); return 0
case 3: return world.showHr
case 20: {
if (world.getResultHr < 0) return world.getResultHr
;(args[0] as unknown[])[0] = itemPtr
return 0
}
case 2: world.released.push('dialog'); return 0
default: throw new Error(`unexpected dialog slot ${slot}`)
}
}
switch (slot) {
case 5: {
if (world.getDisplayNameHr < 0) return world.getDisplayNameHr
;(args[1] as unknown[])[0] = namePtr
return 0
}
case 2: world.released.push('item'); return 0
default: throw new Error(`unexpected item slot ${slot}`)
}
}
vi.doMock('koffi', () => ({
default: {
load: (dll: string) => ({
func: (_convention: string, name: string, _result: string, _args: string[]) => {
switch (name) {
case 'CoInitializeEx': return () => world.coInitHr
case 'CoCreateInstance': return (...args: unknown[]) => {
if (world.coCreateHr < 0) return world.coCreateHr
outBuffers.set(args[4], dialogPtr)
return 0
}
case 'CoTaskMemFree': return (ptr: unknown) => { world.freed.push(ptr) }
case 'GetCurrentThreadId': return () => 31337
case 'SetThreadDpiAwarenessContext': {
if (!world.hasThreadDpi) throw new Error(`${dll}: SetThreadDpiAwarenessContext not found`)
return (context: unknown) => { world.dpiContexts.push(context); return null }
}
case 'EnumThreadWindows': return (_tid: unknown, callback: { fn: (hwnd: unknown, lparam: unknown) => number }, lparam: unknown) => {
if (world.enumThrows) throw new Error('EnumThreadWindows refused')
callback.fn({ kind: 'hwnd', n: 1 }, lparam)
callback.fn({ kind: 'hwnd', n: 2 }, lparam)
return 1
}
case 'PostMessageW': return (hwnd: unknown, message: number) => { world.posted.push({ hwnd, message }); return 1 }
default: throw new Error(`unexpected native import ${dll}/${name}`)
}
},
}),
proto: (declaration: string) => ({ declaration }),
pointer: (type: unknown) => type,
register: (fn: (hwnd: unknown, lparam: unknown) => number) => { world.registered += 1; return { fn } },
unregister: () => { world.unregistered += 1 },
decode: (value: unknown, offsetOrType: unknown): unknown => {
if (offsetOrType === 'str16') return (value as FakePtr).text
if (typeof offsetOrType === 'number') {
// Vtable slot read: hand back a callable-reference sentinel.
const owner = (value as { owner: FakePtr }).owner
return { call: (args: unknown[]) => dispatch(owner, offsetOrType / 8, args) }
}
// decode(x, 'void *'): out-buffer read or vtable read.
if (outBuffers.has(value)) return outBuffers.get(value)
return { owner: value as FakePtr }
},
call: (fn: { call: (args: unknown[]) => number }, _proto: unknown, _self: unknown, ...args: unknown[]) => fn.call(args),
},
}))
}
async function loadBindingsModule(): Promise<typeof import('../src/win32-dialog-bindings.ts')> {
return await import('../src/win32-dialog-bindings.ts')
}
afterEach(() => {
vi.doUnmock('koffi')
vi.doUnmock('node:worker_threads')
vi.doUnmock('../src/win32-dialog-bindings.ts')
vi.resetModules()
})
describe('loadWin32DialogBindings over the fake COM world', () => {
it('drives the full selection conversation with memory hygiene', async () => {
const world = comWorld()
installFakeKoffi(world)
const { loadWin32DialogBindings } = await loadBindingsModule()
const bindings = await loadWin32DialogBindings()
const showing = vi.fn()
expect(runFolderDialog(bindings, '选择工作区目录', showing)).toBe('C:\\选中\\directory')
expect(world.dpiContexts).toEqual([-4])
expect(world.titles).toEqual(['选择工作区目录'])
expect(world.options).toHaveLength(1)
expect(showing).toHaveBeenCalledWith(31337)
expect(world.freed).toHaveLength(1)
expect(world.released).toEqual(['item', 'dialog'])
})
it('maps dismissal, missing DPI support, and the S_FALSE CoInitializeEx', async () => {
const world = comWorld({ showHr: HRESULT_CANCELLED, hasThreadDpi: false, coInitHr: 1 })
installFakeKoffi(world)
const { loadWin32DialogBindings } = await loadBindingsModule()
const bindings = await loadWin32DialogBindings()
expect(runFolderDialog(bindings, 'Pick', vi.fn())).toBeNull()
expect(world.dpiContexts).toEqual([])
expect(world.released).toEqual(['dialog'])
})
it('surfaces creation and extraction failures as HRESULT errors', async () => {
const creationWorld = comWorld({ coCreateHr: E_FAIL })
installFakeKoffi(creationWorld)
let bindings = await (await loadBindingsModule()).loadWin32DialogBindings()
expect(() => bindings.createFolderDialog()).toThrow('CoCreateInstance(FileOpenDialog) failed: HRESULT 0x80004005')
vi.doUnmock('koffi')
vi.resetModules()
const resultWorld = comWorld({ getResultHr: E_FAIL })
installFakeKoffi(resultWorld)
bindings = await (await loadBindingsModule()).loadWin32DialogBindings()
expect(() => runFolderDialog(bindings, 'Pick', vi.fn())).toThrow('GetResult failed')
expect(resultWorld.released).toEqual(['dialog'])
vi.doUnmock('koffi')
vi.resetModules()
const nameWorld = comWorld({ getDisplayNameHr: E_FAIL })
installFakeKoffi(nameWorld)
bindings = await (await loadBindingsModule()).loadWin32DialogBindings()
expect(() => runFolderDialog(bindings, 'Pick', vi.fn())).toThrow('GetResult failed')
// The shell item is released even when its display name cannot be read.
expect(nameWorld.released).toEqual(['item', 'dialog'])
expect(nameWorld.freed).toHaveLength(0)
})
})
describe('closeThreadWindows over the fake COM world', () => {
it('posts WM_CLOSE to every window of the thread and unregisters the callback', async () => {
const world = comWorld()
installFakeKoffi(world)
const { closeThreadWindows } = await loadBindingsModule()
await closeThreadWindows(777)
expect(world.posted).toEqual([
{ hwnd: { kind: 'hwnd', n: 1 }, message: WM_CLOSE },
{ hwnd: { kind: 'hwnd', n: 2 }, message: WM_CLOSE },
])
expect(world.registered).toBe(1)
expect(world.unregistered).toBe(1)
})
it('unregisters the callback even when the enumeration itself throws', async () => {
const world = comWorld({ enumThrows: true })
installFakeKoffi(world)
const { closeThreadWindows } = await loadBindingsModule()
await expect(closeThreadWindows(777)).rejects.toThrow('EnumThreadWindows refused')
expect(world.unregistered).toBe(1)
})
})
describe('the worker entry over a mocked thread boundary', () => {
it('posts showing then done for a completed conversation', async () => {
const posted: unknown[] = []
vi.doMock('node:worker_threads', () => ({
parentPort: { postMessage: (message: unknown) => posted.push(message) },
workerData: { title: 'Pick' },
}))
vi.doMock('../src/win32-dialog-bindings.ts', () => ({
loadWin32DialogBindings: async () => ({
setThreadDpiAwareness: () => undefined,
coInitializeSta: () => 0,
currentThreadId: () => 11,
createFolderDialog: () => ({
setOptions: () => 0,
setTitle: () => 0,
show: () => 0,
resultPath: () => ({ hr: 0, path: 'C:\\from-worker' }),
release: () => undefined,
}),
}),
}))
await import('../src/win32-dialog-worker.ts')
expect(posted).toEqual([
{ kind: 'showing', threadId: 11 },
{ kind: 'done', path: 'C:\\from-worker' },
])
})
it('posts the failure message when the native surface cannot load', async () => {
const posted: { kind: string; message?: string }[] = []
vi.doMock('node:worker_threads', () => ({
parentPort: { postMessage: (message: { kind: string }) => posted.push(message) },
workerData: { title: 'Pick' },
}))
vi.doMock('../src/win32-dialog-bindings.ts', () => ({
loadWin32DialogBindings: async () => { throw new Error('no ole32 here') },
}))
await import('../src/win32-dialog-worker.ts')
expect(posted).toHaveLength(1)
expect(posted[0]?.kind).toBe('error')
expect(posted[0]?.message).toContain('no ole32 here')
})
it('stringifies stackless and non-Error failures', async () => {
const stackless = new Error('bare message')
delete stackless.stack
for (const [thrown, expected] of [[stackless, 'bare message'], ['plain refusal', 'plain refusal']] as const) {
vi.doUnmock('node:worker_threads')
vi.doUnmock('../src/win32-dialog-bindings.ts')
vi.resetModules()
const posted: { kind: string; message?: string }[] = []
vi.doMock('node:worker_threads', () => ({
parentPort: { postMessage: (message: { kind: string }) => posted.push(message) },
workerData: { title: 'Pick' },
}))
vi.doMock('../src/win32-dialog-bindings.ts', () => ({
loadWin32DialogBindings: async () => { throw thrown },
}))
await import('../src/win32-dialog-worker.ts')
expect(posted[0]?.message).toBe(expected)
}
})
it('refuses to run outside a worker thread', async () => {
vi.doMock('node:worker_threads', () => ({ parentPort: null, workerData: undefined }))
await expect(import('../src/win32-dialog-worker.ts')).rejects.toThrow('must run as a worker thread')
})
})

View File

@@ -0,0 +1,90 @@
/**
* The COM conversation's sequencing against fake bindings: outcome mapping
* (selection / cancellation / HRESULT failures at every step) and the
* release-on-every-path guarantee, all platform-independent.
*/
import { describe, expect, it, vi } from 'vitest'
import {
FOS_FORCEFILESYSTEM, FOS_NOCHANGEDIR, FOS_PICKFOLDERS, HRESULT_CANCELLED,
runFolderDialog, type Win32DialogBindings, type Win32FolderDialog,
} from '../src/win32-dialog-logic.ts'
const E_FAIL = 0x80004005 | 0
interface FakeWorld {
bindings: Win32DialogBindings
dpi: ReturnType<typeof vi.fn>
createDialog: ReturnType<typeof vi.fn>
dialog: {
setOptions: ReturnType<typeof vi.fn>
setTitle: ReturnType<typeof vi.fn>
show: ReturnType<typeof vi.fn>
resultPath: ReturnType<typeof vi.fn>
release: ReturnType<typeof vi.fn>
}
}
function world(overrides: Partial<Win32FolderDialog> = {}, coInit = 0): FakeWorld {
const dialog = {
setOptions: vi.fn(() => 0),
setTitle: vi.fn(() => 0),
show: vi.fn(() => 0),
resultPath: vi.fn(() => ({ hr: 0, path: 'C:\\picked\\目录' })),
release: vi.fn(),
...overrides,
}
const dpi = vi.fn()
const createDialog = vi.fn(() => dialog)
const bindings: Win32DialogBindings = {
setThreadDpiAwareness: dpi,
coInitializeSta: vi.fn(() => coInit),
createFolderDialog: createDialog,
currentThreadId: vi.fn(() => 4242),
}
return { bindings, dpi, createDialog, dialog: dialog as FakeWorld['dialog'] }
}
describe('runFolderDialog', () => {
it('sequences DPI, STA, options, title, show, and result extraction', () => {
const { bindings, dpi, dialog } = world()
const showing = vi.fn()
expect(runFolderDialog(bindings, 'Pick', showing)).toBe('C:\\picked\\目录')
expect(dpi).toHaveBeenCalledOnce()
expect(dialog.setOptions).toHaveBeenCalledWith(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_NOCHANGEDIR)
expect(dialog.setTitle).toHaveBeenCalledWith('Pick')
expect(showing).toHaveBeenCalledWith(4242)
expect(showing.mock.invocationCallOrder[0]).toBeLessThan(dialog.show.mock.invocationCallOrder[0] as number)
expect(dialog.release).toHaveBeenCalledOnce()
})
it('maps the cancelled HRESULT to null and still releases the dialog', () => {
const { bindings, dialog } = world({ show: vi.fn(() => HRESULT_CANCELLED) })
expect(runFolderDialog(bindings, 'Pick', vi.fn())).toBeNull()
expect(dialog.resultPath).not.toHaveBeenCalled()
expect(dialog.release).toHaveBeenCalledOnce()
})
it('accepts the S_FALSE re-entry HRESULT from CoInitializeEx', () => {
const { bindings } = world({}, 1)
expect(runFolderDialog(bindings, 'Pick', vi.fn())).toBe('C:\\picked\\目录')
})
it('throws on a failing CoInitializeEx without creating a dialog', () => {
const { bindings, createDialog } = world({}, E_FAIL)
expect(() => runFolderDialog(bindings, 'Pick', vi.fn())).toThrow('CoInitializeEx failed: HRESULT 0x80004005')
expect(createDialog).not.toHaveBeenCalled()
})
it.each([
['SetOptions', { setOptions: vi.fn(() => E_FAIL) }],
['SetTitle', { setTitle: vi.fn(() => E_FAIL) }],
['Show', { show: vi.fn(() => E_FAIL) }],
['GetResult', { resultPath: vi.fn(() => ({ hr: E_FAIL })) }],
] satisfies [string, Partial<Win32FolderDialog>][])('releases the dialog when %s fails', (what, overrides) => {
const { bindings, dialog } = world(overrides)
expect(() => runFolderDialog(bindings, 'Pick', vi.fn())).toThrow(`${what} failed: HRESULT 0x80004005`)
expect(dialog.release).toHaveBeenCalledOnce()
void bindings
})
})

View File

@@ -0,0 +1,136 @@
/**
* Driver tests: the worker message protocol mapped onto the promise, the
* WM_CLOSE abort service (including the show-race retry and the terminate
* last resort) against fakes, plus the real spawn plumbing — POSIX hosts
* prove the default path rejects cleanly (koffi cannot load ole32 there),
* and win32 hosts briefly open and auto-abort a real dialog.
*/
import { EventEmitter } from 'node:events'
import { describe, expect, it, vi } from 'vitest'
import { pickWin32Directory, type Win32DialogInternals, type Win32DialogWorkerLike } from '../src/win32-dialog.ts'
import type { Win32DialogWorkerMessage } from '../src/win32-dialog-worker.ts'
class FakeWorker extends EventEmitter implements Win32DialogWorkerLike {
terminate = vi.fn(async () => 0)
post(message: Win32DialogWorkerMessage): void {
this.emit('message', message)
}
}
interface Harness {
worker: FakeWorker
internals: Win32DialogInternals
close: ReturnType<typeof vi.fn>
}
function harness(overrides: Partial<Win32DialogInternals> = {}): Harness {
const worker = new FakeWorker()
const close = vi.fn(async () => undefined)
return {
worker,
close,
internals: { spawnWorker: () => worker, closeThreadWindows: close, closeRetryMs: 1, ...overrides },
}
}
const live = (): AbortSignal => new AbortController().signal
describe('pickWin32Directory', () => {
it('resolves the selected path and the cancellation null', async () => {
const first = harness()
const picked = pickWin32Directory(live(), first.internals)
first.worker.post({ kind: 'showing', threadId: 7 })
first.worker.post({ kind: 'done', path: 'C:\\picked' })
await expect(picked).resolves.toBe('C:\\picked')
expect(first.close).not.toHaveBeenCalled()
const second = harness()
const cancelled = pickWin32Directory(live(), second.internals)
second.worker.post({ kind: 'done', path: null })
await expect(cancelled).resolves.toBeNull()
})
it('rejects on a reported dialog failure, a worker crash, and a silent exit', async () => {
const reported = harness()
const failing = pickWin32Directory(live(), reported.internals)
reported.worker.post({ kind: 'error', message: 'CoCreateInstance failed' })
await expect(failing).rejects.toThrow('win32 folder dialog failed: CoCreateInstance failed')
const crashed = harness()
const crashing = pickWin32Directory(live(), crashed.internals)
crashed.worker.emit('error', new Error('worker blew up'))
await expect(crashing).rejects.toThrow('worker blew up')
const silent = harness()
const exiting = pickWin32Directory(live(), silent.internals)
silent.worker.emit('exit', 0)
await expect(exiting).rejects.toThrow('exited before reporting a result')
})
it('settles once: a late exit after the result is inert', async () => {
const { worker, internals } = harness()
const picked = pickWin32Directory(live(), internals)
worker.post({ kind: 'done', path: 'C:\\once' })
worker.emit('exit', 0)
await expect(picked).resolves.toBe('C:\\once')
})
it('throws immediately on an already-aborted signal without spawning', async () => {
const spawnWorker = vi.fn()
const controller = new AbortController()
controller.abort()
await expect(pickWin32Directory(controller.signal, { spawnWorker, closeThreadWindows: async () => undefined }))
.rejects.toThrow('native directory picker aborted')
expect(spawnWorker).not.toHaveBeenCalled()
})
it('services an abort by closing the dialog thread windows until the worker reports', async () => {
const { worker, internals, close } = harness()
const controller = new AbortController()
const picked = pickWin32Directory(controller.signal, internals)
worker.post({ kind: 'showing', threadId: 99 })
controller.abort()
await vi.waitFor(() =>{ expect(close).toHaveBeenCalledWith(99) })
worker.post({ kind: 'done', path: null })
await expect(picked).rejects.toThrow('native directory picker aborted')
})
it('starts the close service on the showing notice when the abort came first', async () => {
const closeFailures = vi.fn(async () => { throw new Error('window not there yet') })
const { worker, internals } = harness({ closeThreadWindows: closeFailures })
const controller = new AbortController()
const picked = pickWin32Directory(controller.signal, internals)
controller.abort()
expect(closeFailures).not.toHaveBeenCalled()
worker.post({ kind: 'showing', threadId: 12 })
await vi.waitFor(() =>{ expect(closeFailures.mock.calls.length).toBeGreaterThan(1) })
worker.post({ kind: 'done', path: null })
await expect(picked).rejects.toThrow('native directory picker aborted')
})
it('terminates an unresponsive worker after the close budget', async () => {
const { worker, internals, close } = harness()
const controller = new AbortController()
const picked = pickWin32Directory(controller.signal, internals)
worker.post({ kind: 'showing', threadId: 5 })
controller.abort()
await expect(picked).rejects.toThrow('dialog unresponsive; worker terminated')
expect(worker.terminate).toHaveBeenCalledOnce()
expect(close.mock.calls.length).toBeGreaterThan(10)
})
// POSIX hosts exercise the REAL default plumbing end to end: the tsx-bootstrapped
// worker spawns, loads koffi, fails to load ole32.dll, and reports the error.
it.skipIf(process.platform === 'win32')('rejects through the real worker where the Win32 surface is unavailable', async () => {
await expect(pickWin32Directory(live())).rejects.toThrow('win32 folder dialog failed')
}, 30_000)
// win32 hosts run the true COM smoke instead: a real dialog opens briefly
// and the abort service closes it (the same lever a disconnecting client pulls).
it.skipIf(process.platform !== 'win32')('opens and abort-closes a real dialog', async () => {
const controller = new AbortController()
setTimeout(() =>{ controller.abort() }, 400)
await expect(pickWin32Directory(controller.signal)).rejects.toThrow('native directory picker aborted')
}, 30_000)
})