From e9bb6e86fb9ccac56519be4456f3cd00f467a1b3 Mon Sep 17 00:00:00 2001 From: "hermes-seaeye[bot]" <307254004+hermes-seaeye[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 21:23:02 +0000 Subject: [PATCH] fmt(js): `npm run fix` on merge (#106039) Co-authored-by: github-actions[bot] --- apps/desktop/src/app/types.ts | 3 +- .../src/plugins/hermes-bots/bot-row.tsx | 10 +- .../plugins/hermes-bots/group-membership.ts | 4 +- .../plugins/hermes-bots/group-order.test.ts | 21 ++- .../src/plugins/hermes-bots/group-order.ts | 27 +++- .../plugins/hermes-bots/group-turns.test.ts | 151 ++++++++++++++---- .../src/plugins/hermes-bots/pet.test.tsx | 16 +- .../hermes-bots/roster-pane-groups.tsx | 7 +- .../src/plugins/hermes-bots/roster-pane.tsx | 4 +- .../plugins/hermes-bots/row-helpers.test.ts | 4 +- .../src/plugins/hermes-bots/row-helpers.ts | 8 +- tests-js/install-known-failures.test.ts | 4 + tests-js/install-process-close.test.ts | 1 + tests-js/scripts/mock-server.ts | 5 + 14 files changed, 208 insertions(+), 57 deletions(-) diff --git a/apps/desktop/src/app/types.ts b/apps/desktop/src/app/types.ts index 3bf09f66b1..57e85a0a9c 100644 --- a/apps/desktop/src/app/types.ts +++ b/apps/desktop/src/app/types.ts @@ -162,8 +162,7 @@ export type CommandDispatchResponse = | SendCommandDispatchResponse | PrefillCommandDispatchResponse -export type SidebarNavId = - 'artifacts' | 'command-center' | 'cron' | 'messaging' | 'new-session' | 'settings' | 'skills' +export type SidebarNavId = 'artifacts' | 'command-center' | 'cron' | 'messaging' | 'new-session' | 'settings' | 'skills' export interface SidebarNavItem { /** Built-in view id, or a contributed row's namespaced contribution id. */ diff --git a/apps/desktop/src/plugins/hermes-bots/bot-row.tsx b/apps/desktop/src/plugins/hermes-bots/bot-row.tsx index 36afdb3bdf..07152ac510 100644 --- a/apps/desktop/src/plugins/hermes-bots/bot-row.tsx +++ b/apps/desktop/src/plugins/hermes-bots/bot-row.tsx @@ -62,7 +62,15 @@ import { displayName, stripPreviewMarkdown } from './labels' import { duplicateBot } from './profile-ops' import { openRosterBot } from './roster-actions' import { botRosterMeta, botWorkspaceOwnerKey, setBotsWorkspaceOwner } from './routing' -import { A2A_PREFIX_RE, botCanonicalSessionId, botRowOwnsWorkspace, botWorkingMood, previewKind, useTurnBusy, workerActiveAt } from './row-helpers' +import { + A2A_PREFIX_RE, + botCanonicalSessionId, + botRowOwnsWorkspace, + botWorkingMood, + previewKind, + useTurnBusy, + workerActiveAt +} from './row-helpers' import type { GroupMember, RosterRow, SidebarRowLabels } from './types' import { $botSections, $draggingBot, BOT_DRAG_MIME, botSectionId, moveBotsToSection } from './user-sections' diff --git a/apps/desktop/src/plugins/hermes-bots/group-membership.ts b/apps/desktop/src/plugins/hermes-bots/group-membership.ts index bfbe5814f2..e8d9a86fd8 100644 --- a/apps/desktop/src/plugins/hermes-bots/group-membership.ts +++ b/apps/desktop/src/plugins/hermes-bots/group-membership.ts @@ -28,8 +28,8 @@ export function followGroupChat(group: string, onRename: (name: string) => void) return } - const moved = Object.entries(rooms).find(([, room]) => - !room.tombstone && (prior.roomId ? room.roomId === prior.roomId : room === prior) + const moved = Object.entries(rooms).find( + ([, room]) => !room.tombstone && (prior.roomId ? room.roomId === prior.roomId : room === prior) ) if (!moved) { diff --git a/apps/desktop/src/plugins/hermes-bots/group-order.test.ts b/apps/desktop/src/plugins/hermes-bots/group-order.test.ts index 1dbc43a338..8c4e44cd41 100644 --- a/apps/desktop/src/plugins/hermes-bots/group-order.test.ts +++ b/apps/desktop/src/plugins/hermes-bots/group-order.test.ts @@ -14,16 +14,31 @@ describe('room display order', () => { expect(sortGroupRosterRows(rows, {}).map(row => row.name)).toEqual(['Pinned', 'Newer', 'Bot', 'Older']) const rooms = { Older: { rosterOrder: 0 }, Newer: { rosterOrder: 1 } } expect(sortGroupRosterRows(rows, rooms).map(row => row.name)).toEqual(['Pinned', 'Older', 'Bot', 'Newer']) - expect(sortGroupRosterRows(rows.map(row => ({ ...row, activity: row.name === 'Newer' ? 999 : row.activity })), rooms).filter(row => row.kind === 'group').map(row => row.name)).toEqual(['Pinned', 'Older', 'Newer']) + expect( + sortGroupRosterRows( + rows.map(row => ({ ...row, activity: row.name === 'Newer' ? 999 : row.activity })), + rooms + ) + .filter(row => row.kind === 'group') + .map(row => row.name) + ).toEqual(['Pinned', 'Older', 'Newer']) expect(rows[0].name).toBe('Older') }) it('moves only visible same-band rooms while retaining hidden slots and ignoring stale targets', () => { - const ordered = sortGroupRosterRows(rows.filter(row => row.kind === 'group'), {}) + const ordered = sortGroupRosterRows( + rows.filter(row => row.kind === 'group'), + {} + ) expect(reorderGroupRows(ordered, 'Older', -1)).toEqual(['Pinned', 'Older', 'Newer']) expect(reorderGroupRows(ordered, 'Newer', -1)).toBeNull() expect(reorderGroupRows(ordered, 'deleted', 1)).toBeNull() const hidden = { kind: 'group' as const, name: 'Hidden', activity: 2, pinned: false } - expect(reorderGroupRows([ordered[0], ordered[1], hidden, ordered[2]], 'Older', -1, ['Newer', 'Older'])).toEqual(['Pinned', 'Older', 'Hidden', 'Newer']) + expect(reorderGroupRows([ordered[0], ordered[1], hidden, ordered[2]], 'Older', -1, ['Newer', 'Older'])).toEqual([ + 'Pinned', + 'Older', + 'Hidden', + 'Newer' + ]) }) }) diff --git a/apps/desktop/src/plugins/hermes-bots/group-order.ts b/apps/desktop/src/plugins/hermes-bots/group-order.ts index e3a47fc7cc..0fce045635 100644 --- a/apps/desktop/src/plugins/hermes-bots/group-order.ts +++ b/apps/desktop/src/plugins/hermes-bots/group-order.ts @@ -6,23 +6,32 @@ interface OrderRow { } /** Reorder room slots, not bots or folders. Pinning remains the outer band. */ -export function sortGroupRosterRows(rows: T[], rooms: Record): T[] { +export function sortGroupRosterRows( + rows: T[], + rooms: Record +): T[] { const legacy = rows.slice().sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.activity - a.activity) - const groups = legacy.filter(row => row.kind === 'group').sort((a, b) => - Number(b.pinned) - Number(a.pinned) || - (rooms[a.name!]?.rosterOrder ?? Infinity) - (rooms[b.name!]?.rosterOrder ?? Infinity) - ) + const groups = legacy + .filter(row => row.kind === 'group') + .sort( + (a, b) => + Number(b.pinned) - Number(a.pinned) || + (rooms[a.name!]?.rosterOrder ?? Infinity) - (rooms[b.name!]?.rosterOrder ?? Infinity) + ) let index = 0 - return legacy.map(row => row.kind === 'group' ? groups[index++] : row) + return legacy.map(row => (row.kind === 'group' ? groups[index++] : row)) } /** Swap visible neighbours without dropping filtered-out rooms from the order. */ export function reorderGroupRows(rows: OrderRow[], name: string, delta: -1 | 1, visible?: string[]): string[] | null { const row = rows.find(row => row.name === name) - const band = rows.filter(candidate => candidate.kind === 'group' && candidate.pinned === row?.pinned && (!visible || visible.includes(candidate.name!))) + const band = rows.filter( + candidate => + candidate.kind === 'group' && candidate.pinned === row?.pinned && (!visible || visible.includes(candidate.name!)) + ) const index = band.findIndex(candidate => candidate.name === name) const neighbour = index >= 0 ? band[index + delta] : undefined @@ -30,5 +39,7 @@ export function reorderGroupRows(rows: OrderRow[], name: string, delta: -1 | 1, return null } - return rows.filter(row => row.kind === 'group').map(row => row.name === name ? neighbour.name! : row.name === neighbour.name ? name : row.name!) + return rows + .filter(row => row.kind === 'group') + .map(row => (row.name === name ? neighbour.name! : row.name === neighbour.name ? name : row.name!)) } diff --git a/apps/desktop/src/plugins/hermes-bots/group-turns.test.ts b/apps/desktop/src/plugins/hermes-bots/group-turns.test.ts index bf85c51a62..0c278f9ecd 100644 --- a/apps/desktop/src/plugins/hermes-bots/group-turns.test.ts +++ b/apps/desktop/src/plugins/hermes-bots/group-turns.test.ts @@ -369,7 +369,8 @@ describe('clarify and approvals (#90694)', () => { // on $groupNeedsYou/$groupClarify AFTER the turn lands proves nothing: // the clarify has already resolved and its mirror is gone by then. onResumePoll: () => { - sawPendingAttention = sawPendingAttention || live!.turns.groupHasPendingClarify(live!.chat.$groupClarify.get(), 'Core') + sawPendingAttention = + sawPendingAttention || live!.turns.groupHasPendingClarify(live!.chat.$groupClarify.get(), 'Core') }, turn: () => 'targeting staging' }) @@ -506,17 +507,24 @@ describe('clarify and approvals (#90694)', () => { it('keeps late prompt snapshots on the live room and never revives a disbanded room', async ({ onTestFinished }) => { for (const roomId of ['stable-room', undefined]) { for (const disband of [false, true]) { - const room = await loadRoom({ turn: ({ n }) => n === 1 ? 'Completed reply' : '(pass)' }) + const room = await loadRoom({ turn: ({ n }) => (n === 1 ? 'Completed reply' : '(pass)') }) const view = await import('./group-chat-view') const member = { name: 'research', title: '' } room.chat.updateGroupChat('Core', current => ({ - ...current, roomId, running: true, epoch: 1, + ...current, + roomId, + running: true, + epoch: 1, log: [{ id: 'input', at: 1, from: { kind: 'user', name: 'You' }, text: '@research check', thread: 'thread' }] })) let entered!: () => void let release!: () => void - const polled = new Promise(resolve => { entered = resolve }) - const held = new Promise(resolve => { release = resolve }) + const polled = new Promise(resolve => { + entered = resolve + }) + const held = new Promise(resolve => { + release = resolve + }) const original = host.request as (method: string, params: Record) => Promise let submitted = false let answered = false @@ -525,12 +533,19 @@ describe('clarify and approvals (#90694)', () => { host.request = async (method: string, params: Record) => { const result = await original(method, params) - if (method === 'prompt.submit') {submitted = true} + if (method === 'prompt.submit') { + submitted = true + } - if (method === 'clarify.respond') {answered = true} + if (method === 'clarify.respond') { + answered = true + } if (method === 'session.resume' && submitted && !answered) { - if (++polls === 1) { entered(); await held } + if (++polls === 1) { + entered() + await held + } return { ...result, pending_clarify: CLARIFY } } @@ -546,14 +561,19 @@ describe('clarify and approvals (#90694)', () => { release() await drive expect(Object.values(room.chat.$groupClarify.get())).toHaveLength(0) - expect(room.chat.$groupChats.get().Core === undefined || room.chat.$groupChats.get().Core.tombstone).toBe(true) + expect(room.chat.$groupChats.get().Core === undefined || room.chat.$groupChats.get().Core.tombstone).toBe( + true + ) expect(room.chat.$groupChats.get().Core?.log || []).toHaveLength(0) } else { await view.renameGroupChat('Core', 'Renamed', []) const mirrored = new Promise(resolve => { const stop = room.chat.$groupClarify.listen(entries => { - if (Object.keys(entries).length) { stop(); resolve() } + if (Object.keys(entries).length) { + stop() + resolve() + } }) }) @@ -566,7 +586,12 @@ describe('clarify and approvals (#90694)', () => { expect(correctRoom).toBe('Renamed') expect(Object.keys(room.chat.$groupChats.get())).toEqual(['Renamed']) expect(room.chat.$groupChats.get().Renamed.running).toBe(false) - expect(room.chat.$groupChats.get().Renamed.log.filter(entry => entry.from.kind === 'member').map(entry => entry.text)).toEqual(['Completed reply']) + expect( + room.chat.$groupChats + .get() + .Renamed.log.filter(entry => entry.from.kind === 'member') + .map(entry => entry.text) + ).toEqual(['Completed reply']) expect(Object.values(room.chat.$groupClarify.get())).toHaveLength(0) } } @@ -581,10 +606,20 @@ describe('clarify and approvals (#90694)', () => { const data = await import('./data') const members = [{ name: 'research' }, { name: 'ops' }] room.chat.updateGroupChat('Core', current => ({ - ...current, roomId: 'old-rejection-room', running: true, + ...current, + roomId: 'old-rejection-room', + running: true, log: continuation - ? [{ id: 'handoff', at: 1, from: { kind: 'member', name: 'research' }, text: '@ops check', thread: 'thread' }, - { id: 'input', at: 2, from: { kind: 'user', name: 'You' }, text: '@research check', thread: 'thread' }] + ? [ + { + id: 'handoff', + at: 1, + from: { kind: 'member', name: 'research' }, + text: '@ops check', + thread: 'thread' + }, + { id: 'input', at: 2, from: { kind: 'user', name: 'You' }, text: '@research check', thread: 'thread' } + ] : [{ id: 'input', at: 1, from: { kind: 'user', name: 'You' }, text: 'check', thread: 'thread' }], watermarks: { 'thread::research': continuation ? 2 : 0 } })) @@ -592,23 +627,47 @@ describe('clarify and approvals (#90694)', () => { // handoff is driven by the continuation phase. let phaseEntered!: () => void let release!: () => void - const entered = new Promise(resolve => { phaseEntered = resolve }) - const held = new Promise(resolve => { release = resolve }) + const entered = new Promise(resolve => { + phaseEntered = resolve + }) + const held = new Promise(resolve => { + release = resolve + }) const original = host.request as (method: string, params: Record) => Promise + host.request = async (method: string, params: Record) => { - if (method === rejectedMethod) { phaseEntered(); await held; throw new Error('401 unauthorized late rejection') } + if (method === rejectedMethod) { + phaseEntered() + await held + throw new Error('401 unauthorized late rejection') + } return original(method, params) } + const drive = room.rounds.runGroupChatRounds('Core', members, 'thread') await entered await view.disbandGroupChat('Core', []) - room.chat.updateGroupChat('Core', current => ({ ...current, roomId: 'replacement-rejection-room', tombstone: false })) + room.chat.updateGroupChat('Core', current => ({ + ...current, + roomId: 'replacement-rejection-room', + tombstone: false + })) room.chat.appendGroupChatEntry('Core', { kind: 'member', name: 'research' }, '@user replacement needs you') - const before = structuredClone({ rooms: room.chat.$groupChats.get(), activity: activity.$groupActivity.get(), attention: data.$botAttention.get(), needsYou: room.chat.$groupNeedsYou.get() }) + const before = structuredClone({ + rooms: room.chat.$groupChats.get(), + activity: activity.$groupActivity.get(), + attention: data.$botAttention.get(), + needsYou: room.chat.$groupNeedsYou.get() + }) release() await drive - expect({ rooms: room.chat.$groupChats.get(), activity: activity.$groupActivity.get(), attention: data.$botAttention.get(), needsYou: room.chat.$groupNeedsYou.get() }).toEqual(before) + expect({ + rooms: room.chat.$groupChats.get(), + activity: activity.$groupActivity.get(), + attention: data.$botAttention.get(), + needsYou: room.chat.$groupNeedsYou.get() + }).toEqual(before) expect(room.gateway.rpcFor('prompt.submit')).toHaveLength(0) } } @@ -624,8 +683,12 @@ describe('clarify and approvals (#90694)', () => { room.chat.updateGroupChat('Core', current => ({ ...current, roomId: 'retired-room' })) let entered!: () => void let release!: () => void - const polled = new Promise(resolve => { entered = resolve }) - const held = new Promise(resolve => { release = resolve }) + const polled = new Promise(resolve => { + entered = resolve + }) + const held = new Promise(resolve => { + release = resolve + }) const original = host.request as (method: string, params: Record) => Promise let submitted = false @@ -638,7 +701,9 @@ describe('clarify and approvals (#90694)', () => { const result = await original(method, params) - if (method === 'prompt.submit') { submitted = true } + if (method === 'prompt.submit') { + submitted = true + } return result } @@ -667,30 +732,47 @@ describe('clarify and approvals (#90694)', () => { const view = await import('./group-chat-view') const members = [{ name: 'research' }, { name: 'ops' }] room.chat.updateGroupChat('Core', current => ({ - ...current, roomId: 'old-harvest-room', running: true, + ...current, + roomId: 'old-harvest-room', + running: true, stranded: { research: 0, ops: 0 } })) let tick!: () => void const previousWindow = globalThis.window - vi.stubGlobal('window', { setTimeout: (callback: () => void) => { tick = callback; + vi.stubGlobal('window', { + setTimeout: (callback: () => void) => { + tick = callback - return 0 } }) - onTestFinished(() => { vi.stubGlobal('window', previousWindow) }) + return 0 + } + }) + onTestFinished(() => { + vi.stubGlobal('window', previousWindow) + }) let entered!: () => void let release!: () => void - const polled = new Promise(resolve => { entered = resolve }) - const held = new Promise(resolve => { release = resolve }) + const polled = new Promise(resolve => { + entered = resolve + }) + const held = new Promise(resolve => { + release = resolve + }) let background = false const backgroundProfiles: unknown[] = [] const original = host.request as (method: string, params: Record) => Promise host.request = async (method: string, params: Record) => { - if (method !== 'session.resume') { return original(method, params) } + if (method !== 'session.resume') { + return original(method, params) + } if (background) { backgroundProfiles.push(params.profile) - if (params.profile === 'research') { entered(); await held } + if (params.profile === 'research') { + entered() + await held + } } return { running: true, pending_clarify: CLARIFY } @@ -704,7 +786,9 @@ describe('clarify and approvals (#90694)', () => { const { setImmediate } = await import('node:timers/promises') await setImmediate() room.chat.updateGroupChat('Core', current => ({ - ...current, roomId: 'new-harvest-room', stranded: { research: 0, ops: 0 } + ...current, + roomId: 'new-harvest-room', + stranded: { research: 0, ops: 0 } })) const roomsBefore = structuredClone(room.chat.$groupChats.get()) const promptsBefore = structuredClone(room.chat.$groupClarify.get()) @@ -723,7 +807,8 @@ describe('clarify and approvals (#90694)', () => { const room = await loadRoom({ approvalUntil: { research: { payload: APPROVAL, until: 3 } }, onResumePoll: () => { - sawPendingAttention = sawPendingAttention || live!.turns.groupHasPendingClarify(live!.chat.$groupClarify.get(), 'Core') + sawPendingAttention = + sawPendingAttention || live!.turns.groupHasPendingClarify(live!.chat.$groupClarify.get(), 'Core') }, turn: () => 'build cleaned' }) diff --git a/apps/desktop/src/plugins/hermes-bots/pet.test.tsx b/apps/desktop/src/plugins/hermes-bots/pet.test.tsx index a3d08e8728..2206c51952 100644 --- a/apps/desktop/src/plugins/hermes-bots/pet.test.tsx +++ b/apps/desktop/src/plugins/hermes-bots/pet.test.tsx @@ -103,9 +103,15 @@ describe('the pet gallery', () => { }) it('keeps selection while scrolling for more and resets the search window', async () => { - useQueryMock.mockReturnValue({ data: { pets: Array.from({ length: 60 }, (_, i) => ({ - displayName: `Pet ${i}`, slug: `pet-${i}`, spritesheetUrl: SHEET - })) } }) + useQueryMock.mockReturnValue({ + data: { + pets: Array.from({ length: 60 }, (_, i) => ({ + displayName: `Pet ${i}`, + slug: `pet-${i}`, + spritesheetUrl: SHEET + })) + } + }) stubFetch(async () => ({ blob: async () => new Blob() })) const PetTab = await loadPetTab() const onImage = vi.fn() @@ -115,7 +121,9 @@ describe('the pet gallery', () => { await waitFor(() => expect(onImage).toHaveBeenCalledWith('data:image/png;base64,ok')) const scroller = first.parentElement!.parentElement! Object.defineProperties(scroller, { - clientHeight: { value: 220 }, scrollHeight: { value: 600 }, scrollTop: { value: 400 } + clientHeight: { value: 220 }, + scrollHeight: { value: 600 }, + scrollTop: { value: 400 } }) fireEvent.scroll(scroller) expect(view.getByText('Pet 47')).toBeTruthy() diff --git a/apps/desktop/src/plugins/hermes-bots/roster-pane-groups.tsx b/apps/desktop/src/plugins/hermes-bots/roster-pane-groups.tsx index 417aa65666..cc96cb6ec3 100644 --- a/apps/desktop/src/plugins/hermes-bots/roster-pane-groups.tsx +++ b/apps/desktop/src/plugins/hermes-bots/roster-pane-groups.tsx @@ -43,7 +43,12 @@ export function RosterGroupRowView({ activity: groupLastActivity(current[name]) })) - const order = reorderGroupRows(sortGroupRosterRows(rows, current), name, delta, sortedGroupRows.map(row => row.name)) + const order = reorderGroupRows( + sortGroupRosterRows(rows, current), + name, + delta, + sortedGroupRows.map(row => row.name) + ) order?.forEach((name, rosterOrder) => { updateGroupChat(name, room => ({ ...room, rosterOrder }), { sync: false }) diff --git a/apps/desktop/src/plugins/hermes-bots/roster-pane.tsx b/apps/desktop/src/plugins/hermes-bots/roster-pane.tsx index ceaeae3145..4ea9136411 100644 --- a/apps/desktop/src/plugins/hermes-bots/roster-pane.tsx +++ b/apps/desktop/src/plugins/hermes-bots/roster-pane.tsx @@ -296,7 +296,9 @@ export function BotsPane() { // and the persisted connection registry hydrate. Keep that transition in a // neutral loading state instead of flashing the first-run "No bots" copy. const initialRosterLoading = !data && !error && roster.length === 0 - const activeRosterKeys = new Set(activeBots(roster, workingOwner, turnBusy, Date.now(), activeConnectionId).map(botRosterKey)) + const activeRosterKeys = new Set( + activeBots(roster, workingOwner, turnBusy, Date.now(), activeConnectionId).map(botRosterKey) + ) const gatewayOptions = rosterGatewayOptions(sourceSnapshot, roster) const selectedGateway = gatewayOptions.find(option => option.connectionId === gatewayFilter) const gatewayFilterExists = gatewayFilter === 'all' || Boolean(selectedGateway) diff --git a/apps/desktop/src/plugins/hermes-bots/row-helpers.test.ts b/apps/desktop/src/plugins/hermes-bots/row-helpers.test.ts index 241fa64dd6..bf1a3a746a 100644 --- a/apps/desktop/src/plugins/hermes-bots/row-helpers.test.ts +++ b/apps/desktop/src/plugins/hermes-bots/row-helpers.test.ts @@ -105,7 +105,9 @@ describe('which bots are working right now', () => { expect(activeBots([local, remote], owner, false, NOW)).toEqual([]) expect(activeBots([local, remote], null, true, NOW)).toEqual([]) expect(activeBots([local, remote], { ...owner, authoritative: false }, true, NOW)).toEqual([]) - expect(activeBots([row({ name: 'analyst', remoteSource: true })], { ...owner, connectionId: '' }, true, NOW)).toEqual([]) + expect( + activeBots([row({ name: 'analyst', remoteSource: true })], { ...owner, connectionId: '' }, true, NOW) + ).toEqual([]) }) it('includes activity inside the liveness window and excludes activity outside it', () => { diff --git a/apps/desktop/src/plugins/hermes-bots/row-helpers.ts b/apps/desktop/src/plugins/hermes-bots/row-helpers.ts index 899b7e5daf..09d7f5bc56 100644 --- a/apps/desktop/src/plugins/hermes-bots/row-helpers.ts +++ b/apps/desktop/src/plugins/hermes-bots/row-helpers.ts @@ -108,7 +108,13 @@ export function botWorkingMood( ): 'idle' | 'think' | 'work' { const botConnectionId = bot.connectionId || (bot.remoteSource ? '' : activeConnectionId) - if (turnBusy && owner?.authoritative && owner.connectionId && owner.name === bot.name && owner.connectionId === botConnectionId) { + if ( + turnBusy && + owner?.authoritative && + owner.connectionId && + owner.name === bot.name && + owner.connectionId === botConnectionId + ) { return 'think' } diff --git a/tests-js/install-known-failures.test.ts b/tests-js/install-known-failures.test.ts index 7799f27d7a..acc081e54c 100644 --- a/tests-js/install-known-failures.test.ts +++ b/tests-js/install-known-failures.test.ts @@ -8,11 +8,13 @@ import { describe, expect, it } from 'vitest' const { matchKnownFailure, rules } = createRequire(import.meta.url)('../tests/install/e2e-assets/known-failures.cjs') const classifier = path.resolve(import.meta.dirname, '../tests/install/e2e-assets/known-failures.cjs') + const lockedLog = [ 'error: failed to remove file `C:/install/venv/Lib/site-packages/../../Scripts/hermes.exe`: Access is denied. (os error 5)', 'File "C:/install/venv/Scripts/hermes.exe/__main__.py", line 10, in ', "subprocess.CalledProcessError: Command '['uv', 'pip', 'install', '-e', '.', '--quiet']' returned non-zero exit status 2.", ].join('\n') + const base = { platform: 'windows', phase: 'update', commit: 'a370ab8391ca5f8de7ebbc449f05cb0df36ade7c', installMethod: 'installer-script', updateMethod: 'hermes-update', @@ -42,6 +44,7 @@ describe('known install failures', () => { error: 'E2E ASSERTION FAILED: app driven via captured hermes desktop spec; update completed', logs: { desktop: '[hermes] [updates] no staged updater; surfacing manual `hermes update` for CLI install at C:/install\n[hermes] [updates] manual: hermes update\n' }, } + expect(matchKnownFailure(sample)?.id).toBe('windows-july-manual-app-update') expect(matchKnownFailure({ ...sample, installMethod: 'desktop-installer@latest' })).toBeNull() expect(matchKnownFailure({ ...sample, error: 'onboarding timed out' })).toBeNull() @@ -50,6 +53,7 @@ describe('known install failures', () => { it('CLI writes a receipt and exits zero only on a confirmed match', () => { const root = mkdtempSync(path.join(os.tmpdir(), 'known-install-')) + try { mkdirSync(path.join(root, 'logs')) writeFileSync(path.join(root, 'shas.json'), '\uFEFF' + JSON.stringify({ old: base.commit, current: 'f'.repeat(40), old_ref: 'v2026.3.12' })) diff --git a/tests-js/install-process-close.test.ts b/tests-js/install-process-close.test.ts index ce8b83c9b9..38f69d2d1e 100644 --- a/tests-js/install-process-close.test.ts +++ b/tests-js/install-process-close.test.ts @@ -24,6 +24,7 @@ it('waits for native close, not exit, and retains a close observed before hand-o it('fails if the launched process never closes', async () => { vi.useFakeTimers() + try { const waitForClose = observeProcessClose(Object.assign(new EventEmitter(), { stdio: [], exitCode: null, signalCode: null })) const completion = expect(waitForClose(2_000)).rejects.toThrow('Electron process did not close') diff --git a/tests-js/scripts/mock-server.ts b/tests-js/scripts/mock-server.ts index 9f49873803..d9e0806c0c 100644 --- a/tests-js/scripts/mock-server.ts +++ b/tests-js/scripts/mock-server.ts @@ -505,6 +505,7 @@ export function startMockServer(options: MockServerOptions = {}): Promise typeof message?.content === 'string' && message.content.includes(VERIFICATION_STOP_TRIGGER), ) @@ -517,7 +518,9 @@ export function startMockServer(options: MockServerOptions = {}): Promise { if (stream) { streamScriptedTurn(res, model, turn) @@ -533,6 +536,7 @@ export function startMockServer(options: MockServerOptions = {}): Promise