Files
hermes-ui/tests/e2e/provider-card-actions.spec.ts
T
toor 3ca960f388
Build / build (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s
优化页面交互并增强 Provider 配置能力
- 表单下拉框不再用 label 包裹,改为 div + aria-label,避免点击后
  下拉面板瞬间关闭
- Provider 卡片新增「从上游获取模型」和「修改」入口,支持编辑
  base URL、API Key 与 api_mode,凭据写入对应的环境变量
- 服务端抽出 provider-model-discovery 统一不同 api_mode 的模型发现,
  旧的 custom_providers 与新的 providers 字典结构都能正确更新
- Agent Bridge 改为后台启动,HTTP 接口在 Python 运行时就绪前即可用;
  worker 连接被拒时按类型重试,不重放可能已产生副作用的请求
- 断线重连后恢复 profiles、models 与会话列表,健康检查改为自调度
  的 setTimeout,离线时 2 秒后重试
- 批量删除确认文案改用 Popconfirm content,避免变成侧栏触发器文本
2026-10-22 14:29:48 +08:00

94 lines
5.5 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test'
import { authenticate, mockHermesApi, TEST_ACCESS_KEY } from './fixtures'
async function setup(page: Page, custom = false) {
await authenticate(page, TEST_ACCESS_KEY, 'research')
await mockHermesApi(page)
const group = {
provider: custom ? 'custom:proxy' : 'minimax-cn',
label: custom ? 'Proxy' : 'MiniMax (China)',
base_url: 'https://provider.example.test/v1', api_key: 'existing-test-key',
models: ['original-model'], available_models: ['original-model'],
builtin: !custom, credentials_editable: !custom,
...(!custom ? { base_url_env: 'MINIMAX_CN_BASE_URL' } : { provider_source: 'providers', provider_key: 'proxy_config' }),
}
await page.route(url => url.pathname === '/api/hermes/available-models', route => route.fulfill({ json: {
default: 'original-model', default_provider: group.provider, groups: [group], allProviders: [group],
profiles: [{ profile: 'research', default: 'original-model', default_provider: group.provider, groups: [group] }],
} }))
await page.goto('/#/hermes/models')
const card = page.locator('.provider-card').filter({ hasText: group.label })
await expect(card).toBeVisible()
return { group, card }
}
async function fillUrl(page: Page, url: string) {
await page.locator('.provider-edit-form .arco-form-item').filter({ hasText: 'Base URL' }).locator('input').fill(url)
}
test('card fetches the live catalog, updates the list, and retains its default model', async ({ page }) => {
const { group, card } = await setup(page)
let requestBody: any
let profileHeader: string | undefined
await page.route(url => url.pathname === '/api/hermes/provider-models', async route => {
requestBody = route.request().postDataJSON()
profileHeader = route.request().headers()['x-hermes-profile']
group.models = ['original-model', 'latest-model']
group.available_models = [...group.models]
await route.fulfill({ json: { models: [...group.models] } })
})
await card.getByRole('button', { name: 'Fetch models from upstream' }).click()
await expect(card.locator('.model-tag', { hasText: 'latest-model' })).toBeVisible()
await expect(card.locator('.default-model-select')).toContainText('original-model')
expect(requestBody).toMatchObject({ provider: 'minimax-cn', api_key: 'existing-test-key', update_cache: true })
expect(profileHeader).toBe('research')
})
test('editing built-in URL and key uses PUT and updates the card without recreation', async ({ page }) => {
const { group, card } = await setup(page)
const writes: Array<{ method: string; body: any; profile?: string }> = []
await page.route(url => url.pathname.startsWith('/api/hermes/config/providers/'), async route => {
const body = route.request().postDataJSON()
writes.push({ method: route.request().method(), body, profile: route.request().headers()['x-hermes-profile'] })
group.base_url = body.base_url
if (body.api_key) group.api_key = body.api_key
await route.fulfill({ json: { success: true } })
})
await card.getByRole('button', { name: 'Edit', exact: true }).click()
await fillUrl(page, 'https://new.example.test/v1')
await page.locator('.provider-edit-form input[type=password]').fill('new-test-key')
await page.getByRole('button', { name: 'Save', exact: true }).click()
await expect(page.locator('.provider-edit-form')).toHaveCount(0)
await expect(card).toContainText('https://new.example.test/v1')
expect(writes).toEqual([{ method: 'PUT', body: { base_url: 'https://new.example.test/v1', api_key: 'new-test-key' }, profile: 'research' }])
await expect(card.locator('.default-model-select')).toContainText('original-model')
})
test('editing a dict-backed provider preserves a blank key and sends its storage identity', async ({ page }) => {
const { card } = await setup(page, true)
let requestBody: any
await page.route(url => url.pathname.startsWith('/api/hermes/config/providers/'), async route => {
requestBody = route.request().postDataJSON()
await route.fulfill({ json: { success: true } })
})
await card.getByRole('button', { name: 'Edit', exact: true }).click()
await expect(page.locator('.provider-edit-form input[type=password]')).toHaveValue('')
await fillUrl(page, 'https://changed.example.test/v1')
await page.getByRole('button', { name: 'Save', exact: true }).click()
await expect(page.locator('.provider-edit-form')).toHaveCount(0)
expect(requestBody).toEqual({ base_url: 'https://changed.example.test/v1', source: 'providers', providerKey: 'proxy_config' })
})
test('failed upstream fetch keeps the existing catalog; failed edit stays open', async ({ page }) => {
const { card } = await setup(page)
await page.route(url => url.pathname === '/api/hermes/provider-models', route => route.fulfill({ status: 502, json: { error: 'Upstream unavailable' } }))
await card.getByRole('button', { name: 'Fetch models from upstream' }).click()
await expect(page.getByText('API Error 502: Upstream unavailable', { exact: true })).toBeVisible()
await expect(card.locator('.model-tag', { hasText: 'original-model' })).toBeVisible()
await page.route(url => url.pathname.startsWith('/api/hermes/config/providers/'), route => route.fulfill({ status: 500, json: { error: 'Save failed' } }))
await card.getByRole('button', { name: 'Edit', exact: true }).click()
await page.getByRole('button', { name: 'Save', exact: true }).click()
await expect(page.getByText('API Error 500: Save failed', { exact: true })).toBeVisible()
await expect(page.locator('.provider-edit-form')).toBeVisible()
})