Files
hermes-agent/apps/desktop/electron/external-open.test.ts
hermes-seaeye[bot] 44fc5f629d fmt(js): npm run fix on merge (#129471)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-30 17:36:24 +00:00

246 lines
7.8 KiB
TypeScript

/**
* Tests for electron/external-open.ts — the single route every external URL
* open funnels through. All I/O is injected, so the "open failed → notify"
* behavior is asserted without loading electron. Run with the electron test
* script that runs electron/*.test.ts (same pattern as native-oauth-login).
*/
import assert from 'node:assert/strict'
import type { ChildProcess } from 'node:child_process'
import { EventEmitter } from 'node:events'
import { test } from 'vitest'
import { type ExternalOpenDeps, openExternalUrl, reportPreOpenStatFailure } from './external-open'
function makeDeps(overrides: Partial<ExternalOpenDeps> = {}) {
const calls = {
opened: [] as string[],
fileOpened: [] as string[],
localOpened: [] as string[],
notified: [] as Array<[string, string]>,
logged: [] as string[]
}
const deps: ExternalOpenDeps = {
isWsl: false,
spawn: () => {
throw new Error('not used')
},
openExternal: async url => {
calls.opened.push(url)
},
openFile: async raw => {
calls.fileOpened.push(raw)
},
openLocalPath: async raw => {
calls.localOpened.push(raw)
return true
},
notifyFailure: (url, message) => calls.notified.push([url, message]),
log: line => calls.logged.push(line),
...overrides
}
return { deps, calls }
}
test('resolves ok and opens when openExternal succeeds', async () => {
const { deps, calls } = makeDeps()
const result = await openExternalUrl('https://example.com/x', deps)
assert.deepEqual(result, { ok: true })
assert.deepEqual(calls.opened, ['https://example.com/x'])
assert.equal(calls.notified.length, 0)
})
test('notifies and resolves failed when openExternal rejects', async () => {
const { deps, calls } = makeDeps({
openExternal: async () => {
throw new Error('no method available for opening')
}
})
const result = await openExternalUrl('https://example.com', deps)
assert.deepEqual(result, {
ok: false,
reason: 'failed',
message: 'no method available for opening'
})
assert.deepEqual(calls.notified, [['https://example.com/', 'no method available for opening']])
assert.ok(calls.logged.some(line => line.includes('openExternal failed')))
})
test('resolves invalid for a URL the route does not open, with no notify', async () => {
const { deps, calls } = makeDeps()
for (const url of ['', 'not a url', 'ftp://x.com']) {
const result = await openExternalUrl(url, deps)
assert.equal(result.ok, false)
if (result.ok === false) {
assert.equal(result.reason, 'invalid')
}
}
assert.equal(calls.opened.length, 0)
assert.equal(calls.notified.length, 0)
})
test('dispatches file:// URLs to openFile', async () => {
const { deps, calls } = makeDeps()
const result = await openExternalUrl('file:///C:/x.html', deps)
assert.deepEqual(result, { ok: true })
assert.deepEqual(calls.fileOpened, ['file:///C:/x.html'])
})
test('opens bare local paths through openLocalPath instead of rejecting them', async () => {
const { deps, calls } = makeDeps()
// Every shape `new URL()` cannot express as a web/file URL: a Windows drive
// letter parses as the bogus `c:` scheme, POSIX/UNC/`~` make the parser
// throw. All previously landed in the "Invalid external URL" reject.
for (const raw of [
'C:\\Users\\x\\a.md',
'C:/Work/report.html',
'/tmp/report.pdf',
'~/logs/desktop.log',
'\\\\server\\share\\a.md'
]) {
const result = await openExternalUrl(raw, deps)
assert.deepEqual(result, { ok: true }, `expected ${raw} to open as a local path`)
}
assert.deepEqual(calls.localOpened, [
'C:\\Users\\x\\a.md',
'C:/Work/report.html',
'/tmp/report.pdf',
'~/logs/desktop.log',
'\\\\server\\share\\a.md'
])
assert.equal(calls.fileOpened.length, 0)
assert.equal(calls.opened.length, 0)
})
test('resolves invalid, with the path logged, when openLocalPath cannot resolve the path', async () => {
const { deps, calls } = makeDeps({
openLocalPath: async () => false
})
const result = await openExternalUrl('C:\\Users\\x\\a.md', deps)
assert.deepEqual(result, { ok: false, reason: 'invalid' })
assert.ok(
calls.logged.some(line => line.includes('openPath resolve rejected') && line.includes('C:\\Users\\x\\a.md'))
)
assert.equal(calls.notified.length, 0)
})
test('notifies failed when openLocalPath throws', async () => {
const { deps, calls } = makeDeps({
openLocalPath: async () => {
throw new Error('stat failed')
}
})
const result = await openExternalUrl('/tmp/report.pdf', deps)
assert.deepEqual(result, { ok: false, reason: 'failed', message: 'stat failed' })
assert.deepEqual(calls.notified, [['/tmp/report.pdf', 'stat failed']])
})
test('opens protocol-relative URLs as https, never as local paths', async () => {
const { deps, calls } = makeDeps()
const result = await openExternalUrl('//cdn.example.com/img.png', deps)
assert.deepEqual(result, { ok: true })
assert.deepEqual(calls.opened, ['https://cdn.example.com/img.png'])
assert.equal(calls.localOpened.length, 0)
})
test('wsl: spawns cmd.exe and resolves ok on the happy path', async () => {
const spawned: string[] = []
const proc = new EventEmitter() as unknown as ChildProcess
const { deps } = makeDeps({
isWsl: true,
spawn: (cmd, args) => {
spawned.push(cmd, ...args)
return proc
}
})
const result = await openExternalUrl('https://example.com', deps)
assert.deepEqual(result, { ok: true })
assert.equal(spawned[0], 'cmd.exe')
assert.ok(spawned.some(arg => arg === 'https://example.com/'))
})
test('wsl: falls back to openExternal and notifies when cmd.exe fails to spawn', async () => {
const proc = new EventEmitter() as unknown as ChildProcess
const { deps, calls } = makeDeps({ isWsl: true, spawn: () => proc })
deps.openExternal = async url => {
calls.opened.push(url)
throw new Error('xdg-open missing')
}
const result = await openExternalUrl('https://example.com', deps)
assert.deepEqual(result, { ok: true })
// openExternalUrl's fire-and-forget WSL path resolves before the spawn
// error can arrive; drive the error to exercise the fallback.
;(proc as unknown as EventEmitter).emit('error', new Error('ENOENT'))
await new Promise(resolve => setTimeout(resolve, 20))
assert.deepEqual(calls.opened, ['https://example.com/'])
assert.deepEqual(calls.notified, [['https://example.com/', 'xdg-open missing']])
})
test('guard: missing-file error is reported once and classified as a miss', () => {
const reported: Array<[string, string]> = []
const logged: string[] = []
const error = Object.assign(new Error('This file does not exist: /tmp/gone.html'), { code: 'missing-file' })
const isMiss = reportPreOpenStatFailure(error, 'file:///tmp/gone.html', {
log: line => logged.push(line),
reportMissing: (url, message) => reported.push([url, message])
})
assert.equal(isMiss, true)
assert.deepEqual(reported, [['file:///tmp/gone.html', 'This file does not exist: /tmp/gone.html']])
assert.equal(logged.length, 0)
})
test('guard: a non-missing stat failure is logged and classified as proceed-to-OS', () => {
const reported: Array<[string, string]> = []
const logged: string[] = []
for (const code of ['EACCES', 'ELOOP', 'EPERM', 'ENAMETOOLONG']) {
const error = Object.assign(new Error(`${code}: stat failed`), { code })
const isMiss = reportPreOpenStatFailure(error, 'file:///srv/locked/report.html', {
log: line => logged.push(line),
reportMissing: (url, message) => reported.push([url, message])
})
assert.equal(isMiss, false, code)
}
// Nothing was fabricated as a miss, and every failure left a log line.
assert.equal(reported.length, 0)
assert.equal(logged.length, 4)
assert.ok(logged.every(line => line.includes('[file] pre-open stat failed')))
})