mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-07-16 05:30:20 +00:00
258 lines
7.6 KiB
TypeScript
258 lines
7.6 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const getSessionMock = vi.hoisted(() => vi.fn())
|
|
const createSessionMock = vi.hoisted(() => vi.fn())
|
|
const addMessageMock = vi.hoisted(() => vi.fn())
|
|
const updateSessionStatsMock = vi.hoisted(() => vi.fn())
|
|
const resolveBridgeRunModelConfigMock = vi.hoisted(() => vi.fn())
|
|
const agentRunMock = vi.hoisted(() => vi.fn())
|
|
|
|
vi.mock('../../packages/server/src/db/hermes/session-store', () => ({
|
|
getSession: getSessionMock,
|
|
createSession: createSessionMock,
|
|
addMessage: addMessageMock,
|
|
updateSessionStats: updateSessionStatsMock,
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/services/hermes/run-chat/model-config', () => ({
|
|
resolveBridgeRunModelConfig: resolveBridgeRunModelConfigMock,
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/services/ekko-agent/manager', () => ({
|
|
getGlobalEkkoAgent: vi.fn(() => ({
|
|
run: agentRunMock,
|
|
})),
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/services/ekko-agent/mcp', () => ({
|
|
resolveEkkoMcpServers: vi.fn(() => undefined),
|
|
}))
|
|
|
|
vi.mock('../../packages/ekko-agent/src', () => ({
|
|
createModelClient: vi.fn(() => ({
|
|
provider: 'test',
|
|
requestStyle: 'custom-runtime',
|
|
capabilities: {
|
|
streaming: false,
|
|
tools: true,
|
|
vision: false,
|
|
jsonMode: false,
|
|
systemPrompt: true,
|
|
},
|
|
})),
|
|
resolveModelProviderConfigs: vi.fn(() => ({
|
|
providerConfig: {
|
|
provider: 'test',
|
|
model: 'ekko-test-model',
|
|
apiMode: 'chat_completions',
|
|
},
|
|
})),
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/services/hermes/hermes-profile', () => ({
|
|
getProfileDir: vi.fn(() => '/tmp/hermes-default'),
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/services/hermes/pet-state-socket', () => ({
|
|
observeRunChatPetEvent: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('../../packages/server/src/services/logger', () => ({
|
|
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
}))
|
|
|
|
function makeHarness() {
|
|
const roomTarget = { emit: vi.fn(), except: vi.fn(() => ({ emit: vi.fn() })) }
|
|
const nsp = {
|
|
adapter: { rooms: new Map([['session:session-1', new Set(['socket-1'])]]) },
|
|
to: vi.fn(() => roomTarget),
|
|
}
|
|
const socket = {
|
|
id: 'socket-1',
|
|
connected: true,
|
|
join: vi.fn(),
|
|
emit: vi.fn(),
|
|
to: vi.fn(() => roomTarget),
|
|
}
|
|
const state = {
|
|
messages: [],
|
|
isWorking: false,
|
|
events: [],
|
|
queue: [],
|
|
inputTokens: 10,
|
|
outputTokens: 5,
|
|
}
|
|
const sessionMap = new Map<string, any>([['session-1', state]])
|
|
const events: Array<{ event: string; payload: any }> = []
|
|
return { nsp, socket, sessionMap, state, events }
|
|
}
|
|
|
|
describe('ekko-agent context usage events', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
getSessionMock.mockReturnValue({
|
|
id: 'session-1',
|
|
profile: 'default',
|
|
source: 'coding_agent',
|
|
agent: 'ekko-agent',
|
|
model: 'ekko-test-model',
|
|
provider: 'test-provider',
|
|
workspace: '/tmp/workspace',
|
|
})
|
|
addMessageMock.mockReturnValue(1)
|
|
resolveBridgeRunModelConfigMock.mockResolvedValue({
|
|
model: 'ekko-test-model',
|
|
provider: 'test-provider',
|
|
})
|
|
})
|
|
|
|
it('does not publish step context estimates as formal usage updates', async () => {
|
|
agentRunMock.mockImplementationOnce(async (input: any) => {
|
|
input.onEvent({ type: 'run.started', runId: 'run-1', maxSteps: 3 })
|
|
input.onEvent({
|
|
type: 'context.estimated',
|
|
runId: 'run-1',
|
|
step: 1,
|
|
estimate: {
|
|
contextTokens: 10_000,
|
|
systemPromptTokens: 1_000,
|
|
messageTokens: 2_000,
|
|
toolTokens: 7_000,
|
|
modelContextTokens: 0,
|
|
messageCount: 2,
|
|
toolCount: 5,
|
|
},
|
|
})
|
|
input.onEvent({
|
|
type: 'context.estimated',
|
|
runId: 'run-1',
|
|
step: 2,
|
|
estimate: {
|
|
contextTokens: 30_000,
|
|
systemPromptTokens: 1_000,
|
|
messageTokens: 22_000,
|
|
toolTokens: 7_000,
|
|
modelContextTokens: 0,
|
|
messageCount: 4,
|
|
toolCount: 5,
|
|
},
|
|
})
|
|
return {
|
|
runId: 'run-1',
|
|
output: { role: 'assistant', content: 'done', usage: { inputTokens: 3, outputTokens: 2 } },
|
|
steps: [],
|
|
messages: [],
|
|
events: [],
|
|
contextEstimate: { contextTokens: 30_000 },
|
|
}
|
|
})
|
|
const { handleEkkoAgentRun } = await import('../../packages/server/src/services/hermes/run-chat/handle-ekko-agent-run')
|
|
const { nsp, socket, sessionMap, state, events } = makeHarness()
|
|
|
|
await handleEkkoAgentRun(nsp as any, socket as any, {
|
|
session_id: 'session-1',
|
|
input: 'continue',
|
|
coding_agent_id: 'ekko-agent',
|
|
onEvent: (event: string, payload: any) => events.push({ event, payload }),
|
|
}, 'default', sessionMap, vi.fn(() => false))
|
|
|
|
expect(events.filter(item => item.event === 'context.estimated').map(item => item.payload.contextTokens)).toEqual([
|
|
10_000,
|
|
30_000,
|
|
])
|
|
const usageEvents = events.filter(item => item.event === 'usage.updated')
|
|
expect(usageEvents).toHaveLength(1)
|
|
expect(usageEvents[0].payload).toEqual(expect.objectContaining({
|
|
input_tokens: 13,
|
|
output_tokens: 7,
|
|
total_tokens: 20,
|
|
contextTokens: 30_000,
|
|
}))
|
|
expect(state.contextTokens).toBe(30_000)
|
|
})
|
|
|
|
it('includes paired tool results in Ekko history for follow-up turns', async () => {
|
|
agentRunMock.mockResolvedValueOnce({
|
|
runId: 'run-1',
|
|
output: { role: 'assistant', content: 'follow-up done', usage: { inputTokens: 3, outputTokens: 2 } },
|
|
steps: [],
|
|
messages: [],
|
|
events: [],
|
|
contextEstimate: { contextTokens: 12_000 },
|
|
})
|
|
const { handleEkkoAgentRun } = await import('../../packages/server/src/services/hermes/run-chat/handle-ekko-agent-run')
|
|
const { nsp, socket, sessionMap, state } = makeHarness()
|
|
state.messages = [
|
|
{
|
|
id: 1,
|
|
session_id: 'session-1',
|
|
role: 'user',
|
|
content: 'check weather',
|
|
timestamp: 1000,
|
|
},
|
|
{
|
|
id: 2,
|
|
session_id: 'session-1',
|
|
role: 'assistant',
|
|
content: '',
|
|
tool_calls: [{
|
|
id: 'call_weather',
|
|
type: 'function',
|
|
function: {
|
|
name: 'browser_navigate',
|
|
arguments: '{"url":"https://weather.example"}',
|
|
},
|
|
}],
|
|
timestamp: 1001,
|
|
},
|
|
{
|
|
id: 3,
|
|
session_id: 'session-1',
|
|
role: 'tool',
|
|
content: '{"forecast":"sunny"}',
|
|
tool_call_id: 'call_weather',
|
|
tool_name: 'browser_navigate',
|
|
timestamp: 1002,
|
|
},
|
|
{
|
|
id: 4,
|
|
session_id: 'session-1',
|
|
role: 'tool',
|
|
content: 'orphan result',
|
|
tool_call_id: 'call_orphan',
|
|
tool_name: 'browser_navigate',
|
|
timestamp: 1003,
|
|
},
|
|
]
|
|
|
|
await handleEkkoAgentRun(nsp as any, socket as any, {
|
|
session_id: 'session-1',
|
|
input: 'thanks',
|
|
coding_agent_id: 'ekko-agent',
|
|
}, 'default', sessionMap, vi.fn(() => false))
|
|
|
|
const runInput = agentRunMock.mock.calls[0][0]
|
|
expect(runInput.messages).toMatchObject([
|
|
{ role: 'user', content: 'check weather' },
|
|
{
|
|
role: 'assistant',
|
|
content: '',
|
|
toolCalls: [{
|
|
id: 'call_weather',
|
|
name: 'browser_navigate',
|
|
arguments: { url: 'https://weather.example' },
|
|
rawArguments: '{"url":"https://weather.example"}',
|
|
}],
|
|
},
|
|
{
|
|
role: 'tool',
|
|
content: '{"forecast":"sunny"}',
|
|
toolCallId: 'call_weather',
|
|
name: 'browser_navigate',
|
|
},
|
|
{ role: 'user', content: 'thanks' },
|
|
])
|
|
expect(runInput.messages.some((message: any) => message.content === 'orphan result')).toBe(false)
|
|
})
|
|
})
|