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
+53 -7
View File
@@ -3,14 +3,15 @@
"inventorySha256": "79928688a7b1a4961286aaaf48c028eb13ae8286d72d132561d6b5fdd192e323",
"historicalCalls": 188,
"previouslyResolved": 108,
"additionalResolved": 18,
"resolved": 126,
"pending": 62,
"additionalResolved": 23,
"resolved": 131,
"pending": 57,
"evidence": [
"CHECKLIST_DYNAMIC_REVIEW.md",
"FINITE_OWNER_MAP_REVIEW.md",
"UNKNOWN_LABEL_FALLBACKS.md",
"PIPELINE_ALIAS_FALLBACK.md"
"PIPELINE_ALIAS_FALLBACK.md",
"PROCESSING_LABEL_FALLBACKS.md"
],
"entries": [
{
@@ -144,7 +145,7 @@
"historicalLine": 153,
"expression": "t(tasks[value])",
"historicalSourceSha256": "13973a963ec247c2b61230f5b96ebe1e5feb73ccc9374f15c9bd05f2b3699d1c",
"reviewedSourceSha256": "13973a963ec247c2b61230f5b96ebe1e5feb73ccc9374f15c9bd05f2b3699d1c",
"reviewedSourceSha256": "dadaf0c481af705c6e6f8ac2ce0e63dd3453680d575d3f7262721d3c2574005a",
"status": "finite-key-domain-reviewed",
"runtimeCoverage": "see named focused regression reports; static review alone is not runtime coverage"
},
@@ -153,7 +154,7 @@
"historicalLine": 173,
"expression": "t(key as TranslationKey)",
"historicalSourceSha256": "13973a963ec247c2b61230f5b96ebe1e5feb73ccc9374f15c9bd05f2b3699d1c",
"reviewedSourceSha256": "13973a963ec247c2b61230f5b96ebe1e5feb73ccc9374f15c9bd05f2b3699d1c",
"reviewedSourceSha256": "dadaf0c481af705c6e6f8ac2ce0e63dd3453680d575d3f7262721d3c2574005a",
"status": "finite-key-domain-reviewed",
"runtimeCoverage": "see named focused regression reports; static review alone is not runtime coverage"
},
@@ -162,7 +163,7 @@
"historicalLine": 205,
"expression": "t(key)",
"historicalSourceSha256": "13973a963ec247c2b61230f5b96ebe1e5feb73ccc9374f15c9bd05f2b3699d1c",
"reviewedSourceSha256": "13973a963ec247c2b61230f5b96ebe1e5feb73ccc9374f15c9bd05f2b3699d1c",
"reviewedSourceSha256": "dadaf0c481af705c6e6f8ac2ce0e63dd3453680d575d3f7262721d3c2574005a",
"status": "finite-key-domain-reviewed",
"runtimeCoverage": "see named focused regression reports; static review alone is not runtime coverage"
},
@@ -174,6 +175,51 @@
"reviewedSourceSha256": "672a0cc113838f266a578d01ec445299ed12465c08fcf3cff0b5cfae464a4cbe",
"status": "finite-key-domain-reviewed",
"runtimeCoverage": "see named focused regression reports; static review alone is not runtime coverage"
},
{
"file": "job-tracker-ui/src/views/AdminProcessingPage.tsx",
"historicalLine": 130,
"expression": "t(statuses[value] ?? 'processingUnknown')",
"historicalSourceSha256": "13973a963ec247c2b61230f5b96ebe1e5feb73ccc9374f15c9bd05f2b3699d1c",
"reviewedSourceSha256": "dadaf0c481af705c6e6f8ac2ce0e63dd3453680d575d3f7262721d3c2574005a",
"status": "finite-key-domain-reviewed",
"runtimeCoverage": "PROCESSING_LABEL_FALLBACKS.md; six inherited-key/language regressions plus eleven existing cases"
},
{
"file": "job-tracker-ui/src/views/AdminProcessingPage.tsx",
"historicalLine": 131,
"expression": "t(stages[value])",
"historicalSourceSha256": "13973a963ec247c2b61230f5b96ebe1e5feb73ccc9374f15c9bd05f2b3699d1c",
"reviewedSourceSha256": "dadaf0c481af705c6e6f8ac2ce0e63dd3453680d575d3f7262721d3c2574005a",
"status": "finite-key-domain-reviewed",
"runtimeCoverage": "PROCESSING_LABEL_FALLBACKS.md; six inherited-key/language regressions plus eleven existing cases"
},
{
"file": "job-tracker-ui/src/views/AdminProcessingPage.tsx",
"historicalLine": 175,
"expression": "t(tasks[item.taskType] ?? 'processingOther')",
"historicalSourceSha256": "13973a963ec247c2b61230f5b96ebe1e5feb73ccc9374f15c9bd05f2b3699d1c",
"reviewedSourceSha256": "dadaf0c481af705c6e6f8ac2ce0e63dd3453680d575d3f7262721d3c2574005a",
"status": "finite-key-domain-reviewed",
"runtimeCoverage": "PROCESSING_LABEL_FALLBACKS.md; six inherited-key/language regressions plus eleven existing cases"
},
{
"file": "job-tracker-ui/src/views/AdminProcessingPage.tsx",
"historicalLine": 191,
"expression": "t(workers[item.name] ?? 'processingWorkerUntracked')",
"historicalSourceSha256": "13973a963ec247c2b61230f5b96ebe1e5feb73ccc9374f15c9bd05f2b3699d1c",
"reviewedSourceSha256": "dadaf0c481af705c6e6f8ac2ce0e63dd3453680d575d3f7262721d3c2574005a",
"status": "finite-key-domain-reviewed",
"runtimeCoverage": "PROCESSING_LABEL_FALLBACKS.md; six inherited-key/language regressions plus eleven existing cases"
},
{
"file": "job-tracker-ui/src/views/AdminProcessingPage.tsx",
"historicalLine": 212,
"expression": "t(kinds[item.kind] ?? 'processingUnknown')",
"historicalSourceSha256": "13973a963ec247c2b61230f5b96ebe1e5feb73ccc9374f15c9bd05f2b3699d1c",
"reviewedSourceSha256": "dadaf0c481af705c6e6f8ac2ce0e63dd3453680d575d3f7262721d3c2574005a",
"status": "finite-key-domain-reviewed",
"runtimeCoverage": "PROCESSING_LABEL_FALLBACKS.md; six inherited-key/language regressions plus eleven existing cases"
}
],
"limitations": [
+13
View File
@@ -0,0 +1,13 @@
# Processing-monitor translation fallbacks — 2 November 2026
CONFIRMED functional defect: runtime status/stage/task/event/worker names matching inherited Object properties bypassed existing unknown-value fallbacks. Six EN/Bokmål cases using constructor, toString and __proto__ failed before the fix. No prototype mutation, privilege escalation or backend validation bypass is asserted.
Minimal shared fix: the five existing label dictionaries now use null-prototype objects, retaining their exact entries and Record typing. All callers, Object.keys filter controls and existing fallback expressions remain intact. No auth/role, API, polling, account-isolation, queue, model or production behavior changed. Known fields translate normally; unknown names reach the existing localized Unknown/Other operation/Unregistered worker/Not recorded labels.
PASSED: all 17 admin-processing-page tests, zero failures/pending, native exit 0. Six new cases exercise all five map domains, open the collapsed worker-details section, then check timeline event/status/stage fallbacks; eleven existing cases cover normal monitor workflows and safety. An intermediate rerun failed only because the new worker-label visibility assertion inspected a closed details element; the test now opens its summary through the normal UI. That run is not counted as passing or added to the final total.
PASSED: full typecheck and scoped two-file ESLint, native exits 0. Thirty-eight distinct map/fallback keys asserted present in both catalogues. Local native report: job-tracker-ui/.artifacts/processing-label-fallbacks.json. AST-only graph update required after code edits; keep generated graph separate from the implementation commit. Full build/browser and final integrated security acceptance are not claimed for this patch.
Five historical call sites resolved: AdminProcessingPage.tsx:130/131/175/191/212. Machine register binds each expression and reviewed source hash: 131 resolved / 57 pending of 188. The earlier safe map iterators retain their coverage with updated source hashes. This is key-domain/fallback evidence, not complete locale or native-language acceptance.
Rollback: revert the five dictionary constructions and six regression cases; no data migration or restoration needed. Continue remaining owner-map review; preserve unapproved final design and live/security/release gates.
@@ -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() {