Files
Hermes-ui/tests/server/group-chat-mention-routing.test.ts
Zhicheng Han 904ca8c648 feat(group-chat): add @all mention routing (#857)
Add modular group-chat mention routing helpers for the reserved @all token, route it to every non-sender agent, and strip routing tokens before model input.

Expose @all in mention autocomplete, highlight it in group messages, reserve literal all agent names, and cover boundary/partial-match regressions with tests.
2026-05-20 10:21:57 +08:00

81 lines
4.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
isAllAgentsMentioned,
isAgentMentioned,
isReservedMentionName,
resolveMentionTargets,
stripMentionRoutingTokens,
} from '../../packages/server/src/services/hermes/group-chat/mention-routing'
type TestAgent = { name: string; id?: string; agentId?: string; profile?: string }
const agents: TestAgent[] = [
{ name: 'Alice', id: 'socket-alice', agentId: 'agent-alice' },
{ name: 'Bob', id: 'socket-bob', agentId: 'agent-bob' },
{ name: 'Regex.Bot', id: 'socket-regex', agentId: 'agent-regex' },
]
describe('group chat mention routing', () => {
it('reserves @all so it cannot be confused with a literal agent name', () => {
expect(isReservedMentionName('all')).toBe(true)
expect(isReservedMentionName(' ALL ')).toBe(true)
expect(isReservedMentionName('Alice')).toBe(false)
})
it('recognizes @all as a standalone mention with safe boundaries', () => {
expect(isAllAgentsMentioned('@all please compare notes')).toBe(true)
expect(isAllAgentsMentioned('please compare notes @ALL')).toBe(true)
expect(isAllAgentsMentioned('@all, compare notes')).toBe(true)
expect(isAllAgentsMentioned('email user@all.example')).toBe(false)
expect(isAllAgentsMentioned('@alligator should not notify everyone')).toBe(false)
expect(isAllAgentsMentioned('prefix@all should not notify everyone')).toBe(false)
})
it('keeps exact agent mentions boundary-aware and regex-safe', () => {
expect(isAgentMentioned('@Regex.Bot please review', 'Regex.Bot')).toBe(true)
expect(isAgentMentioned('@RegexxBot should not match', 'Regex.Bot')).toBe(false)
expect(isAgentMentioned('@Alice, please review', 'Alice')).toBe(true)
expect(isAgentMentioned('mailto@Alice.example', 'Alice')).toBe(false)
})
it('routes @all to every room agent except the sender identity', () => {
expect(resolveMentionTargets(agents, '@all summarize the options', 'socket-alice').map(a => a.name)).toEqual(['Bob', 'Regex.Bot'])
})
it('keeps same-name human senders routable because sender exclusion uses identity, not display name', () => {
const sameNameAgents: TestAgent[] = [
{ name: 'test', id: 'socket-agent-test', agentId: 'agent-test' },
{ name: 'tt', id: 'socket-agent-tt', agentId: 'agent-tt' },
]
expect(resolveMentionTargets(sameNameAgents, '@all can you talk to me?', 'human-test-user').map(a => a.name)).toEqual(['test', 'tt'])
expect(resolveMentionTargets(sameNameAgents, '@test why no response?', 'human-test-user').map(a => a.name)).toEqual(['test'])
})
it('still excludes an agent from routing to itself when the sender identity matches that agent', () => {
const sameNameAgents: TestAgent[] = [
{ name: 'test', id: 'socket-agent-test', agentId: 'agent-test' },
{ name: 'tt', id: 'socket-agent-tt', agentId: 'agent-tt' },
]
expect(resolveMentionTargets(sameNameAgents, '@all compare plans', 'socket-agent-test').map(a => a.name)).toEqual(['tt'])
expect(resolveMentionTargets(sameNameAgents, '@all compare plans', 'agent-test').map(a => a.name)).toEqual(['tt'])
expect(resolveMentionTargets(sameNameAgents, '@test check yourself', 'socket-agent-test').map(a => a.name)).toEqual([])
})
it('routes explicit mentions without treating partial @all text as broadcast', () => {
expect(resolveMentionTargets(agents, '@Bob and @Regex.Bot compare plans', 'socket-alice').map(a => a.name)).toEqual(['Bob', 'Regex.Bot'])
expect(resolveMentionTargets(agents, '@alligator and @Bob compare plans', 'socket-alice').map(a => a.name)).toEqual(['Bob'])
})
it('dedupes mixed @all and explicit mentions', () => {
expect(resolveMentionTargets(agents, '@all @Bob compare plans', 'socket-alice').map(a => a.name)).toEqual(['Bob', 'Regex.Bot'])
})
it('strips the broadcast token and this agent mention before routing to the model', () => {
expect(stripMentionRoutingTokens('@all @Bob please review', 'Bob')).toBe('please review')
expect(stripMentionRoutingTokens('@ALL, @Regex.Bot: please review', 'Regex.Bot')).toBe('please review')
expect(stripMentionRoutingTokens('@all please review', 'all')).toBe('please review')
})
})