mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(web): show machine + last-active in session detail header (#1244)
* feat(web): show machine and last-active in session header Multi-machine estates lose the machine signal after leaving list filter chips; surface machine label + relative age in SessionHeader meta row. Fixes #1241. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): keep session-header age fresh under keep-alive Treat detail-cache activeAt keep-alives as render-relevant now that the header reads them, and tick relative age every minute so labels advance without a session prop change. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): skip sub-minute activeAt keep-alives in detail cache Relative age only changes at 60s boundaries; accepting every ~10s heartbeat replaced the Session object and re-rendered the chat tree for no visible header change. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retrigger Codex PR review after stream disconnect Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,50 +1,121 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { act, cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Session } from '@/types/api'
|
||||
import { I18nProvider } from '@/lib/i18n-context'
|
||||
import { ToastProvider } from '@/lib/toast-context'
|
||||
import { SessionHeader } from './SessionHeader'
|
||||
import { resolveSessionHeaderMachineLabel, SessionHeader } from './SessionHeader'
|
||||
|
||||
afterEach(() => cleanup())
|
||||
|
||||
describe('SessionHeader', () => {
|
||||
it('shows an inherited catalog-default Fast tier', () => {
|
||||
const session: Session = {
|
||||
id: 'session-1',
|
||||
namespace: 'default',
|
||||
seq: 0,
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
active: true,
|
||||
activeAt: 0,
|
||||
metadata: { flavor: 'codex', path: '/repo', host: 'machine' },
|
||||
metadataVersion: 0,
|
||||
agentState: null,
|
||||
agentStateVersion: 0,
|
||||
thinking: false,
|
||||
thinkingAt: 0,
|
||||
model: null,
|
||||
modelReasoningEffort: null,
|
||||
effort: null,
|
||||
serviceTier: null
|
||||
}
|
||||
function baseSession(overrides: Partial<Session> = {}): Session {
|
||||
return {
|
||||
id: 'session-1',
|
||||
namespace: 'default',
|
||||
seq: 0,
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
active: true,
|
||||
activeAt: 0,
|
||||
metadata: { flavor: 'codex', path: '/repo', host: 'machine' },
|
||||
metadataVersion: 0,
|
||||
agentState: null,
|
||||
agentStateVersion: 0,
|
||||
thinking: false,
|
||||
thinkingAt: 0,
|
||||
model: null,
|
||||
modelReasoningEffort: null,
|
||||
effort: null,
|
||||
serviceTier: null,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<ToastProvider>
|
||||
<I18nProvider>
|
||||
<SessionHeader
|
||||
session={session}
|
||||
serviceTier="priority"
|
||||
onBack={vi.fn()}
|
||||
api={null}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</ToastProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
function renderHeader(session: Session, extra?: { serviceTier?: string | null }) {
|
||||
return render(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<ToastProvider>
|
||||
<I18nProvider>
|
||||
<SessionHeader
|
||||
session={session}
|
||||
serviceTier={extra?.serviceTier}
|
||||
onBack={vi.fn()}
|
||||
api={null}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</ToastProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('resolveSessionHeaderMachineLabel', () => {
|
||||
it('prefers cached/display labels, then host, then short machine id', () => {
|
||||
expect(resolveSessionHeaderMachineLabel(
|
||||
baseSession({ metadata: { flavor: 'cursor', path: '/r', host: 'host.local', machineId: 'abc123456789' } }),
|
||||
{ abc123456789: 'Workstation' }
|
||||
)).toBe('Workstation')
|
||||
|
||||
expect(resolveSessionHeaderMachineLabel(
|
||||
baseSession({ metadata: { flavor: 'cursor', path: '/r', host: 'host.local', machineId: 'abc123456789' } }),
|
||||
{}
|
||||
)).toBe('host.local')
|
||||
|
||||
expect(resolveSessionHeaderMachineLabel(
|
||||
baseSession({ metadata: { flavor: 'cursor', path: '/r', host: '', machineId: 'abc123456789' } }),
|
||||
{}
|
||||
)).toBe('abc12345')
|
||||
|
||||
expect(resolveSessionHeaderMachineLabel(
|
||||
baseSession({ metadata: { flavor: 'cursor', path: '/r', host: '' } }),
|
||||
{}
|
||||
)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('SessionHeader', () => {
|
||||
it('shows an inherited catalog-default Fast tier', () => {
|
||||
renderHeader(baseSession(), { serviceTier: 'priority' })
|
||||
expect(screen.getByText('fast')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows machine label and relative last-active age in the meta row', () => {
|
||||
const fiveMinutesAgo = Date.now() - 5 * 60_000
|
||||
renderHeader(baseSession({
|
||||
activeAt: fiveMinutesAgo,
|
||||
updatedAt: fiveMinutesAgo,
|
||||
metadata: {
|
||||
flavor: 'cursor',
|
||||
path: '/home/heavygee/coding/hapi',
|
||||
host: 'oos-linux',
|
||||
machineId: 'machine-deadbeef'
|
||||
}
|
||||
}))
|
||||
|
||||
expect(screen.getByTestId('session-header-machine')).toHaveTextContent(/oos-linux/)
|
||||
expect(screen.getByTestId('session-header-age')).toHaveTextContent(/5m ago|5分钟前/)
|
||||
})
|
||||
|
||||
it('advances relative age on the minute tick without a session prop change', () => {
|
||||
vi.useFakeTimers()
|
||||
const now = new Date('2026-07-29T16:00:00.000Z')
|
||||
vi.setSystemTime(now)
|
||||
|
||||
try {
|
||||
renderHeader(baseSession({
|
||||
activeAt: now.getTime() - 30_000,
|
||||
updatedAt: now.getTime() - 30_000,
|
||||
metadata: { flavor: 'cursor', path: '/r', host: 'host.local' }
|
||||
}))
|
||||
|
||||
expect(screen.getByTestId('session-header-age')).toHaveTextContent(/just now|刚刚/)
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(60_000)
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('session-header-age')).toHaveTextContent(/1m ago|1分钟前/)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user