mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-05-25 21:40:13 +00:00
df41d6b051
* feat: add session deep links for chats * feat: add deep links for history and group chat * Fix profile-aware session deep links --------- Co-authored-by: Maxim Kirilyuk <werserk@inbox.ru>
170 lines
5.7 KiB
TypeScript
170 lines
5.7 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test'
|
|
import { authenticate, mockChatSocket, mockHermesApi, TEST_ACCESS_KEY } from './fixtures'
|
|
|
|
const inputPlaceholder = 'Type a message... (Enter to send, Shift+Enter for new line)'
|
|
|
|
type SessionSeed = {
|
|
id: string
|
|
title: string
|
|
lastActive: number
|
|
}
|
|
|
|
function sessionSummary({ id, title, lastActive }: SessionSeed) {
|
|
return {
|
|
id,
|
|
profile: 'research',
|
|
source: 'cli',
|
|
model: 'test-model',
|
|
provider: 'test-provider',
|
|
title,
|
|
preview: title,
|
|
started_at: lastActive - 10,
|
|
ended_at: null,
|
|
last_active: lastActive,
|
|
message_count: 1,
|
|
tool_call_count: 0,
|
|
input_tokens: 0,
|
|
output_tokens: 0,
|
|
cache_read_tokens: 0,
|
|
cache_write_tokens: 0,
|
|
reasoning_tokens: 0,
|
|
billing_provider: null,
|
|
estimated_cost_usd: 0,
|
|
actual_cost_usd: null,
|
|
cost_status: 'estimated',
|
|
}
|
|
}
|
|
|
|
function resumePayload(sessionId: string, content: string) {
|
|
return {
|
|
session_id: sessionId,
|
|
messages: [
|
|
{
|
|
id: 1,
|
|
session_id: sessionId,
|
|
role: 'user',
|
|
content,
|
|
timestamp: Date.now() / 1000,
|
|
tool_call_id: null,
|
|
tool_calls: null,
|
|
tool_name: null,
|
|
token_count: null,
|
|
finish_reason: null,
|
|
reasoning: null,
|
|
},
|
|
],
|
|
isWorking: false,
|
|
events: [],
|
|
}
|
|
}
|
|
|
|
const sessions = [
|
|
sessionSummary({ id: 'session-a', title: 'Alpha chat', lastActive: 100 }),
|
|
sessionSummary({ id: 'session-b', title: 'Beta chat', lastActive: 200 }),
|
|
]
|
|
|
|
const resumes = {
|
|
'session-a': resumePayload('session-a', 'Alpha route content'),
|
|
'session-b': resumePayload('session-b', 'Beta route content'),
|
|
}
|
|
|
|
async function setupChatPage(page: Page) {
|
|
await authenticate(page, TEST_ACCESS_KEY, 'research')
|
|
await page.addInitScript((payload) => {
|
|
;(window as any).__PW_CHAT_SOCKET_RESUMES__ = payload
|
|
window.localStorage.setItem('hermes_active_session_research', 'session-b')
|
|
}, resumes)
|
|
const api = await mockHermesApi(page, { sessions })
|
|
await mockChatSocket(page)
|
|
return api
|
|
}
|
|
|
|
async function sendChatMessage(page: Page, message: string) {
|
|
const input = page.getByPlaceholder(inputPlaceholder)
|
|
await expect(input).toBeVisible()
|
|
await input.fill(message)
|
|
await page.getByRole('button', { name: 'Send' }).click()
|
|
}
|
|
|
|
async function waitForRun(page: Page, index = 0) {
|
|
const handle = await page.waitForFunction((runIndex) => {
|
|
const state = (window as any).__PW_CHAT_SOCKET__
|
|
const runs = state?.emitted?.filter((item: any) => item.event === 'run') || []
|
|
const run = runs[runIndex]
|
|
return run ? run.payload : null
|
|
}, index)
|
|
return handle.jsonValue() as Promise<any>
|
|
}
|
|
|
|
test('route session id wins over shared active-session localStorage', async ({ page }) => {
|
|
const api = await setupChatPage(page)
|
|
|
|
await page.goto('/#/hermes/session/session-a')
|
|
|
|
await expect(page.getByText('Alpha route content')).toBeVisible()
|
|
await expect(page.getByText('Beta route content')).toHaveCount(0)
|
|
await expect(page).toHaveURL(/#\/hermes\/session\/session-a$/)
|
|
expect(api.unexpectedRequests).toEqual([])
|
|
})
|
|
|
|
test('two tabs can show different sessions and keep them after reload', async ({ context }) => {
|
|
const pageA = await context.newPage()
|
|
const pageB = await context.newPage()
|
|
const apiA = await setupChatPage(pageA)
|
|
const apiB = await setupChatPage(pageB)
|
|
|
|
await pageA.goto('/#/hermes/session/session-a')
|
|
await pageB.goto('/#/hermes/session/session-b')
|
|
|
|
await expect(pageA.getByText('Alpha route content')).toBeVisible()
|
|
await expect(pageB.getByText('Beta route content')).toBeVisible()
|
|
|
|
await pageA.reload()
|
|
await pageB.reload()
|
|
|
|
await expect(pageA.getByText('Alpha route content')).toBeVisible()
|
|
await expect(pageB.getByText('Beta route content')).toBeVisible()
|
|
await expect(pageA).toHaveURL(/#\/hermes\/session\/session-a$/)
|
|
await expect(pageB).toHaveURL(/#\/hermes\/session\/session-b$/)
|
|
expect(apiA.unexpectedRequests).toEqual([])
|
|
expect(apiB.unexpectedRequests).toEqual([])
|
|
})
|
|
|
|
test('parallel tabs send runs and render progress only for their own session', async ({ context }) => {
|
|
const pageA = await context.newPage()
|
|
const pageB = await context.newPage()
|
|
const apiA = await setupChatPage(pageA)
|
|
const apiB = await setupChatPage(pageB)
|
|
|
|
await pageA.goto('/#/hermes/session/session-a')
|
|
await pageB.goto('/#/hermes/session/session-b')
|
|
await expect(pageA.getByText('Alpha route content')).toBeVisible()
|
|
await expect(pageB.getByText('Beta route content')).toBeVisible()
|
|
|
|
await sendChatMessage(pageA, 'Question for Alpha')
|
|
await sendChatMessage(pageB, 'Question for Beta')
|
|
|
|
const runA = await waitForRun(pageA)
|
|
const runB = await waitForRun(pageB)
|
|
expect(runA.session_id).toBe('session-a')
|
|
expect(runB.session_id).toBe('session-b')
|
|
|
|
await pageA.evaluate((sid) => {
|
|
const socket = (window as any).__PW_CHAT_SOCKET__.latest
|
|
socket.__trigger('run.started', { event: 'run.started', session_id: sid, run_id: 'run-a' })
|
|
socket.__trigger('message.delta', { event: 'message.delta', session_id: sid, run_id: 'run-a', delta: 'Alpha progress' })
|
|
}, runA.session_id)
|
|
await pageB.evaluate((sid) => {
|
|
const socket = (window as any).__PW_CHAT_SOCKET__.latest
|
|
socket.__trigger('run.started', { event: 'run.started', session_id: sid, run_id: 'run-b' })
|
|
socket.__trigger('message.delta', { event: 'message.delta', session_id: sid, run_id: 'run-b', delta: 'Beta progress' })
|
|
}, runB.session_id)
|
|
|
|
await expect(pageA.getByText('Alpha progress')).toBeVisible()
|
|
await expect(pageA.getByText('Beta progress')).toHaveCount(0)
|
|
await expect(pageB.getByText('Beta progress')).toBeVisible()
|
|
await expect(pageB.getByText('Alpha progress')).toHaveCount(0)
|
|
expect(apiA.unexpectedRequests).toEqual([])
|
|
expect(apiB.unexpectedRequests).toEqual([])
|
|
})
|