fix(desktop): latch a confirmed remote reauth failure so the overlay stays clickable

shouldLatchBackendStartFailure deliberately never latches a remote failure:
remote faults are usually transient and must stay retryable without an app
restart. A confirmed reauth rejection is the exception — it cannot self-heal,
because nothing changes until the user signs in again.

Worse, not latching actively prevents the recovery it was protecting. A
non-latching remote boot failure re-runs startHermes on every
getConnection/api call, re-emits running: true, and the boot-failure overlay
(visible = Boolean(boot.error) && !boot.running) hides itself — so the
"Sign in" button flickers out from under the user before it can be clicked.

Add shouldLatchRemoteReauthFailure as a separate predicate rather than
changing the existing one, so transient remote failures keep self-healing.
The two latches are complementary and never fire for the same failure.
This commit is contained in:
Brooklyn Nicholson 2026-07-25 21:52:45 -05:00
parent cf4fb8993f
commit 36e2228a4c
2 changed files with 61 additions and 1 deletions

View file

@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
import { test } from 'vitest'
import { shouldLatchBackendStartFailure } from './backend-start-failure'
import { shouldLatchBackendStartFailure, shouldLatchRemoteReauthFailure } from './backend-start-failure'
test('latches a LOCAL backend failure so the install-retry loop is broken', () => {
assert.equal(shouldLatchBackendStartFailure({ attemptedRemote: false }), true)
@ -21,3 +21,32 @@ test('the two branches are mutually exclusive (a failure either latches or stays
assert.equal(latched, !attemptedRemote)
}
})
test('latches a CONFIRMED remote reauth failure so the overlay stays clickable', () => {
// Without this the non-latching remote path re-runs startHermes on every
// getConnection/api call, re-emits running:true, and the overlay hides
// itself — the "Sign in" button flickers away before it can be clicked.
assert.equal(shouldLatchRemoteReauthFailure({ attemptedRemote: true, isReauth: true }), true)
})
test('does not latch a transient remote failure as reauth', () => {
// A mint timeout or a host unreachable across sleep must still self-heal.
assert.equal(shouldLatchRemoteReauthFailure({ attemptedRemote: true, isReauth: false }), false)
})
test('never latches a LOCAL failure as reauth (that is backendStartFailure job)', () => {
assert.equal(shouldLatchRemoteReauthFailure({ attemptedRemote: false, isReauth: true }), false)
assert.equal(shouldLatchRemoteReauthFailure({ attemptedRemote: false, isReauth: false }), false)
})
test('the two latches never fire for the same failure', () => {
// They are complementary, not overlapping: local failures latch via
// backendStartFailure, confirmed remote reauth latches via its own flag.
for (const attemptedRemote of [true, false]) {
for (const isReauth of [true, false]) {
const start = shouldLatchBackendStartFailure({ attemptedRemote })
const reauth = shouldLatchRemoteReauthFailure({ attemptedRemote, isReauth })
assert.ok(!(start && reauth), `both latched for remote=${attemptedRemote} reauth=${isReauth}`)
}
}
})

View file

@ -39,3 +39,34 @@ export interface BackendStartFailureContext {
export function shouldLatchBackendStartFailure(context: BackendStartFailureContext): boolean {
return !context.attemptedRemote
}
export interface RemoteReauthFailureContext {
/** True when the boot that just failed was dialing a REMOTE (or cloud) backend. */
attemptedRemote: boolean
/**
* True when the failure was a CONFIRMED auth rejection (a credentialed
* probe got 401/403), not a transient connectivity fault.
*/
isReauth: boolean
}
/**
* Whether a failed remote boot should latch as a reauth failure.
*
* This is the deliberate counterpart to `shouldLatchBackendStartFailure`,
* which never latches a remote failure because remote faults are usually
* transient and must stay retryable. A *confirmed* reauth rejection is the
* exception: it cannot self-heal, because nothing will change until the user
* signs in again.
*
* Without a latch, the non-latching remote path actively prevents recovery.
* Every subsequent `getConnection`/`api` call re-runs `startHermes`, re-emits
* `running: true`, and the boot-failure overlay (`visible = Boolean(boot.error)
* && !boot.running`) hides itself — so the "Sign in" button flickers out from
* under the user before they can click it. Latching holds the overlay still
* and clickable. Cleared on every recovery path (reset, repair, apply-config,
* and a confirmed sign-in) so a fresh session boots normally.
*/
export function shouldLatchRemoteReauthFailure(context: RemoteReauthFailureContext): boolean {
return context.attemptedRemote && context.isReauth
}