fix(host): keep path entry available when the home listing fails

With no listed level (an unreadable or missing home directory), the
path-edit zone previously disabled forever, stranding the operator on the
alert with only Cancel; it now opens with an empty draft so an absolute
path remains the way forward. Covered by a recovery test.
This commit is contained in:
creatixchu
2026-07-29 00:04:38 +08:00
parent af049cceaf
commit d2b16380d1
2 changed files with 20 additions and 3 deletions
@@ -282,9 +282,11 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
type="button"
className={css.crumbEditZone}
aria-label={t('browser.editPath')}
disabled={parent === null || parentInert}
/* v8 ignore next -- narrowing guard: the zone disables while the level is null. */
onClick={() => { if (parent !== null) setPathDraft(selected?.path ?? parent.path) }}
// Stays available with no listed level: when the home
// listing itself fails, typing an absolute path is the one
// remaining way forward.
disabled={parentInert}
onClick={() => { setPathDraft(selected?.path ?? parent?.path ?? '') }}
/>
</>
)
@@ -269,6 +269,21 @@ describe('DirectoryBrowser', () => {
expect(screen.getByRole<HTMLButtonElement>('button', { name: 'browser.newFolder' }).disabled).toBe(false)
})
it('keeps path entry available when the home listing fails', async () => {
const listDirectory = vi.fn(async (): Promise<DirectoryListing> => {
throw new DirectoryBrowseError({ code: 'directory-unreadable', message: 'home unreadable', details: { path: HOME } })
})
mount({ listDirectory })
await waitFor(() => { expect(screen.getByRole('alert').textContent).toBe('home unreadable') })
// With no listed level, typing an absolute path is the one way forward.
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
const input = screen.getByLabelText('browser.editPath')
fireEvent.change(input, { target: { value: DOCS } })
listDirectory.mockImplementation(async (path?: string) => listingFor(path))
fireEvent.keyDown(input, { key: 'Enter' })
await waitFor(() => { expect(screen.getByText('harness')).toBeTruthy() })
})
it('ignores dismissal while adoption is busy', async () => {
const b = mount({ busy: true })
await waitFor(() => { expect(screen.getByRole('dialog')).toBeTruthy() })