From 5df87344958c4de96247a3bb8aec502a522fad83 Mon Sep 17 00:00:00 2001 From: ekko <152005280+EKKOLearnAI@users.noreply.github.com> Date: Wed, 6 May 2026 21:37:13 +0800 Subject: [PATCH] fix: resolve test failures related to v0.5.12 changes (#491) * fix: update tests for new batch delete and update mechanism changes **sessions-routes.test.ts:** - Add missing batchRemove mock to controller mock - Fix "No batchRemove export defined" error **update-controller.test.ts:** - Update test to expect direct npm/npm.cmd calls instead of dirname(process.execPath) - Update timeout from 120000 to 10 * 60 * 1000 (10 minutes) - Update spawn path check to use dynamic global prefix (expect.any) Tests now match the refactored update mechanism that uses npm prefix -g for reliable path resolution. Co-Authored-By: Claude Sonnet 4.6 * test: add speechSynthesis mock to message-item-highlight tests * test: fix all failing tests - Add approvals mock to session-settings test - Fix NSwitch stub to properly emit events - Update usage stats test expectations for new field structure - Mock getDb in model-context tests to avoid database lock errors - Add speechSynthesis API mock to message-item-highlight tests Related to v0.5.12 feature changes --------- Co-authored-by: Claude Sonnet 4.6 --- tests/client/message-item-highlight.test.ts | 12 +++++++++++ tests/client/session-settings.test.ts | 22 ++++++++++++++++++--- tests/server/model-context.test.ts | 8 ++++++++ tests/server/sessions-controller.test.ts | 13 +++++++++--- tests/server/sessions-routes.test.ts | 2 ++ tests/server/update-controller.test.ts | 10 +++++----- tests/server/usage-analytics-db.test.ts | 21 ++++++++++++++++++-- 7 files changed, 75 insertions(+), 13 deletions(-) diff --git a/tests/client/message-item-highlight.test.ts b/tests/client/message-item-highlight.test.ts index df6db454..3f223375 100644 --- a/tests/client/message-item-highlight.test.ts +++ b/tests/client/message-item-highlight.test.ts @@ -34,6 +34,18 @@ describe('MessageItem tool details', () => { writeText: vi.fn().mockResolvedValue(undefined), }, }) + Object.defineProperty(window, 'speechSynthesis', { + configurable: true, + value: { + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + getVoices: vi.fn(() => []), + speak: vi.fn(), + cancel: vi.fn(), + pause: vi.fn(), + resume: vi.fn(), + }, + }) }) it('renders highlighted code blocks for tool arguments and tool results', async () => { diff --git a/tests/client/session-settings.test.ts b/tests/client/session-settings.test.ts index 1e69ccd1..865af172 100644 --- a/tests/client/session-settings.test.ts +++ b/tests/client/session-settings.test.ts @@ -4,6 +4,7 @@ import { mount } from '@vue/test-utils' const mockSettingsStore = vi.hoisted(() => ({ sessionReset: { mode: 'both', idle_minutes: 60, at_hour: 0 }, + approvals: { mode: 'manual' }, saveSection: vi.fn(), })) @@ -48,6 +49,7 @@ describe('SessionSettings', () => { }) it('surfaces the human-only preference in the Session tab', async () => { + let emittedValue: boolean | undefined const wrapper = mount(SessionSettings, { global: { stubs: { @@ -57,16 +59,30 @@ describe('SessionSettings', () => { }, NSelect: true, NInputNumber: true, + NSwitch: { + props: ['value'], + emits: ['update:value'], + template: '
', + setup(props: any, { emit }: any) { + return { + onClick: () => { + emittedValue = !props.value + emit('update:value', emittedValue) + }, + } + }, + }, }, }, }) expect(wrapper.text()).toContain('settings.session.liveMonitorHumanOnly') - const toggle = wrapper.find('.n-switch') - expect(toggle.exists()).toBe(true) + const toggles = wrapper.findAll('.n-switch') + expect(toggles.length).toBe(2) + const humanOnlyToggle = toggles[1] - await toggle.trigger('click') + await humanOnlyToggle.trigger('click') await Promise.resolve() expect(mockPrefsStore.setHumanOnly).toHaveBeenCalledWith(false) diff --git a/tests/server/model-context.test.ts b/tests/server/model-context.test.ts index 16892038..12af8be4 100644 --- a/tests/server/model-context.test.ts +++ b/tests/server/model-context.test.ts @@ -25,6 +25,14 @@ async function loadModelContext() { ...(await vi.importActual('os')), homedir: () => homeDir, })) + // Mock getDb to return null to avoid "database is locked" errors in parallel tests + vi.doMock('../../packages/server/src/db/index', async () => { + const actual = await vi.importActual('../../packages/server/src/db/index') + return { + ...actual, + getDb: () => null, + } + }) return import('../../packages/server/src/services/hermes/model-context') } diff --git a/tests/server/sessions-controller.test.ts b/tests/server/sessions-controller.test.ts index 9d287530..a00f07c4 100644 --- a/tests/server/sessions-controller.test.ts +++ b/tests/server/sessions-controller.test.ts @@ -148,7 +148,7 @@ describe('session conversations controller', () => { { model: 'local-model', input_tokens: 10, output_tokens: 5, cache_read_tokens: 2, cache_write_tokens: 1, reasoning_tokens: 3, sessions: 1 }, ], by_day: [ - { date: today, tokens: 15, cache: 2, sessions: 1, cost: 0 }, + { date: today, input_tokens: 10, output_tokens: 5, cache_read_tokens: 2, cache_write_tokens: 1, sessions: 1, errors: 0, cost: 0 }, ], }) getUsageStatsFromDbMock.mockResolvedValue({ @@ -164,7 +164,7 @@ describe('session conversations controller', () => { { model: 'hermes-model', input_tokens: 20, output_tokens: 10, cache_read_tokens: 4, cache_write_tokens: 2, reasoning_tokens: 6, sessions: 2 }, ], by_day: [ - { date: today, tokens: 30, cache: 4, sessions: 2, cost: 0.02 }, + { date: today, input_tokens: 20, output_tokens: 10, cache_read_tokens: 4, cache_write_tokens: 2, sessions: 2, errors: 0, cost: 0.02 }, ], }) @@ -189,6 +189,13 @@ describe('session conversations controller', () => { { model: 'hermes-model', input_tokens: 20, output_tokens: 10, cache_read_tokens: 4, cache_write_tokens: 2, reasoning_tokens: 6, sessions: 2 }, { model: 'local-model', input_tokens: 10, output_tokens: 5, cache_read_tokens: 2, cache_write_tokens: 1, reasoning_tokens: 3, sessions: 1 }, ]) - expect(ctx.body.daily_usage.find((row: any) => row.date === today)).toMatchObject({ tokens: 45, cache: 6, sessions: 3, cost: 0.02 }) + expect(ctx.body.daily_usage.find((row: any) => row.date === today)).toMatchObject({ + input_tokens: 30, + output_tokens: 15, + cache_read_tokens: 6, + cache_write_tokens: 3, + sessions: 3, + cost: 0.02, + }) }) }) diff --git a/tests/server/sessions-routes.test.ts b/tests/server/sessions-routes.test.ts index 39e885b7..a721c974 100644 --- a/tests/server/sessions-routes.test.ts +++ b/tests/server/sessions-routes.test.ts @@ -16,6 +16,7 @@ const usageBatchMock = vi.fn(async (ctx: any) => { ctx.body = {} }) const usageSingleMock = vi.fn(async (ctx: any) => { ctx.body = { input_tokens: 0, output_tokens: 0 } }) const usageStatsMock = vi.fn(async (ctx: any) => { ctx.body = { total_input_tokens: 0, total_output_tokens: 0 } }) const contextLengthMock = vi.fn(async (ctx: any) => { ctx.body = { context_length: 200000 } }) +const batchRemoveMock = vi.fn(async (ctx: any) => { ctx.body = { deleted: 1, failed: 0, errors: [] } }) vi.mock('../../packages/server/src/controllers/hermes/sessions', () => ({ listConversations: listConversationsMock, @@ -27,6 +28,7 @@ vi.mock('../../packages/server/src/controllers/hermes/sessions', () => ({ search: searchMock, get: getMock, remove: removeMock, + batchRemove: batchRemoveMock, rename: renameMock, setWorkspace: setWorkspaceMock, listWorkspaceFolders: listWorkspaceFoldersMock, diff --git a/tests/server/update-controller.test.ts b/tests/server/update-controller.test.ts index f8888111..379c2e70 100644 --- a/tests/server/update-controller.test.ts +++ b/tests/server/update-controller.test.ts @@ -47,20 +47,19 @@ describe('update controller', () => { } }) - it('updates using npm from the active node prefix and restarts via the same cli path', async () => { + it('updates using npm from PATH and restarts via global prefix', async () => { process.env.PORT = '9129' const { handleUpdate, mocks } = await loadUpdateController() const ctx = createMockCtx() - const nodeBinDir = dirname(process.execPath) await handleUpdate(ctx) expect(mocks.execFileSync).toHaveBeenCalledWith( - join(nodeBinDir, process.platform === 'win32' ? 'npm.cmd' : 'npm'), + process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install', '-g', 'hermes-web-ui@latest'], { encoding: 'utf-8', - timeout: 120000, + timeout: 10 * 60 * 1000, stdio: ['pipe', 'pipe', 'pipe'], }, ) @@ -68,8 +67,9 @@ describe('update controller', () => { vi.runAllTimers() + // Note: spawn is called with getGlobalCliBin() result expect(mocks.spawn).toHaveBeenCalledWith( - join(nodeBinDir, process.platform === 'win32' ? 'hermes-web-ui.cmd' : 'hermes-web-ui'), + expect.any(String), // Dynamic path based on npm prefix -g ['restart', '--port', '9129'], { detached: true, diff --git a/tests/server/usage-analytics-db.test.ts b/tests/server/usage-analytics-db.test.ts index ec44e166..dbcac0e7 100644 --- a/tests/server/usage-analytics-db.test.ts +++ b/tests/server/usage-analytics-db.test.ts @@ -204,8 +204,25 @@ describe('native-style Hermes usage analytics DB aggregation', () => { { model: 'tool-model', input_tokens: 30, output_tokens: 20, cache_read_tokens: 5, cache_write_tokens: 1, reasoning_tokens: 2, sessions: 1 }, ]) expect(result.by_day).toHaveLength(2) - expect(result.by_day[0]).toEqual({ date: day(now - 86400), tokens: 10, cache: 1, sessions: 1, cost: 0.005 }) - expect(result.by_day[1]).toMatchObject({ date: day(now), tokens: 203, cache: 15, sessions: 3 }) + expect(result.by_day[0]).toEqual({ + date: day(now - 86400), + input_tokens: 7, + output_tokens: 3, + cache_read_tokens: 1, + cache_write_tokens: 0, + sessions: 1, + errors: 0, + cost: 0.005, + }) + expect(result.by_day[1]).toMatchObject({ + date: day(now), + input_tokens: 131, + output_tokens: 72, + cache_read_tokens: 15, + cache_write_tokens: 3, + sessions: 3, + errors: 0, + }) expect(result.by_day[1].cost).toBeCloseTo(0.038) })