Files
hermes-ui/tests/server/usage-recorder.test.ts
toor 8c1ddf415f Phase 003: remove the coding-agent and ekko-agent runtimes
Every chat session now runs through the Hermes bridge, so the three chat
chains become feature-equivalent instead of three partially-maintained
dispatch paths. Keeping the coding-agent and in-house agent runtimes alive
meant every bridge change had to be mirrored into two more runtimes that
had already drifted.

Historical rows are read, not migrated: sessions stored with the retired
source normalize to cli on load, workflow graphs naming the retired agents
keep loading and run on the bridge, and the visible-source check plus
SOURCE_LABELS still recognize the old value so those sessions stay listed.

The public surface follows the code: the OpenAPI generator and the MCP
chat-run tool schema drop the six retired entry fields so external callers
are not told about runtimes that no longer exist.

harness-check gains a RETIRED_RUNTIME scan so the residual check is a
mechanical gate instead of a manual grep, with the two surviving read-side
compat sites recorded with reasons.

Refs: docs/phases/phase_003.md
2026-10-16 13:27:50 +08:00

125 lines
3.2 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
const updateUsageMock = vi.hoisted(() => vi.fn())
vi.mock('../../packages/server/src/db/hermes/usage-store', () => ({
updateUsage: updateUsageMock,
}))
vi.mock('../../packages/server/src/services/logger', () => ({
logger: { warn: vi.fn() },
}))
import { normalizeTokenUsage, recordSessionUsage } from '../../packages/server/src/services/usage-recorder'
describe('usage recorder', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('normalizes OpenAI and Anthropic token detail fields', () => {
expect(normalizeTokenUsage({
input_tokens: 120,
output_tokens: 45,
input_tokens_details: { cached_tokens: 30 },
output_tokens_details: { reasoning_tokens: 12 },
})).toEqual({
inputTokens: 120,
outputTokens: 45,
cacheReadTokens: 30,
cacheWriteTokens: 0,
reasoningTokens: 12,
isEstimated: false,
})
expect(normalizeTokenUsage({
usage: {
input_tokens: 80,
output_tokens: 20,
cache_read_input_tokens: 15,
cache_creation_input_tokens: 4,
},
})).toEqual({
inputTokens: 80,
outputTokens: 20,
cacheReadTokens: 15,
cacheWriteTokens: 4,
reasoningTokens: 0,
isEstimated: false,
})
expect(normalizeTokenUsage({
prompt_tokens: 90,
completion_tokens: 18,
prompt_tokens_details: { cached_tokens: 40 },
completion_tokens_details: { reasoning_tokens: 6 },
})).toEqual({
inputTokens: 90,
outputTokens: 18,
cacheReadTokens: 40,
cacheWriteTokens: 0,
reasoningTokens: 6,
isEstimated: false,
})
})
it('fills missing provider fields from the per-run estimate', () => {
expect(normalizeTokenUsage(
{ input_tokens: null, output_tokens: 9 },
{ inputTokens: 25, outputTokens: 8 },
)).toEqual({
inputTokens: 25,
outputTokens: 9,
cacheReadTokens: 0,
cacheWriteTokens: 0,
reasoningTokens: 0,
isEstimated: true,
})
})
it('splits cached tokens from provider input totals when requested', () => {
expect(normalizeTokenUsage({
input_tokens: 120,
output_tokens: 7,
input_tokens_details: { cached_tokens: 30 },
}, {}, { inputIncludesCache: true })).toEqual({
inputTokens: 90,
outputTokens: 7,
cacheReadTokens: 30,
cacheWriteTokens: 0,
reasoningTokens: 0,
isEstimated: false,
})
})
it('persists normalized metadata through the shared store entrypoint', () => {
recordSessionUsage({
sessionId: 'session-1',
runId: 'run-1',
source: 'hermes',
agent: 'hermes',
profile: 'work',
model: 'gpt-5',
provider: 'openai',
usage: { prompt_tokens: 10, completion_tokens: 4 },
})
expect(updateUsageMock).toHaveBeenCalledWith('session-1', {
runId: 'run-1',
source: 'hermes',
agent: 'hermes',
usageScope: undefined,
apiCalls: undefined,
inputTokens: 10,
outputTokens: 4,
cacheReadTokens: 0,
cacheWriteTokens: 0,
reasoningTokens: 0,
model: 'gpt-5',
provider: 'openai',
profile: 'work',
isEstimated: false,
})
})
})