Files
hapi/web/src/lib/runnerSkewBannerState.test.ts
T
1cd4d1137a feat(hub,cli,web): fleet runner version governance (skew, self-upgrade, soft-fail reopen) (#1108)
* fix(hub): govern runner capabilities so Cursor reopen soft-fails on skew

Hub↔runner protocol drift was reported as missing Cursor chat data when
cursor-chat-store-status was unregistered. Soft-fail reopen on probe errors,
advertise required machine capabilities, surface an unmissable upgrade banner,
and stop-runner when a newer CLI binary is already on disk.

Fixes #1084

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web,hub): make runner skew banner dismissible; gate auto-upgrade

Compact the out-of-date banner (minimize + 1h snooze + per-host Restart)
so it no longer blocks the session list. Auto stop-runner on skew stays
opt-in via HAPI_AUTO_UPGRADE_RUNNERS / autoUpgradeRunners (default off).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): tolerate full sessionStorage on skew banner minimize

QuotaExceededError from setItem aborted minimize before React state
updated, leaving the banner stuck over the session list. Persist to
memory when storage fails; only enable Restart when a newer CLI is
already on disk; clarify opt-in is stop-runner only, not package push.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub): drop redundant autoUpgradeRunners; runners already self-restart

CLI version handoff already reloads the runner when the on-disk binary
mtime changes. Hub-driven stop-runner on skew duplicated that. Keep the
skew banner and manual Restart only as a stuck/disabled-handoff escape.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli,hub,web): runner-only caps ads; gate Restart on supervisor

Address #1108 bot Majors on the thin tip: terminal/lazy bootstraps no
longer merge CURRENT_MACHINE_CAPABILITIES into the machine row (only
asRunner registration does). Banner Restart refuses unsupervised hosts
so stop-runner cannot leave a detached laptop offline; supervised
runners advertise supervisedRestart via HAPI_RUNNER_SUPERVISED=1.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub,cli,web): clear sticky runner ads; docs SUPERVISED; i18n skew label

Omit-means-clear on runner registration so rollback cannot leave
supervisedRestart/capabilities sticky; always advertise boolean
supervisedRestart from asRunner. Document HAPI_RUNNER_SUPERVISED=1
and localize MachineSelector UPDATE REQUIRED.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 22:24:39 +08:00

52 lines
1.8 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
clearRunnerSkewTempDismiss,
isRunnerSkewMinimized,
isRunnerSkewTempDismissed,
resetRunnerSkewBannerMemoryForTests,
setRunnerSkewMinimized,
tempDismissRunnerSkew,
} from './runnerSkewBannerState'
describe('runnerSkewBannerState', () => {
beforeEach(() => {
window.sessionStorage.clear()
resetRunnerSkewBannerMemoryForTests()
})
afterEach(() => {
vi.restoreAllMocks()
window.sessionStorage.clear()
resetRunnerSkewBannerMemoryForTests()
})
it('persists minimize to sessionStorage', () => {
setRunnerSkewMinimized(true)
expect(isRunnerSkewMinimized()).toBe(true)
expect(window.sessionStorage.getItem('hapi.runnerSkew.minimized.v1')).toBe('1')
})
it('still minimizes when sessionStorage setItem throws QuotaExceededError', () => {
const proto = Object.getPrototypeOf(window.sessionStorage) as Storage
vi.spyOn(proto, 'setItem').mockImplementation(() => {
throw new DOMException('quota', 'QuotaExceededError')
})
expect(() => setRunnerSkewMinimized(true)).not.toThrow()
expect(isRunnerSkewMinimized()).toBe(true)
})
it('still temp-dismisses when sessionStorage is full', () => {
const proto = Object.getPrototypeOf(window.sessionStorage) as Storage
vi.spyOn(proto, 'setItem').mockImplementation(() => {
throw new DOMException('quota', 'QuotaExceededError')
})
const now = 1_700_000_000_000
expect(() => tempDismissRunnerSkew(now)).not.toThrow()
expect(isRunnerSkewTempDismissed(now + 1)).toBe(true)
clearRunnerSkewTempDismiss()
expect(isRunnerSkewTempDismissed(now + 1)).toBe(false)
})
})