emma-hermes/apps/desktop/src/app/skills/index.test.tsx

119 lines
4.2 KiB
TypeScript

// @vitest-environment jsdom
import { QueryClientProvider } from '@tanstack/react-query'
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as HermesApi from '@/hermes'
import { queryClient } from '@/lib/query-client'
const getSkills = vi.fn()
const getToolsets = vi.fn()
const toggleSkill = vi.fn()
const toggleToolset = vi.fn()
const getToolsetConfig = vi.fn()
const selectToolsetProvider = vi.fn()
const getUsageAnalytics = vi.fn()
// Partial mock: keep the real module (SkillsView pulls in @/store/profile,
// whose import-time subscription calls setApiRequestProfile) and stub only the
// calls we assert on.
vi.mock('@/hermes', async importOriginal => ({
...(await importOriginal<typeof HermesApi>()),
getSkills: () => getSkills(),
getToolsets: () => getToolsets(),
toggleSkill: (name: string, enabled: boolean) => toggleSkill(name, enabled),
toggleToolset: (name: string, enabled: boolean) => toggleToolset(name, enabled),
getToolsetConfig: (name: string) => getToolsetConfig(name),
selectToolsetProvider: (toolset: string, provider: string) => selectToolsetProvider(toolset, provider),
getUsageAnalytics: (days: number) => getUsageAnalytics(days)
}))
// Notifications hit nanostores/timers we don't care about here.
vi.mock('@/store/notifications', () => ({
notify: vi.fn(),
notifyError: vi.fn()
}))
function toolset(overrides: Record<string, unknown> = {}) {
return {
name: 'web',
label: 'Web Search',
description: 'web_search, web_extract',
enabled: true,
available: true,
configured: true,
tools: ['web_search', 'web_extract'],
...overrides
}
}
async function renderSkills() {
const { SkillsView } = await import('./index')
let result: ReturnType<typeof render>
await act(async () => {
result = render(
// SkillsView reads skills/toolsets via useQuery, so it needs a provider.
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={['/skills?tab=toolsets']}>
<SkillsView />
</MemoryRouter>
</QueryClientProvider>
)
})
return result!
}
beforeEach(() => {
getSkills.mockResolvedValue([])
getToolsets.mockResolvedValue([toolset()])
toggleToolset.mockResolvedValue({ ok: true, name: 'web', enabled: false })
getToolsetConfig.mockResolvedValue({ has_category: true, active_provider: null, providers: [] })
getUsageAnalytics.mockResolvedValue({ tools: [] })
})
afterEach(() => {
cleanup()
vi.clearAllMocks()
// Shared singleton client — drop cached skills/toolsets so each test refetches.
queryClient.clear()
})
describe('SkillsView toolset management', () => {
it('renders a switch for each toolset and toggles it off', async () => {
await renderSkills()
const sw = await screen.findByRole('switch', { name: 'Toggle Web Search toolset' })
expect(sw.getAttribute('aria-checked')).toBe('true')
await act(async () => {
fireEvent.click(sw)
})
await waitFor(() => expect(toggleToolset).toHaveBeenCalledWith('web', false))
})
it('renders toolset titles without leading emoji', async () => {
getToolsets.mockResolvedValue([toolset({ name: 'cronjob', label: '⏰ Cron Jobs', description: 'cron tools' })])
await renderSkills()
// The label renders in both the row and the auto-selected detail header, so
// assert via the switch's (emoji-stripped) accessible name and the absence
// of the emoji rather than a single-match text lookup.
await screen.findByRole('switch', { name: 'Toggle Cron Jobs toolset' })
expect(screen.queryByText(/⏰/)).toBeNull()
})
it('renders the provider config panel inline for the selected toolset', async () => {
// The master-detail UI dropped the resting "Configured" pill and the
// "Configure" expander: the detail column auto-selects the first toolset
// and renders its config panel directly, which fetches on mount.
await renderSkills()
await screen.findByRole('switch', { name: 'Toggle Web Search toolset' })
await waitFor(() => expect(getToolsetConfig).toHaveBeenCalledWith('web'))
})
})