Files
hapi/web/src/components/CodexSessionSyncDialog.test.tsx
T
64834467e3 feat(codex): import and resume sessions from runners (#1088)
* fix codex import resume flow

* fix hub restart session active state

* fix codex transcript workspace scoping

* Address Codex import review findings

* Fix Codex import machine selection

* Update Codex sessions error test

* Address Codex import review findings

* Preserve forked Codex session id on sync

* Make Codex duplicate cleanup source-aware

* Handle Codex archive failures

* Limit existing session flag to Codex

* Preserve Codex import machine binding

* fix: rebase runner Codex import onto current main

* fix: preserve runner-scoped Codex import behavior

---------

Co-authored-by: syy <815728149@qq.com>
2026-07-19 14:14:42 +08:00

283 lines
10 KiB
TypeScript

import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { I18nProvider } from '@/lib/i18n-context'
import { CodexSessionSyncDialog } from './CodexSessionSyncDialog'
import type { CodexLocalSessionSummary } from '@/types/api'
function renderDialog(
sessions: CodexLocalSessionSummary[] = [],
onConfirm = vi.fn(async () => {}),
currentCodexSessionId: string | null = null,
onRestartCodexDesktop = vi.fn(async () => {}),
currentWorkDirectory: string | null = null
) {
const view = render(
<I18nProvider>
<CodexSessionSyncDialog
isOpen={true}
onClose={vi.fn()}
sessions={sessions}
currentCodexSessionId={currentCodexSessionId}
currentWorkDirectory={currentWorkDirectory}
onConfirm={onConfirm}
onRestartCodexDesktop={onRestartCodexDesktop}
isPending={false}
isRestartingCodexDesktop={false}
isLoading={false}
/>
</I18nProvider>
)
return { ...view, onConfirm, onRestartCodexDesktop }
}
describe('CodexSessionSyncDialog', () => {
afterEach(() => {
cleanup()
window.localStorage.clear()
})
it('shows the working directory for local Codex sessions', () => {
renderDialog([
{
id: 'codex-session-1',
title: 'Session title',
lastUserMessage: 'Last prompt',
cwd: '/home/user/project',
file: '/home/user/.codex/sessions/session.jsonl',
modifiedAt: Date.UTC(2026, 0, 2, 3, 4, 5),
originator: 'codex_cli',
cliVersion: '0.124.0'
}
])
expect(screen.getByText('Working directory')).toBeInTheDocument()
expect(screen.getAllByText('/home/user/project')).toHaveLength(2)
})
it('defaults the work directory filter to the current session directory when available', () => {
renderDialog([
{
id: 'codex-session-1',
title: 'Project one',
cwd: '/home/user/project-one',
file: '/home/user/.codex/sessions/one.jsonl',
modifiedAt: Date.UTC(2026, 0, 2, 3, 4, 5)
},
{
id: 'codex-session-2',
title: 'Project two',
cwd: '/home/user/project-two',
file: '/home/user/.codex/sessions/two.jsonl',
modifiedAt: Date.UTC(2026, 0, 3, 3, 4, 5)
}
], undefined, null, undefined, '/home/user/project-two')
expect(screen.queryByText('Project one')).not.toBeInTheDocument()
expect(screen.getByText('Project two')).toBeInTheDocument()
expect(screen.getByLabelText('Work directory')).toHaveValue('/home/user/project-two')
})
it('keeps all work directories when the current session directory is not in Codex sessions', () => {
renderDialog([
{
id: 'codex-session-1',
title: 'Project one',
cwd: '/home/user/project-one',
file: '/home/user/.codex/sessions/one.jsonl',
modifiedAt: Date.UTC(2026, 0, 2, 3, 4, 5)
}
], undefined, null, undefined, '/home/user/project-missing')
expect(screen.getByText('Project one')).toBeInTheDocument()
expect(screen.getByLabelText('Work directory')).toHaveValue('__all__')
})
it('filters sessions by working directory', () => {
renderDialog([
{
id: 'codex-session-1',
title: 'Project one',
cwd: '/home/user/project-one',
file: '/home/user/.codex/sessions/one.jsonl',
modifiedAt: Date.UTC(2026, 0, 2, 3, 4, 5)
},
{
id: 'codex-session-2',
title: 'Project two',
cwd: '/home/user/project-two',
file: '/home/user/.codex/sessions/two.jsonl',
modifiedAt: Date.UTC(2026, 0, 3, 3, 4, 5)
}
])
fireEvent.change(screen.getByLabelText('Work directory'), {
target: { value: '/home/user/project-two' }
})
expect(screen.queryByText('Project one')).not.toBeInTheDocument()
expect(screen.getByText('Project two')).toBeInTheDocument()
})
it('selects only filtered sessions when selecting all', async () => {
const onConfirm = vi.fn(async () => {})
renderDialog([
{
id: 'codex-session-1',
title: 'Project one',
cwd: '/home/user/project-one',
file: '/home/user/.codex/sessions/one.jsonl',
modifiedAt: Date.UTC(2026, 0, 2, 3, 4, 5)
},
{
id: 'codex-session-2',
title: 'Project two',
cwd: '/home/user/project-two',
file: '/home/user/.codex/sessions/two.jsonl',
modifiedAt: Date.UTC(2026, 0, 3, 3, 4, 5)
}
], onConfirm)
fireEvent.change(screen.getByLabelText('Work directory'), {
target: { value: '/home/user/project-two' }
})
fireEvent.click(screen.getByRole('button', { name: 'Select all' }))
await waitFor(() => {
expect(screen.getByText('1 sessions selected')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Import' })).toBeEnabled()
})
fireEvent.click(screen.getByRole('button', { name: 'Import' }))
expect(onConfirm).toHaveBeenCalledWith(['codex-session-2'])
})
it('replaces hidden selections when selecting all filtered sessions', async () => {
const onConfirm = vi.fn(async () => {})
renderDialog([
{
id: 'codex-session-1',
title: 'Project one',
cwd: '/home/user/project-one',
file: '/home/user/.codex/sessions/one.jsonl',
modifiedAt: Date.UTC(2026, 0, 2, 3, 4, 5)
},
{
id: 'codex-session-2',
title: 'Project two',
cwd: '/home/user/project-two',
file: '/home/user/.codex/sessions/two.jsonl',
modifiedAt: Date.UTC(2026, 0, 3, 3, 4, 5)
}
], onConfirm, 'codex-session-1')
await waitFor(() => {
expect(screen.getByText('1 sessions selected')).toBeInTheDocument()
})
fireEvent.change(screen.getByLabelText('Work directory'), {
target: { value: '/home/user/project-two' }
})
fireEvent.click(screen.getByRole('button', { name: 'Select all' }))
await waitFor(() => {
expect(screen.getByText('1 sessions selected')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Import' })).toBeEnabled()
})
fireEvent.click(screen.getByRole('button', { name: 'Import' }))
expect(onConfirm).toHaveBeenCalledWith(['codex-session-2'])
})
it('opens archive menu on context menu and archives the selected Codex session', async () => {
const onArchiveSession = vi.fn(async () => {})
render(
<I18nProvider>
<CodexSessionSyncDialog
isOpen={true}
onClose={vi.fn()}
sessions={[
{
id: 'codex-session-1',
title: 'Session title',
cwd: '/home/user/project',
file: '/home/user/.codex/sessions/session.jsonl',
modifiedAt: Date.UTC(2026, 0, 2, 3, 4, 5)
}
]}
currentCodexSessionId={null}
onConfirm={vi.fn(async () => {})}
onRestartCodexDesktop={vi.fn(async () => {})}
onArchiveSession={onArchiveSession}
isPending={false}
isRestartingCodexDesktop={false}
isLoading={false}
/>
</I18nProvider>
)
fireEvent.contextMenu(screen.getByText('Session title'))
fireEvent.click(await screen.findByRole('button', { name: 'Archive in Codex' }))
await waitFor(() => {
expect(onArchiveSession).toHaveBeenCalledWith(expect.objectContaining({ id: 'codex-session-1' }))
})
})
it('keeps the restart control clear of the close button area', () => {
renderDialog()
const header = screen.getByTestId('codex-import-dialog-header')
expect(header).toHaveClass('flex')
expect(header).toHaveClass('pr-10')
expect(screen.getByRole('button', { name: 'Restart Codex client' })).toHaveClass('shrink-0')
})
it('restarts Codex Desktop from the header control', () => {
const onRestartCodexDesktop = vi.fn(async () => {})
renderDialog([], undefined, null, onRestartCodexDesktop)
fireEvent.click(screen.getByRole('button', { name: 'Restart Codex client' }))
expect(onRestartCodexDesktop).toHaveBeenCalledTimes(1)
})
it('shows imported badge on original session and fork badge on forked session', () => {
window.localStorage.setItem('hapi.codexImportedSessions', JSON.stringify({
'original-session-id': Date.now()
}))
renderDialog([
{
id: 'fork-session-id',
title: 'Fork session',
cwd: '/home/user/project',
file: '/home/user/.codex/sessions/fork.jsonl',
modifiedAt: Date.UTC(2026, 0, 3, 3, 4, 5),
originator: 'hapi-codex-client',
cliVersion: '0.142.3',
source: 'vscode',
forkedFromId: 'original-session-id'
},
{
id: 'original-session-id',
title: 'Original session',
cwd: '/home/user/project',
file: '/home/user/.codex/sessions/original.jsonl',
modifiedAt: Date.UTC(2026, 0, 2, 3, 4, 5),
originator: 'Codex Desktop',
cliVersion: '0.142.2',
source: 'vscode',
threadSource: 'user'
}
])
expect(screen.getByText('Original')).toBeInTheDocument()
expect(screen.getByText('Imported')).toBeInTheDocument()
expect(screen.getByText('Fork')).toBeInTheDocument()
})
})