mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
163 lines
5.2 KiB
TypeScript
163 lines
5.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import type { ResumableSession } from '@hapi/protocol'
|
|
import {
|
|
filterResumeSessions,
|
|
formatResumeSessionRelativeTime,
|
|
getResumeSessionName,
|
|
reducePickerState,
|
|
type PickerState
|
|
} from './resumeSessionPickerState'
|
|
|
|
function session(overrides: Partial<ResumableSession>): ResumableSession {
|
|
return {
|
|
sessionId: 'session-1',
|
|
flavor: 'codex',
|
|
directory: '/tmp/project',
|
|
machineId: 'machine-1',
|
|
active: false,
|
|
thinking: false,
|
|
controlledByUser: false,
|
|
agentSessionId: 'agent-1',
|
|
updatedAt: 1,
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
describe('resumeSessionPickerState', () => {
|
|
it('uses the first user message as the list label before title or summary', () => {
|
|
expect(getResumeSessionName(session({
|
|
sessionId: 'session-title',
|
|
name: 'Generated title',
|
|
summary: 'Generated summary',
|
|
firstUserMessage: 'First prompt'
|
|
}))).toBe('First prompt')
|
|
})
|
|
|
|
it('formats updatedAt as relative time', () => {
|
|
const now = 1_700_000_000_000
|
|
|
|
expect(formatResumeSessionRelativeTime(now - 10_000, now)).toBe('now')
|
|
expect(formatResumeSessionRelativeTime(now - 5 * 60_000, now)).toBe('5m ago')
|
|
expect(formatResumeSessionRelativeTime(now - 3 * 60 * 60_000, now)).toBe('3h ago')
|
|
expect(formatResumeSessionRelativeTime(now - 2 * 24 * 60 * 60_000, now)).toBe('2d ago')
|
|
expect(formatResumeSessionRelativeTime(Math.floor((now - 5 * 60_000) / 1000), now)).toBe('5m ago')
|
|
expect(formatResumeSessionRelativeTime(NaN, now)).toBe('unknown')
|
|
})
|
|
|
|
it('filters sessions by searchable fields case-insensitively', () => {
|
|
const sessions = [
|
|
session({
|
|
sessionId: 'alpha',
|
|
name: 'Payment Refactor',
|
|
firstUserMessage: 'Implement billing flow',
|
|
directory: '/repo/api',
|
|
agentSessionId: 'thread-a'
|
|
}),
|
|
session({
|
|
sessionId: 'beta',
|
|
flavor: 'claude',
|
|
directory: '/repo/mobile',
|
|
summary: 'Fix login screen',
|
|
agentSessionId: 'thread-b'
|
|
}),
|
|
session({
|
|
sessionId: 'gamma',
|
|
active: true,
|
|
controlledByUser: false,
|
|
directory: '/repo/web',
|
|
agentSessionId: 'thread-c'
|
|
})
|
|
]
|
|
|
|
expect(filterResumeSessions(sessions, 'billing').map((item) => item.sessionId)).toEqual(['alpha'])
|
|
expect(filterResumeSessions(sessions, 'MOBILE').map((item) => item.sessionId)).toEqual(['beta'])
|
|
expect(filterResumeSessions(sessions, 'thread-c').map((item) => item.sessionId)).toEqual(['gamma'])
|
|
expect(filterResumeSessions(sessions, 'remote').map((item) => item.sessionId)).toEqual(['gamma'])
|
|
})
|
|
|
|
it('resets selection and scroll when query changes', () => {
|
|
const initial: PickerState = {
|
|
query: 'abc',
|
|
selectedIndex: 5,
|
|
scrollOffset: 3
|
|
}
|
|
|
|
expect(reducePickerState(initial, {
|
|
type: 'char',
|
|
value: 'd'
|
|
}, {
|
|
itemCount: 10,
|
|
visibleCount: 5
|
|
})).toEqual({
|
|
query: 'abcd',
|
|
selectedIndex: 0,
|
|
scrollOffset: 0
|
|
})
|
|
|
|
expect(reducePickerState(initial, {
|
|
type: 'key',
|
|
key: 'backspace'
|
|
}, {
|
|
itemCount: 10,
|
|
visibleCount: 5
|
|
})).toEqual({
|
|
query: 'ab',
|
|
selectedIndex: 0,
|
|
scrollOffset: 0
|
|
})
|
|
})
|
|
|
|
it('keeps keyboard navigation inside list bounds and visible window', () => {
|
|
let state: PickerState = {
|
|
query: '',
|
|
selectedIndex: 0,
|
|
scrollOffset: 0
|
|
}
|
|
|
|
state = reducePickerState(state, { type: 'key', key: 'up' }, {
|
|
itemCount: 20,
|
|
visibleCount: 5
|
|
})
|
|
expect(state.selectedIndex).toBe(0)
|
|
expect(state.scrollOffset).toBe(0)
|
|
|
|
state = reducePickerState(state, { type: 'key', key: 'pageDown' }, {
|
|
itemCount: 20,
|
|
visibleCount: 5
|
|
})
|
|
expect(state.selectedIndex).toBe(5)
|
|
expect(state.scrollOffset).toBe(1)
|
|
|
|
state = reducePickerState(state, { type: 'key', key: 'end' }, {
|
|
itemCount: 20,
|
|
visibleCount: 5
|
|
})
|
|
expect(state.selectedIndex).toBe(19)
|
|
expect(state.scrollOffset).toBe(15)
|
|
|
|
state = reducePickerState(state, { type: 'key', key: 'down' }, {
|
|
itemCount: 20,
|
|
visibleCount: 5
|
|
})
|
|
expect(state.selectedIndex).toBe(19)
|
|
expect(state.scrollOffset).toBe(15)
|
|
})
|
|
|
|
it('uses null-equivalent selection when there are no items', () => {
|
|
const state = reducePickerState({
|
|
query: '',
|
|
selectedIndex: 0,
|
|
scrollOffset: 0
|
|
}, {
|
|
type: 'key',
|
|
key: 'down'
|
|
}, {
|
|
itemCount: 0,
|
|
visibleCount: 5
|
|
})
|
|
|
|
expect(state.selectedIndex).toBe(0)
|
|
expect(state.scrollOffset).toBe(0)
|
|
})
|
|
})
|