fix(i18n): restore safe processing label fallbacks for runtime keys

This commit is contained in:
cesnimda
2026-11-02 22:44:25 +01:00
parent ebd33fcf00
commit 5d59fd50b6
4 changed files with 97 additions and 12 deletions
@@ -20,6 +20,32 @@ beforeEach(() => {
});
afterEach(() => jest.useRealTimers());
test.each([
['en', 'constructor', 'Other operation', 'Unknown', 'Unregistered worker', 'Not recorded'],
['en', 'toString', 'Other operation', 'Unknown', 'Unregistered worker', 'Not recorded'],
['en', '__proto__', 'Other operation', 'Unknown', 'Unregistered worker', 'Not recorded'],
['nb', 'constructor', 'Annen oppgave', 'Ukjent', 'Uregistrert bakgrunnsprosess', 'Ikke registrert'],
['nb', 'toString', 'Annen oppgave', 'Ukjent', 'Uregistrert bakgrunnsprosess', 'Ikke registrert'],
['nb', '__proto__', 'Annen oppgave', 'Ukjent', 'Uregistrert bakgrunnsprosess', 'Ikke registrert'],
])('uses safe %s fallbacks for inherited processing key %s', async (language, key, other, unknown, unregistered, notRecorded) => {
localStorage.setItem('uiLanguage', language);
const unknownItem = { ...item, taskType: key, status: key, stage: key };
get.mockImplementation((url: string) => Promise.resolve({ data: url.includes('/events')
? { items: [{ id: '1', kind: key, status: key, stage: key, attempt: 1, failureCode: null, atUtc: item.createdAtUtc }], hasMore: false, retentionDays: 30 }
: url.includes(item.id) ? unknownItem
: { ...listing, items: [unknownItem], counts: { [key]: 1 }, workers: [{ ...listing.workers[0], name: key }] } }));
open();
const details = await screen.findByText(other, { selector: 'button' });
expect(screen.getByText(`${unknown}: 1`)).toBeVisible();
const workerLabel = screen.getByText(new RegExp(`^${unregistered}:`));
fireEvent.click(workerLabel.closest('details')!.querySelector('summary')!);
expect(workerLabel).toBeVisible();
fireEvent.click(details);
const events = await screen.findByRole('list');
expect(events).toHaveTextContent(`${unknown} · ${unknown}`);
expect(events).toHaveTextContent(notRecorded);
});
test('shows safe job metadata and real events with a copyable correlation ID', async () => {
open();
expect(await screen.findByText('Synthetic owner')).toBeVisible();
@@ -19,11 +19,11 @@ type Item = {
type Listing = { items: Item[]; total: number; counts: Record<string, number>; workers: { name: string; flagEnabled: boolean; individualOperationsRecorded: boolean }[] };
type Event = { id: string; atUtc: string; kind: string; status: string; stage: string | null; attempt: number; failureCode: string | null };
type Events = { items: Event[]; hasMore: boolean; retentionDays: number };
const statuses: Record<string, TranslationKey> = { queued: 'operationsStatusQueued', running: 'operationsStatusRunning', waiting_for_retry: 'operationsStatusWaitingForRetry', waiting_for_external_fallback: 'operationsStatusWaitingForFallback', succeeded: 'operationsStatusSucceeded', failed: 'operationsStatusFailed', cancelled: 'operationsStatusCancelled' };
const tasks: Record<string, TranslationKey> = { 'cv.process': 'operationsTaskCv', 'strategy.snapshot': 'operationsTaskStrategy' };
const stages: Record<string, TranslationKey> = { cv_validating_file: 'cvImportStageValidating', cv_extracting: 'cvImportStageExtracting', cv_identifying_sections: 'cvImportStageSections', cv_analysing: 'cvImportStageAnalysing', cv_validating_details: 'cvImportStageDetails', cv_preparing_review: 'cvImportStagePreparing', cv_review_ready: 'cvImportStageReady' };
const kinds: Record<string, TranslationKey> = { Created: 'processingEventCreated', Claimed: 'processingEventClaimed', StageChanged: 'processingEventStageChanged', Completed: 'processingEventCompleted', RetryScheduled: 'processingEventRetryScheduled', Failed: 'processingEventFailed', CancellationRequested: 'processingEventCancellationRequested', Cancelled: 'processingEventCancelled', Retried: 'processingEventRetried', LeaseRecovered: 'processingEventLeaseRecovered', Expired: 'processingEventExpired' };
const workers: Record<string, TranslationKey> = { AiOperations: 'processingWorkerAi', CvDocumentImports: 'processingWorkerDocument', JobEnrichment: 'processingWorkerEnrichment', Rules: 'processingWorkerRules', FollowUpReminders: 'processingWorkerReminders', DailyExport: 'processingWorkerExport', SavedJobSearchAlerts: 'processingWorkerSearch' };
const statuses: Record<string, TranslationKey> = Object.assign(Object.create(null), { queued: 'operationsStatusQueued', running: 'operationsStatusRunning', waiting_for_retry: 'operationsStatusWaitingForRetry', waiting_for_external_fallback: 'operationsStatusWaitingForFallback', succeeded: 'operationsStatusSucceeded', failed: 'operationsStatusFailed', cancelled: 'operationsStatusCancelled' });
const tasks: Record<string, TranslationKey> = Object.assign(Object.create(null), { 'cv.process': 'operationsTaskCv', 'strategy.snapshot': 'operationsTaskStrategy' });
const stages: Record<string, TranslationKey> = Object.assign(Object.create(null), { cv_validating_file: 'cvImportStageValidating', cv_extracting: 'cvImportStageExtracting', cv_identifying_sections: 'cvImportStageSections', cv_analysing: 'cvImportStageAnalysing', cv_validating_details: 'cvImportStageDetails', cv_preparing_review: 'cvImportStagePreparing', cv_review_ready: 'cvImportStageReady' });
const kinds: Record<string, TranslationKey> = Object.assign(Object.create(null), { Created: 'processingEventCreated', Claimed: 'processingEventClaimed', StageChanged: 'processingEventStageChanged', Completed: 'processingEventCompleted', RetryScheduled: 'processingEventRetryScheduled', Failed: 'processingEventFailed', CancellationRequested: 'processingEventCancellationRequested', Cancelled: 'processingEventCancelled', Retried: 'processingEventRetried', LeaseRecovered: 'processingEventLeaseRecovered', Expired: 'processingEventExpired' });
const workers: Record<string, TranslationKey> = Object.assign(Object.create(null), { AiOperations: 'processingWorkerAi', CvDocumentImports: 'processingWorkerDocument', JobEnrichment: 'processingWorkerEnrichment', Rules: 'processingWorkerRules', FollowUpReminders: 'processingWorkerReminders', DailyExport: 'processingWorkerExport', SavedJobSearchAlerts: 'processingWorkerSearch' });
const failureCodes = ['timeout', 'lease_expired', 'deadline_exceeded', 'entitlement_changed', 'document_unavailable', 'sidecar_unavailable', 'upstream_unavailable', 'invalid_response', 'circuit_open', 'rate_limited', 'permission_denied', 'cancelled', 'upload_rejected', 'parser_failed', 'malformed_output', 'provider_unavailable', 'processing_failed'];
export default function AdminProcessingPage() {