hermes-agent/apps/desktop/src/hermes-parity.test.ts
Teknium c7103c637c
feat(desktop): CLI/dashboard parity — skills hub, MCP test/toggle/catalog, maintenance ops, log filters (#57441)
* feat(desktop): CLI/dashboard parity — skills hub browser, MCP test/toggle/catalog, maintenance ops, log filters

Brings desktop GUI to parity with hermes skills/mcp/doctor/backup/debug-share/
curator/memory CLI commands and the dashboard's System + Skills-hub pages:

- Skills page: new Browse Hub tab (search official/GitHub/community sources,
  preview SKILL.md, security scan verdicts, install/update with live action log)
- MCP settings: connection test (tool listing), per-server enable/disable
  toggle, and a Catalog tab installing Nous-approved MCP servers with env prompts
- Command Center: new Maintenance section (doctor, security audit, backup,
  debug share links, curator status/pause/run, memory file status + reset)
- Command Center system logs: file (agent/errors/gateway/desktop), level, and
  substring filters instead of a fixed agent.log tail
- hermes.ts API client + types for all the above; en/zh locale strings (ja and
  zh-hant inherit via defineLocale)

* feat(desktop): backend model catalogs in toolset config — hermes tools parity

Completes the `hermes tools` parity gap: after picking an image/video
generation backend the CLI runs a model picker (e.g. FAL's multi-model
catalog with speed/strengths/price); the desktop toolset drawer now has the
same flow as a radio-card list.

- web_server: GET /api/tools/toolsets/{name}/models (catalog + current +
  default for the active or named provider row) and PUT .../model
  (validated write to image_gen.model / video_gen.model), reusing the CLI's
  plugin catalog helpers so GUI and `hermes tools` stay in lockstep
- desktop: ModelCatalogPicker in ToolsetConfigPanel — per-model cards with
  speed/strengths/price, in-use + default badges, disabled until the
  backend is the active one; provider selection now mirrors is_active
  locally so the catalog unlocks without a refetch
- tests: 3 backend endpoint tests (catalog shape invariants, persist +
  validation), 2 component tests, 2 API-contract tests; en/zh strings
2026-07-03 01:02:47 -07:00

140 lines
3.9 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
getCuratorStatus,
getMcpCatalog,
getMemoryStatus,
getSkillHubSources,
getToolsetModels,
installSkillFromHub,
resetMemory,
runDebugShare,
searchSkillsHub,
selectToolsetModel,
setCuratorPaused,
setMcpServerEnabled,
testMcpServer
} from './hermes'
describe('Hermes REST parity helpers (hub / mcp / maintenance)', () => {
let api: ReturnType<typeof vi.fn>
beforeEach(() => {
api = vi.fn().mockResolvedValue({})
Object.defineProperty(window, 'hermesDesktop', {
configurable: true,
value: { api }
})
})
afterEach(() => {
vi.restoreAllMocks()
Reflect.deleteProperty(window, 'hermesDesktop')
})
it('loads hub sources with a network-tolerant timeout', async () => {
await getSkillHubSources()
expect(api).toHaveBeenCalledWith(expect.objectContaining({ path: '/api/skills/hub/sources', timeoutMs: 45_000 }))
})
it('encodes hub search params', async () => {
await searchSkillsHub('gif search', 'official', 5)
expect(api).toHaveBeenCalledWith(
expect.objectContaining({ path: '/api/skills/hub/search?q=gif+search&source=official&limit=5' })
)
})
it('installs a hub skill by identifier', async () => {
await installSkillFromHub('official/gifs/gif-search')
expect(api).toHaveBeenCalledWith(
expect.objectContaining({
path: '/api/skills/hub/install',
method: 'POST',
body: { identifier: 'official/gifs/gif-search' }
})
)
})
it('tests an MCP server with a boot-tolerant timeout and encoded name', async () => {
await testMcpServer('file system')
expect(api).toHaveBeenCalledWith(
expect.objectContaining({
path: '/api/mcp/servers/file%20system/test',
method: 'POST',
timeoutMs: 60_000
})
)
})
it('toggles MCP server enablement', async () => {
await setMcpServerEnabled('filesystem', false)
expect(api).toHaveBeenCalledWith(
expect.objectContaining({
path: '/api/mcp/servers/filesystem/enabled',
method: 'PUT',
body: { enabled: false }
})
)
})
it('reads the MCP catalog', async () => {
await getMcpCatalog()
expect(api).toHaveBeenCalledWith(expect.objectContaining({ path: '/api/mcp/catalog' }))
})
it('reads memory status and resets a specific target', async () => {
await getMemoryStatus()
await resetMemory('user')
expect(api).toHaveBeenNthCalledWith(1, expect.objectContaining({ path: '/api/memory' }))
expect(api).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ path: '/api/memory/reset', method: 'POST', body: { target: 'user' } })
)
})
it('manages the curator', async () => {
await getCuratorStatus()
await setCuratorPaused(true)
expect(api).toHaveBeenNthCalledWith(1, expect.objectContaining({ path: '/api/curator' }))
expect(api).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ path: '/api/curator/paused', method: 'PUT', body: { paused: true } })
)
})
it('runs debug share synchronously with an upload-tolerant timeout', async () => {
await runDebugShare()
expect(api).toHaveBeenCalledWith(
expect.objectContaining({ path: '/api/ops/debug-share', method: 'POST', timeoutMs: 120_000 })
)
})
it('reads a backend model catalog scoped to a provider row', async () => {
await getToolsetModels('image_gen', 'FAL.ai')
expect(api).toHaveBeenCalledWith(
expect.objectContaining({ path: '/api/tools/toolsets/image_gen/models?provider=FAL.ai' })
)
})
it('persists a backend model selection', async () => {
await selectToolsetModel('image_gen', 'z-image-turbo', 'FAL.ai')
expect(api).toHaveBeenCalledWith(
expect.objectContaining({
path: '/api/tools/toolsets/image_gen/model',
method: 'PUT',
body: { model: 'z-image-turbo', provider: 'FAL.ai' }
})
)
})
})