test: 添加浏览器烟测套件 (#750)

* test: add Playwright browser smoke suite

* Update playwright.yml

---------

Co-authored-by: ekko <152005280+EKKOLearnAI@users.noreply.github.com>
This commit is contained in:
Zhicheng Han
2026-05-15 11:35:10 +02:00
committed by GitHub
parent e0bfa828cb
commit 97b15d6514
7 changed files with 339 additions and 1 deletions
+191
View File
@@ -0,0 +1,191 @@
import type { Page, Request, Route } from '@playwright/test'
export const TEST_ACCESS_KEY = 'playwright-access-key'
export interface MockedRequest {
method: string
pathname: string
search: string
headers: Record<string, string>
postData: string | null
}
interface MockHermesApiOptions {
tokenValidationStatus?: number
}
const sampleModelGroup = {
provider: 'test-provider',
label: 'Test Provider',
base_url: 'https://example.invalid/v1',
models: ['test-model'],
available_models: ['test-model'],
api_key: '',
builtin: true,
}
const sampleJob = {
job_id: 'job-smoke',
id: 'job-smoke',
name: 'Nightly Smoke',
prompt: 'Run the smoke check',
prompt_preview: 'Run the smoke check',
skills: [],
skill: null,
model: 'test-model',
provider: 'test-provider',
base_url: null,
script: null,
schedule: '0 9 * * *',
schedule_display: '0 9 * * *',
repeat: { times: null, completed: 0 },
enabled: true,
state: 'scheduled',
paused_at: null,
paused_reason: null,
created_at: '2026-01-01T00:00:00.000Z',
next_run_at: '2026-01-02T09:00:00.000Z',
last_run_at: null,
last_status: null,
last_error: null,
deliver: 'origin',
origin: null,
last_delivery_error: null,
}
function jsonResponse(body: unknown, status = 200) {
return {
status,
contentType: 'application/json',
body: JSON.stringify(body),
}
}
function recordRequest(request: Request): MockedRequest {
const url = new URL(request.url())
return {
method: request.method(),
pathname: url.pathname,
search: url.search,
headers: request.headers(),
postData: request.postData(),
}
}
export async function mockHermesApi(page: Page, options: MockHermesApiOptions = {}) {
const requests: MockedRequest[] = []
const unexpectedRequests: MockedRequest[] = []
const tokenValidationStatus = options.tokenValidationStatus ?? 200
await page.route('**/*', async (route: Route) => {
const request = route.request()
const url = new URL(request.url())
const { pathname } = url
if (!(pathname === '/health' || pathname.startsWith('/api/') || pathname.startsWith('/v1/'))) {
await route.continue()
return
}
requests.push(recordRequest(request))
if (pathname === '/health') {
await route.fulfill(jsonResponse({ status: 'ok', webui_version: '0.5.23', node_version: '23.0.0' }))
return
}
if (pathname === '/api/auth/status') {
await route.fulfill(jsonResponse({ hasPasswordLogin: false, username: null }))
return
}
if (pathname === '/api/hermes/sessions') {
await route.fulfill(jsonResponse({ sessions: [] }, tokenValidationStatus))
return
}
if (pathname === '/api/hermes/sessions/hermes') {
await route.fulfill(jsonResponse({ sessions: [] }))
return
}
if (pathname === '/api/hermes/sessions/context-length') {
await route.fulfill(jsonResponse({ context_length: 200000 }))
return
}
if (pathname === '/api/hermes/files/list') {
await route.fulfill(jsonResponse({ entries: [], path: '' }))
return
}
if (pathname === '/api/hermes/auth/copilot/check-token') {
await route.fulfill(jsonResponse({ has_token: false, source: null, enabled: false }))
return
}
if (pathname === '/api/auth/locked-ips') {
await route.fulfill(jsonResponse({ locks: [] }))
return
}
if (pathname === '/api/hermes/available-models') {
await route.fulfill(jsonResponse({
default: 'test-model',
default_provider: 'test-provider',
groups: [sampleModelGroup],
allProviders: [sampleModelGroup],
model_aliases: {},
model_visibility: {},
}))
return
}
if (pathname === '/api/hermes/profiles') {
await route.fulfill(jsonResponse({
profiles: [
{ name: 'default', active: false, model: 'test-model', gateway: 'test', alias: 'Default' },
{ name: 'research', active: true, model: 'test-model', gateway: 'test', alias: 'Research' },
],
}))
return
}
if (pathname === '/api/hermes/config') {
await route.fulfill(jsonResponse({
display: { streaming: true, show_reasoning: true, show_cost: true },
agent: {},
memory: {},
session_reset: {},
privacy: {},
approvals: {},
}))
return
}
if (pathname === '/api/hermes/jobs') {
await route.fulfill(jsonResponse({ jobs: [sampleJob] }))
return
}
if (pathname === '/api/cron-history') {
await route.fulfill(jsonResponse({ runs: [] }))
return
}
unexpectedRequests.push(recordRequest(request))
await route.fulfill(jsonResponse({ error: `Unexpected mocked route: ${request.method()} ${pathname}` }, 404))
})
return { requests, unexpectedRequests }
}
export async function authenticate(page: Page, accessKey = TEST_ACCESS_KEY, profileName?: string) {
await page.addInitScript((state: { storedToken: string; storedProfileName?: string }) => {
const { storedToken, storedProfileName } = state
window.localStorage.setItem('hermes_api_key', storedToken)
if (storedProfileName) {
window.localStorage.setItem('hermes_active_profile_name', storedProfileName)
}
}, { storedToken: accessKey, storedProfileName: profileName })
}