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.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user