fix(host,client): require fully qualified browse paths; clear the picker kind on close

ds-review-bot round 4. On Windows, isAbsolute admits rooted drive-less
forms (\foo, /foo) that resolve() then rebases onto the process's current
drive; both browse primitives now gate on a fullyQualified check (drive
letter or UNC on win32, POSIX-absolute elsewhere) with a platform test
seam, per-platform unit cases, and the contract wording updated on the
seam, the backend README pair, and the error messages.

The picker-kind effect also kept a resolved 'dialog' across close, so a
backend swapped while the menu was closed could paint the stale entry for
one frame on reopen; the close arm now clears the state, pinned by a
reopen-under-pending-read race test.
This commit is contained in:
creatixchu
2026-07-28 18:13:20 +08:00
parent c4bf919895
commit b211a80b1f
9 changed files with 74 additions and 22 deletions

View File

@@ -79,11 +79,17 @@ export function WorkspaceCreateFlow({
// flow open — no cache to go stale across reconnects.
const [dialogPicker, setDialogPicker] = useState(false)
useEffect(() => {
if (!open) return
// Reset before each read: a reconnect can change the composed backend, so
// a previous open's answer must not leak into this one; and a settlement
// from a superseded open (flow closed, or a newer read started) is
// discarded via the cleanup-toggled flag.
if (!open) {
// Close discards the answer: a reconnect or HMR can swap the composed
// backend while the menu is closed, and the reopened menu must never
// paint the previous host's entry before the fresh read lands.
setDialogPicker(false)
return
}
// Reset before each read: the injected reader can also change identity
// while the flow stays open, and that prior answer must not leak either;
// a settlement from a superseded read is discarded via the
// cleanup-toggled flag.
setDialogPicker(false)
let stale = false
void directoryPickerKind()

View File

@@ -300,6 +300,20 @@ describe('WorkspacePicker', () => {
expect(screen.queryByRole('menuitem', { name: 'Open local folder…' })).toBeNull()
})
it('clears the advertised kind on close so a reopen cannot paint the previous host entry', async () => {
const directoryPickerKind = vi.fn<() => Promise<string>>()
.mockImplementationOnce(async () => 'dialog')
// The reopened read never settles: the assertion below sees the paint
// that precedes any fresh answer.
.mockImplementation(() => new Promise<string>(() => {}))
const t = togglable(directoryPickerKind)
await screen.findByRole('menuitem', { name: 'Open local folder…' })
t.setOpen(false)
t.setOpen(true)
await screen.findByRole('menuitem', { name: 'Create a new workspace' })
expect(screen.queryByRole('menuitem', { name: 'Open local folder…' })).toBeNull()
})
it('discards a stale describe failure after a newer open already answered', async () => {
let rejectFirst!: (reason: Error) => void
const first = new Promise<string>((_settle, reject) => { rejectFirst = reject })