Files
hermes-desktop/tests/server/group-chat-routes-baseline.test.ts
toor 01d9e301fc
Build / build (push) Canceled after 0s
Playwright / browser (push) Canceled after 0s
initialize Electron desktop app and remove unused files
2026-10-25 01:09:01 +08:00

179 lines
8.0 KiB
TypeScript

import Koa from 'koa'
import bodyParser from '@koa/bodyparser'
import { createServer, type Server as HttpServer } from 'http'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { groupChatRoutes, setGroupChatServer } from '../../packages/server/src/routes/hermes/group-chat'
function listen(server: HttpServer): Promise<string> {
return new Promise(resolve => server.listen(0, '127.0.0.1', () => {
const addr = server.address()
if (!addr || typeof addr === 'string') throw new Error('missing address')
resolve(`http://127.0.0.1:${addr.port}`)
}))
}
describe('group chat REST route baseline', () => {
let httpServer: HttpServer
let baseUrl: string
let storage: any
let agentClients: any
let clearRoomRuntimeState: ReturnType<typeof vi.fn>
beforeEach(async () => {
storage = {
rooms: new Map<string, any>(),
agents: new Map<string, any[]>(),
messages: new Map<string, any[]>(),
members: new Map<string, any[]>(),
saveRoom: vi.fn((id, name, inviteCode, config) => storage.rooms.set(id, { id, name, inviteCode, totalTokens: 0, sessionSeed: '0', ...config })),
getRoom: vi.fn((id) => storage.rooms.get(id)),
getAllRooms: vi.fn(() => [...storage.rooms.values()]),
getRoomsForProfiles: vi.fn(() => [...storage.rooms.values()]),
getRecentMessagesForUI: vi.fn((roomId, limit = 150, offset = 0) => (storage.messages.get(roomId) || []).slice(offset, offset + limit)),
getMessageCount: vi.fn((roomId) => (storage.messages.get(roomId) || []).length),
getRoomAgents: vi.fn((roomId) => storage.agents.get(roomId) || []),
getRoomMembers: vi.fn((roomId) => storage.members.get(roomId) || []),
getRoomByInviteCode: vi.fn((code) => [...storage.rooms.values()].find((r: any) => r.inviteCode === code)),
addRoomAgent: vi.fn((roomId, agentId, profile, name, description, invited) => {
const row = { id: `row-${agentId}`, roomId, agentId, profile, name, description, invited }
storage.agents.set(roomId, [...(storage.agents.get(roomId) || []), row])
return row
}),
getRoomAgent: vi.fn((roomId, ref) => (storage.agents.get(roomId) || []).find((a: any) => a.id === ref || a.agentId === ref) || null),
removeRoomMembersForAgent: vi.fn(),
removeRoomAgent: vi.fn((roomId, ref) => storage.agents.set(roomId, (storage.agents.get(roomId) || []).filter((a: any) => a.id !== ref && a.agentId !== ref))),
clearRoomContext: vi.fn((roomId) => { const room = storage.rooms.get(roomId); if (room) Object.assign(room, { totalTokens: 0, sessionSeed: 'rotated' }) }),
deleteRoom: vi.fn((roomId) => storage.rooms.delete(roomId)),
}
agentClients = {
createAgent: vi.fn(async (cfg: any) => {
if (cfg.profile === 'bad-profile') throw new Error('agent runtime unavailable')
return { ...cfg, joinRoom: vi.fn(async () => ({})), disconnect: vi.fn() }
}),
addAgentToRoom: vi.fn(async () => ({})),
removeAgentFromRoom: vi.fn(),
disconnectRoom: vi.fn(),
}
clearRoomRuntimeState = vi.fn()
setGroupChatServer({ getStorage: () => storage, agentClients, clearRoomRuntimeState } as any)
const app = new Koa()
app.use(bodyParser())
app.use(groupChatRoutes.routes())
httpServer = createServer(app.callback())
baseUrl = await listen(httpServer)
})
afterEach(() => {
httpServer.close()
setGroupChatServer(null as any)
})
it('requires name and inviteCode when creating a room', async () => {
const res = await fetch(`${baseUrl}/api/hermes/group-chat/rooms`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Room' }),
})
expect(res.status).toBe(400)
await expect(res.json()).resolves.toEqual({ error: 'name and inviteCode are required' })
})
it('rejects reserved @all agent names when creating a room', async () => {
const res = await fetch(`${baseUrl}/api/hermes/group-chat/rooms`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Room', inviteCode: 'ROOM1', agents: [{ profile: 'default', name: 'all' }] }),
})
expect(res.status).toBe(400)
await expect(res.json()).resolves.toMatchObject({ error: '`all` is reserved for @all mentions' })
})
it('creates a room, persists successful agents, and reports agent connection failures', async () => {
const res = await fetch(`${baseUrl}/api/hermes/group-chat/rooms`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Room',
inviteCode: 'ROOM1',
agents: [
{ profile: 'default', name: 'Worker' },
{ profile: 'bad-profile', name: 'Broken' },
],
}),
})
const body = await res.json()
expect(res.status).toBe(200)
expect(body.room).toMatchObject({ name: 'Room', inviteCode: 'ROOM1' })
expect(body.agents).toHaveLength(1)
expect(body.agentResults).toEqual([
expect.objectContaining({ profile: 'default', ok: true }),
expect.objectContaining({ profile: 'bad-profile', ok: false, code: 'PROFILE_AGENT_CONNECT_FAILED' }),
])
expect(storage.saveRoom).toHaveBeenCalled()
})
it('returns room detail with paging metadata, agents, and members', async () => {
storage.rooms.set('room-1', { id: 'room-1', name: 'Room', inviteCode: 'ROOM1' })
storage.messages.set('room-1', [{ id: 'msg-1' }, { id: 'msg-2' }])
storage.agents.set('room-1', [{ id: 'row-agent', agentId: 'agent-1', profile: 'default', name: 'Agent' }])
storage.members.set('room-1', [{ userId: 'user-1', name: 'Alice' }])
const res = await fetch(`${baseUrl}/api/hermes/group-chat/rooms/room-1?limit=1&offset=1`)
const body = await res.json()
expect(res.status).toBe(200)
expect(body).toMatchObject({
room: { id: 'room-1', name: 'Room' },
messages: [{ id: 'msg-2' }],
agents: [{ agentId: 'agent-1' }],
members: [{ userId: 'user-1' }],
total: 2,
offset: 1,
limit: 1,
hasMore: false,
})
})
it('rejects duplicate room agent profiles', async () => {
storage.rooms.set('room-1', { id: 'room-1', name: 'Room', inviteCode: 'ROOM1' })
storage.agents.set('room-1', [{ id: 'row-agent', agentId: 'agent-1', profile: 'default', name: 'Agent' }])
const res = await fetch(`${baseUrl}/api/hermes/group-chat/rooms/room-1/agents`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ profile: 'default', name: 'Agent' }),
})
expect(res.status).toBe(409)
await expect(res.json()).resolves.toEqual({ error: 'Agent already in room' })
})
it('removes an agent by row id and disconnects runtime by persisted agent id', async () => {
const agent = { id: 'row-agent', roomId: 'room-1', agentId: 'agent-1', profile: 'default', name: 'Agent' }
storage.agents.set('room-1', [agent])
const res = await fetch(`${baseUrl}/api/hermes/group-chat/rooms/room-1/agents/row-agent`, { method: 'DELETE' })
const body = await res.json()
expect(res.status).toBe(200)
expect(storage.removeRoomMembersForAgent).toHaveBeenCalledWith('room-1', agent)
expect(storage.removeRoomAgent).toHaveBeenCalledWith('room-1', 'row-agent')
expect(agentClients.removeAgentFromRoom).toHaveBeenCalledWith('room-1', 'agent-1')
expect(body).toMatchObject({ success: true, agents: [], members: [] })
})
it('clears room context and runtime state while returning the updated room', async () => {
storage.rooms.set('room-1', { id: 'room-1', name: 'Room', inviteCode: 'ROOM1', totalTokens: 99, sessionSeed: 'old' })
const res = await fetch(`${baseUrl}/api/hermes/group-chat/rooms/room-1/clear-context`, { method: 'POST' })
const body = await res.json()
expect(res.status).toBe(200)
expect(storage.clearRoomContext).toHaveBeenCalledWith('room-1')
expect(clearRoomRuntimeState).toHaveBeenCalledWith('room-1')
expect(body).toMatchObject({ success: true, room: { id: 'room-1', totalTokens: 0, sessionSeed: 'rotated' } })
})
})