Files
hermes-agent/apps/desktop/scripts/desktop-update-ui.test.mjs
Axl Ibiza, MBA cacf1218d7 fix(desktop-update): acknowledge Windows progress completion
A delayed browser could miss the 900ms terminal event and spin forever after the updater exited. Retain terminal delivery until the page acknowledges it, bound unavailable-client teardown and failed requests, and preserve a truthful final display.

Fixes #103747. Builds on OutThisLife and Teknium detached handoff work in #83634 and the #75895 quiet-window design. Continues Axl Ibiza Windows update investigation (#60233, #94107, #100763), including source/review contributions carried by merged #93353 and #85170. Existing #102373, #103140, #95719, #97299 and #103632 retain their separate scopes.
2026-09-06 07:19:21 -07:00

101 lines
4.3 KiB
JavaScript

import assert from 'node:assert/strict'
import fs from 'node:fs'
import { JSDOM } from 'jsdom'
import { afterEach, test, vi } from 'vitest'
// Execute the shipped page, including its inline script, rather than matching
// source strings or testing a second implementation of the progress client.
const html = fs.readFileSync(new URL('../../../scripts/desktop-update/ui.html', import.meta.url), 'utf8')
const windows = []
function openPage(fetch) {
vi.useFakeTimers()
const dom = new JSDOM(html, {
url: 'http://127.0.0.1:12345/',
runScripts: 'dangerously',
beforeParse(window) {
window.fetch = fetch
window.AbortController = AbortController
window.setTimeout = setTimeout
window.clearTimeout = clearTimeout
window.requestAnimationFrame = () => 1
window.cancelAnimationFrame = () => {}
}
})
windows.push(dom.window)
return dom.window.document
}
afterEach(() => {
windows.splice(0).forEach(window => window.close())
vi.useRealTimers()
})
test.each(['done', 'manual', 'error'])('renders %s before acknowledging terminal delivery', async status => {
let document
const requests = []
const receipt = '550e8400-e29b-41d4-a716-446655440000'
const fetch = vi.fn(async (url, options) => {
requests.push(url)
if (url.startsWith('/ack/')) {
assert.equal(options.method, 'POST')
assert.equal(document.body.className, status === 'error' ? 'error' : 'done')
assert.notEqual(document.getElementById('title').textContent, 'Updating Hermes')
return { ok: true }
}
return { ok: true, json: async () => ({ status, receipt, message: 'The updater result' }) }
})
document = openPage(fetch)
await vi.advanceTimersByTimeAsync(1000)
assert.equal(document.body.className, status === 'error' ? 'error' : 'done')
assert.notEqual(document.getElementById('title').textContent, 'Updating Hermes')
assert.deepEqual(requests, ['/progress', `/ack/${receipt}`])
})
test.each(['disconnect', 'hung', 'hung-body', 'http', 'invalid'])('bounds %s progress failures without inventing an update outcome', async failure => {
let attempts = 0
const fetch = vi.fn((_url, options) => {
attempts++
if (attempts === 1) {
return Promise.resolve({ ok: true, json: async () => ({ status: 'running', message: 'Installing dependencies' }) })
}
if (failure === 'hung' || failure === 'hung-body') {
const pending = () => new Promise((_resolve, reject) => {
options.signal?.addEventListener('abort', () => reject(new Error('timeout')), { once: true })
})
return failure === 'hung' ? pending() : Promise.resolve({ ok: true, json: pending })
}
if (failure === 'http') return Promise.resolve({ ok: false })
if (failure === 'invalid') return Promise.resolve({ ok: true, json: async () => ({}) })
return Promise.reject(new Error('connection refused'))
})
const document = openPage(fetch)
await vi.advanceTimersByTimeAsync(20_000)
assert.equal(document.body.className, 'disconnected')
assert.equal(document.getElementById('title').textContent, 'Update status unavailable')
assert.match(document.getElementById('line').textContent, /Check Hermes/)
assert.ok(attempts <= 4, `unbounded retry loop: ${attempts}`)
})
test.each(['legacy', 'transient', 'ack-failure'])('preserves terminal truth with %s servers', async mode => {
let attempts = 0
const fetch = vi.fn(async url => {
if (url.startsWith('/ack/')) throw new Error('server already stopped')
if (++attempts === 1 && mode === 'transient') throw new Error('temporary disconnect')
return { ok: true, json: async () => ({ status: 'done', ...(mode === 'ack-failure' ? { receipt: 'test-receipt' } : {}) }) }
})
const document = openPage(fetch)
await vi.advanceTimersByTimeAsync(20_000)
assert.equal(document.body.className, 'done')
assert.equal(document.getElementById('title').textContent, 'Update complete')
})
test('continues displaying a healthy long update while progress remains reachable', async () => {
const fetch = vi.fn(async () => ({ ok: true, json: async () => ({ status: 'running', message: 'Building Desktop' }) }))
const document = openPage(fetch)
await vi.advanceTimersByTimeAsync(60_000)
assert.equal(document.body.className, '')
assert.equal(document.getElementById('title').textContent, 'Updating Hermes')
assert.equal(document.getElementById('line').textContent, 'Building Desktop')
})