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.
588 lines
24 KiB
TypeScript
588 lines
24 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { SummaryCache } from '../../packages/server/src/services/hermes/context-engine/summary-cache'
|
|
import {
|
|
buildAgentInstructions,
|
|
buildSummarizationSystemPrompt,
|
|
buildFullSummaryPrompt,
|
|
buildIncrementalUpdatePrompt,
|
|
} from '../../packages/server/src/services/hermes/context-engine/prompt'
|
|
import { ContextEngine } from '../../packages/server/src/services/hermes/context-engine/compressor'
|
|
import type { StoredMessage, MessageFetcher, GatewayCaller } from '../../packages/server/src/services/hermes/context-engine/types'
|
|
|
|
// ─── Helpers ─────────────────────────────────────────────────
|
|
|
|
function makeMessage(overrides: Partial<StoredMessage> = {}): StoredMessage {
|
|
return {
|
|
id: 'msg-1',
|
|
roomId: 'room-1',
|
|
senderId: 'user-1',
|
|
senderName: 'Alice',
|
|
content: 'Hello world',
|
|
timestamp: 1000,
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
function makeMessages(count: number, roomId = 'room-1', startTimestamp = 1000): StoredMessage[] {
|
|
return Array.from({ length: count }, (_, i) => makeMessage({
|
|
id: `msg-${i}`,
|
|
roomId,
|
|
senderId: i % 3 === 0 ? 'agent-socket' : `user-${i}`,
|
|
senderName: i % 3 === 0 ? 'Claude' : `User${i}`,
|
|
content: `Message ${i} with some content`,
|
|
timestamp: startTimestamp + i * 1000,
|
|
}))
|
|
}
|
|
|
|
// ─── SummaryCache ─────────────────────────────────────────────
|
|
|
|
describe('SummaryCache', () => {
|
|
it('stores and retrieves entries', () => {
|
|
const cache = new SummaryCache(60_000)
|
|
cache.set('room-1', {
|
|
summary: 'Summary text',
|
|
lastMessageId: 'msg-10',
|
|
lastMessageTimestamp: 5000,
|
|
createdAt: Date.now(),
|
|
})
|
|
const entry = cache.get('room-1')
|
|
expect(entry).toBeDefined()
|
|
expect(entry!.summary).toBe('Summary text')
|
|
})
|
|
|
|
it('returns undefined for expired entries', () => {
|
|
const cache = new SummaryCache(100) // 100ms TTL
|
|
cache.set('room-1', {
|
|
summary: 'Old summary',
|
|
lastMessageId: 'msg-5',
|
|
lastMessageTimestamp: 5000,
|
|
createdAt: Date.now() - 200, // created 200ms ago
|
|
})
|
|
expect(cache.get('room-1')).toBeUndefined()
|
|
})
|
|
|
|
it('invalidates entries for a room', () => {
|
|
const cache = new SummaryCache(60_000)
|
|
cache.set('room-1', { summary: 'A', lastMessageId: 'msg-1', lastMessageTimestamp: 1000, createdAt: Date.now() })
|
|
cache.set('room-2', { summary: 'C', lastMessageId: 'msg-3', lastMessageTimestamp: 3000, createdAt: Date.now() })
|
|
|
|
cache.invalidate('room-1')
|
|
expect(cache.get('room-1')).toBeUndefined()
|
|
expect(cache.get('room-2')).toBeDefined()
|
|
})
|
|
|
|
it('enforces max entry limit', () => {
|
|
const cache = new SummaryCache(60_000)
|
|
// Fill cache beyond limit (internal MAX_ENTRIES = 200)
|
|
for (let i = 0; i < 210; i++) {
|
|
cache.set(`room-${i}`, {
|
|
summary: `Summary ${i}`,
|
|
lastMessageId: `msg-${i}`,
|
|
lastMessageTimestamp: i * 1000,
|
|
createdAt: Date.now() - (210 - i), // earlier entries have older createdAt
|
|
})
|
|
}
|
|
// Cache should not exceed 200 entries
|
|
expect(cache.size).toBeLessThanOrEqual(200)
|
|
})
|
|
})
|
|
|
|
// ─── Prompts ──────────────────────────────────────────────────
|
|
|
|
describe('prompts', () => {
|
|
it('builds agent instructions with all fields', () => {
|
|
const result = buildAgentInstructions({
|
|
agentName: 'Claude',
|
|
roomName: 'general',
|
|
agentDescription: 'AI coding assistant',
|
|
memberNames: ['Alice', 'Bob', 'Claude'],
|
|
members: [
|
|
{ userId: 'u1', name: 'Alice', description: 'dev' },
|
|
{ userId: 'u2', name: 'Bob', description: 'designer' },
|
|
{ userId: 'u3', name: 'Claude', description: '' },
|
|
],
|
|
})
|
|
expect(result).toContain('"Claude"')
|
|
expect(result).toContain('general')
|
|
expect(result).toContain('AI coding assistant')
|
|
expect(result).toContain('Alice')
|
|
expect(result).toContain('Bob')
|
|
expect(result).toContain('- Claude')
|
|
expect(result).not.toContain('@Claude')
|
|
})
|
|
|
|
it('builds agent instructions with empty member list', () => {
|
|
const result = buildAgentInstructions({
|
|
agentName: 'GPT',
|
|
roomName: 'dev',
|
|
agentDescription: 'Helper',
|
|
memberNames: [],
|
|
members: [],
|
|
})
|
|
expect(result).toContain('"GPT"')
|
|
expect(result).toContain('未知')
|
|
})
|
|
|
|
it('builds agent instructions using memberNames when members is empty', () => {
|
|
const result = buildAgentInstructions({
|
|
agentName: 'GPT',
|
|
roomName: 'dev',
|
|
agentDescription: 'Helper',
|
|
memberNames: ['Alice', 'Bob'],
|
|
members: [],
|
|
})
|
|
expect(result).toContain('Alice')
|
|
expect(result).toContain('Bob')
|
|
})
|
|
|
|
it('builds summarization system prompt', () => {
|
|
const result = buildSummarizationSystemPrompt()
|
|
expect(result).toContain('摘要')
|
|
})
|
|
|
|
it('builds full summary prompt', () => {
|
|
const result = buildFullSummaryPrompt()
|
|
expect(result).toContain('摘要')
|
|
})
|
|
|
|
it('builds incremental update prompt', () => {
|
|
const result = buildIncrementalUpdatePrompt()
|
|
expect(result).toContain('更新')
|
|
})
|
|
})
|
|
|
|
// ─── ContextEngine.buildContext ────────────────────────────────
|
|
|
|
describe('ContextEngine.buildContext', () => {
|
|
let mockSummarize = vi.fn().mockResolvedValue({ summary: 'Summary of conversation.', sessionId: 'comp-1' })
|
|
const mockGatewayCaller: GatewayCaller = {
|
|
summarize: mockSummarize,
|
|
}
|
|
|
|
let mockFetcher: MessageFetcher
|
|
let engine: ContextEngine
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockFetcher = {
|
|
getMessagesForContext: vi.fn().mockReturnValue([]),
|
|
getContextSnapshot: vi.fn().mockReturnValue(null),
|
|
saveContextSnapshot: vi.fn(),
|
|
deleteContextSnapshot: vi.fn(),
|
|
}
|
|
engine = new ContextEngine({
|
|
config: { maxHistoryTokens: 4000, tailMessageCount: 10, triggerTokens: 100_000, charsPerToken: 4, summarizationTimeoutMs: 30_000 },
|
|
messageFetcher: mockFetcher,
|
|
gatewayCaller: { summarize: mockSummarize },
|
|
})
|
|
})
|
|
|
|
it('returns all messages as history when under threshold', async () => {
|
|
const messages = makeMessages(10) // 10 messages, under trigger threshold
|
|
mockFetcher.getMessagesForContext = vi.fn().mockReturnValue(messages)
|
|
|
|
const result = await engine.buildContext({
|
|
roomId: 'room-1',
|
|
agentId: 'agent-1',
|
|
agentName: 'Claude',
|
|
agentDescription: 'Helper',
|
|
agentSocketId: 'agent-socket',
|
|
roomName: 'general',
|
|
memberNames: ['Alice'],
|
|
members: [{ userId: 'u1', name: 'Alice', description: '' }],
|
|
upstream: 'http://localhost:8642',
|
|
apiKey: null,
|
|
currentMessage: messages[messages.length - 1],
|
|
})
|
|
|
|
expect(result.meta.totalMessages).toBe(10)
|
|
expect(result.meta.compressed).toBe(false)
|
|
expect(result.conversationHistory).toHaveLength(10)
|
|
expect(result.instructions).toContain('Claude')
|
|
// No LLM call for short conversations
|
|
expect(mockSummarize).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('records full context token estimates without compressing when under threshold', async () => {
|
|
const messages = makeMessages(3)
|
|
mockFetcher.getMessagesForContext = vi.fn().mockReturnValue(messages)
|
|
const contextTokenEstimator = vi.fn().mockResolvedValue(19_379)
|
|
const onProgress = vi.fn()
|
|
|
|
const result = await engine.buildContext({
|
|
roomId: 'room-1',
|
|
agentId: 'agent-1',
|
|
agentName: 'Claude',
|
|
agentDescription: 'Helper',
|
|
agentSocketId: 'agent-socket',
|
|
roomName: 'general',
|
|
memberNames: ['Alice'],
|
|
members: [{ userId: 'u1', name: 'Alice', description: '' }],
|
|
upstream: 'http://localhost:8642',
|
|
apiKey: null,
|
|
currentMessage: messages[messages.length - 1],
|
|
contextTokenEstimator,
|
|
onProgress,
|
|
})
|
|
|
|
expect(result.meta.compressed).toBe(false)
|
|
expect(result.meta.contextTokenEstimate).toBe(19_379)
|
|
expect(result.meta.messageTokenEstimate).toBeGreaterThan(0)
|
|
expect(contextTokenEstimator).toHaveBeenCalledWith(
|
|
expect.arrayContaining([{ role: 'assistant', content: expect.stringContaining('[Claude]') }]),
|
|
expect.stringContaining('"Claude"'),
|
|
)
|
|
expect(mockSummarize).not.toHaveBeenCalled()
|
|
expect(onProgress).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('uses full context token estimates to trigger group compression', async () => {
|
|
const messages = makeMessages(20)
|
|
mockFetcher.getMessagesForContext = vi.fn().mockReturnValue(messages)
|
|
const onProgress = vi.fn()
|
|
|
|
const result = await engine.buildContext({
|
|
roomId: 'room-1',
|
|
agentId: 'agent-1',
|
|
agentName: 'Claude',
|
|
agentDescription: 'Helper',
|
|
agentSocketId: 'agent-socket',
|
|
roomName: 'general',
|
|
memberNames: [],
|
|
members: [],
|
|
upstream: 'http://localhost:8642',
|
|
apiKey: null,
|
|
currentMessage: messages[messages.length - 1],
|
|
contextTokenEstimator: vi.fn().mockResolvedValue(120_000),
|
|
onProgress,
|
|
})
|
|
|
|
expect(result.meta.compressed).toBe(true)
|
|
expect(result.meta.contextTokenEstimate).toBe(120_000)
|
|
expect(mockSummarize).toHaveBeenCalledTimes(1)
|
|
expect(mockFetcher.saveContextSnapshot).toHaveBeenCalledTimes(1)
|
|
expect(onProgress).toHaveBeenCalledWith({
|
|
status: 'compressing',
|
|
path: 'full',
|
|
messageCount: 20,
|
|
tokenCount: 120_000,
|
|
})
|
|
})
|
|
|
|
it('throws when group prompt and tools exceed threshold with too little history to compress', async () => {
|
|
const messages = makeMessages(4)
|
|
mockFetcher.getMessagesForContext = vi.fn().mockReturnValue(messages)
|
|
|
|
await expect(engine.buildContext({
|
|
roomId: 'room-1',
|
|
agentId: 'agent-1',
|
|
agentName: 'Claude',
|
|
agentDescription: 'Helper',
|
|
agentSocketId: 'agent-socket',
|
|
roomName: 'general',
|
|
memberNames: [],
|
|
members: [],
|
|
upstream: 'http://localhost:8642',
|
|
apiKey: null,
|
|
currentMessage: messages[messages.length - 1],
|
|
contextTokenEstimator: vi.fn().mockResolvedValue(120_000),
|
|
})).rejects.toThrow('Context window is too small')
|
|
|
|
expect(mockSummarize).not.toHaveBeenCalled()
|
|
expect(mockFetcher.saveContextSnapshot).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('throws on snapshot path when overhead plus new messages exceed threshold without compressible history', async () => {
|
|
const messages = makeMessages(12)
|
|
mockFetcher.getMessagesForContext = vi.fn().mockReturnValue(messages)
|
|
mockFetcher.getContextSnapshot = vi.fn().mockReturnValue({
|
|
roomId: 'room-1',
|
|
summary: 'Existing summary',
|
|
lastMessageId: 'msg-9',
|
|
lastMessageTimestamp: messages[9].timestamp,
|
|
updatedAt: Date.now(),
|
|
})
|
|
|
|
await expect(engine.buildContext({
|
|
roomId: 'room-1',
|
|
agentId: 'agent-1',
|
|
agentName: 'Claude',
|
|
agentDescription: 'Helper',
|
|
agentSocketId: 'agent-socket',
|
|
roomName: 'general',
|
|
memberNames: [],
|
|
members: [],
|
|
upstream: 'http://localhost:8642',
|
|
apiKey: null,
|
|
currentMessage: messages[messages.length - 1],
|
|
contextTokenEstimator: vi.fn().mockResolvedValue(120_000),
|
|
})).rejects.toThrow('Context window is too small')
|
|
|
|
expect(mockSummarize).not.toHaveBeenCalled()
|
|
expect(mockFetcher.saveContextSnapshot).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('splits into head/tail and compresses middle when over threshold', async () => {
|
|
const messages = makeMessages(20)
|
|
mockFetcher.getMessagesForContext = vi.fn().mockReturnValue(messages)
|
|
|
|
const result = await engine.buildContext({
|
|
roomId: 'room-1',
|
|
agentId: 'agent-1',
|
|
agentName: 'Claude',
|
|
agentDescription: 'Helper',
|
|
agentSocketId: 'agent-socket',
|
|
roomName: 'general',
|
|
memberNames: [],
|
|
members: [],
|
|
upstream: 'http://localhost:8642',
|
|
apiKey: null,
|
|
currentMessage: messages[messages.length - 1],
|
|
compression: { triggerTokens: 10 }, // Force compression with tiny threshold
|
|
})
|
|
|
|
expect(result.meta.totalMessages).toBe(20)
|
|
expect(result.meta.compressed).toBe(true)
|
|
expect(mockSummarize).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('uses cache hit when available and no new messages', async () => {
|
|
const messages = makeMessages(20)
|
|
mockFetcher.getMessagesForContext = vi.fn().mockReturnValue(messages)
|
|
|
|
// First call — creates snapshot (with forced compression)
|
|
await engine.buildContext({
|
|
roomId: 'room-1', agentId: 'agent-1', agentName: 'Claude',
|
|
agentDescription: '', agentSocketId: 'agent-socket', roomName: 'general',
|
|
memberNames: [], members: [], upstream: 'http://localhost:8642', apiKey: null,
|
|
currentMessage: messages[messages.length - 1],
|
|
compression: { triggerTokens: 10 },
|
|
})
|
|
|
|
// Verify snapshot was saved
|
|
expect(mockFetcher.saveContextSnapshot).toHaveBeenCalledTimes(1)
|
|
|
|
// Simulate that the snapshot now exists in storage
|
|
const savedSnapshot = mockFetcher.saveContextSnapshot.mock.calls[0]
|
|
mockFetcher.getContextSnapshot = vi.fn().mockReturnValue({
|
|
roomId: 'room-1',
|
|
summary: savedSnapshot[1],
|
|
lastMessageId: savedSnapshot[2],
|
|
lastMessageTimestamp: savedSnapshot[3],
|
|
updatedAt: Date.now(),
|
|
})
|
|
|
|
// Second call — cache hit (snapshot exists, same messages)
|
|
const result2 = await engine.buildContext({
|
|
roomId: 'room-1', agentId: 'agent-1', agentName: 'Claude',
|
|
agentDescription: '', agentSocketId: 'agent-socket', roomName: 'general',
|
|
memberNames: [], members: [], upstream: 'http://localhost:8642', apiKey: null,
|
|
currentMessage: messages[messages.length - 1],
|
|
})
|
|
|
|
expect(result2.meta.hadSnapshot).toBe(true)
|
|
// Only one LLM call (from the first buildContext)
|
|
expect(mockSummarize).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('does incremental update when cache hit with new messages', async () => {
|
|
const messages = makeMessages(20)
|
|
mockFetcher.getMessagesForContext = vi.fn().mockReturnValue(messages)
|
|
|
|
// First call — full compression (with forced compression)
|
|
await engine.buildContext({
|
|
roomId: 'room-1', agentId: 'agent-1', agentName: 'Claude',
|
|
agentDescription: '', agentSocketId: 'agent-socket', roomName: 'general',
|
|
memberNames: [], members: [], upstream: 'http://localhost:8642', apiKey: null,
|
|
currentMessage: messages[messages.length - 1],
|
|
compression: { triggerTokens: 10 },
|
|
})
|
|
|
|
// Simulate that the snapshot now exists in storage
|
|
const savedSnapshot = mockFetcher.saveContextSnapshot.mock.calls[0]
|
|
mockFetcher.getContextSnapshot = vi.fn().mockReturnValue({
|
|
roomId: 'room-1',
|
|
summary: savedSnapshot[1],
|
|
lastMessageId: savedSnapshot[2],
|
|
lastMessageTimestamp: savedSnapshot[3],
|
|
updatedAt: Date.now(),
|
|
})
|
|
|
|
expect(mockSummarize).toHaveBeenCalledTimes(1)
|
|
// First call: no previousSummary
|
|
// GatewayCaller.summarize signature: upstream, apiKey, systemPrompt, messages, roomId, profile, previousSummary
|
|
const firstCallArgs = mockSummarize.mock.calls[0]
|
|
expect(firstCallArgs[4]).toBe('room-1') // roomId
|
|
expect(firstCallArgs[5]).toBe('default') // profile
|
|
expect(firstCallArgs[6]).toBeUndefined() // previousSummary not passed
|
|
|
|
// Insert a new message
|
|
const middleInsert = makeMessage({
|
|
id: 'msg-new', roomId: 'room-1', senderId: 'user-99',
|
|
senderName: 'NewUser', content: 'New middle message', timestamp: 12000,
|
|
})
|
|
const updatedMessages = [...messages.slice(0, 9), middleInsert, ...messages.slice(9)]
|
|
mockFetcher.getMessagesForContext = vi.fn().mockReturnValue(updatedMessages)
|
|
|
|
const onProgress = vi.fn()
|
|
// Second call — incremental update
|
|
await engine.buildContext({
|
|
roomId: 'room-1', agentId: 'agent-1', agentName: 'Claude',
|
|
agentDescription: '', agentSocketId: 'agent-socket', roomName: 'general',
|
|
memberNames: [], members: [], upstream: 'http://localhost:8642', apiKey: null,
|
|
currentMessage: updatedMessages[updatedMessages.length - 1],
|
|
compression: { triggerTokens: 10 },
|
|
onProgress,
|
|
})
|
|
|
|
expect(mockSummarize).toHaveBeenCalledTimes(2)
|
|
// Second call: has previousSummary
|
|
const secondCallArgs = mockSummarize.mock.calls[1]
|
|
expect(secondCallArgs[6]).toBe('Summary of conversation.')
|
|
expect(onProgress).toHaveBeenCalledWith(expect.objectContaining({
|
|
status: 'compressing',
|
|
path: 'snapshot',
|
|
}))
|
|
})
|
|
|
|
it('falls back to no-summary on LLM failure', async () => {
|
|
mockSummarize.mockRejectedValue(new Error('LLM timeout'))
|
|
|
|
const messages = makeMessages(20)
|
|
mockFetcher.getMessagesForContext = vi.fn().mockReturnValue(messages)
|
|
|
|
const result = await engine.buildContext({
|
|
roomId: 'room-1', agentId: 'agent-1', agentName: 'Claude',
|
|
agentDescription: '', agentSocketId: 'agent-socket', roomName: 'general',
|
|
memberNames: [], members: [], upstream: 'http://localhost:8642', apiKey: null,
|
|
currentMessage: messages[messages.length - 1],
|
|
compression: { triggerTokens: 10 },
|
|
})
|
|
|
|
// Should not throw, and should still return history
|
|
expect(result.conversationHistory.length).toBeGreaterThan(0)
|
|
// No summary pair in the output
|
|
expect(result.conversationHistory[0]?.content).not.toContain('Previous conversation summary')
|
|
})
|
|
|
|
it('trims tail when over token budget', async () => {
|
|
const engine = new ContextEngine({
|
|
config: {
|
|
maxHistoryTokens: 200, // small budget
|
|
tailMessageCount: 10,
|
|
triggerTokens: 10, // force compression
|
|
charsPerToken: 4,
|
|
summarizationTimeoutMs: 30_000,
|
|
},
|
|
messageFetcher: mockFetcher,
|
|
gatewayCaller: { summarize: mockSummarize },
|
|
})
|
|
|
|
const messages = makeMessages(20)
|
|
mockFetcher.getMessagesForContext = vi.fn().mockReturnValue(messages)
|
|
|
|
const result = await engine.buildContext({
|
|
roomId: 'room-1', agentId: 'agent-1', agentName: 'Claude',
|
|
agentDescription: '', agentSocketId: 'agent-socket', roomName: 'general',
|
|
memberNames: [], members: [], upstream: 'http://localhost:8642', apiKey: null,
|
|
currentMessage: messages[messages.length - 1],
|
|
})
|
|
|
|
// History should be trimmed to fit within 200 tokens
|
|
// Use same estimation logic as compressor: CJK * 1.5 + other / charsPerToken
|
|
const totalChars = result.conversationHistory.reduce((sum, m) => sum + m.content.length, 0)
|
|
const cjk = (result.conversationHistory.map(m => m.content).join('').match(/[⺀-鿿가- -〿-]/g) || []).length
|
|
const other = totalChars - cjk
|
|
const estimatedTokens = Math.ceil(cjk * 1.5 + other / 4)
|
|
expect(estimatedTokens).toBeLessThanOrEqual(200)
|
|
})
|
|
|
|
it('maps agent messages to assistant role', async () => {
|
|
const messages = [
|
|
makeMessage({ senderId: 'user-1', senderName: 'Alice', content: 'Hello', timestamp: 1000 }),
|
|
makeMessage({ senderId: 'agent-socket', senderName: 'Claude', content: 'Hi there', timestamp: 2000 }),
|
|
]
|
|
mockFetcher.getMessagesForContext = vi.fn().mockReturnValue(messages)
|
|
|
|
const result = await engine.buildContext({
|
|
roomId: 'room-1', agentId: 'agent-1', agentName: 'Claude',
|
|
agentDescription: '', agentSocketId: 'agent-socket', roomName: 'general',
|
|
memberNames: [], members: [], upstream: 'http://localhost:8642', apiKey: null,
|
|
currentMessage: messages[messages.length - 1],
|
|
})
|
|
|
|
// First message from user → 'user' role with name prefix
|
|
expect(result.conversationHistory[0].role).toBe('user')
|
|
expect(result.conversationHistory[0].content).toContain('[Alice]')
|
|
|
|
// Second message from agent → 'assistant' role with sender prefix for group-chat context.
|
|
expect(result.conversationHistory[1].role).toBe('assistant')
|
|
expect(result.conversationHistory[1].content).toBe('[Claude]: Hi there')
|
|
})
|
|
|
|
it('maps other messages to user role with name prefix', async () => {
|
|
const messages = [
|
|
makeMessage({ senderId: 'user-2', senderName: 'Bob', content: 'Hey', timestamp: 1000 }),
|
|
]
|
|
mockFetcher.getMessagesForContext = vi.fn().mockReturnValue(messages)
|
|
|
|
const result = await engine.buildContext({
|
|
roomId: 'room-1', agentId: 'agent-1', agentName: 'Claude',
|
|
agentDescription: '', agentSocketId: 'agent-socket', roomName: 'general',
|
|
memberNames: [], members: [], upstream: 'http://localhost:8642', apiKey: null,
|
|
currentMessage: messages[messages.length - 1],
|
|
})
|
|
|
|
expect(result.conversationHistory[0].role).toBe('user')
|
|
expect(result.conversationHistory[0].content).toBe('[Bob]: Hey')
|
|
})
|
|
|
|
it('generates instructions with agent identity', async () => {
|
|
const messages = makeMessages(1)
|
|
mockFetcher.getMessagesForContext = vi.fn().mockReturnValue(messages)
|
|
|
|
const result = await engine.buildContext({
|
|
roomId: 'room-1', agentId: 'agent-1', agentName: 'Claude',
|
|
agentDescription: 'Code helper', agentSocketId: 'agent-socket', roomName: 'dev',
|
|
memberNames: ['Alice', 'Bob'],
|
|
members: [
|
|
{ userId: 'u1', name: 'Alice', description: 'dev' },
|
|
{ userId: 'u2', name: 'Bob', description: 'designer' },
|
|
],
|
|
upstream: 'http://localhost:8642', apiKey: null,
|
|
currentMessage: messages[0],
|
|
})
|
|
|
|
expect(result.instructions).toContain('"Claude"')
|
|
expect(result.instructions).toContain('Code helper')
|
|
expect(result.instructions).toContain('dev')
|
|
expect(result.instructions).toContain('Alice')
|
|
})
|
|
|
|
it('invalidates room cache', async () => {
|
|
// Create a snapshot via the fetcher mock
|
|
mockFetcher.getContextSnapshot = vi.fn().mockReturnValue({
|
|
roomId: 'room-1',
|
|
summary: 'Test',
|
|
lastMessageId: 'msg-10',
|
|
lastMessageTimestamp: 1000,
|
|
updatedAt: Date.now(),
|
|
})
|
|
|
|
const messages = makeMessages(5)
|
|
mockFetcher.getMessagesForContext = vi.fn().mockReturnValue(messages)
|
|
|
|
// Build context to create snapshot
|
|
await engine.buildContext({
|
|
roomId: 'room-1', agentId: 'agent-1', agentName: 'Claude',
|
|
agentDescription: '', agentSocketId: 'agent-socket', roomName: 'general',
|
|
memberNames: [], members: [], upstream: 'http://localhost:8642', apiKey: null,
|
|
currentMessage: messages[messages.length - 1],
|
|
})
|
|
|
|
// Invalidate
|
|
engine.invalidateRoom('room-1')
|
|
expect(mockFetcher.deleteContextSnapshot).toHaveBeenCalledWith('room-1')
|
|
})
|
|
})
|