Files
hermes-ui/tests/server/group-chat-streaming.test.ts
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

102 lines
4.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
connectGroupChatClient,
createTestGroupChatServer,
emitAck,
once,
} from './group-chat-test-helpers'
import { GROUP_CHAT_AGENT_SOCKET_SECRET, groupBridgeSessionId } from '../../packages/server/src/services/hermes/group-chat/agent-clients'
import type { GroupChatServer } from '../../packages/server/src/services/hermes/group-chat'
describe('group chat streaming baseline', () => {
let harness: Awaited<ReturnType<typeof createTestGroupChatServer>>
let groupServer: GroupChatServer
let port: number
beforeEach(async () => {
vi.clearAllMocks()
harness = await createTestGroupChatServer()
groupServer = harness.groupServer
port = harness.port
groupServer.getStorage().saveRoom('room-1', 'Room 1', 'ROOM1')
groupServer.getStorage().addRoomAgent('room-1', 'agent-worker', 'default', 'Worker', '', 0)
})
afterEach(() => {
harness?.cleanup()
})
async function joinPair() {
const alice = await connectGroupChatClient(port, 'user-a', 'Alice')
const bob = await connectGroupChatClient(port, 'user-b', 'Bob')
const worker = await connectGroupChatClient(port, 'agent-worker', 'Worker', {
source: 'agent',
agentSocketSecret: GROUP_CHAT_AGENT_SOCKET_SECRET,
})
harness.sockets.push(alice, bob, worker)
await emitAck(alice, 'join', { roomId: 'room-1', inviteCode: 'ROOM1' })
await emitAck(bob, 'join', { roomId: 'room-1', inviteCode: 'ROOM1' })
await emitAck(worker, 'join', { roomId: 'room-1' })
const agentSessionId = groupBridgeSessionId(
'room-1',
'default',
'Worker',
String(groupServer.getStorage().getRoom('room-1')?.sessionSeed || '0'),
)
return { alice, bob, worker, agentSessionId }
}
it('relays stream start, content delta, reasoning delta, and stream end to room members', async () => {
const { worker, bob, agentSessionId } = await joinPair()
const streamStart = once<any>(bob, 'message_stream_start')
worker.emit('message_stream_start', { roomId: 'room-1', id: 'stream-1', senderName: 'Spoofed', timestamp: 10, agentSessionId })
expect(await streamStart).toMatchObject({
id: 'stream-1',
roomId: 'room-1',
senderName: 'Worker',
role: 'assistant',
finish_reason: 'streaming',
})
const contentDelta = once<any>(bob, 'message_stream_delta')
worker.emit('message_stream_delta', { roomId: 'room-1', id: 'stream-1', delta: 'hello', agentSessionId })
expect(await contentDelta).toEqual({ roomId: 'room-1', id: 'stream-1', delta: 'hello' })
const reasoningDelta = once<any>(bob, 'message_reasoning_delta')
worker.emit('message_reasoning_delta', { roomId: 'room-1', id: 'stream-1', delta: 'thinking', agentSessionId })
expect(await reasoningDelta).toEqual({ roomId: 'room-1', id: 'stream-1', delta: 'thinking' })
const streamEnd = once<any>(bob, 'message_stream_end')
worker.emit('message_stream_end', { roomId: 'room-1', id: 'stream-1', agentSessionId })
expect(await streamEnd).toEqual({ roomId: 'room-1', id: 'stream-1' })
})
it('ignores stream events emitted by human sockets', async () => {
const { alice, bob } = await joinPair()
const unexpectedStart = once<any>(bob, 'message_stream_start', 100)
const unexpectedDelta = once<any>(bob, 'message_stream_delta', 100)
const unexpectedReasoning = once<any>(bob, 'message_reasoning_delta', 100)
const unexpectedEnd = once<any>(bob, 'message_stream_end', 100)
alice.emit('message_stream_start', { roomId: 'room-1', id: 'stream-human', senderName: 'Worker' })
alice.emit('message_stream_delta', { roomId: 'room-1', id: 'stream-human', delta: 'hello' })
alice.emit('message_reasoning_delta', { roomId: 'room-1', id: 'stream-human', delta: 'thinking' })
alice.emit('message_stream_end', { roomId: 'room-1', id: 'stream-human' })
await expect(unexpectedStart).rejects.toThrow('timeout waiting for message_stream_start')
await expect(unexpectedDelta).rejects.toThrow('timeout waiting for message_stream_delta')
await expect(unexpectedReasoning).rejects.toThrow('timeout waiting for message_reasoning_delta')
await expect(unexpectedEnd).rejects.toThrow('timeout waiting for message_stream_end')
})
it('ignores a representative invalid stream id', async () => {
const { alice, bob } = await joinPair()
const unexpected = once<any>(bob, 'message_stream_start', 100)
alice.emit('message_stream_start', { roomId: 'room-1', id: 'bad id with spaces' })
await expect(unexpected).rejects.toThrow('timeout waiting for message_stream_start')
})
})