mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
Merge pull request #71709 from NousResearch/bb/first-run-local-start-error
fix(desktop): keep the first-run local-start error from being wiped on mount
This commit is contained in:
commit
8a71feb84c
2 changed files with 94 additions and 11 deletions
|
|
@ -52,6 +52,33 @@ function installDesktopMock(state: DesktopBootstrapState) {
|
|||
return desktop
|
||||
}
|
||||
|
||||
// Resolve the instant a node commits, via MutationObserver rather than
|
||||
// waitFor's polling timer. findBy* only settles on a timer tick, by which
|
||||
// point React has already drained its passive effects — that hides any bug
|
||||
// living in the window between paint and effect.
|
||||
function whenPresent(text: string): Promise<HTMLElement> {
|
||||
return new Promise(resolve => {
|
||||
const existing = screen.queryByText(text)
|
||||
|
||||
if (existing) {
|
||||
resolve(existing)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const node = screen.queryByText(text)
|
||||
|
||||
if (node) {
|
||||
observer.disconnect()
|
||||
resolve(node)
|
||||
}
|
||||
})
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true, characterData: true })
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
|
@ -120,6 +147,56 @@ describe('DesktopInstallOverlay first-run setup', () => {
|
|||
expect(install.disabled).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps the local-start error when the first snapshot commits under the click', async () => {
|
||||
const desktop = installDesktopMock(
|
||||
bootstrapState({
|
||||
setupChoice: { platform: 'win32', activeRoot: 'C:\\Users\\me\\AppData\\Local\\hermes\\hermes-agent' }
|
||||
})
|
||||
)
|
||||
|
||||
desktop.continueBootstrapLocal = undefined as never
|
||||
render(<DesktopInstallOverlay />)
|
||||
|
||||
// Click the instant the choice paints, before React drains the passive
|
||||
// effect that reacts to the first snapshot. A loaded runner hits this
|
||||
// window by accident; observing the DOM directly hits it every time.
|
||||
const install = (await whenPresent('Install Hermes locally')).closest('button') as HTMLButtonElement
|
||||
fireEvent.click(install)
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
expect(screen.queryByText('Local installation could not start. Restart Hermes Desktop and try again.')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('clears a stale local-start error when a repair presents a different root', async () => {
|
||||
const desktop = installDesktopMock(
|
||||
bootstrapState({
|
||||
setupChoice: { platform: 'win32', activeRoot: 'C:\\Users\\me\\AppData\\Local\\hermes\\hermes-agent' }
|
||||
})
|
||||
)
|
||||
|
||||
desktop.continueBootstrapLocal = undefined as never
|
||||
render(<DesktopInstallOverlay />)
|
||||
|
||||
fireEvent.click((await screen.findByText('Install Hermes locally')).closest('button') as HTMLButtonElement)
|
||||
expect(
|
||||
await screen.findByText('Local installation could not start. Restart Hermes Desktop and try again.')
|
||||
).toBeTruthy()
|
||||
|
||||
act(() => {
|
||||
desktop.emitBootstrapEvent({
|
||||
type: 'setup-choice',
|
||||
active: false,
|
||||
platform: 'win32',
|
||||
activeRoot: 'C:\\Users\\me\\AppData\\Local\\hermes\\hermes-agent-repaired'
|
||||
})
|
||||
})
|
||||
|
||||
expect(screen.queryByText('Local installation could not start. Restart Hermes Desktop and try again.')).toBeNull()
|
||||
})
|
||||
|
||||
it('opens the remote connection form from the first-run choice', async () => {
|
||||
installDesktopMock(
|
||||
bootstrapState({
|
||||
|
|
|
|||
|
|
@ -277,8 +277,6 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
|
|||
const [copied, setCopied] = useState(false)
|
||||
const [cancelling, setCancelling] = useState(false)
|
||||
const [remoteOpen, setRemoteOpen] = useState(false)
|
||||
const [localStarting, setLocalStarting] = useState(false)
|
||||
const [localStartError, setLocalStartError] = useState<string | null>(null)
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
const logEndRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
|
|
@ -347,11 +345,21 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
|
|||
|
||||
// The choice remains mounted while main hands off to local bootstrap. Once
|
||||
// a manifest/failure takes ownership (or a later repair presents a fresh
|
||||
// choice), clear the transient button state so it cannot leak across phases.
|
||||
useEffect(() => {
|
||||
setLocalStarting(false)
|
||||
setLocalStartError(null)
|
||||
}, [state.setupChoice?.activeRoot])
|
||||
// choice), this transient button state must not leak across phases — so it
|
||||
// records the root it was produced under and is read back only under that
|
||||
// same root. Deriving it beats clearing it in an effect: the choice paints
|
||||
// as soon as the first snapshot commits, and a click landing before such an
|
||||
// effect flushed would have its error wiped before it ever rendered.
|
||||
const [localStart, setLocalStart] = useState<{
|
||||
root: string | null
|
||||
starting: boolean
|
||||
error: string | null
|
||||
}>({ root: null, starting: false, error: null })
|
||||
|
||||
const activeRoot = state.setupChoice?.activeRoot ?? null
|
||||
const forActiveRoot = localStart.root === activeRoot
|
||||
const localStarting = forActiveRoot && localStart.starting
|
||||
const localStartError = forActiveRoot ? localStart.error : null
|
||||
|
||||
// Mount logic: show whenever a bootstrap is in flight, completed-with-error,
|
||||
// or actively running with a manifest. Hide entirely after a successful
|
||||
|
|
@ -417,8 +425,7 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
|
|||
className="rounded-lg border border-(--ui-stroke-tertiary) bg-(--ui-bg-quinary) p-4 text-left transition hover:bg-(--chrome-action-hover) disabled:cursor-wait disabled:opacity-60"
|
||||
disabled={localStarting}
|
||||
onClick={async () => {
|
||||
setLocalStarting(true)
|
||||
setLocalStartError(null)
|
||||
setLocalStart({ root: activeRoot, starting: true, error: null })
|
||||
|
||||
try {
|
||||
const desktop = window.hermesDesktop
|
||||
|
|
@ -429,8 +436,7 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
|
|||
|
||||
await desktop.continueBootstrapLocal()
|
||||
} catch (err) {
|
||||
setLocalStartError(errorMessage(err))
|
||||
setLocalStarting(false)
|
||||
setLocalStart({ root: activeRoot, starting: false, error: errorMessage(err) })
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue