Files
hermes-ui/tests/e2e/fixtures.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

461 lines
14 KiB
TypeScript

import type { Page, Request, Route } from '@playwright/test'
export const TEST_ACCESS_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwidXNlcm5hbWUiOiJwbGF5d3JpZ2h0Iiwicm9sZSI6InN1cGVyX2FkbWluIiwidHlwZSI6ImFjY2VzcyIsImF1ZCI6Imhlcm1lcy13ZWItdWkiLCJpYXQiOjE3NjAwMDAwMDAsImV4cCI6NDEwMjQ0NDgwMH0.playwright-signature'
export interface MockedRequest {
method: string
pathname: string
search: string
headers: Record<string, string>
postData: string | null
}
interface MockHermesApiOptions {
tokenValidationStatus?: number
initialProfileName?: 'default' | 'research'
sessions?: unknown[]
}
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,
}
const sampleAuxiliaryModelTasks = [
{ key: 'vision', label: 'Vision', default_timeout: 120, default_download_timeout: 30 },
{ key: 'web_extract', label: 'Web extract', default_timeout: 360 },
{ key: 'compression', label: 'Compression', default_timeout: 120 },
{ key: 'skills_hub', label: 'Skills hub', default_timeout: 30 },
{ key: 'approval', label: 'Approval', default_timeout: 30 },
{ key: 'mcp', label: 'MCP', default_timeout: 30 },
{ key: 'title_generation', label: 'Title generation', default_timeout: 30 },
{ key: 'triage_specifier', label: 'Triage specifier', default_timeout: 120 },
{ key: 'kanban_decomposer', label: 'Kanban decomposer', default_timeout: 180 },
{ key: 'profile_describer', label: 'Profile describer', default_timeout: 60 },
{ key: 'curator', label: 'Curator', default_timeout: 600 },
{ key: 'session_search', label: 'Session search', default_timeout: 30 },
{ key: 'flush_memories', label: 'Flush memories', default_timeout: 30 },
]
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
let activeProfileName = options.initialProfileName ?? 'research'
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: true, username: 'playwright' }))
return
}
if (pathname === '/api/auth/login') {
if (request.method() !== 'POST') {
await route.fulfill(jsonResponse({ error: 'Method not allowed' }, 405))
return
}
if (tokenValidationStatus !== 200) {
await route.fulfill(jsonResponse({ error: 'Invalid username or password' }, tokenValidationStatus))
return
}
await route.fulfill(jsonResponse({ token: TEST_ACCESS_KEY }))
return
}
if (pathname === '/api/auth/me') {
await route.fulfill(jsonResponse({
user: {
id: 1,
username: 'playwright',
role: 'super_admin',
status: 'active',
created_at: 0,
updated_at: 0,
last_login_at: 0,
avatar: '',
},
}))
return
}
if (pathname === '/api/auth/avatar') {
if (request.method() === 'GET') {
await route.fulfill(jsonResponse({ avatar: '' }))
return
}
if (request.method() === 'PUT') {
await route.fulfill(jsonResponse({ success: true, avatar: '' }))
return
}
await route.fulfill(jsonResponse({ error: 'Method not allowed' }, 405))
return
}
if (pathname === '/api/hermes/sessions') {
await route.fulfill(jsonResponse({ sessions: options.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: 256000 }))
return
}
if (/^\/api\/hermes\/sessions\/[^/]+\/workspace-run-changes$/.test(pathname)) {
await route.fulfill(jsonResponse({ changes: [] }))
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/provider-models') {
await route.fulfill(jsonResponse({ models: ['proxy-model-a', 'proxy-model-b'] }))
return
}
if (pathname === '/api/hermes/config/auxiliary-models') {
await route.fulfill(jsonResponse({ tasks: sampleAuxiliaryModelTasks, auxiliary: {} }))
return
}
if (pathname === '/api/hermes/pets/active') {
await route.fulfill(jsonResponse({ pet: null }))
return
}
if (pathname === '/api/hermes/profiles') {
await route.fulfill(jsonResponse({
profiles: [
{ name: 'default', active: activeProfileName === 'default', model: 'test-model', gateway: 'test', alias: 'Default' },
{ name: 'research', active: activeProfileName === 'research', model: 'test-model', gateway: 'test', alias: 'Research' },
],
}))
return
}
if (pathname === '/api/hermes/profiles/runtime-statuses') {
await route.fulfill(jsonResponse({
profiles: [
{
profile: 'default',
bridge: { running: activeProfileName === 'default', profile: 'default', reachable: true },
gateway: { running: true, profile: 'default' },
},
{
profile: 'research',
bridge: { running: activeProfileName === 'research', profile: 'research', reachable: true },
gateway: { running: true, profile: 'research' },
},
],
}))
return
}
if (pathname === '/api/hermes/profiles/active') {
if (request.method() !== 'PUT') {
await route.fulfill(jsonResponse({ error: 'Method not allowed' }, 405))
return
}
let body: { name?: unknown }
try {
body = JSON.parse(request.postData() || '{}')
} catch {
await route.fulfill(jsonResponse({ error: 'Invalid JSON body' }, 400))
return
}
if (body.name !== 'default' && body.name !== 'research') {
await route.fulfill(jsonResponse({ error: 'Unknown profile' }, 400))
return
}
activeProfileName = body.name
await route.fulfill(jsonResponse({ success: true, active: activeProfileName }))
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.getItem('hermes_active_profile_name')) {
window.localStorage.setItem('hermes_active_profile_name', storedProfileName)
}
}, { storedToken: accessKey, storedProfileName: profileName })
}
export async function mockChatSocket(page: Page) {
await page.route('**/node_modules/.vite/deps/socket__io-client.js*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/javascript',
body: `
const state = window.__PW_CHAT_SOCKET__ || (window.__PW_CHAT_SOCKET__ = { sockets: [], emitted: [] })
function makeSocket(url, options) {
const listeners = new Map()
const onceListeners = new Map()
const socket = {
connected: true,
url,
options,
on(event, handler) {
const handlers = listeners.get(event) || []
handlers.push(handler)
listeners.set(event, handlers)
return this
},
once(event, handler) {
const handlers = onceListeners.get(event) || []
handlers.push(handler)
onceListeners.set(event, handlers)
return this
},
emit(event, payload) {
state.emitted.push({ event, payload })
if (event === 'resume') {
const sessionId = payload && payload.session_id
const resumes = window.__PW_CHAT_SOCKET_RESUMES__ || {}
const response = sessionId ? resumes[sessionId] : null
if (response) {
setTimeout(() => this.__trigger('resumed', response), 0)
}
}
return this
},
removeAllListeners() {
listeners.clear()
onceListeners.clear()
return this
},
disconnect() {
this.connected = false
return this
},
__trigger(event, payload) {
for (const handler of listeners.get(event) || []) handler(payload)
const handlers = onceListeners.get(event) || []
onceListeners.delete(event)
for (const handler of handlers) handler(payload)
},
}
state.sockets.push(socket)
state.latest = socket
return socket
}
export function io(url, options) {
return makeSocket(url, options)
}
export default { io }
`,
})
})
}
export async function mockTerminalWebSocket(page: Page) {
await page.addInitScript(() => {
const state = (window as any).__PW_TERMINAL_WS__ = {
sockets: [] as any[],
sent: [] as any[],
createdCount: 0,
latest: null as any,
}
const RealEvent = window.Event
const RealMessageEvent = window.MessageEvent
class MockTerminalWebSocket extends EventTarget {
static CONNECTING = 0
static OPEN = 1
static CLOSING = 2
static CLOSED = 3
readonly CONNECTING = 0
readonly OPEN = 1
readonly CLOSING = 2
readonly CLOSED = 3
binaryType: BinaryType = 'blob'
bufferedAmount = 0
extensions = ''
protocol = ''
readyState = MockTerminalWebSocket.CONNECTING
onopen: ((event: Event) => void) | null = null
onmessage: ((event: MessageEvent) => void) | null = null
onerror: ((event: Event) => void) | null = null
onclose: ((event: CloseEvent) => void) | null = null
constructor(readonly url: string | URL) {
super()
state.sockets.push(this)
state.latest = this
setTimeout(() => {
this.readyState = MockTerminalWebSocket.OPEN
const openEvent = new RealEvent('open')
this.onopen?.(openEvent)
this.dispatchEvent(openEvent)
this.__createSession('term-1', 'zsh', 101)
}, 0)
}
send(data: string | ArrayBufferLike | Blob | ArrayBufferView) {
const normalized = typeof data === 'string' ? data : String(data)
state.sent.push({ socket: this.url.toString(), data: normalized })
if (normalized.charCodeAt(0) !== 0x7B) return
try {
const message = JSON.parse(normalized)
if (message.type === 'create') {
this.__createSession(`term-${state.createdCount + 1}`, 'bash', 200 + state.createdCount)
}
if (message.type === 'switch') {
this.__emitMessage(JSON.stringify({ type: 'switched', id: message.sessionId }))
}
} catch {}
}
close() {
this.readyState = MockTerminalWebSocket.CLOSED
}
__createSession(id: string, shell: string, pid: number) {
state.createdCount += 1
this.__emitMessage(JSON.stringify({ type: 'created', id, shell, pid }))
}
__emitMessage(data: string) {
const event = new RealMessageEvent('message', { data })
this.onmessage?.(event)
this.dispatchEvent(event)
}
}
;(window as any).WebSocket = MockTerminalWebSocket
})
}