Files
deepseek-harness/packages/host/directory-picker-native/tests/win32-dialog-bindings.spec.ts
Huanqi Cao 089f4dfad8 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).
2026-08-05 00:31:43 +08:00

285 lines
11 KiB
TypeScript

/**
* 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')
})
})