cleanup(desktop): lint&fmt all

This commit is contained in:
ethernet 2026-07-09 11:59:49 -04:00
parent 0fac4cd8e9
commit 7c98c65163
15 changed files with 122 additions and 15 deletions

View file

@ -44,12 +44,14 @@ function appendUniquePathEntries(entries, { delimiter = path.delimiter } = {}) {
if (!entry) {
continue
}
const parts = Array.isArray(entry) ? entry : String(entry).split(delimiter)
for (const part of parts) {
if (!part || seen.has(part)) {
continue
}
seen.add(part)
ordered.push(part)
}
@ -77,6 +79,7 @@ function normalizeHermesHomeRoot(hermesHome, { pathModule = pathModuleForPlatfor
if (!hermesHome) {
return hermesHome
}
const resolved = pathModule.resolve(String(hermesHome))
const parent = pathModule.dirname(resolved)

View file

@ -60,6 +60,7 @@ function waitForDashboardPort(child, timeoutMs = resolvePortAnnounceTimeoutMs())
if (done) {
return
}
done = true
clearTimeout(timer)
child.stdout.off('data', onData)
@ -130,12 +131,14 @@ function waitForDashboardReadyFile(readyFile, child, timeoutMs = resolvePortAnno
if (done) {
return
}
done = true
clearTimeout(timer)
if (interval) {
clearInterval(interval)
}
child.off('exit', onExit)
child.off('error', onError)
}
@ -171,6 +174,7 @@ function waitForDashboardReadyFile(readyFile, child, timeoutMs = resolvePortAnno
if (typeof interval.unref === 'function') {
interval.unref()
}
check()
})
}

View file

@ -42,7 +42,6 @@ import { hiddenWindowsChildOptions } from './windows-child-options'
const IS_WINDOWS = process.platform === 'win32'
const STAMP_COMMIT_RE = /^[0-9a-f]{7,40}$/i
// Stages flagged needs_user_input=true in the manifest are skipped by the
@ -68,6 +67,7 @@ function resolveLocalInstallScript(sourceRepoRoot) {
if (!sourceRepoRoot) {
return null
}
const candidate = path.join(sourceRepoRoot, 'scripts', installScriptName())
try {
@ -91,6 +91,7 @@ function installedAgentInstallScript(hermesHome) {
if (!hermesHome) {
return null
}
const candidate = path.join(hermesHome, 'hermes-agent', 'scripts', installScriptName())
try {
@ -420,6 +421,7 @@ function spawnPowerShell(scriptPath, args, { emit, stageName, abortSignal, herme
if (abortSignal) {
abortSignal.removeEventListener('abort', onAbort)
}
reject(err)
})
@ -436,6 +438,7 @@ function spawnPowerShell(scriptPath, args, { emit, stageName, abortSignal, herme
if (stderrBuf) {
emit && emit({ type: 'log', stage: stageName, line: stderrBuf, stream: 'stderr' } as any)
}
resolve({ stdout, stderr, code, signal, killed } as any)
})
})
@ -512,6 +515,7 @@ function spawnBash(scriptPath, args, { emit, stageName, abortSignal, hermesHome
if (abortSignal) {
abortSignal.removeEventListener('abort', onAbort)
}
reject(err)
})
@ -527,6 +531,7 @@ function spawnBash(scriptPath, args, { emit, stageName, abortSignal, hermesHome
if (stderrBuf) {
emit && emit({ type: 'log', stage: stageName, line: stderrBuf, stream: 'stderr' })
}
resolve({ stdout, stderr, code, signal, killed })
})
})

View file

@ -106,6 +106,7 @@ function resolveRemovableAppPath(execPath, platform, env: any = {}) {
if (env.APPIMAGE) {
return env.APPIMAGE
}
// Unpacked electron-builder tree: …/linux-unpacked/hermes
const dir = p.dirname(exe)

View file

@ -80,6 +80,7 @@ function probeGatewayWebSocket<T>(
if (settled) {
return
}
settled = true
clearTimers()
@ -107,6 +108,7 @@ function probeGatewayWebSocket<T>(
if (settled) {
return
}
opened = true
// Upgrade accepted. Give the server a brief window to reject the
// credential post-handshake (early close) before declaring success.
@ -189,6 +191,7 @@ function extractErrorReason(event) {
if (event instanceof Error) {
return event.message
}
const err = event.error || event.message
if (err instanceof Error) {

View file

@ -112,6 +112,7 @@ function sensitiveFileBlockReason(filePath) {
function ipcPathError(code: any, message: string): Error & { code: any } {
const error = new Error(message) as Error & { code: any }
;(error as any).code = code
return error

View file

@ -55,6 +55,7 @@ export function readLinkTitleWindowTitle(window) {
if (!window || window.isDestroyed()) {
return ''
}
const contents = window.webContents
if (!contents || contents.isDestroyed()) {

View file

@ -28,7 +28,7 @@ import {
systemPreferences
} from 'electron'
import nodePty from 'node-pty'
import { decideProfileDeleteAction, profileNameFromDeleteRequest, resolveRouteProfile } from './profile-delete-routing'
import { stopBackendChild as stopBackendChildImpl } from './backend-child'
import { dashboardFallbackArgs, sourceDeclaresServe } from './backend-command'
import { buildDesktopBackendEnv, normalizeHermesHomeRoot } from './backend-env'
@ -96,6 +96,7 @@ import {
} from './hardening'
import { createLinkTitleWindow, guardLinkTitleSession, readLinkTitleWindowTitle } from './link-title-window'
import { serializeJsonBody, setJsonRequestHeaders } from './oauth-net-request'
import { decideProfileDeleteAction, profileNameFromDeleteRequest, resolveRouteProfile } from './profile-delete-routing'
import {
buildSessionWindowUrl,
chatWindowWebPreferences,
@ -126,10 +127,10 @@ import {
MIN_WIDTH as WINDOW_MIN_WIDTH
} from './window-state'
import { hiddenWindowsChildOptions } from './windows-child-options'
import { buildPathExtCandidates, chooseUpdaterArgs, resolveVenvHermesCommand } from './windows-hermes-path'
import { readWindowsUserEnvVar } from './windows-user-env'
import { isPackagedInstallPath as isPackagedInstallPathUnderRoots } from './workspace-cwd'
import { readWslWindowsClipboardImage } from './wsl-clipboard-image'
import { buildPathExtCandidates, chooseUpdaterArgs, resolveVenvHermesCommand } from './windows-hermes-path'
const USER_DATA_OVERRIDE = process.env.HERMES_DESKTOP_USER_DATA_DIR
@ -154,7 +155,6 @@ const APP_ROOT = app.getAppPath()
// Dev (`npm run dev`) and prod both load the esbuild output from dist/.
const PRELOAD_PATH = path.join(APP_ROOT, 'dist', 'electron-preload.js')
// Remote displays (SSH X11 forwarding, VNC, RDP) make Chromium's GPU
// compositor flicker — accelerated layers can't be presented cleanly over the
// wire, so the window flashes during scroll/streaming/animation. Local
@ -865,6 +865,7 @@ function planDesktopLogRotation(size) {
if (size < DESKTOP_LOG_MAX_BYTES) {
return []
}
const backups = n => Array.from({ length: n }, (_, i) => desktopLogBackupPath(i + 1))
// Pathological boot-loop log: reclaim live + every backup outright.
@ -932,6 +933,7 @@ function flushDesktopLogBufferSync() {
if (!desktopLogBuffer) {
return
}
const chunk = desktopLogBuffer
desktopLogBuffer = ''
@ -948,6 +950,7 @@ function flushDesktopLogBufferAsync() {
if (!desktopLogBuffer) {
return desktopLogFlushPromise
}
const chunk = desktopLogBuffer
desktopLogBuffer = ''
@ -968,6 +971,7 @@ function scheduleDesktopLogFlush() {
if (desktopLogFlushTimer) {
return
}
desktopLogFlushTimer = setTimeout(() => {
desktopLogFlushTimer = null
void flushDesktopLogBufferAsync()
@ -980,6 +984,7 @@ function rememberLog(chunk) {
if (!text) {
return
}
const lines = text.split(/\r?\n/).map(line => `[hermes] ${line}`)
hermesLog.push(...lines)
@ -1178,11 +1183,13 @@ function broadcastBootProgress() {
if (!mainWindow || mainWindow.isDestroyed()) {
return
}
const { webContents } = mainWindow
if (!webContents || webContents.isDestroyed()) {
return
}
webContents.send('hermes:boot-progress', bootProgressState)
}
@ -1262,11 +1269,13 @@ function broadcastBootstrapEvent(ev) {
if (!mainWindow || mainWindow.isDestroyed()) {
return
}
const { webContents } = mainWindow
if (!webContents || webContents.isDestroyed()) {
return
}
webContents.send('hermes:bootstrap:event', ev)
}
@ -1469,6 +1478,7 @@ function backendSupportsServe(backend) {
if (!backend || !backend.command) {
return true
}
const key = `${backend.command}::${backend.root || ''}`
if (_serveSupportCache.has(key)) {
@ -2269,6 +2279,7 @@ function isShimLocked(shimPath) {
if (!IS_WINDOWS) {
return false
}
let fd
try {
@ -2406,6 +2417,7 @@ async function releaseBackendLock(updateRoot, tag) {
for (const pid of stragglers) {
forceKillProcessTree(pid)
}
await new Promise(r => setTimeout(r, 300))
}
@ -2700,6 +2712,7 @@ function runningAppBundle() {
if (!IS_MAC) {
return null
}
let dir = path.dirname(app.getPath('exe')) // .../Contents/MacOS
for (let i = 0; i < 2; i++) {
@ -3126,6 +3139,7 @@ function resolveRendererIndex() {
if (found) {
return found
}
// Nothing on disk. A packaged build with no renderer bundle blank-pages with
// a bare ERR_FILE_NOT_FOUND and no clue why (see #39484). Surface the cause
// and the fix before Electron loads the missing file.
@ -3174,6 +3188,7 @@ function resolveHermesCwd() {
if (!candidate) {
continue
}
const resolved = path.resolve(String(candidate))
if (isPackagedInstallPath(resolved)) {
@ -3698,6 +3713,7 @@ function fetchJson(url, token, options: any = {}) {
if (body) {
req.write(body)
}
req.end()
})
}
@ -3787,6 +3803,7 @@ function fetchPublicJson(url, options: any = {}) {
if (body) {
req.write(body)
}
req.end()
})
}
@ -3904,6 +3921,7 @@ function cacheTitle(key, title) {
if (titleCache.size >= TITLE_CACHE_LIMIT) {
titleCache.delete(titleCache.keys().next().value)
}
titleCache.set(key, title)
}
@ -3958,6 +3976,7 @@ function fetchHtmlTitleWithCurl(rawUrl: string): Promise<string> {
if (bytes >= TITLE_BYTE_BUDGET) {
return
}
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
const remaining = TITLE_BYTE_BUDGET - bytes
const next = buffer.length > remaining ? buffer.subarray(0, remaining) : buffer
@ -3970,6 +3989,7 @@ function fetchHtmlTitleWithCurl(rawUrl: string): Promise<string> {
if (!chunks.length) {
return resolve('')
}
resolve(parseHtmlTitle(Buffer.concat(chunks).toString('utf8')))
})
})
@ -3979,6 +3999,7 @@ function getLinkTitleSession() {
if (linkTitleSession || !app.isReady()) {
return linkTitleSession
}
linkTitleSession = session.fromPartition('hermes:link-titles', { cache: false })
linkTitleSession.webRequest.onBeforeRequest((details, callback) => {
callback({ cancel: RENDER_TITLE_BLOCKED_RESOURCES.has(details.resourceType) })
@ -4021,6 +4042,7 @@ function runRenderTitleJob(rawUrl) {
if (settled) {
return
}
settled = true
if (hardTimer) {
@ -4030,6 +4052,7 @@ function runRenderTitleJob(rawUrl) {
if (graceTimer) {
clearTimeout(graceTimer)
}
const value = (title || '').replace(/\s+/g, ' ').trim()
try {
@ -4055,6 +4078,7 @@ function runRenderTitleJob(rawUrl) {
if (graceTimer) {
clearTimeout(graceTimer)
}
graceTimer = setTimeout(finishWithTitle, RENDER_TITLE_GRACE_MS)
}
@ -4136,6 +4160,7 @@ async function resourceBufferFromUrl(rawUrl) {
if (!match) {
throw new Error('Invalid data URL')
}
const mimeType = match[1] || 'application/octet-stream'
const encoded = match[3] || ''
const buffer = match[2] ? Buffer.from(encoded, 'base64') : Buffer.from(decodeURIComponent(encoded), 'utf8')
@ -4184,6 +4209,7 @@ async function copyImageFromUrl(rawUrl) {
if (image.isEmpty()) {
throw new Error('Could not read image')
}
clipboard.writeImage(image)
}
@ -4199,6 +4225,7 @@ async function saveImageFromUrl(rawUrl) {
if (result.canceled || !result.filePath) {
return false
}
await fs.promises.writeFile(result.filePath, buffer)
return true
@ -4333,11 +4360,13 @@ function sendPreviewFileChanged(payload) {
if (!mainWindow || mainWindow.isDestroyed()) {
return
}
const { webContents } = mainWindow
if (!webContents || webContents.isDestroyed()) {
return
}
webContents.send('hermes:preview-file-changed', payload)
}
@ -4358,12 +4387,14 @@ async function watchPreviewFile(rawUrl) {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
timer = null
if (!fileExists(filePath)) {
return
}
sendPreviewFileChanged({ id, path: filePath, url: pathToFileURL(filePath).toString() })
}, PREVIEW_WATCH_DEBOUNCE_MS)
})
@ -4373,6 +4404,7 @@ async function watchPreviewFile(rawUrl) {
if (timer) {
clearTimeout(timer)
}
watcher.close()
}
})
@ -4447,11 +4479,13 @@ function sendBackendExit(payload) {
if (!mainWindow || mainWindow.isDestroyed()) {
return
}
const { webContents } = mainWindow
if (!webContents || webContents.isDestroyed()) {
return
}
webContents.send('hermes:backend-exit', payload)
}
@ -4459,11 +4493,13 @@ function sendClosePreviewRequested() {
if (!mainWindow || mainWindow.isDestroyed()) {
return
}
const { webContents } = mainWindow
if (!webContents || webContents.isDestroyed()) {
return
}
webContents.send('hermes:close-preview-requested')
}
@ -4474,11 +4510,13 @@ function sendPowerResume() {
if (!mainWindow || mainWindow.isDestroyed()) {
return
}
const { webContents } = mainWindow
if (!webContents || webContents.isDestroyed()) {
return
}
webContents.send('hermes:power-resume')
}
@ -4488,6 +4526,7 @@ function registerPowerResumeListeners() {
if (powerResumeRegistered) {
return
}
powerResumeRegistered = true
try {
@ -4509,16 +4548,19 @@ function sendOpenUpdatesRequested() {
if (!mainWindow || mainWindow.isDestroyed()) {
return
}
const { webContents } = mainWindow
if (!webContents || webContents.isDestroyed()) {
return
}
webContents.send('hermes:open-updates')
if (!mainWindow.isVisible()) {
mainWindow.show()
}
mainWindow.focus()
}
@ -4526,11 +4568,13 @@ function sendWindowStateChanged(nextIsFullscreen?: boolean) {
if (!mainWindow || mainWindow.isDestroyed()) {
return
}
const { webContents } = mainWindow
if (!webContents || webContents.isDestroyed()) {
return
}
const state = getWindowState()
if (typeof nextIsFullscreen === 'boolean') {
@ -4675,6 +4719,7 @@ function installDevToolsShortcut(window) {
if (!isInspectShortcut) {
return
}
event.preventDefault()
toggleDevTools(window)
})
@ -4710,6 +4755,7 @@ function setAndPersistZoomLevel(window, zoomLevel) {
if (!window || window.isDestroyed()) {
return
}
const next = clampZoomLevel(zoomLevel)
window.webContents.setZoomLevel(next)
// Keep any open settings UI in sync, including changes made via the
@ -4726,6 +4772,7 @@ function restorePersistedZoomLevel(window) {
if (!window || window.isDestroyed()) {
return
}
window.webContents
.executeJavaScript(
`(() => { try { return localStorage.getItem(${JSON.stringify(ZOOM_STORAGE_KEY)}) } catch { return null } })()`
@ -4734,6 +4781,7 @@ function restorePersistedZoomLevel(window) {
if (stored == null || !window || window.isDestroyed()) {
return
}
const level = clampZoomLevel(Number(stored))
window.webContents.setZoomLevel(level)
})
@ -4810,6 +4858,7 @@ function installContextMenu(window) {
if (template.length) {
template.push({ type: 'separator' })
}
template.push(
{
label: 'Open Link',
@ -4963,6 +5012,7 @@ function getOauthSession() {
if (oauthSession || !app.isReady()) {
return oauthSession
}
oauthSession = session.fromPartition(OAUTH_SESSION_PARTITION)
return oauthSession
@ -4978,6 +5028,7 @@ async function hasOauthSessionCookie(baseUrl) {
if (!sess) {
return false
}
const parsed = new URL(baseUrl)
try {
@ -5010,6 +5061,7 @@ async function hasLiveOauthSession(baseUrl) {
if (!sess) {
return false
}
const parsed = new URL(baseUrl)
try {
@ -5092,6 +5144,7 @@ function openOauthLoginWindow(baseUrl, { silent = false } = {}) {
if (settled) {
return
}
settled = true
if (pollTimer) {
@ -5261,6 +5314,7 @@ function fetchJsonViaOauthSession(url, options: any = {}) {
if (timedOut) {
return
}
clearTimeout(timer)
const text = Buffer.concat(chunks).toString('utf8')
const statusCode = res.statusCode || 500
@ -5299,6 +5353,7 @@ function fetchJsonViaOauthSession(url, options: any = {}) {
if (timedOut) {
return
}
clearTimeout(timer)
reject(error)
})
@ -5306,6 +5361,7 @@ function fetchJsonViaOauthSession(url, options: any = {}) {
if (body) {
request.write(body)
}
request.end()
})
}
@ -5711,6 +5767,7 @@ function sanitizeConnectionProfiles(raw: Record<string, any>) {
} = {
mode: modeIsRemoteLike(entry.mode) ? entry.mode : 'local'
}
const url = String(entry.url || '').trim()
if (url) {
@ -6400,6 +6457,7 @@ function touchPoolBackend(profile) {
if (!key) {
return
}
const entry = backendPool.get(key)
if (entry) {
@ -6415,6 +6473,7 @@ function evictLruPoolBackends(keep) {
if (backendPool.size <= keep) {
return
}
const now = Date.now()
const evictable = [...backendPool.entries()]
@ -6427,6 +6486,7 @@ function evictLruPoolBackends(keep) {
if (removable <= 0) {
break
}
rememberLog(`Evicting idle profile backend "${profile}" (LRU cap ${POOL_MAX_BACKENDS})`)
stopPoolBackend(profile)
removable -= 1
@ -6437,6 +6497,7 @@ function startPoolIdleReaper() {
if (poolIdleReaper) {
return
}
poolIdleReaper = setInterval(() => {
const now = Date.now()
@ -6589,6 +6650,7 @@ function stopPoolBackend(profile) {
if (!entry) {
return
}
backendPool.delete(profile)
stopBackendChild(entry.process)
}
@ -6599,6 +6661,7 @@ async function teardownPoolBackendAndWait(profile) {
if (!entry) {
return
}
backendPool.delete(profile)
stopBackendChild(entry.process)
@ -6623,6 +6686,7 @@ function stopAllPoolBackends() {
// that decision calls for.
async function prepareProfileDeleteRequest(request) {
const profile = profileNameFromDeleteRequest(request)
const decision = decideProfileDeleteAction(profile, {
isDefaultProfile: p => p === 'default',
isValidProfileName: p => PROFILE_NAME_RE.test(p),
@ -6923,6 +6987,7 @@ function focusWindow(win) {
if (!win.isVisible()) {
win.show()
}
win.focus()
}
@ -7360,6 +7425,7 @@ ipcMain.on('hermes:zoom:set-percent', (event, percent) => {
if (!window || window.isDestroyed()) {
return
}
setAndPersistZoomLevel(window, percentToZoomLevel(Number(percent)))
})
@ -7861,6 +7927,7 @@ ipcMain.handle('hermes:notify', (_event, payload) => {
if (!Notification.isSupported()) {
return false
}
// Action buttons render only on signed macOS builds; elsewhere they're dropped
// and the body click still works.
const actions = Array.isArray(payload?.actions) ? payload.actions : []
@ -7876,6 +7943,7 @@ ipcMain.handle('hermes:notify', (_event, payload) => {
if (!mainWindow || mainWindow.isDestroyed()) {
return
}
focusWindow(mainWindow)
if (payload?.sessionId) {
@ -7886,6 +7954,7 @@ ipcMain.handle('hermes:notify', (_event, payload) => {
if (!mainWindow || mainWindow.isDestroyed()) {
return
}
const action = actions[index]
if (action?.id) {
@ -8687,6 +8756,7 @@ async function getUninstallSummary() {
if (settled) {
return
}
settled = true
resolve(value)
}
@ -8881,6 +8951,7 @@ function handleDeepLink(url) {
if (!url || typeof url !== 'string') {
return
}
let parsed
try {
@ -8910,6 +8981,7 @@ function handleDeepLink(url) {
if (mainWindow.isMinimized()) {
mainWindow.restore()
}
mainWindow.focus()
mainWindow.webContents.send('hermes:deep-link', payload)
rememberLog(`[deeplink] delivered ${kind}/${name}`)
@ -8966,6 +9038,7 @@ if (!_gotSingleInstanceLock) {
if (mainWindow.isMinimized()) {
mainWindow.restore()
}
mainWindow.focus()
}
})

View file

@ -40,6 +40,7 @@ const ALIVE: typeof process.kill = () => true // injected kill that "succeeds" =
const DEAD: typeof process.kill = () => {
const err = new Error('no such process')
;(err as any).code = 'ESRCH'
throw err
}
@ -93,6 +94,7 @@ test('isPidAlive: own pid is alive, impossible pid is dead', () => {
test('isPidAlive: EPERM counts as alive (process owned by another user)', () => {
const eperm = () => {
const err = new Error('operation not permitted')
;(err as any).code = 'EPERM'
throw err
}

View file

@ -63,6 +63,7 @@ function resolveUnpackedRelease(execPath, updateRoot, platform) {
if (!execPath || !updateRoot) {
return null
}
const releaseDir = path.join(updateRoot, 'apps', 'desktop', 'release')
const unpacked = path.join(releaseDir, unpackedDirName(platform))
const normalizedExec = path.resolve(String(execPath))
@ -116,6 +117,7 @@ function sandboxPreflight(unpackedDir, statSync) {
if (!unpackedDir) {
return { ok: false, reason: 'no-unpacked-dir', path: null }
}
const sandboxPath = path.join(unpackedDir, 'chrome-sandbox')
let st

View file

@ -22,6 +22,7 @@ function canonicalGitHubRemote(url) {
if (!url) {
return ''
}
let value = String(url).trim()
if (value.startsWith('git@github.com:')) {

View file

@ -61,6 +61,7 @@ function onScreen(bounds, displays) {
if (!a) {
return false
}
const x = Math.min(bounds.x + bounds.width, a.x + a.width) - Math.max(bounds.x, a.x)
const y = Math.min(bounds.y + bounds.height, a.y + a.height) - Math.max(bounds.y, a.y)

View file

@ -23,6 +23,7 @@ function parseRegQueryValue(stdout, name) {
if (!stdout || !name) {
return null
}
const typePattern = /^(\S+)\s+(?:REG_SZ|REG_EXPAND_SZ|REG_MULTI_SZ|REG_DWORD|REG_QWORD|REG_BINARY|REG_NONE)\s+(.*)$/
for (const rawLine of String(stdout).split(/\r?\n/)) {
@ -70,6 +71,7 @@ function readWindowsUserEnvVar(
if (platform !== 'win32' || !name) {
return null
}
let stdout
try {
@ -88,6 +90,7 @@ function readWindowsUserEnvVar(
if (raw == null) {
return null
}
const expanded = expandWindowsEnvRefs(raw, env).trim()
return expanded || null

View file

@ -1,3 +1,4 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'

View file

@ -1,5 +1,5 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { cleanup, findByText, fireEvent, render } from '@testing-library/react'
import { cleanup, fireEvent, render } from '@testing-library/react'
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { DropdownMenu, DropdownMenuContent } from '@/components/ui/dropdown-menu'
@ -39,7 +39,8 @@ afterEach(() => {
function renderPanel(onSelectModel = vi.fn()) {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
render(
const content = render(
<QueryClientProvider client={client}>
<DropdownMenu open>
<DropdownMenuContent>
@ -49,15 +50,15 @@ function renderPanel(onSelectModel = vi.fn()) {
</QueryClientProvider>
)
return onSelectModel
return { onSelectModel, content }
}
describe('ModelMenuPanel MoA presets', () => {
it('selecting a MoA preset switches PERSISTENTLY via onSelectModel (not the one-shot dispatch)', async () => {
const onSelectModel = renderPanel()
const { content, onSelectModel } = renderPanel()
// moaOptions is async (useQuery) — wait for the preset row to mount.
const row = await findByText(document.body, 'MoA: BeastMode')
const row = await content.findByText('MoA: BeastMode')
fireEvent.click(row)
// #54670: must route through the persistent model-switch path
@ -69,30 +70,35 @@ describe('ModelMenuPanel MoA presets', () => {
it('shows the check on the preset that matches the current moa selection', async () => {
$currentProvider.set('moa')
$currentModel.set('BeastMode')
renderPanel()
const { content } = renderPanel()
const row = await findByText(document.body, 'MoA: BeastMode')
const row = await content.findByText('MoA: BeastMode')
// The check codicon renders as a sibling within the same row item.
const item = row.closest('[role="menuitem"]') ?? row.parentElement
expect(item?.querySelector('.codicon-check')).not.toBeNull()
})
it('keeps the virtual moa provider out of the main model groups (presets section only)', async () => {
renderPanel()
const { content } = renderPanel()
await findByText(document.body, 'MoA: BeastMode')
await content.findByText('MoA: BeastMode')
// The provider group header would read "Mixture of Agents"; the presets
// section header reads "MoA presets". Only the latter should exist.
// Radix DropdownMenu portals its content to document.body, so assert
// against the body (not content.container) to see the rendered items.
// eslint-disable-next-line no-restricted-globals
expect(document.body.textContent).toContain('MoA presets')
// eslint-disable-next-line no-restricted-globals
expect(document.body.textContent).not.toContain('Mixture of Agents')
})
it('renders presets from the catalog even before a session exists', async () => {
$activeSessionId.set('')
const onSelectModel = renderPanel()
const { onSelectModel, content } = renderPanel()
const row = await findByText(document.body, 'MoA: BeastMode')
const row = await content.findByText('MoA: BeastMode')
fireEvent.click(row)
// Pre-session picks are UI state shipped on the next session.create — the