diff --git a/docs/i18n/DYNAMIC_RESOLUTION_REGISTER.json b/docs/i18n/DYNAMIC_RESOLUTION_REGISTER.json index 89afab90..dc4a3b5a 100644 --- a/docs/i18n/DYNAMIC_RESOLUTION_REGISTER.json +++ b/docs/i18n/DYNAMIC_RESOLUTION_REGISTER.json @@ -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": [ diff --git a/docs/i18n/PROCESSING_LABEL_FALLBACKS.md b/docs/i18n/PROCESSING_LABEL_FALLBACKS.md new file mode 100644 index 00000000..1b5f00b2 --- /dev/null +++ b/docs/i18n/PROCESSING_LABEL_FALLBACKS.md @@ -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. diff --git a/job-tracker-ui/src/admin-processing-page.test.tsx b/job-tracker-ui/src/admin-processing-page.test.tsx index d527f4e6..c46b949c 100644 --- a/job-tracker-ui/src/admin-processing-page.test.tsx +++ b/job-tracker-ui/src/admin-processing-page.test.tsx @@ -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(); diff --git a/job-tracker-ui/src/views/AdminProcessingPage.tsx b/job-tracker-ui/src/views/AdminProcessingPage.tsx index fcef4c13..111f639c 100644 --- a/job-tracker-ui/src/views/AdminProcessingPage.tsx +++ b/job-tracker-ui/src/views/AdminProcessingPage.tsx @@ -19,11 +19,11 @@ type Item = { type Listing = { items: Item[]; total: number; counts: Record; 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 = { queued: 'operationsStatusQueued', running: 'operationsStatusRunning', waiting_for_retry: 'operationsStatusWaitingForRetry', waiting_for_external_fallback: 'operationsStatusWaitingForFallback', succeeded: 'operationsStatusSucceeded', failed: 'operationsStatusFailed', cancelled: 'operationsStatusCancelled' }; -const tasks: Record = { 'cv.process': 'operationsTaskCv', 'strategy.snapshot': 'operationsTaskStrategy' }; -const stages: Record = { 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 = { 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 = { AiOperations: 'processingWorkerAi', CvDocumentImports: 'processingWorkerDocument', JobEnrichment: 'processingWorkerEnrichment', Rules: 'processingWorkerRules', FollowUpReminders: 'processingWorkerReminders', DailyExport: 'processingWorkerExport', SavedJobSearchAlerts: 'processingWorkerSearch' }; +const statuses: Record = 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 = Object.assign(Object.create(null), { 'cv.process': 'operationsTaskCv', 'strategy.snapshot': 'operationsTaskStrategy' }); +const stages: Record = 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 = 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 = 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() {