Skip to content
Open
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
60 changes: 60 additions & 0 deletions apps/desktop/electron/api-expected-404.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import assert from 'node:assert/strict'

import { test } from 'vitest'

import { HERMES_API_EXPECTED_404, isExpectedNotFoundSentinel, unwrapExpectedNotFound } from './api-expected-404'

test('a handler-produced sentinel is recognized', () => {
assert.equal(isExpectedNotFoundSentinel({ [HERMES_API_EXPECTED_404]: '404: {"detail":"Session not found"}' }), true)
})

test('real backend payloads are never mistaken for a sentinel', () => {
const notSentinels: unknown[] = [
null,
undefined,
'string',
42,
[],
[{ [HERMES_API_EXPECTED_404]: 'x' }],
{},
{ session_id: 's1' },
// right key, wrong value type
{ [HERMES_API_EXPECTED_404]: 404 },
// right key, but carries real data alongside — not our shape
{ [HERMES_API_EXPECTED_404]: 'x', session_id: 's1' }
]

for (const value of notSentinels) {
assert.equal(isExpectedNotFoundSentinel(value), false, `treated ${JSON.stringify(value)} as a sentinel`)
}
})

test('unwrap rethrows the sentinel as the exact rejection the renderer expects', () => {
const message = '404: {"detail":"Session not found"}'

assert.throws(
() => unwrapExpectedNotFound({ [HERMES_API_EXPECTED_404]: message }),
(error: Error) => error instanceof Error && error.message === message
)
})

test('unwrap passes real responses through untouched, by reference', () => {
const payload = { session_id: 's1', messages: [] }

assert.equal(unwrapExpectedNotFound(payload), payload)
assert.equal(unwrapExpectedNotFound(null), null)
assert.equal(unwrapExpectedNotFound(''), '')
})

test('the rethrown message still matches the renderer 404 probe predicate', () => {
// `resolveStoredSession` and friends branch on a `404`-shaped message; the
// seam must not change what they see.
const message = '404: {"detail":"Session not found"}'

try {
unwrapExpectedNotFound({ [HERMES_API_EXPECTED_404]: message })
assert.fail('expected a rejection')
} catch (error) {
assert.match(String((error as Error).message), /(?:^|\s)404\b/)
}
})
42 changes: 42 additions & 0 deletions apps/desktop/electron/api-expected-404.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// The `hermes:api` expected-404 seam, shared by the main-process handler and
// preload so both sides agree on one literal.
//
// Electron logs "Error occurred in handler for 'hermes:api'" with a full stack
// trace for every rejected `ipcMain.handle` invoke, and offers no way to opt a
// handler out. Desktop's session resolution is a deliberate probe ladder
// (`resolveStoredSession`: cache → active backend → each other profile), so a
// 404 there is a normal rung outcome the renderer already handles — but each
// one printed a stack into the launching terminal.
//
// The handler therefore RESOLVES with this sentinel for that one expected case,
// and preload rethrows it as an ordinary `Error` carrying the identical
// `404: <body>` message. The renderer sees exactly the rejection it saw before;
// only Electron's logging is bypassed. Any other failure rejects as usual and
// still logs in full.

const HERMES_API_EXPECTED_404 = '__hermesExpected404__'

// True when `value` is a handler-resolved expected-404 sentinel, not real
// response data. Kept narrow: a plain object whose ONLY key is the sentinel and
// whose value is a string, so a backend payload can't be mistaken for one.
function isExpectedNotFoundSentinel(value: unknown): value is Record<string, string> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return false
}

const keys = Object.keys(value as Record<string, unknown>)

return keys.length === 1 && keys[0] === HERMES_API_EXPECTED_404 && typeof (value as any)[HERMES_API_EXPECTED_404] === 'string'
}

// Restore the caller-visible contract: a sentinel becomes the rejection the
// renderer expects; anything else passes through untouched.
function unwrapExpectedNotFound(value: unknown): unknown {
if (isExpectedNotFoundSentinel(value)) {
throw new Error(value[HERMES_API_EXPECTED_404])
}

return value
}

export { HERMES_API_EXPECTED_404, isExpectedNotFoundSentinel, unwrapExpectedNotFound }
Loading