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.
93 lines
4.8 KiB
TypeScript
93 lines
4.8 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('handles prefix collisions and special-character agent names', () => {
|
|
const specialAgents: TestAgent[] = [
|
|
{ name: 'Bob', id: 'socket-bob', agentId: 'agent-bob' },
|
|
{ name: 'Bobcat', id: 'socket-bobcat', agentId: 'agent-bobcat' },
|
|
{ name: 'C++', id: 'socket-cpp', agentId: 'agent-cpp' },
|
|
]
|
|
|
|
expect(resolveMentionTargets(specialAgents, '@Bobcat inspect', 'human-1').map(a => a.name)).toEqual(['Bobcat'])
|
|
expect(resolveMentionTargets(specialAgents, '@Bob inspect', 'human-1').map(a => a.name)).toEqual(['Bob'])
|
|
expect(resolveMentionTargets(specialAgents, '@C++: inspect', 'human-1').map(a => a.name)).toEqual(['C++'])
|
|
})
|
|
|
|
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')
|
|
})
|
|
})
|