Files
hermes-ui/tests/server/studio-mcp-autoinject.test.ts
T
toor 1e9ea89d86
Build / build (push) Canceled after 0s
NPM Lockfile Check / npm ci --ignore-scripts (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s
chore: bootstrap hermes-ui skeleton from 0.6.29 snapshot
Import the client, server, skills, tests, and build scripts as the starting
point for the rebuild, then trim the surfaces this project does not ship:
Electron desktop packaging, the marketing website, and the ESP32 firmware
tree. CI keeps the web build, e2e, lockfile, release, and Docker workflows.

Rename the package and CLI entry points to hermes-ui, drop the prepare hook so
npm ci no longer triggers a build, and drop the removed tsconfig and nodemon
references. Rewrite AGENTS.md, ARCHITECTURE.md, DEVELOPMENT.md, and the READMEs
so the documented package map and hard rules match this repository.

reference_code/ is ignored: it is a read-only snapshot, not an input to the build.
2026-10-16 10:34:35 +08:00

271 lines
10 KiB
TypeScript

import { join } from 'node:path'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const updateConfigYamlForProfileMock = vi.fn()
const listProfileNamesFromDiskMock = vi.fn()
const configMock = vi.hoisted(() => ({
port: 8648,
appHome: '/Users/test/.hermes-web-ui',
}))
vi.mock('../../packages/server/src/services/config-helpers', () => ({
updateConfigYamlForProfile: updateConfigYamlForProfileMock,
}))
vi.mock('../../packages/server/src/services/hermes/hermes-profile', () => ({
listProfileNamesFromDisk: listProfileNamesFromDiskMock,
}))
vi.mock('../../packages/server/src/config', () => ({
config: configMock,
}))
vi.mock('../../packages/server/src/services/logger', () => ({
logger: {
info: vi.fn(),
warn: vi.fn(),
},
}))
describe('studio MCP autoinject', () => {
beforeEach(() => {
vi.resetModules()
vi.clearAllMocks()
delete process.env.HERMES_DESKTOP
delete process.env.HERMES_AGENT_NODE
delete process.env.AUTH_TOKEN
delete process.env.HERMES_WEB_UI_DISABLE_MCP_AUTOINJECT
delete process.env.HERMES_WEB_UI_ALLOW_TRANSIENT_MCP_AUTOINJECT
configMock.port = 8648
configMock.appHome = '/Users/test/.hermes-web-ui'
listProfileNamesFromDiskMock.mockReturnValue(['default', 'work'])
updateConfigYamlForProfileMock.mockImplementation(async (_profile: string, updater: any) => {
const updated = await updater({})
return updated.result
})
})
it('injects bundled MCP server into every profile without relying on a global PATH shim', async () => {
const { injectBundledMcpServer } = await import('../../packages/server/src/services/hermes/studio-mcp-autoinject')
const result = await injectBundledMcpServer()
expect(result.targets.map(target => target.profile)).toEqual(['default', 'work'])
expect(updateConfigYamlForProfileMock).toHaveBeenCalledTimes(2)
const injectedDefault = await updateConfigYamlForProfileMock.mock.calls[0][1]({})
expect(injectedDefault.data.mcp_servers['hermes-studio-api']).toEqual({
command: process.execPath,
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'api'],
env: {
HERMES_WEB_UI_URL: 'http://127.0.0.1:8648',
HERMES_WEB_UI_HOME: '/Users/test/.hermes-web-ui',
HERMES_WEBUI_STATE_DIR: '/Users/test/.hermes-web-ui',
HERMES_WEB_UI_PROFILE: 'default',
HERMES_MCP_SERVER_NAME: 'hermes-studio-api',
HERMES_MCP_TOOLSET: 'api',
HERMES_WEB_UI_MANAGED_MCP: '1',
},
enabled: true,
})
expect(injectedDefault.data.mcp_servers['hermes-studio-devices']).toMatchObject({
command: process.execPath,
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'devices'],
env: {
HERMES_MCP_SERVER_NAME: 'hermes-studio-devices',
HERMES_MCP_TOOLSET: 'devices',
},
enabled: true,
})
expect(injectedDefault.data.mcp_servers['hermes-studio-use']).toMatchObject({
command: process.execPath,
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'use'],
env: {
HERMES_MCP_SERVER_NAME: 'hermes-studio-use',
HERMES_MCP_TOOLSET: 'use',
},
enabled: true,
})
const injectedWork = await updateConfigYamlForProfileMock.mock.calls[1][1]({})
expect(injectedWork.data.mcp_servers['hermes-studio-api'].env.HERMES_WEB_UI_PROFILE).toBe('work')
expect(result.serverNames).toEqual(['hermes-studio-api', 'hermes-studio-devices', 'hermes-studio-use'])
expect(result.command).toBe(process.execPath)
})
it('skips autoinject for transient preview homes by default', async () => {
configMock.appHome = '/private/tmp/wui-preview-home'
const { injectBundledMcpServer } = await import('../../packages/server/src/services/hermes/studio-mcp-autoinject')
const result = await injectBundledMcpServer()
expect(result.targets).toEqual([])
expect(updateConfigYamlForProfileMock).not.toHaveBeenCalled()
})
it('allows transient preview autoinject when explicitly requested', async () => {
configMock.appHome = '/private/tmp/wui-preview-home'
process.env.HERMES_WEB_UI_ALLOW_TRANSIENT_MCP_AUTOINJECT = '1'
const { injectBundledMcpServer } = await import('../../packages/server/src/services/hermes/studio-mcp-autoinject')
await injectBundledMcpServer()
expect(updateConfigYamlForProfileMock).toHaveBeenCalledTimes(2)
})
it('respects a user-disabled managed MCP server entry', async () => {
const { injectBundledMcpServer } = await import('../../packages/server/src/services/hermes/studio-mcp-autoinject')
await injectBundledMcpServer()
const updated = await updateConfigYamlForProfileMock.mock.calls[0][1]({
mcp_servers: {
'hermes-studio-api': {
command: process.execPath,
args: [join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'api'],
env: {
HERMES_WEB_UI_URL: 'http://127.0.0.1:8648',
HERMES_WEB_UI_HOME: '/Users/test/.hermes-web-ui',
HERMES_WEBUI_STATE_DIR: '/Users/test/.hermes-web-ui',
HERMES_WEB_UI_PROFILE: 'default',
HERMES_MCP_SERVER_NAME: 'hermes-studio-api',
HERMES_MCP_TOOLSET: 'api',
HERMES_WEB_UI_MANAGED_MCP: '1',
},
enabled: false,
},
},
})
expect(updated.write).toBe(false)
expect(updated.result).toMatchObject({
status: 'skipped',
reason: 'existing hermes-studio-api MCP server is disabled by user',
})
expect(updated.data.mcp_servers['hermes-studio-api'].enabled).toBe(false)
})
it('cleans a disabled legacy managed MCP server entry before injecting split servers', async () => {
const { injectBundledMcpServer } = await import('../../packages/server/src/services/hermes/studio-mcp-autoinject')
await injectBundledMcpServer()
const updated = await updateConfigYamlForProfileMock.mock.calls[0][1]({
mcp_servers: {
'hermes-studio': {
command: 'hermes-web-ui-mcp',
env: {
HERMES_WEB_UI_MANAGED_MCP: '1',
},
enabled: false,
},
},
})
expect(updated.write).not.toBe(false)
expect(updated.result).toMatchObject({
status: 'updated',
})
expect(updated.data.mcp_servers['hermes-studio']).toBeUndefined()
expect(updated.data.mcp_servers['hermes-studio-api']).toBeDefined()
expect(updated.data.mcp_servers['hermes-studio-devices']).toBeDefined()
expect(updated.data.mcp_servers['hermes-studio-use']).toBeDefined()
})
it('updates old managed PATH-only MCP entries to the bundled node script', async () => {
const { injectBundledMcpServer } = await import('../../packages/server/src/services/hermes/studio-mcp-autoinject')
await injectBundledMcpServer()
const updated = await updateConfigYamlForProfileMock.mock.calls[0][1]({
mcp_servers: {
'hermes-studio': {
command: 'hermes-web-ui-mcp',
env: {
HERMES_WEB_UI_URL: 'http://127.0.0.1:8648',
HERMES_WEB_UI_HOME: '/tmp/hermes-web-ui-home',
HERMES_WEBUI_STATE_DIR: '/tmp/hermes-web-ui-home',
HERMES_WEB_UI_PROFILE: 'default',
HERMES_WEB_UI_MANAGED_MCP: '1',
},
enabled: true,
},
'hermes-web-ui-mcp': {
command: 'hermes-web-ui-mcp',
env: {
HERMES_WEB_UI_MANAGED_MCP: '1',
},
enabled: true,
},
},
})
expect(updated.result.status).toBe('updated')
expect(updated.data.mcp_servers['hermes-studio']).toBeUndefined()
expect(updated.data.mcp_servers['hermes-web-ui-mcp']).toBeUndefined()
expect(updated.data.mcp_servers['hermes-studio-api'].command).toBe(process.execPath)
expect(updated.data.mcp_servers['hermes-studio-api'].args).toEqual([join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'api'])
expect(updated.data.mcp_servers['hermes-studio-devices'].args).toEqual([join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'devices'])
expect(updated.data.mcp_servers['hermes-studio-use'].args).toEqual([join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'use'])
})
it('uses the desktop runtime node for bundled MCP servers when available', async () => {
process.env.HERMES_DESKTOP = 'true'
process.env.HERMES_AGENT_NODE = '/runtime/node'
const { injectBundledMcpServer } = await import('../../packages/server/src/services/hermes/studio-mcp-autoinject')
await injectBundledMcpServer()
const injected = await updateConfigYamlForProfileMock.mock.calls[0][1]({})
expect(injected.data.mcp_servers['hermes-studio-api'].command).toBe('/runtime/node')
expect(injected.data.mcp_servers['hermes-studio-api'].args).toEqual([
join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'api',
])
expect(injected.data.mcp_servers['hermes-studio-devices'].args).toEqual([
join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'devices',
])
expect(injected.data.mcp_servers['hermes-studio-use'].args).toEqual([
join(process.cwd(), 'bin/hermes-studio-mcp.mjs'), 'use',
])
})
it('removes stale injected tokens from managed server config', async () => {
const { injectBundledMcpServer } = await import('../../packages/server/src/services/hermes/studio-mcp-autoinject')
await injectBundledMcpServer()
const updated = await updateConfigYamlForProfileMock.mock.calls[0][1]({
mcp_servers: {
'hermes-studio-api': {
command: 'hermes-web-ui-mcp',
args: ['api'],
env: {
HERMES_WEB_UI_URL: 'http://127.0.0.1:8648',
HERMES_WEB_UI_HOME: '/tmp/hermes-web-ui-home',
HERMES_WEBUI_STATE_DIR: '/tmp/hermes-web-ui-home',
HERMES_WEB_UI_PROFILE: 'default',
HERMES_MCP_SERVER_NAME: 'hermes-studio-api',
HERMES_MCP_TOOLSET: 'api',
HERMES_WEB_UI_MANAGED_MCP: '1',
HERMES_WEB_UI_TOKEN: 'old-token',
},
enabled: true,
},
},
})
expect(updated.result.status).toBe('updated')
expect(updated.data.mcp_servers['hermes-studio-api'].env.HERMES_WEB_UI_TOKEN).toBeUndefined()
})
it('skips an unmanaged existing server entry', async () => {
const { injectBundledMcpServer } = await import('../../packages/server/src/services/hermes/studio-mcp-autoinject')
await injectBundledMcpServer()
const updated = await updateConfigYamlForProfileMock.mock.calls[0][1]({
mcp_servers: {
'hermes-studio': { command: 'custom-command' },
},
})
expect(updated.write).toBe(false)
expect(updated.result.status).toBe('skipped')
})
})