fix(admin): complete processing diagnostics and failure evidence
This commit is contained in:
@@ -8,7 +8,7 @@ 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: { name: 'CV.pdf', type: 'application/pdf', bytes: 500, pages: 2, state: 'running' } };
|
||||
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(<MemoryRouter><I18nProvider><AdminProcessingPage /></I18nProvider></MemoryRouter>); }
|
||||
beforeEach(() => {
|
||||
@@ -31,6 +31,57 @@ test('shows safe job metadata and real events with a copyable correlation ID', a
|
||||
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(); });
|
||||
@@ -65,6 +116,16 @@ test('malformed successful responses produce a recoverable error instead of rend
|
||||
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; }));
|
||||
|
||||
Reference in New Issue
Block a user