import React from 'react'; import '@testing-library/jest-dom'; import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import AdminProcessingPage from './views/AdminProcessingPage'; import { I18nProvider } from './i18n/I18nProvider'; import { api } from './api'; import { setAuthUserKey } from './auth'; const get = api.get as jest.Mock; const item = { id: '11111111-1111-4111-8111-111111111111', owner: { id: 'owner', name: 'Synthetic owner', email: 'owner@example.test' }, taskType: 'cv.process', status: 'running', stage: 'cv_extracting', attemptCount: 1, maxAttempts: 3, createdAtUtc: '2026-10-28T12:00:00Z', startedAtUtc: '2026-10-28T12:00:00Z', completedAtUtc: null, lastHeartbeatAtUtc: null, durationSeconds: 8, worker: 'document', workerEnabled: true, failureCode: null, job: null, document: { importId: 7, name: 'CV.pdf', type: 'application/pdf', bytes: 500, pages: 2, state: 'running' } }; const listing = { items: [item], total: 1, page: 1, pageSize: 25, counts: { running: 1 }, workers: [{ name: 'AiOperations', flagEnabled: false, individualOperationsRecorded: true }], cvInterpretation: 'deterministic' }; function open() { return render(); } beforeEach(() => { localStorage.clear(); setAuthUserKey('admin', false); get.mockReset(); get.mockImplementation((url: string) => Promise.resolve({ data: url.endsWith('/events') || url.includes('/events?') ? { items: [{ id: '9007199254740993', kind: 'Claimed', status: 'running', stage: null, attempt: 1, failureCode: null, atUtc: '2026-10-28T12:00:00Z' }], hasMore: false, retentionDays: 30 } : url.includes(item.id) ? item : listing })); }); afterEach(() => jest.useRealTimers()); test('shows safe job metadata and real events with a copyable correlation ID', async () => { open(); expect(await screen.findByText('Synthetic owner')).toBeVisible(); fireEvent.click(screen.getByRole('button', { name: /View processing details/ })); expect(await screen.findByLabelText('Correlation ID')).toHaveValue(item.id); expect(await screen.findByText(/^Worker claimed job/)).toBeVisible(); expect(screen.getAllByText('CV.pdf').length).toBeGreaterThan(0); await waitFor(() => expect(screen.getByRole('heading', { name: 'Processing details' })).toHaveFocus()); expect(screen.queryByText(/SECRET/)).not.toBeInTheDocument(); }); test('submits diagnostic filters and displays explicit owner, import and unrelated-job metadata', async () => { open(); await screen.findByText('Synthetic owner'); fireEvent.change(screen.getByLabelText('Search users, jobs or task ID'), { target: { value: 'Owned job' } }); fireEvent.change(screen.getByLabelText('Application ID'), { target: { value: '2' } }); fireEvent.change(screen.getByLabelText('Submitted from (UTC)'), { target: { value: '2026-10-01' } }); fireEvent.change(screen.getByLabelText('Submitted before (UTC)'), { target: { value: '2026-11-01' } }); fireEvent.change(screen.getByLabelText('Worker filter'), { target: { value: 'document' } }); fireEvent.change(screen.getByLabelText('Failure code'), { target: { value: 'timeout' } }); fireEvent.click(screen.getByRole('button', { name: 'Apply filters' })); await waitFor(() => expect(get.mock.calls.some(([url]) => { const query = new URLSearchParams(url.split('?')[1]); return query.get('q') === 'Owned job' && query.get('jobId') === '2' && query.get('worker') === 'document' && query.get('failureCode') === 'timeout' && query.get('createdFrom') === '2026-10-01T00:00:00Z' && query.get('createdBefore') === '2026-11-01T00:00:00Z'; })).toBe(true)); fireEvent.click(screen.getByRole('button', { name: /View processing details/ })); expect(await screen.findByLabelText('Submitting user ID')).toHaveValue('owner'); expect(screen.getByText('No linked job')).toBeVisible(); expect(screen.getByText('Import ID: 7')).toBeVisible(); }); test('shows stable failure evidence and links an application only when the server permits it', async () => { const selected = { ...item, status: 'failed', failureCode: 'timeout', completedAtUtc: '2026-10-28T12:00:08Z', job: { id: 2, title: 'Owned job', company: 'Synthetic company', canOpen: false } }; get.mockImplementation((url: string) => Promise.resolve({ data: url.includes('/events') ? { items: [], hasMore: false, retentionDays: 30 } : url.includes(item.id) ? selected : listing })); open(); await screen.findByText('Synthetic owner'); fireEvent.click(screen.getByRole('button', { name: /View processing details/ })); expect(await screen.findByRole('alert')).toHaveTextContent('Failure code: timeout'); expect(screen.getByRole('alert')).toHaveTextContent('Whether this failure is transient or permanent has not been established.'); expect(screen.queryByRole('link', { name: 'Owned job' })).not.toBeInTheDocument(); selected.job.canOpen = true; fireEvent.click(screen.getByRole('button', { name: 'Refresh' })); expect(await screen.findByRole('link', { name: 'Owned job' })).toHaveAttribute('href', '/jobs/2'); }); test('a successful retry keeps prior failure evidence separate from its success timestamp', async () => { const failedAt = '2026-10-28T12:01:00Z'; const completedAt = '2026-10-28T12:05:00Z'; const selected = { ...item, status: 'succeeded', failureCode: 'timeout', completedAtUtc: completedAt, lastFailureAtUtc: failedAt, lastFailureStage: 'cv_extracting' }; get.mockImplementation((url: string) => Promise.resolve({ data: url.includes('/events') ? { items: [], hasMore: false, retentionDays: 30 } : url.includes(item.id) ? selected : listing })); open(); await screen.findByText('Synthetic owner'); fireEvent.click(screen.getByRole('button', { name: /View processing details/ })); const alert = await screen.findByRole('alert'); expect(alert).toHaveTextContent('Previous failure: Processing timed out.'); expect(alert).toHaveTextContent(new Date(failedAt).toLocaleString('en-GB')); expect(alert).not.toHaveTextContent(new Date(completedAt).toLocaleString('en-GB')); }); test('pause stops polling and resume uses an exact string event cursor', async () => { jest.useFakeTimers(); open(); await act(async () => { await Promise.resolve(); }); fireEvent.click(screen.getByRole('button', { name: /View processing details/ })); await act(async () => { await Promise.resolve(); }); await act(async () => { jest.advanceTimersByTime(5100); }); expect(get.mock.calls.some(([url]) => url.includes('after=9007199254740993'))).toBe(true); fireEvent.click(screen.getByRole('button', { name: 'Pause updates' })); const count = get.mock.calls.length; await act(async () => { jest.advanceTimersByTime(15000); }); expect(get).toHaveBeenCalledTimes(count); fireEvent.click(screen.getByRole('button', { name: 'Resume updates' })); await act(async () => { await Promise.resolve(); }); expect(get.mock.calls.length).toBeGreaterThan(count); }); test('forbidden responses clear metadata and do not retry automatically', async () => { jest.useFakeTimers(); get.mockRejectedValue({ response: { status: 403, data: 'SECRET server error' } }); open(); await act(async () => { await Promise.resolve(); }); expect(screen.getByRole('alert')).toHaveTextContent('Administrator access is required.'); const count = get.mock.calls.length; await act(async () => { jest.advanceTimersByTime(30000); }); expect(get).toHaveBeenCalledTimes(count); expect(screen.queryByText('Synthetic owner')).not.toBeInTheDocument(); expect(screen.queryByText(/SECRET/)).not.toBeInTheDocument(); }); test('malformed successful responses produce a recoverable error instead of rendering raw data', async () => { get.mockResolvedValue({ data: 'SECRET malformed body' }); open(); expect(await screen.findByRole('alert')).toHaveTextContent('Processing information could not be loaded.'); expect(screen.queryByText(/SECRET/)).not.toBeInTheDocument(); }); test('invalid filters explain the rejected request and stop automatic retries', async () => { jest.useFakeTimers(); get.mockRejectedValue({ response: { status: 400 } }); open(); await act(async () => { await Promise.resolve(); }); expect(screen.getByRole('alert')).toHaveTextContent('Check the filter values'); const count = get.mock.calls.length; await act(async () => { jest.advanceTimersByTime(30000); }); expect(get).toHaveBeenCalledTimes(count); }); test('identity changes abort pending reads and clear the previous administrator view', async () => { let finish!: (value: unknown) => void; get.mockImplementationOnce(() => new Promise(resolve => { finish = resolve; })); open(); await waitFor(() => expect(get).toHaveBeenCalled()); const signal = get.mock.calls[0][1].signal; get.mockRejectedValue({ response: { status: 403 } }); act(() => setAuthUserKey('ordinary')); await act(async () => { finish({ data: listing }); }); expect(signal.aborted).toBe(true); expect(screen.queryByText('Synthetic owner')).not.toBeInTheDocument(); }); test('a failed cycle aborts sibling reads and reconnects with bounded backoff', async () => { jest.useFakeTimers(); get.mockRejectedValueOnce(new Error('SECRET transport failure')); open(); await act(async () => { await Promise.resolve(); }); expect(screen.getByRole('alert')).toHaveTextContent('Processing information could not be loaded.'); expect(get.mock.calls[0][1].signal.aborted).toBe(true); const count = get.mock.calls.length; await act(async () => { jest.advanceTimersByTime(14999); }); expect(get).toHaveBeenCalledTimes(count); await act(async () => { jest.advanceTimersByTime(1); }); expect(screen.getByText('Synthetic owner')).toBeVisible(); expect(screen.queryByText(/SECRET/)).not.toBeInTheDocument(); }); test('older event pages pause updates and retain exact cursors until returning to latest', async () => { jest.useFakeTimers(); get.mockImplementation((url: string) => Promise.resolve({ data: url.includes('/events') ? { items: [{ id: url.includes('before=') ? '9007199254740992' : '9007199254740993', kind: url.includes('before=') ? 'Created' : 'Claimed', status: 'running', stage: null, attempt: 1, failureCode: null, atUtc: '2026-10-28T12:00:00Z' }], hasMore: true, retentionDays: 30 } : url.includes(item.id) ? item : listing })); open(); await act(async () => { await Promise.resolve(); }); fireEvent.click(screen.getByRole('button', { name: /View processing details/ })); await act(async () => { await Promise.resolve(); }); fireEvent.click(screen.getByRole('button', { name: 'Older events' })); await act(async () => { await Promise.resolve(); }); expect(get.mock.calls.some(([url]) => url.includes('before=9007199254740993'))).toBe(true); expect(screen.getByText(/^Job created/)).toBeVisible(); const count = get.mock.calls.length; await act(async () => { jest.advanceTimersByTime(30000); }); expect(get).toHaveBeenCalledTimes(count); fireEvent.click(screen.getByRole('button', { name: 'Latest events' })); await act(async () => { await Promise.resolve(); }); expect(screen.getByText(/^Worker claimed job/)).toBeVisible(); });