mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
225 lines
7.5 KiB
TypeScript
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')
|
|
})
|
|
})
|