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:
@@ -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()
|
||||
|
||||
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user