Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions apps/desktop/electron/dashboard-token.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
extractInjectedDashboardToken,
fetchPublicText,
isForeignBackendToken,
isHeadlessServe404,
resolveServedDashboardToken
} from './dashboard-token'

Expand Down Expand Up @@ -145,3 +146,85 @@ test('adoptServedDashboardToken falls back to the spawn token when the fetch fai
assert.equal(logs.length, 1)
assert.match(logs[0], /could not read served dashboard token \(Hermes backend\): boom/)
})

test('isHeadlessServe404 matches only the headless-serve 404 signature', () => {
assert.equal(
isHeadlessServe404(new Error('404: {"error":"Headless backend (hermes serve): web UI disabled — use `hermes dashboard` for the browser UI."}')),
true
)
// Wrong status with the headless body is still an error.
assert.equal(isHeadlessServe404(new Error('500: {"error":"Headless backend (hermes serve): web UI disabled"}')), false)
// Right status without the headless body is still an error.
assert.equal(isHeadlessServe404(new Error('404: Not Found')), false)
// Network-level failures are not responses.
assert.equal(isHeadlessServe404(new TypeError('fetch failed')), false)
assert.equal(isHeadlessServe404('404: Headless backend'), false)
})

test('adoptServedDashboardToken treats a headless-serve 404 as benign and uses the spawn token', async () => {
const logs = []

const token = await adoptServedDashboardToken('http://127.0.0.1:9120', 'spawn-token', {
childAlive: () => true,
fetchText: async () => {
throw new Error('404: {"error":"Headless backend (hermes serve): web UI disabled — use `hermes dashboard` for the browser UI."}')
},
rememberLog: line => logs.push(line)
})

assert.equal(token, 'spawn-token')
assert.equal(logs.length, 1)
assert.match(logs[0], /headless Hermes backend/)
assert.doesNotMatch(logs[0], /could not read served dashboard token/)
})

test('adoptServedDashboardToken keeps error logging for a 404 without the headless signature', async () => {
const logs = []

const token = await adoptServedDashboardToken('http://127.0.0.1:9120', 'spawn-token', {
childAlive: () => true,
fetchText: async () => {
throw new Error('404: Not Found')
},
rememberLog: line => logs.push(line)
})

assert.equal(token, 'spawn-token')
assert.equal(logs.length, 1)
assert.match(logs[0], /could not read served dashboard token \(Hermes backend\): 404: Not Found/)
})

test('adoptServedDashboardToken keeps error logging for network failures', async () => {
const logs = []

const token = await adoptServedDashboardToken('http://127.0.0.1:9120', 'spawn-token', {
childAlive: () => true,
fetchText: async () => {
throw new TypeError('fetch failed')
},
rememberLog: line => logs.push(line)
})

assert.equal(token, 'spawn-token')
assert.equal(logs.length, 1)
assert.match(logs[0], /could not read served dashboard token \(Hermes backend\): fetch failed/)
})

test('adoptServedDashboardToken does not flag a headless 404 as a foreign backend when the child is dead', async () => {
// Foreign-backend detection only applies to a SERVED token that differs
// from the spawn token; a headless 404 serves no token at all, so it must
// resolve to the spawn token even with a dead child.
const logs = []

const token = await adoptServedDashboardToken('http://127.0.0.1:9120', 'spawn-token', {
childAlive: () => false,
fetchText: async () => {
throw new Error('404: {"error":"Headless backend (hermes serve): web UI disabled — use `hermes dashboard` for the browser UI."}')
},
rememberLog: line => logs.push(line)
})

assert.equal(token, 'spawn-token')
assert.equal(logs.length, 1)
assert.match(logs[0], /headless Hermes backend/)
})
24 changes: 24 additions & 0 deletions apps/desktop/electron/dashboard-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,36 @@ function isForeignBackendToken({ servedToken, spawnToken, childAlive }) {
return Boolean(servedToken) && servedToken !== spawnToken && !childAlive
}

/**
* A 404 whose body is the headless-serve SPA-off message is a known-absent
* response, not an error: in headless mode (HERMES_SERVE_HEADLESS=1) the
* backend never serves the web UI, so the root-document fetch 404s BY DESIGN
* and the session token can only come from the spawn env. Matches the error
* thrown by fetchPublicText for a non-ok response: `${status}: ${body}`.
*/
function isHeadlessServe404(error) {
if (!(error instanceof Error)) {
return false
}

const message = error.message

return message.startsWith('404:') && message.includes('Headless backend') && message.includes('web UI disabled')
}

/**
* Resolve the token the backend actually serves, adopting benign drift and
* failing loudly on a foreign backend. `childAlive` is a thunk so liveness is
* sampled after the fetch, not before.
*/
async function adoptServedDashboardToken(baseUrl, spawnToken, { childAlive, label = 'Hermes backend', ...options }) {
const servedToken = await resolveServedDashboardToken(baseUrl, spawnToken, options).catch(error => {
if (isHeadlessServe404(error)) {
options.rememberLog?.('[boot] headless Hermes backend: using spawn session token (no web UI served)')

return spawnToken
}

options.rememberLog?.(`[boot] could not read served dashboard token (${label}): ${error.message}`)

return spawnToken
Expand All @@ -108,5 +131,6 @@ export {
extractInjectedDashboardToken,
fetchPublicText,
isForeignBackendToken,
isHeadlessServe404,
resolveServedDashboardToken
}
Loading