Files
hapi/web/src/chat/presentation.test.ts
T

225 lines
7.5 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { getEventPresentation, formatMessageTimestamp, formatOutlineTimestamp, formatResetTime } from './presentation'
describe('formatOutlineTimestamp', () => {
it('shows only the time for same-day messages', () => {
const date = new Date(2026, 6, 21, 9, 55)
const now = new Date(2026, 6, 21, 12, 0)
expect(formatOutlineTimestamp(date, 'en', now)).toBe(
date.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
)
})
it('zero-pads Chinese dates within the current year', () => {
const date = new Date(2026, 8, 9, 10, 31)
const now = new Date(2026, 6, 21, 12, 0)
expect(formatOutlineTimestamp(date, 'zh-CN', now)).toBe(
`09月09日 ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })}`
)
})
it('uses a zero-padded numeric date in English', () => {
const date = new Date(2026, 9, 1, 10, 31)
const now = new Date(2026, 6, 21, 12, 0)
expect(formatOutlineTimestamp(date, 'en', now)).toBe(
`10/01 ${date.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })}`
)
})
it('includes the year for older years in both locales', () => {
const date = new Date(2025, 8, 9, 10, 31)
const now = new Date(2026, 6, 21, 12, 0)
const zhTime = date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
const enTime = date.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
expect(formatOutlineTimestamp(date, 'zh-CN', now)).toBe(`2025年09月09日 ${zhTime}`)
expect(formatOutlineTimestamp(date, 'en', now)).toBe(`2025/09/09 ${enTime}`)
})
})
describe('getEventPresentation — agent errors', () => {
it('formats error events with warning icon and message text', () => {
const result = getEventPresentation({
type: 'error',
message: 'Cursor Agent failed: authentication required'
})
expect(result.icon).toBe('⚠️')
expect(result.text).toBe('Cursor Agent failed: authentication required')
})
})
describe('getEventPresentation — limit-warning', () => {
it('formats five_hour warning', () => {
const result = getEventPresentation({
type: 'limit-warning',
utilization: 0.9,
endsAt: 1774278000,
limitType: 'five_hour',
})
expect(result.icon).toBe('⚠️')
expect(result.text).toMatch(/Usage limit 90% \(5-hour\)/)
expect(result.text).toMatch(/resets/)
})
it('formats seven_day warning', () => {
const result = getEventPresentation({
type: 'limit-warning',
utilization: 0.85,
endsAt: 1774850400,
limitType: 'seven_day',
})
expect(result.text).toMatch(/Usage limit 85% \(7-day\)/)
})
it('omits type label when limitType is empty', () => {
const result = getEventPresentation({
type: 'limit-warning',
utilization: 1,
endsAt: 1774278000,
limitType: '',
})
expect(result.text).toMatch(/^Usage limit 100% · resets/)
expect(result.text).not.toMatch(/\(/)
})
it('formats unknown limitType with underscore replacement', () => {
const result = getEventPresentation({
type: 'limit-warning',
utilization: 0.5,
endsAt: 1774278000,
limitType: 'thirty_day',
})
expect(result.text).toMatch(/\(thirty day\)/)
})
})
describe('getEventPresentation — limit-reached', () => {
it('shows limitType when present', () => {
const result = getEventPresentation({
type: 'limit-reached',
endsAt: 1774278000,
limitType: 'five_hour',
})
expect(result.icon).toBe('⏳')
expect(result.text).toMatch(/^Usage limit reached \(5-hour\) until/)
})
it('omits limitType when empty', () => {
const result = getEventPresentation({
type: 'limit-reached',
endsAt: 1774278000,
limitType: '',
})
expect(result.icon).toBe('⏳')
expect(result.text).toMatch(/^Usage limit reached until/)
expect(result.text).not.toMatch(/\(/)
})
})
describe('getEventPresentation — token-count', () => {
it('formats Codex token-count as compact context usage', () => {
const result = getEventPresentation({
type: 'token-count',
info: {
total: {
totalTokens: 23745,
inputTokens: 23631,
cachedInputTokens: 18176,
outputTokens: 114,
reasoningOutputTokens: 0
},
modelContextWindow: 258400
}
})
expect(result.icon).toBe('◷')
expect(result.text).toBe('Context 23.6k / 258.4k (9%) · out 114 · cached 18.2k')
})
})
describe('getEventPresentation — thread goals', () => {
it('formats goal status updates', () => {
const result = getEventPresentation({
type: 'thread-goal-updated',
goal: {
threadId: 'thread-1',
objective: 'ship goal support',
status: 'budgetLimited',
tokenBudget: 5000,
tokensUsed: 4100,
timeUsedSeconds: 0,
createdAt: 1,
updatedAt: 2
}
})
expect(result.text).toBe('Goal limited by budget · 4k / 5k')
})
it('formats goal clear events', () => {
const result = getEventPresentation({ type: 'thread-goal-cleared', threadId: 'thread-1' })
expect(result.text).toBe('Goal cleared')
})
})
describe('getEventPresentation — recap (away_summary)', () => {
it('formats the recap with a recap: prefix', () => {
const result = getEventPresentation({
type: 'recap',
text: 'Building the login flow, next: wire up the submit handler.'
})
expect(result.icon).toBe('💭')
expect(result.text).toBe('recap: Building the login flow, next: wire up the submit handler.')
})
})
describe('formatResetTime', () => {
it('formats a unix timestamp to a non-empty string', () => {
const result = formatResetTime(1774278000)
expect(result).toBeTruthy()
expect(typeof result).toBe('string')
})
it('handles millisecond timestamps', () => {
const result = formatResetTime(1774278000000)
expect(result).toBeTruthy()
})
it('returns raw value for invalid timestamps', () => {
const result = formatResetTime(NaN)
expect(result).toBeTruthy()
})
})
describe('formatMessageTimestamp', () => {
it('formats today without requiring a date prefix', () => {
const now = new Date(2026, 4, 22, 14, 30)
const date = new Date(2026, 4, 22, 9, 5)
const result = formatMessageTimestamp(date, now)
expect(result).toBe(date.toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23'
}))
expect(result).not.toContain('2026')
})
it('includes a year for messages outside the current year', () => {
const now = new Date(2026, 4, 22, 14, 30)
const result = formatMessageTimestamp(new Date(2025, 11, 31, 23, 59), now)
expect(result).toContain('2025')
})
})