diff --git a/docs/redesign/phase-5/p5-15/p5-15-01/diagnostics/README.md b/docs/redesign/phase-5/p5-15/p5-15-01/diagnostics/README.md new file mode 100644 index 0000000..4fd81ce --- /dev/null +++ b/docs/redesign/phase-5/p5-15/p5-15-01/diagnostics/README.md @@ -0,0 +1,28 @@ +# P5-15-01 deferred preference diagnostics + +These native Playwright probes run separately from the ordinary `job-tracker-ui/e2e` suite. They assert the intended behavior and fail on a genuine HTTP 500, incorrect persisted readback, or lost unsaved rule draft. There are no success response mocks, expected failures, retries within the probes, real account credentials, or provider calls. + +From `job-tracker-ui`, after the normal Release API build and frontend preparation: + +```powershell +rtk proxy node node_modules/@playwright/test/cli.js test --config=../docs/redesign/phase-5/p5-15/p5-15-01/diagnostics/playwright.config.ts +``` + +The config imports the ordinary Playwright config: the same local ports, disposable API database, synthetic administrative bootstrap account, serial worker and retained failure trace apply. The working directory must remain `job-tracker-ui`. Use the ordinary static-export / prebuilt / server-reuse environment options only when the corresponding local servers or export have already been prepared. Do not run alongside the main suite on the shared ports. + +The first probe edits a rule, switches to Notifications and returns without saving. Its attachment records the initial value, draft, displayed return value, API readback status/value and count of rule writes. The second probe uses two pages in one genuine session: language selection and notification Save run alongside four genuine report/notification reads, for three alternating language rounds. Each round records actual mutation/read statuses, persisted language/notification values, document language and alert DOM counts before the success assertions. The raw `errorsVisible` name is misleading: counts include the Next route announcer and do not establish visible errors. The portable summary labels them accordingly. Partial evidence is retained when a round fails. The final reload checks persisted UI state. + +## Native results and source characterization + +Fresh final execution on 2026-10-09: **one pass, one failure**, separate from the ordinary browser suite. [Portable browser evidence](../evidence/browser-summary.json) contains actual outcomes, commands, times and fingerprints. Rules started at 14, was edited to 5, then returned to 14 after the tab round trip; API readback remained 14, with zero rule writes. The desired draft-retention assertion fails. No application repair or expected-failure flag is included. + +The three native overlap rounds passed: all language writes 204, notification writes 200, concurrent report/notification reads 200, persisted values correct, and final reload retained Bokmål plus disabled notification preference. This did **not** reproduce the recorded preference-save 500 and does **not** close it or establish concurrency/load safety. No exception/root cause for the historical 500 is inferred from this run. + +- `SettingsView.tsx:43`: `TabPanel` returns `null` for an inactive tab, unmounting `RulesSettingsCard`. +- `RulesSettingsCard.tsx:23`: rule drafts live in component state; mounting reads `/rules` again. A tab round trip has no draft owner above the unmounted card. The native failed assertion confirms that draft-loss risk. +- `I18nProvider.tsx:33`: `setLanguage` applies local state/storage immediately and catches the language PUT rejection without displaying it. A visually translated page is not evidence of a persisted successful preference. +- `AuthController.cs:549`: the language endpoint loads the local Identity user, sets `UiLanguage` and calls `UserManager.UpdateAsync`. Identity-result failure becomes 400; an uncaught storage failure can become 500. This source alone does not establish the historical 500's cause. +- `NotificationSettingsController.cs:29`: notification PUT updates another property of the same Identity user through `UpdateAsync`; unsuccessful Identity results explicitly become 500. Concurrent full-user writes, optimistic concurrency and SQLite contention remain hypotheses until response/server evidence identifies the failure. +- `SettingsView.tsx:70`: language-dependent notification reads can overlap the writes. The diagnostic exercises that real caller behavior, rather than substituting successful responses. + +No application fix is included. An all-success bounded run cannot establish that an intermittent concurrency defect is resolved; preserve the observed statuses and report that limit. A failed run remains failed, with a separately recorded defect for follow-up ownership. diff --git a/docs/redesign/phase-5/p5-15/p5-15-01/diagnostics/playwright.config.ts b/docs/redesign/phase-5/p5-15/p5-15-01/diagnostics/playwright.config.ts new file mode 100644 index 0000000..21db2e9 --- /dev/null +++ b/docs/redesign/phase-5/p5-15/p5-15-01/diagnostics/playwright.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from '../../../../../../job-tracker-ui/node_modules/@playwright/test'; +import main from '../../../../../../job-tracker-ui/playwright.config'; +import path from 'node:path'; + +// Playwright otherwise defaults webServer cwd to this diagnostic config's directory. +const workspace = path.resolve(__dirname, '../../../../../../job-tracker-ui'); +const servers = Array.isArray(main.webServer) ? main.webServer : [main.webServer!]; +export default defineConfig({ + ...main, + testDir: __dirname, + testMatch: 'preferences.spec.ts', + webServer: servers.map(server => ({ ...server, cwd: workspace })), +}); diff --git a/docs/redesign/phase-5/p5-15/p5-15-01/diagnostics/preferences.spec.ts b/docs/redesign/phase-5/p5-15/p5-15-01/diagnostics/preferences.spec.ts new file mode 100644 index 0000000..c83a913 --- /dev/null +++ b/docs/redesign/phase-5/p5-15/p5-15-01/diagnostics/preferences.spec.ts @@ -0,0 +1,110 @@ +import { expect, test as base, type APIRequestContext } from '../../../../../../job-tracker-ui/node_modules/@playwright/test'; +import { translations } from '../../../../../../job-tracker-ui/src/i18n/translations'; + +const apiUrl = 'http://localhost:5302/api'; +const password = 'E2ePassword123!'; +async function login(api: APIRequestContext, email: string) { + expect((await api.post(`${apiUrl}/auth/login`, { data: { email, password } })).status()).toBe(200); + const me = await api.get(`${apiUrl}/auth/me`); + expect(me.status()).toBe(200); + const csrf = (await api.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value; + return { 'X-CSRF-TOKEN': csrf, 'X-Session-Context': (await me.json()).sessionContext as string }; +} +const test = base.extend<{ headers: Record }>({ + headers: async ({ playwright, context, page }, provide) => { + const admin = await playwright.request.newContext(); + try { + const adminHeaders = await login(admin, 'e2e@example.test'); + const email = `preference-diagnostic-${Date.now()}-${Math.random().toString(36).slice(2)}@example.test`; + expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: [] } })).status()).toBe(200); + await context.addInitScript(() => localStorage.setItem('uiLanguage', 'en')); + await context.route('**/*', route => { + const url = new URL(route.request().url()); + return ['localhost', '127.0.0.1'].includes(url.hostname) || ['data:', 'blob:', 'about:'].includes(url.protocol) ? route.continue() : route.abort(); + }); + await provide(await login(page.request, email)); + } finally { await admin.dispose(); } + }, +}); + +test('[P5-15-01 diagnostic] an unsaved Rules draft survives switching to Notifications and back', async ({ page, headers }, info) => { + expect((await page.request.get(`${apiUrl}/auth/me`, { headers })).status()).toBe(200); + const before = await page.request.get(`${apiUrl}/rules`); + expect(before.status()).toBe(200); + const original = (await before.json()).appliedFollowUpDays as number; + const draft = original === 5 ? 6 : 5; + let ruleWrites = 0; + page.on('request', request => { if (new URL(request.url()).pathname === '/api/rules' && request.method() === 'PUT') ruleWrites++; }); + await page.goto('/settings'); + await page.getByRole('tab', { name: 'Follow-ups', exact: true }).click(); + await page.getByLabel('Applied: follow-up days', { exact: true }).fill(String(draft)); + await page.getByRole('tab', { name: 'Notifications', exact: true }).click(); + await expect(page.getByLabel('Email reminders for follow-ups', { exact: true })).toBeVisible(); + await page.getByRole('tab', { name: 'Follow-ups', exact: true }).click(); + const field = page.getByLabel('Applied: follow-up days', { exact: true }); + await expect(field).toBeVisible(); + // Wait for the remounted card's genuine read, without treating the original value as success. + await expect(field).not.toHaveValue(''); + const readback = await page.request.get(`${apiUrl}/rules`); + expect(readback.status()).toBe(200); + const persisted = (await readback.json()).appliedFollowUpDays; + const visibleDraft = await field.inputValue(); + await info.attach('unsaved-rule-tab-retention', { body: JSON.stringify({ original, draft, visibleDraft, persisted, ruleWrites, readbackStatus: readback.status() }), contentType: 'application/json' }); + expect(ruleWrites).toBe(0); + expect(persisted).toBe(original); + expect(visibleDraft).toBe(String(draft)); +}); + +test('[P5-15-01 diagnostic] native language and notification saves persist during concurrent report reads', async ({ page, context, headers }, info) => { + test.setTimeout(90_000); + const notification = await context.newPage(); + const evidence: unknown[] = []; + try { + await page.goto('/settings'); + await notification.goto('/settings'); + await notification.getByRole('tab', { name: 'Notifications', exact: true }).click(); + // Three alternating rounds reproduce concurrency; this is bounded diagnosis, not a load test. + for (const language of ['nb', 'en', 'nb'] as const) { + const current = language === 'nb' ? translations.en : translations.nb; + const target = translations[language]; + const enabled = language === 'en'; + await notification.getByLabel(current.settingsNotificationsFollowUpReminders, { exact: true }).setChecked(enabled); + await page.getByRole('combobox', { name: current.settingsPreferredLanguage, exact: true }).click(); + const languageResponse = page.waitForResponse(response => response.url() === `${apiUrl}/auth/preferences/language` && response.request().method() === 'PUT'); + const notificationResponse = notification.waitForResponse(response => response.url() === `${apiUrl}/notification-settings` && response.request().method() === 'PUT'); + const readPaths = ['/jobapplications/stats', '/jobapplications/analytics-overview', '/jobapplications/analytics?months=6', '/notifications?limit=10']; + const reads = readPaths.map(path => page.request.get(`${apiUrl}${path}`, { headers })); + await Promise.all([ + page.getByRole('option', { name: current[language === 'nb' ? 'settingsNorwegian' : 'settingsEnglish'], exact: true }).click(), + notification.getByRole('button', { name: new RegExp(`^(${translations.en.settingsNotificationsSave}|${translations.nb.settingsNotificationsSave})$`) }).click(), + ]); + const [savedLanguage, savedNotification, ...readResponses] = await Promise.all([languageResponse, notificationResponse, ...reads]); + const me = await page.request.get(`${apiUrl}/auth/me`, { headers }); + const prefs = await page.request.get(`${apiUrl}/notification-settings`, { headers }); + const languageReadback = me.ok() ? (await me.json()).uiLanguage : undefined; + const notificationReadback = prefs.ok() ? (await prefs.json()).emailFollowUpRemindersEnabled : undefined; + const errorsVisible = { language: await page.getByRole('alert').count(), notification: await notification.getByRole('alert').count() }; + evidence.push({ requestedLanguage: language === 'nb' ? 'nb-NO' : 'en', requestedNotifications: enabled, + languageStatus: savedLanguage.status(), notificationStatus: savedNotification.status(), + reads: readPaths.map((path, index) => ({ path, status: readResponses[index].status() })), + readback: { meStatus: me.status(), notificationStatus: prefs.status(), language: languageReadback, notifications: notificationReadback }, + localLanguage: await page.locator('html').getAttribute('lang'), errorsVisible }); + // Capture genuine 500s and persistence before asserting; no expected-failure or success mock. + await info.attach(`preference-round-${evidence.length}`, { body: JSON.stringify(evidence[evidence.length - 1], null, 2), contentType: 'application/json' }); + expect(savedLanguage.status()).toBe(204); + expect(savedNotification.status()).toBe(200); + for (const response of readResponses) expect(response.status()).toBe(200); + expect(me.status()).toBe(200); expect(prefs.status()).toBe(200); + expect(languageReadback).toBe(language === 'nb' ? 'nb-NO' : 'en'); + expect(notificationReadback).toBe(enabled); + await expect(page.getByRole('combobox', { name: target.settingsPreferredLanguage, exact: true })).toBeVisible(); + } + await page.reload(); await notification.reload(); + await expect(page.locator('html')).toHaveAttribute('lang', 'nb-NO'); + await notification.getByRole('tab', { name: translations.nb.settingsTabNotifications, exact: true }).click(); + await expect(notification.getByLabel(translations.nb.settingsNotificationsFollowUpReminders, { exact: true })).not.toBeChecked(); + } finally { + await info.attach('native-preference-concurrency', { body: JSON.stringify(evidence, null, 2), contentType: 'application/json' }); + await notification.close(); + } +}); diff --git a/docs/redesign/phase-5/p5-15/p5-15-01/evidence/browser-summary.json b/docs/redesign/phase-5/p5-15/p5-15-01/evidence/browser-summary.json new file mode 100644 index 0000000..e0f7916 --- /dev/null +++ b/docs/redesign/phase-5/p5-15/p5-15-01/evidence/browser-summary.json @@ -0,0 +1,1905 @@ +{ + "sourceCommit": "932b962b9038ddb5ad59b982c0199475bfb50454", + "applicationAndProductionChanged": false, + "scope": "Fresh disposable local Chromium/API execution. Test-only audit additions; no live providers, production access, model qualification or manual AT/device acceptance.", + "commands": [ + { + "name": "main", + "command": [ + "rtk", + "proxy", + "node", + "node_modules/@playwright/test/cli.js", + "test", + "--retries=0", + "--reporter=list,json", + "--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\p5-15-01\\browser\\main-results" + ], + "startedUtc": "2026-10-09T06:53:00.676204+00:00", + "finishedUtc": "2026-10-09T07:14:42.139426+00:00", + "exitCode": 1, + "stats": { + "startTime": "2026-10-09T06:53:01.285Z", + "duration": 1300776.4749999999, + "expected": 119, + "skipped": 0, + "unexpected": 3, + "flaky": 0 + } + }, + { + "name": "http", + "command": [ + "rtk", + "proxy", + "node", + "node_modules/@playwright/test/cli.js", + "test", + "--config=playwright.cv-ai.config.ts", + "--retries=0", + "--reporter=list,json", + "--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\p5-15-01\\browser\\http-results" + ], + "startedUtc": "2026-10-09T07:14:42.156720+00:00", + "finishedUtc": "2026-10-09T07:14:56.505465+00:00", + "exitCode": 0, + "stats": { + "startTime": "2026-10-09T07:14:42.701Z", + "duration": 13773.988, + "expected": 2, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + } + }, + { + "name": "rate-limit", + "command": [ + "rtk", + "proxy", + "node", + "node_modules/@playwright/test/cli.js", + "test", + "--config=playwright.rate-limits.config.ts", + "--retries=0", + "--reporter=list,json", + "--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\p5-15-01\\browser\\rate-limit-results" + ], + "startedUtc": "2026-10-09T07:14:56.506489+00:00", + "finishedUtc": "2026-10-09T07:15:07.606135+00:00", + "exitCode": 0, + "stats": { + "startTime": "2026-10-09T07:14:57.024Z", + "duration": 10550.624, + "expected": 9, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + } + }, + { + "name": "ingress", + "command": [ + "rtk", + "proxy", + "node", + "node_modules/@playwright/test/cli.js", + "test", + "--config=playwright.ingress.config.ts", + "--retries=0", + "--reporter=list,json", + "--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\p5-15-01\\browser\\ingress-results" + ], + "startedUtc": "2026-10-09T07:15:07.607648+00:00", + "finishedUtc": "2026-10-09T07:15:14.581932+00:00", + "exitCode": 0, + "stats": { + "startTime": "2026-10-09T07:15:08.143Z", + "duration": 6404.126, + "expected": 6, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + } + }, + { + "name": "pdf-admission", + "command": [ + "rtk", + "proxy", + "node", + "node_modules/@playwright/test/cli.js", + "test", + "--config=playwright.pdf.config.ts", + "--retries=0", + "--reporter=list,json", + "--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\p5-15-01\\browser\\pdf-admission-results" + ], + "startedUtc": "2026-10-09T07:15:14.583454+00:00", + "finishedUtc": "2026-10-09T07:15:24.305344+00:00", + "exitCode": 0, + "stats": { + "startTime": "2026-10-09T07:15:15.106Z", + "duration": 9157.975, + "expected": 1, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + } + }, + { + "name": "route-correction", + "command": [ + "rtk", + "proxy", + "node", + "node_modules/@playwright/test/cli.js", + "test", + "e2e/functional-acceptance.spec.ts", + "--retries=0", + "--reporter=list,json", + "--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\p5-15-01\\browser\\route-correction-results" + ], + "startedUtc": "2026-10-09T11:38:17.683665+00:00", + "finishedUtc": "2026-10-09T11:40:39.903519+00:00", + "exitCode": 1, + "stats": { + "startTime": "2026-10-09T11:38:18.227Z", + "duration": 141643.00699999998, + "expected": 2, + "skipped": 0, + "unexpected": 3, + "flaky": 0 + } + }, + { + "name": "diagnostics", + "command": [ + "rtk", + "proxy", + "node", + "node_modules/@playwright/test/cli.js", + "test", + "--config=../docs/redesign/phase-5/p5-15/p5-15-01/diagnostics/playwright.config.ts", + "--retries=0", + "--reporter=list,json", + "--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\p5-15-01\\browser\\diagnostics-results" + ], + "startedUtc": "2026-10-09T11:40:39.904005+00:00", + "finishedUtc": "2026-10-09T11:40:54.270783+00:00", + "exitCode": 1, + "stats": { + "startTime": "2026-10-09T11:40:40.514Z", + "duration": 13725.845000000001, + "expected": 1, + "skipped": 0, + "unexpected": 1, + "flaky": 0 + } + } + ], + "ordinaryBrowserReconciliation": { + "inheritedUniquePassed": 117, + "newUniqueCases": 5, + "newFinalPassed": 2, + "newFinalFailed": 3, + "latestUniquePassed": 119, + "latestUniqueFailed": 3, + "uniqueTotal": 122, + "skipped": 0, + "expectedFailures": 0, + "method": "All117 inherited cases passed in full main run; final owning-file rerun completes5 corrected new cases with2pass/3genuine failures. This is a reconciled unique-case result across runs, not a single122-case all-green run.", + "initialFullRun": { + "passed": 119, + "failed": 3, + "classification": "All3 failures were ambiguous missing-link alert locator errors in new harness; retained, corrected and rerun." + }, + "finalOwningFileRun": { + "passed": 2, + "failed": 3, + "classification": "Anonymous login loses Analytics query; normal/admin saved-search reads500. No skip/expected-failure masking." + } + }, + "standaloneDiagnostics": { + "passed": 1, + "failed": 1, + "total": 2, + "includedInOrdinaryBrowserTotal": false, + "rulesDraft": { + "original": 14, + "draft": 5, + "visibleDraft": "14", + "persisted": 14, + "ruleWrites": 0, + "readbackStatus": 200 + }, + "preferenceRounds": [ + { + "requestedLanguage": "nb-NO", + "requestedNotifications": false, + "languageStatus": 204, + "notificationStatus": 200, + "reads": [ + { + "path": "/jobapplications/stats", + "status": 200 + }, + { + "path": "/jobapplications/analytics-overview", + "status": 200 + }, + { + "path": "/jobapplications/analytics?months=6", + "status": 200 + }, + { + "path": "/notifications?limit=10", + "status": 200 + } + ], + "readback": { + "meStatus": 200, + "notificationStatus": 200, + "language": "nb-NO", + "notifications": false + }, + "localLanguage": "nb-NO", + "alertDomCountsNotErrorEvidence": { + "language": 2, + "notification": 2 + } + }, + { + "requestedLanguage": "en", + "requestedNotifications": true, + "languageStatus": 204, + "notificationStatus": 200, + "reads": [ + { + "path": "/jobapplications/stats", + "status": 200 + }, + { + "path": "/jobapplications/analytics-overview", + "status": 200 + }, + { + "path": "/jobapplications/analytics?months=6", + "status": 200 + }, + { + "path": "/notifications?limit=10", + "status": 200 + } + ], + "readback": { + "meStatus": 200, + "notificationStatus": 200, + "language": "en", + "notifications": true + }, + "localLanguage": "en", + "alertDomCountsNotErrorEvidence": { + "language": 2, + "notification": 2 + } + }, + { + "requestedLanguage": "nb-NO", + "requestedNotifications": false, + "languageStatus": 204, + "notificationStatus": 200, + "reads": [ + { + "path": "/jobapplications/stats", + "status": 200 + }, + { + "path": "/jobapplications/analytics-overview", + "status": 200 + }, + { + "path": "/jobapplications/analytics?months=6", + "status": 200 + }, + { + "path": "/notifications?limit=10", + "status": 200 + } + ], + "readback": { + "meStatus": 200, + "notificationStatus": 200, + "language": "nb-NO", + "notifications": false + }, + "localLanguage": "nb-NO", + "alertDomCountsNotErrorEvidence": { + "language": 2, + "notification": 2 + } + } + ], + "limits": [ + "Three successful rounds do not close the recorded intermittent language/notification500.", + "Alert DOM counts include Next route announcer and are not visible-error counts." + ] + }, + "routeCensus": { + "originalPatterns": 31, + "addedAnalyticsPatterns": 1, + "currentPatterns": 32, + "identityEntryRefreshObservations": 192, + "expectedEntryRefreshObservations": 188, + "failedNativeReadObservations": [ + { + "identity": "normal", + "pattern": "/discover", + "mode": "entry", + "destination": "/discover", + "status": 500 + }, + { + "identity": "normal", + "pattern": "/discover", + "mode": "refresh", + "destination": "/discover", + "status": 500 + }, + { + "identity": "admin", + "pattern": "/discover", + "mode": "entry", + "destination": "/discover", + "status": 500 + }, + { + "identity": "admin", + "pattern": "/discover", + "mode": "refresh", + "destination": "/discover", + "status": 500 + } + ], + "postLoginQueryFailure": { + "requested": "/analytics?months=6", + "actual": "/analytics" + }, + "observations": { + "anonymous": [ + { + "pattern": "/", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/login", + "mode": "entry", + "destination": "/login" + }, + { + "pattern": "/login", + "mode": "refresh", + "destination": "/login" + }, + { + "pattern": "/register", + "mode": "entry", + "destination": "/register" + }, + { + "pattern": "/register", + "mode": "refresh", + "destination": "/register" + }, + { + "pattern": "/forgot-password", + "mode": "entry", + "destination": "/forgot-password" + }, + { + "pattern": "/forgot-password", + "mode": "refresh", + "destination": "/forgot-password" + }, + { + "pattern": "/reset-password", + "mode": "entry", + "destination": "/reset-password" + }, + { + "pattern": "/reset-password", + "mode": "refresh", + "destination": "/reset-password" + }, + { + "pattern": "/verify-email", + "mode": "entry", + "destination": "/verify-email" + }, + { + "pattern": "/verify-email", + "mode": "refresh", + "destination": "/verify-email" + }, + { + "pattern": "/confirm-email-change", + "mode": "entry", + "destination": "/confirm-email-change" + }, + { + "pattern": "/confirm-email-change", + "mode": "refresh", + "destination": "/confirm-email-change" + }, + { + "pattern": "/microsoft-legacy-relink", + "mode": "entry", + "destination": "/microsoft-legacy-relink" + }, + { + "pattern": "/microsoft-legacy-relink", + "mode": "refresh", + "destination": "/microsoft-legacy-relink" + }, + { + "pattern": "/cv/:slug", + "mode": "entry", + "destination": "/cv/acceptance-unpublished-link", + "status": 404 + }, + { + "pattern": "/cv/:slug", + "mode": "refresh", + "destination": "/cv/acceptance-unpublished-link", + "status": 404 + }, + { + "pattern": "/dashboard", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/dashboard", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/analytics", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/analytics", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/discover", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/discover", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/jobs", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/jobs", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/jobs/:id", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/jobs/:id", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/reminders", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/reminders", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/operations", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/operations", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/kanban", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/kanban", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/companies", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/companies", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/offers", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/offers", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/correspondence", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/correspondence", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/correspondence/review", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/correspondence/review", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/profile", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/profile", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/applications/:id", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/applications/:id", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/career", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/career", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/career/builder", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/career/builder", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/career/builder/:id", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/career/builder/:id", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/admin/audit", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/admin/audit", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/admin/users", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/admin/users", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/admin/system", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/admin/system", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/trash", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/trash", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/settings", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/settings", + "mode": "refresh", + "destination": "/" + }, + { + "pattern": "/settings/connected-accounts", + "mode": "entry", + "destination": "/" + }, + { + "pattern": "/settings/connected-accounts", + "mode": "refresh", + "destination": "/" + } + ], + "normal": [ + { + "pattern": "/", + "mode": "entry", + "destination": "/dashboard" + }, + { + "pattern": "/", + "mode": "refresh", + "destination": "/dashboard" + }, + { + "pattern": "/login", + "mode": "entry", + "destination": "/login" + }, + { + "pattern": "/login", + "mode": "refresh", + "destination": "/login" + }, + { + "pattern": "/register", + "mode": "entry", + "destination": "/register" + }, + { + "pattern": "/register", + "mode": "refresh", + "destination": "/register" + }, + { + "pattern": "/forgot-password", + "mode": "entry", + "destination": "/forgot-password" + }, + { + "pattern": "/forgot-password", + "mode": "refresh", + "destination": "/forgot-password" + }, + { + "pattern": "/reset-password", + "mode": "entry", + "destination": "/reset-password" + }, + { + "pattern": "/reset-password", + "mode": "refresh", + "destination": "/reset-password" + }, + { + "pattern": "/verify-email", + "mode": "entry", + "destination": "/verify-email" + }, + { + "pattern": "/verify-email", + "mode": "refresh", + "destination": "/verify-email" + }, + { + "pattern": "/confirm-email-change", + "mode": "entry", + "destination": "/confirm-email-change" + }, + { + "pattern": "/confirm-email-change", + "mode": "refresh", + "destination": "/confirm-email-change" + }, + { + "pattern": "/microsoft-legacy-relink", + "mode": "entry", + "destination": "/microsoft-legacy-relink" + }, + { + "pattern": "/microsoft-legacy-relink", + "mode": "refresh", + "destination": "/microsoft-legacy-relink" + }, + { + "pattern": "/cv/:slug", + "mode": "entry", + "destination": "/cv/acceptance-unpublished-link", + "status": 404 + }, + { + "pattern": "/cv/:slug", + "mode": "refresh", + "destination": "/cv/acceptance-unpublished-link", + "status": 404 + }, + { + "pattern": "/dashboard", + "mode": "entry", + "destination": "/dashboard", + "status": 200 + }, + { + "pattern": "/dashboard", + "mode": "refresh", + "destination": "/dashboard", + "status": 200 + }, + { + "pattern": "/analytics", + "mode": "entry", + "destination": "/analytics?months=6", + "status": 200 + }, + { + "pattern": "/analytics", + "mode": "refresh", + "destination": "/analytics?months=6", + "status": 200 + }, + { + "pattern": "/discover", + "mode": "entry", + "destination": "/discover", + "status": 500 + }, + { + "pattern": "/discover", + "mode": "refresh", + "destination": "/discover", + "status": 500 + }, + { + "pattern": "/jobs", + "mode": "entry", + "destination": "/jobs?q=Acceptance", + "status": 200 + }, + { + "pattern": "/jobs", + "mode": "refresh", + "destination": "/jobs?q=Acceptance", + "status": 200 + }, + { + "pattern": "/jobs/:id", + "mode": "entry", + "destination": "/jobs/1?section=documents", + "status": 200 + }, + { + "pattern": "/jobs/:id", + "mode": "refresh", + "destination": "/jobs/1?section=documents", + "status": 200 + }, + { + "pattern": "/reminders", + "mode": "entry", + "destination": "/reminders", + "status": 200 + }, + { + "pattern": "/reminders", + "mode": "refresh", + "destination": "/reminders", + "status": 200 + }, + { + "pattern": "/operations", + "mode": "entry", + "destination": "/operations", + "status": 200 + }, + { + "pattern": "/operations", + "mode": "refresh", + "destination": "/operations", + "status": 200 + }, + { + "pattern": "/kanban", + "mode": "entry", + "destination": "/jobs?q=Acceptance&status=Saved&view=board#board", + "status": 200 + }, + { + "pattern": "/kanban", + "mode": "refresh", + "destination": "/jobs?q=Acceptance&status=Saved&view=board#board", + "status": 200 + }, + { + "pattern": "/companies", + "mode": "entry", + "destination": "/companies", + "status": 200 + }, + { + "pattern": "/companies", + "mode": "refresh", + "destination": "/companies", + "status": 200 + }, + { + "pattern": "/offers", + "mode": "entry", + "destination": "/offers", + "status": 200 + }, + { + "pattern": "/offers", + "mode": "refresh", + "destination": "/offers", + "status": 200 + }, + { + "pattern": "/correspondence", + "mode": "entry", + "destination": "/correspondence", + "status": 200 + }, + { + "pattern": "/correspondence", + "mode": "refresh", + "destination": "/correspondence", + "status": 200 + }, + { + "pattern": "/correspondence/review", + "mode": "entry", + "destination": "/correspondence?view=review", + "status": 409 + }, + { + "pattern": "/correspondence/review", + "mode": "refresh", + "destination": "/correspondence?view=review", + "status": 409 + }, + { + "pattern": "/profile", + "mode": "entry", + "destination": "/profile", + "status": 200 + }, + { + "pattern": "/profile", + "mode": "refresh", + "destination": "/profile", + "status": 200 + }, + { + "pattern": "/applications/:id", + "mode": "entry", + "destination": "/jobs/1?section=documents&followMode=waiting-update", + "status": 200 + }, + { + "pattern": "/applications/:id", + "mode": "refresh", + "destination": "/jobs/1?section=documents&followMode=waiting-update", + "status": 200 + }, + { + "pattern": "/career", + "mode": "entry", + "destination": "/career?section=profile", + "status": 200 + }, + { + "pattern": "/career", + "mode": "refresh", + "destination": "/career?section=profile", + "status": 200 + }, + { + "pattern": "/career/builder", + "mode": "entry", + "destination": "/career/builder", + "status": 200 + }, + { + "pattern": "/career/builder", + "mode": "refresh", + "destination": "/career/builder", + "status": 200 + }, + { + "pattern": "/career/builder/:id", + "mode": "entry", + "destination": "/career/builder/1", + "status": 200 + }, + { + "pattern": "/career/builder/:id", + "mode": "refresh", + "destination": "/career/builder/1", + "status": 200 + }, + { + "pattern": "/admin/audit", + "mode": "entry", + "destination": "/admin/audit", + "status": 403 + }, + { + "pattern": "/admin/audit", + "mode": "refresh", + "destination": "/admin/audit", + "status": 403 + }, + { + "pattern": "/admin/users", + "mode": "entry", + "destination": "/admin/users", + "status": 403 + }, + { + "pattern": "/admin/users", + "mode": "refresh", + "destination": "/admin/users", + "status": 403 + }, + { + "pattern": "/admin/system", + "mode": "entry", + "destination": "/admin/system", + "status": 403 + }, + { + "pattern": "/admin/system", + "mode": "refresh", + "destination": "/admin/system", + "status": 403 + }, + { + "pattern": "/trash", + "mode": "entry", + "destination": "/trash", + "status": 200 + }, + { + "pattern": "/trash", + "mode": "refresh", + "destination": "/trash", + "status": 200 + }, + { + "pattern": "/settings", + "mode": "entry", + "destination": "/settings", + "status": 200 + }, + { + "pattern": "/settings", + "mode": "refresh", + "destination": "/settings", + "status": 200 + }, + { + "pattern": "/settings/connected-accounts", + "mode": "entry", + "destination": "/settings/connected-accounts", + "status": 200 + }, + { + "pattern": "/settings/connected-accounts", + "mode": "refresh", + "destination": "/settings/connected-accounts", + "status": 200 + } + ], + "admin": [ + { + "pattern": "/", + "mode": "entry", + "destination": "/dashboard" + }, + { + "pattern": "/", + "mode": "refresh", + "destination": "/dashboard" + }, + { + "pattern": "/login", + "mode": "entry", + "destination": "/login" + }, + { + "pattern": "/login", + "mode": "refresh", + "destination": "/login" + }, + { + "pattern": "/register", + "mode": "entry", + "destination": "/register" + }, + { + "pattern": "/register", + "mode": "refresh", + "destination": "/register" + }, + { + "pattern": "/forgot-password", + "mode": "entry", + "destination": "/forgot-password" + }, + { + "pattern": "/forgot-password", + "mode": "refresh", + "destination": "/forgot-password" + }, + { + "pattern": "/reset-password", + "mode": "entry", + "destination": "/reset-password" + }, + { + "pattern": "/reset-password", + "mode": "refresh", + "destination": "/reset-password" + }, + { + "pattern": "/verify-email", + "mode": "entry", + "destination": "/verify-email" + }, + { + "pattern": "/verify-email", + "mode": "refresh", + "destination": "/verify-email" + }, + { + "pattern": "/confirm-email-change", + "mode": "entry", + "destination": "/confirm-email-change" + }, + { + "pattern": "/confirm-email-change", + "mode": "refresh", + "destination": "/confirm-email-change" + }, + { + "pattern": "/microsoft-legacy-relink", + "mode": "entry", + "destination": "/microsoft-legacy-relink" + }, + { + "pattern": "/microsoft-legacy-relink", + "mode": "refresh", + "destination": "/microsoft-legacy-relink" + }, + { + "pattern": "/cv/:slug", + "mode": "entry", + "destination": "/cv/acceptance-unpublished-link", + "status": 404 + }, + { + "pattern": "/cv/:slug", + "mode": "refresh", + "destination": "/cv/acceptance-unpublished-link", + "status": 404 + }, + { + "pattern": "/dashboard", + "mode": "entry", + "destination": "/dashboard", + "status": 200 + }, + { + "pattern": "/dashboard", + "mode": "refresh", + "destination": "/dashboard", + "status": 200 + }, + { + "pattern": "/analytics", + "mode": "entry", + "destination": "/analytics?months=6", + "status": 200 + }, + { + "pattern": "/analytics", + "mode": "refresh", + "destination": "/analytics?months=6", + "status": 200 + }, + { + "pattern": "/discover", + "mode": "entry", + "destination": "/discover", + "status": 500 + }, + { + "pattern": "/discover", + "mode": "refresh", + "destination": "/discover", + "status": 500 + }, + { + "pattern": "/jobs", + "mode": "entry", + "destination": "/jobs?q=Acceptance", + "status": 200 + }, + { + "pattern": "/jobs", + "mode": "refresh", + "destination": "/jobs?q=Acceptance", + "status": 200 + }, + { + "pattern": "/jobs/:id", + "mode": "entry", + "destination": "/jobs/2?section=documents", + "status": 200 + }, + { + "pattern": "/jobs/:id", + "mode": "refresh", + "destination": "/jobs/2?section=documents", + "status": 200 + }, + { + "pattern": "/reminders", + "mode": "entry", + "destination": "/reminders", + "status": 200 + }, + { + "pattern": "/reminders", + "mode": "refresh", + "destination": "/reminders", + "status": 200 + }, + { + "pattern": "/operations", + "mode": "entry", + "destination": "/operations", + "status": 200 + }, + { + "pattern": "/operations", + "mode": "refresh", + "destination": "/operations", + "status": 200 + }, + { + "pattern": "/kanban", + "mode": "entry", + "destination": "/jobs?q=Acceptance&status=Saved&view=board#board", + "status": 200 + }, + { + "pattern": "/kanban", + "mode": "refresh", + "destination": "/jobs?q=Acceptance&status=Saved&view=board#board", + "status": 200 + }, + { + "pattern": "/companies", + "mode": "entry", + "destination": "/companies", + "status": 200 + }, + { + "pattern": "/companies", + "mode": "refresh", + "destination": "/companies", + "status": 200 + }, + { + "pattern": "/offers", + "mode": "entry", + "destination": "/offers", + "status": 200 + }, + { + "pattern": "/offers", + "mode": "refresh", + "destination": "/offers", + "status": 200 + }, + { + "pattern": "/correspondence", + "mode": "entry", + "destination": "/correspondence", + "status": 200 + }, + { + "pattern": "/correspondence", + "mode": "refresh", + "destination": "/correspondence", + "status": 200 + }, + { + "pattern": "/correspondence/review", + "mode": "entry", + "destination": "/correspondence?view=review", + "status": 409 + }, + { + "pattern": "/correspondence/review", + "mode": "refresh", + "destination": "/correspondence?view=review", + "status": 409 + }, + { + "pattern": "/profile", + "mode": "entry", + "destination": "/profile", + "status": 200 + }, + { + "pattern": "/profile", + "mode": "refresh", + "destination": "/profile", + "status": 200 + }, + { + "pattern": "/applications/:id", + "mode": "entry", + "destination": "/jobs/2?section=documents&followMode=waiting-update", + "status": 200 + }, + { + "pattern": "/applications/:id", + "mode": "refresh", + "destination": "/jobs/2?section=documents&followMode=waiting-update", + "status": 200 + }, + { + "pattern": "/career", + "mode": "entry", + "destination": "/career?section=profile", + "status": 200 + }, + { + "pattern": "/career", + "mode": "refresh", + "destination": "/career?section=profile", + "status": 200 + }, + { + "pattern": "/career/builder", + "mode": "entry", + "destination": "/career/builder", + "status": 200 + }, + { + "pattern": "/career/builder", + "mode": "refresh", + "destination": "/career/builder", + "status": 200 + }, + { + "pattern": "/career/builder/:id", + "mode": "entry", + "destination": "/career/builder/2", + "status": 200 + }, + { + "pattern": "/career/builder/:id", + "mode": "refresh", + "destination": "/career/builder/2", + "status": 200 + }, + { + "pattern": "/admin/audit", + "mode": "entry", + "destination": "/admin/audit", + "status": 200 + }, + { + "pattern": "/admin/audit", + "mode": "refresh", + "destination": "/admin/audit", + "status": 200 + }, + { + "pattern": "/admin/users", + "mode": "entry", + "destination": "/admin/users", + "status": 200 + }, + { + "pattern": "/admin/users", + "mode": "refresh", + "destination": "/admin/users", + "status": 200 + }, + { + "pattern": "/admin/system", + "mode": "entry", + "destination": "/admin/system", + "status": 200 + }, + { + "pattern": "/admin/system", + "mode": "refresh", + "destination": "/admin/system", + "status": 200 + }, + { + "pattern": "/trash", + "mode": "entry", + "destination": "/trash", + "status": 200 + }, + { + "pattern": "/trash", + "mode": "refresh", + "destination": "/trash", + "status": 200 + }, + { + "pattern": "/settings", + "mode": "entry", + "destination": "/settings", + "status": 200 + }, + { + "pattern": "/settings", + "mode": "refresh", + "destination": "/settings", + "status": 200 + }, + { + "pattern": "/settings/connected-accounts", + "mode": "entry", + "destination": "/settings/connected-accounts", + "status": 200 + }, + { + "pattern": "/settings/connected-accounts", + "mode": "refresh", + "destination": "/settings/connected-accounts", + "status": 200 + } + ] + }, + "limits": [ + "Headers/expected native reads/auth denial/refresh are finite route evidence, not all route workflows.", + "Non-admin pages return403; provider-not-connected409/unpublished public CV404 and missing-token/registration-disabled states are intentional.", + "Connected-account census observes Gmail status, not full provider lifecycle." + ] + }, + "foreignMissingRoutes": { + "observations": [ + { + "kind": "foreign-job", + "mode": "entry", + "status": 404 + }, + { + "kind": "foreign-job", + "mode": "refresh", + "status": 404 + }, + { + "kind": "missing-job", + "mode": "entry", + "status": 404 + }, + { + "kind": "missing-job", + "mode": "refresh", + "status": 404 + }, + { + "kind": "foreign-cv", + "mode": "entry", + "status": 404 + }, + { + "kind": "foreign-cv", + "mode": "refresh", + "status": 404 + }, + { + "kind": "missing-cv", + "mode": "entry", + "status": 404 + }, + { + "kind": "missing-cv", + "mode": "refresh", + "status": 404 + } + ], + "privateContentLeaked": false, + "limits": [ + "Finite Job/CV entry+refresh404 and absence assertions; generic alert.first() may match Next announcer, so specific user-facing error text is not proved by this assertion." + ] + }, + "finiteBoardObservation": { + "initialFullRun": { + "fixtureRows": 250, + "matchedRows": 250, + "tableRows": 250, + "renderedRows": 250, + "boardRequests": 1, + "responseBytes": 296881, + "cardPopulationReadyMs": 1056, + "limitation": "Finite functional observation only; no capacity or latency acceptance threshold, and full-population O(N) debt remains." + }, + "finalOwningFileRun": { + "fixtureRows": 250, + "matchedRows": 250, + "tableRows": 250, + "renderedRows": 250, + "boardRequests": 1, + "responseBytes": 296265, + "cardPopulationReadyMs": 1044, + "limitation": "Finite functional observation only; no capacity or latency acceptance threshold, and full-population O(N) debt remains." + }, + "tableReadPages": 17, + "tablePageSize": 15, + "limits": [ + "Exact250 seeded/Table/API/DOM ID membership, not only counts.", + "No capacity/latency threshold; full-result O(N) memory/response/DOM debt remains." + ] + }, + "freshAutomatedAxe": { + "scans": 242, + "violationEntries": 0, + "incompleteNodeOccurrences": 1834, + "incompleteNodeOccurrencesByRule": { + "color-contrast": 1570, + "aria-hidden-focus": 33, + "aria-prohibited-attr": 209, + "aria-valid-attr-value": 18, + "th-has-data-cells": 4 + }, + "limits": [ + "Only JSON attachments containing violations/incomplete are counted.", + "Incomplete occurrences repeat nodes across states; not unique defects or passes.", + "No human screen-reader, physical-device or full contrast/zoom acceptance." + ] + }, + "auxiliary": { + "realHttp": 2, + "rateLimit": 9, + "ingress": 6, + "pdfAdmission": 1, + "failures": 0, + "limits": [ + "Real localhost HTTP against deterministic fixtures/Production environment guard, not a deployed model or actual production ingress." + ] + }, + "attemptHistory": [ + { + "name": "main", + "stats": { + "startTime": "2026-10-09T06:53:01.285Z", + "duration": 1300776.4749999999, + "expected": 119, + "skipped": 0, + "unexpected": 3, + "flaky": 0 + }, + "reportErrors": 0, + "failures": [ + { + "test": "[P5-15-01 E02 E07 A01] all 32 routes retain native entry, refresh and anonymous permissions", + "status": "unexpected", + "errorCount": 1, + "reason": "ambiguous alert locator harness error" + }, + { + "test": "[P5-15-01 E02 E07 A01] all 32 routes retain native entry, refresh and normal permissions", + "status": "unexpected", + "errorCount": 1, + "reason": "ambiguous alert locator harness error" + }, + { + "test": "[P5-15-01 E02 E07 A01] all 32 routes retain native entry, refresh and admin permissions", + "status": "unexpected", + "errorCount": 1, + "reason": "ambiguous alert locator harness error" + } + ] + }, + { + "name": "rerun1-route-correction", + "stats": { + "startTime": "2026-10-09T11:21:27.453Z", + "duration": 78404.278, + "expected": 2, + "skipped": 0, + "unexpected": 3, + "flaky": 0 + }, + "reportErrors": 0, + "failures": [ + { + "test": "[P5-15-01 E02 E07 A01] all 32 routes retain native entry, refresh and anonymous permissions", + "status": "unexpected", + "errorCount": 1, + "reason": "auth query discarded" + }, + { + "test": "[P5-15-01 E02 E07 A01] all 32 routes retain native entry, refresh and normal permissions", + "status": "unexpected", + "errorCount": 1, + "reason": "saved-search GET500; old observation wait may also terminate census" + }, + { + "test": "[P5-15-01 E02 E07 A01] all 32 routes retain native entry, refresh and admin permissions", + "status": "unexpected", + "errorCount": 1, + "reason": "saved-search GET500; old observation wait may also terminate census" + } + ] + }, + { + "name": "rerun2-route-correction", + "stats": { + "startTime": "2026-10-09T11:25:23.093Z", + "duration": 657241.6009999999, + "expected": 2, + "skipped": 0, + "unexpected": 3, + "flaky": 0 + }, + "reportErrors": 0, + "failures": [ + { + "test": "[P5-15-01 E02 E07 A01] all 32 routes retain native entry, refresh and anonymous permissions", + "status": "unexpected", + "errorCount": 1, + "reason": "auth query discarded" + }, + { + "test": "[P5-15-01 E02 E07 A01] all 32 routes retain native entry, refresh and normal permissions", + "status": "unexpected", + "errorCount": 4, + "reason": "saved-search GET500; old observation wait may also terminate census" + }, + { + "test": "[P5-15-01 E02 E07 A01] all 32 routes retain native entry, refresh and admin permissions", + "status": "unexpected", + "errorCount": 4, + "reason": "saved-search GET500; old observation wait may also terminate census" + } + ] + }, + { + "name": "route-correction", + "stats": { + "startTime": "2026-10-09T11:38:18.227Z", + "duration": 141643.00699999998, + "expected": 2, + "skipped": 0, + "unexpected": 3, + "flaky": 0 + }, + "reportErrors": 0, + "failures": [ + { + "test": "[P5-15-01 E02 E07 A01] all 32 routes retain native entry, refresh and anonymous permissions", + "status": "unexpected", + "errorCount": 1, + "reason": "auth query discarded" + }, + { + "test": "[P5-15-01 E02 E07 A01] all 32 routes retain native entry, refresh and normal permissions", + "status": "unexpected", + "errorCount": 2, + "reason": "saved-search GET500; old observation wait may also terminate census" + }, + { + "test": "[P5-15-01 E02 E07 A01] all 32 routes retain native entry, refresh and admin permissions", + "status": "unexpected", + "errorCount": 2, + "reason": "saved-search GET500; old observation wait may also terminate census" + } + ] + }, + { + "name": "rerun1-diagnostics", + "stats": { + "startTime": "2026-10-09T11:22:46.497Z", + "duration": 575.5, + "expected": 0, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "reportErrors": 1, + "failures": [] + }, + { + "name": "rerun2-diagnostics", + "stats": { + "startTime": "2026-10-09T11:36:20.984Z", + "duration": 595.4850000000001, + "expected": 0, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "reportErrors": 1, + "failures": [] + }, + { + "name": "diagnostics", + "stats": { + "startTime": "2026-10-09T11:40:40.514Z", + "duration": 13725.845000000001, + "expected": 1, + "skipped": 0, + "unexpected": 1, + "flaky": 0 + }, + "reportErrors": 0, + "failures": [ + { + "test": "[P5-15-01 diagnostic] an unsaved Rules draft survives switching to Notifications and back", + "status": "unexpected", + "errorCount": 1, + "reason": "unsaved Rules draft discarded on tab return" + } + ] + } + ], + "harnessCorrections": [ + "Missing-token alert locator now selects the translated actual warning.", + "ConnectedAccounts actually reads Gmail/Graph/IMAP status; census waits for Gmail instead of nonexistent email/providers.", + "Soft assertion retains each saved-search500 and finishes census, still failing the case.", + "Standalone config uses existing servers with correct UI cwd in one defineConfig object; earlier config failures executed zero checks." + ], + "testSources": [ + { + "path": "job-tracker-ui/e2e/functional-acceptance.spec.ts", + "sha256": "ccc5a7234e3a1ab9714da38563f04e6c58f652d33274bfcef2c2547e015ecdd8" + }, + { + "path": "docs/redesign/phase-5/p5-15/p5-15-01/diagnostics/preferences.spec.ts", + "sha256": "d0c3a8c0dc13973e8eb1d4c9686cdd92d69bad47971f12f23ac23ca67ca8c440" + }, + { + "path": "docs/redesign/phase-5/p5-15/p5-15-01/diagnostics/playwright.config.ts", + "sha256": "f05fcaee1d3e63b740677f6974b8be44b07678e9d0eda98e2c696290e39be85d" + } + ], + "rawReportsAndFailureEvidence": [ + { + "path": ".artifacts/p5-15-01/browser/commands.json", + "sha256": "1ee728f8b85e7bb9c6a1f733a6b798e0826a71f88a95a0b33d829d7979b7e550" + }, + { + "path": ".artifacts/p5-15-01/browser/diagnostics-command.json", + "sha256": "47db92dd30acb625a8571952c9628851e78d9235d1954785576a69d68ca0fbe0" + }, + { + "path": ".artifacts/p5-15-01/browser/diagnostics.json", + "sha256": "e54558572d74012ebefeb67e38d04a41abee112956cf72f98d85b3ca3ed705d0" + }, + { + "path": ".artifacts/p5-15-01/browser/http.json", + "sha256": "87eeb605b2cd179b7034ecebd217fdf19712e8ed2b9f084a6457fcfc45e95d36" + }, + { + "path": ".artifacts/p5-15-01/browser/ingress.json", + "sha256": "0a81246d7e3982520d9fd42c8eae06ba05cb84782533763a1b1660348111f627" + }, + { + "path": ".artifacts/p5-15-01/browser/main.json", + "sha256": "e7314f7a2390ec63c0a03647f15345b3504a07726f05f2c4b498ea7ae7d7bd02" + }, + { + "path": ".artifacts/p5-15-01/browser/pdf-admission.json", + "sha256": "1ee59d5e5c05f5a1011a14ac20dbf0690d5b148c82ae725a7756c28995c933e8" + }, + { + "path": ".artifacts/p5-15-01/browser/rate-limit.json", + "sha256": "e593d341c8983e6a4cb7e189e6168bf4e5c3d26c2fa029cfccedf67310c7aa68" + }, + { + "path": ".artifacts/p5-15-01/browser/rerun1-diagnostics-command.json", + "sha256": "c7663fd30b30a2567d8355270d08d9ec2f5f09d80ff74db287a7c62022fb806f" + }, + { + "path": ".artifacts/p5-15-01/browser/rerun1-diagnostics.json", + "sha256": "54a59072a12478b6842f7109e5bb6d411225e75b6b43d0ae573c98801f8b037c" + }, + { + "path": ".artifacts/p5-15-01/browser/rerun1-route-correction-command.json", + "sha256": "3f10954d375150a00d5e229239d68d05499dfc85fea20a3a254cb44bbf32d58f" + }, + { + "path": ".artifacts/p5-15-01/browser/rerun1-route-correction.json", + "sha256": "72dc2f6eff45c4438502a1cf8f0d88b2795d7816735ba82f35a9347dbd626dd2" + }, + { + "path": ".artifacts/p5-15-01/browser/rerun2-diagnostics-command.json", + "sha256": "9c58d5c742613a300020da2aa60cbbbc6412c56dc2d38b8bc91e0e1b0d40c29d" + }, + { + "path": ".artifacts/p5-15-01/browser/rerun2-diagnostics.json", + "sha256": "b641f9f305e02f0d55f8b036f6f29be44ed0214d574fae1cab9170e1fb9a22cf" + }, + { + "path": ".artifacts/p5-15-01/browser/rerun2-route-correction-command.json", + "sha256": "dcd48e61498a27ee107eec888eca0573091122d30cbfe1d47a713bd5c7786dbc" + }, + { + "path": ".artifacts/p5-15-01/browser/rerun2-route-correction.json", + "sha256": "bf42556ff7d1f7ff1364eddba0468d2f48695b3ea61bcc71d15f49b4662a3b53" + }, + { + "path": ".artifacts/p5-15-01/browser/route-correction-command.json", + "sha256": "c851f630b3fc877e68f36ddd5b817d7d4f3cbf354e5c16570f19160ce1c5263d" + }, + { + "path": ".artifacts/p5-15-01/browser/route-correction.json", + "sha256": "295e213f6ef1cb9d00d3ba1f3b7a0fc4df1a68b6eb0c80a9ace95e9bc790cf2d" + }, + { + "path": ".artifacts/p5-15-01/browser/diagnostics.log", + "sha256": "17c04a6f8fe8604abbbed85ae75247c4c2b8540dbfb9e1984cf052911f315e0c" + }, + { + "path": ".artifacts/p5-15-01/browser/http.log", + "sha256": "8c59f9d6a317a35b1273f17ba0bb19de928a429c82108c56b729d54693c179da" + }, + { + "path": ".artifacts/p5-15-01/browser/ingress.log", + "sha256": "77d6646ded3ae2b857c361eeae9457cc6ca2a508f6bd3cd66dbad1121f7f1e0d" + }, + { + "path": ".artifacts/p5-15-01/browser/main.log", + "sha256": "910171fdfb34e80f42a292253e44a35044fdb527a64db183b498c4fde1c3e9a6" + }, + { + "path": ".artifacts/p5-15-01/browser/pdf-admission.log", + "sha256": "bf9665bc067dac504500d3434d7802f96b46cbfa0d0d59ee32f7449bd2580404" + }, + { + "path": ".artifacts/p5-15-01/browser/rate-limit.log", + "sha256": "6ac3fff11eed9064faa4909903cb4d1353188d59b9470fad4a015688ab394750" + }, + { + "path": ".artifacts/p5-15-01/browser/rerun1-diagnostics.log", + "sha256": "00ed33feb58fa50fe7f73edb51a246d65731f74a4cdc3e5cdab0b20ee462cdf9" + }, + { + "path": ".artifacts/p5-15-01/browser/rerun1-route-correction.log", + "sha256": "edb95d562056b42536e2c971fb2bdc78a0f6226c57007c22e86d27d4b52ff20b" + }, + { + "path": ".artifacts/p5-15-01/browser/rerun2-diagnostics.log", + "sha256": "00ed33feb58fa50fe7f73edb51a246d65731f74a4cdc3e5cdab0b20ee462cdf9" + }, + { + "path": ".artifacts/p5-15-01/browser/rerun2-route-correction.log", + "sha256": "a83e789790031c78721a4244cd8556d4658c0802cca954109169e5c8885f6614" + }, + { + "path": ".artifacts/p5-15-01/browser/route-correction.log", + "sha256": "37515b0b279bed793b595701f52150ed03eff0c284c30ff9c85a69f2aafd7a35" + }, + { + "path": ".artifacts/p5-15-01/browser/route-correction-results/functional-acceptance--P5--22142-h-and-anonymous-permissions-chromium/test-failed-1.png", + "sha256": "d9a7897706a2a3964f9840375dd47c24a8a7108b29097978753f09c174405537" + }, + { + "path": ".artifacts/p5-15-01/browser/route-correction-results/functional-acceptance--P5--22142-h-and-anonymous-permissions-chromium/error-context.md", + "sha256": "1192dc1ef3b2a0757b84c48a76b3c45eae58934eba4f61455fa024479abdf6fb" + }, + { + "path": ".artifacts/p5-15-01/browser/route-correction-results/functional-acceptance--P5--22142-h-and-anonymous-permissions-chromium/trace.zip", + "sha256": "045bd291b90d2d2154798ec2c80cf6f7cd8abf13b66fd7cb52fe6078838f8f64" + }, + { + "path": ".artifacts/p5-15-01/browser/route-correction-results/functional-acceptance--P5--ece7d-resh-and-normal-permissions-chromium/test-failed-1.png", + "sha256": "a1d58cab3c1323232db9b0ec61b1416ba16c0e400fcce25a9d0d36c8cf82e3c6" + }, + { + "path": ".artifacts/p5-15-01/browser/route-correction-results/functional-acceptance--P5--ece7d-resh-and-normal-permissions-chromium/error-context.md", + "sha256": "ce460483aad22556dacb6ec82e2afe6efe60d76e99d8e43f48173715ee6b575c" + }, + { + "path": ".artifacts/p5-15-01/browser/route-correction-results/functional-acceptance--P5--ece7d-resh-and-normal-permissions-chromium/trace.zip", + "sha256": "02e662edd2fa89f6c2758546e16d5975642c01d84925ccfd63e2371edb4c60b4" + }, + { + "path": ".artifacts/p5-15-01/browser/route-correction-results/functional-acceptance--P5--beb39-fresh-and-admin-permissions-chromium/test-failed-1.png", + "sha256": "dbb1f6437439669feff8ce8019b9a1c18b4003979fd782f62f8d343f78252213" + }, + { + "path": ".artifacts/p5-15-01/browser/route-correction-results/functional-acceptance--P5--beb39-fresh-and-admin-permissions-chromium/error-context.md", + "sha256": "03a5df89db0e5ca1c00c4e279729b614a5ad50c7661a5f4e30ca601a91cafc94" + }, + { + "path": ".artifacts/p5-15-01/browser/route-correction-results/functional-acceptance--P5--beb39-fresh-and-admin-permissions-chromium/trace.zip", + "sha256": "3e47cd1be9c0fb3b3aa20005a30a99436c14208f038d9bf2d56f212a8c8003e0" + }, + { + "path": ".artifacts/p5-15-01/browser/diagnostics-results/preferences--P5-15-01-diag-fa997-g-to-Notifications-and-back-chromium/test-failed-1.png", + "sha256": "6a1562075dc2ee8bef635916d88178b7e88eca8a151c6df7985251893ffff0c4" + }, + { + "path": ".artifacts/p5-15-01/browser/diagnostics-results/preferences--P5-15-01-diag-fa997-g-to-Notifications-and-back-chromium/error-context.md", + "sha256": "7921aa15fe9390e815279b837df66ea5270c460c9871c83bcad5fa33a1771a80" + }, + { + "path": ".artifacts/p5-15-01/browser/diagnostics-results/preferences--P5-15-01-diag-fa997-g-to-Notifications-and-back-chromium/trace.zip", + "sha256": "ca6f4e9228d051408e1cf4332c046eae454a028f894eb2a0b9b1e2ea938d98e2" + } + ] +} diff --git a/job-tracker-ui/e2e/functional-acceptance.spec.ts b/job-tracker-ui/e2e/functional-acceptance.spec.ts new file mode 100644 index 0000000..215c93a --- /dev/null +++ b/job-tracker-ui/e2e/functional-acceptance.spec.ts @@ -0,0 +1,256 @@ +import { expect, test, type APIRequestContext, type Page } from '@playwright/test'; +import { translations } from '../src/i18n/translations'; + +const apiUrl = 'http://localhost:5302/api'; +const password = 'E2ePassword123!'; +const t = translations.en; + +// Same disposable local account / CSRF / session-context setup as preservation tests. +async function login(api: APIRequestContext, email: string) { + expect((await api.post(`${apiUrl}/auth/login`, { data: { email, password } })).status()).toBe(200); + const me = await api.get(`${apiUrl}/auth/me`); + expect(me.status()).toBe(200); + const csrf = (await api.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value; + return { 'X-CSRF-TOKEN': csrf, 'X-Session-Context': (await me.json()).sessionContext as string }; +} + +async function createAccount(admin: APIRequestContext, headers: Record, roles: string[] = []) { + const email = `acceptance-${Date.now()}-${Math.random().toString(36).slice(2)}@example.test`; + expect((await admin.post(`${apiUrl}/users`, { headers, data: { email, password, roles } })).status()).toBe(200); + return email; +} + +type RouteCase = { pattern: string; path: string; final?: string; heading?: string; endpoint?: string; status?: number; public?: boolean }; +function routes(jobId: number, cvId: number): RouteCase[] { + return [ + { pattern: '/', path: '/', public: true }, + { pattern: '/login', path: '/login', heading: t.signInTitle, public: true }, + { pattern: '/register', path: '/register', heading: t.createAccount, public: true }, + { pattern: '/forgot-password', path: '/forgot-password', heading: t.forgotPasswordTitle, public: true }, + { pattern: '/reset-password', path: '/reset-password', heading: t.resetPasswordTitle, public: true }, + { pattern: '/verify-email', path: '/verify-email', heading: t.verifyEmailTitle, public: true }, + { pattern: '/confirm-email-change', path: '/confirm-email-change', heading: t.confirmEmailChangeTitle, public: true }, + { pattern: '/microsoft-legacy-relink', path: '/microsoft-legacy-relink', heading: t.microsoftLegacyRelinkTitle, public: true }, + { pattern: '/cv/:slug', path: '/cv/acceptance-unpublished-link', endpoint: '/public-cv/acceptance-unpublished-link', status: 404, public: true }, + { pattern: '/dashboard', path: '/dashboard', heading: t.dashboard, endpoint: '/jobapplications/stats' }, + { pattern: '/analytics', path: '/analytics?months=6', heading: t.analytics, endpoint: '/jobapplications/stats' }, + { pattern: '/discover', path: '/discover', heading: t.discoverJobs, endpoint: '/job-discovery/saved-searches' }, + { pattern: '/jobs', path: '/jobs?q=Acceptance', heading: t.jobApplications, endpoint: '/jobapplications' }, + { pattern: '/jobs/:id', path: `/jobs/${jobId}?section=documents`, heading: t.jobDetails, endpoint: `/jobapplications/${jobId}/workspace` }, + { pattern: '/reminders', path: '/reminders', heading: t.reminders, endpoint: '/jobapplications/reminders' }, + { pattern: '/operations', path: '/operations', heading: t.operations, endpoint: '/operations' }, + { pattern: '/kanban', path: '/kanban?q=Acceptance&status=Saved#board', final: '/jobs?q=Acceptance&status=Saved&view=board#board', heading: t.jobApplications, endpoint: '/jobapplications/board' }, + { pattern: '/companies', path: '/companies', heading: t.companies, endpoint: '/companies' }, + { pattern: '/offers', path: '/offers', heading: t.offerComparisonTitle, endpoint: '/jobapplications' }, + { pattern: '/correspondence', path: '/correspondence', heading: t.correspondenceInboxTitle, endpoint: '/correspondence/page' }, + { pattern: '/correspondence/review', path: '/correspondence/review?route=all#review', final: '/correspondence?view=review', heading: t.correspondenceInboxTitle, endpoint: '/gmail/review-candidates', status: 409 }, + { pattern: '/profile', path: '/profile', heading: t.profile, endpoint: '/auth/me' }, + { pattern: '/applications/:id', path: `/applications/${jobId}?section=documents&followMode=waiting-update#workspace`, final: `/jobs/${jobId}?section=documents&followMode=waiting-update`, heading: t.jobDetails, endpoint: `/jobapplications/${jobId}/workspace` }, + { pattern: '/career', path: '/career?section=profile', heading: t.careerWorkspace, endpoint: '/career/profile' }, + { pattern: '/career/builder', path: '/career/builder', heading: t.cvBuilder, endpoint: '/cv/variants' }, + { pattern: '/career/builder/:id', path: `/career/builder/${cvId}`, heading: t.cvBuilder, endpoint: `/cv/variants/${cvId}` }, + { pattern: '/admin/audit', path: '/admin/audit', heading: t.auditLog, endpoint: '/admin/audit' }, + { pattern: '/admin/users', path: '/admin/users', heading: t.users, endpoint: '/users' }, + { pattern: '/admin/system', path: '/admin/system', heading: t.systemStatus, endpoint: '/admin/system' }, + { pattern: '/trash', path: '/trash', heading: t.trash, endpoint: '/jobapplications' }, + { pattern: '/settings', path: '/settings', heading: t.settings, endpoint: '/notification-settings' }, + { pattern: '/settings/connected-accounts', path: '/settings/connected-accounts', heading: t.connectedAccounts, endpoint: '/gmail/status' }, + ]; +} + +async function publicState(page: Page, row: RouteCase) { + if (row.pattern === '/') await expect(page.getByRole('heading', { name: 'Two clear plans' })).toBeVisible(); + else if (row.pattern === '/cv/:slug') await expect(page.getByText(/This CV isn.t available/)).toBeVisible(); + else { + await expect(page.getByRole('heading', { name: row.heading!, exact: true })).toBeVisible(); + const missingLinks: Record = { + '/reset-password': t.missingResetLinkInfo, + '/verify-email': t.missingVerifyLinkInfo, + '/confirm-email-change': t.missingEmailChangeLinkInfo, + '/microsoft-legacy-relink': t.microsoftLegacyRelinkMissing, + }; + const missingLink = missingLinks[row.pattern]; + if (missingLink) { + await expect(page.getByRole('alert').filter({ hasText: missingLink })).toBeVisible(); + } + if (row.pattern === '/register') await expect(page.getByRole('button', { name: t.createAccount, exact: true })).toBeDisabled(); + } +} + +test.beforeEach(async ({ context }) => { + await context.addInitScript(() => localStorage.setItem('uiLanguage', 'en')); + await context.route('**/*', route => { + const url = new URL(route.request().url()); + return ['localhost', '127.0.0.1'].includes(url.hostname) || ['data:', 'blob:', 'about:'].includes(url.protocol) ? route.continue() : route.abort(); + }); +}); + +for (const identity of ['anonymous', 'normal', 'admin'] as const) { + test(`[P5-15-01 E02 E07 A01] all 32 routes retain native entry, refresh and ${identity} permissions`, async ({ page, playwright }, info) => { + test.setTimeout(300_000); + const admin = await playwright.request.newContext(); + const evidence: { pattern: string; mode: string; destination: string; status?: number }[] = []; + try { + const adminHeaders = await login(admin, 'e2e@example.test'); + const email = await createAccount(admin, adminHeaders, identity === 'admin' ? ['Admin'] : []); + let jobId = 2147483647; + let cvId = 2147483647; + if (identity !== 'anonymous') { + const headers = await login(page.request, email); + const company = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: 'Acceptance company' } }); + expect(company.status()).toBe(201); + const job = await page.request.post(`${apiUrl}/jobapplications`, { headers, data: { companyId: (await company.json()).id, jobTitle: 'Acceptance engineer', status: 'Saved' } }); + expect(job.status()).toBe(201); jobId = (await job.json()).id; + const cv = await page.request.post(`${apiUrl}/cv/variants`, { headers, data: { name: 'Acceptance CV' } }); + expect(cv.status()).toBe(200); cvId = (await cv.json()).id; + } + const census = routes(jobId, cvId); + expect(new Set(census.map(row => row.pattern)).size).toBe(32); + for (const row of census) { + await test.step(row.pattern, async () => { + for (const mode of ['entry', 'refresh']) { + const protectedAnonymous = identity === 'anonymous' && !row.public; + const endpoint = protectedAnonymous ? undefined : row.endpoint; + const response = endpoint ? page.waitForResponse(response => new URL(response.url()).pathname === `/api${endpoint}` && response.request().method() === 'GET', { timeout: 20_000 }) : undefined; + if (mode === 'entry') await page.goto(row.path); else await page.reload(); + const destination = protectedAnonymous ? '/' : row.pattern === '/' && identity !== 'anonymous' ? '/dashboard' : row.final ?? row.path; + await expect(page).toHaveURL(url => `${url.pathname}${url.search}${url.hash}` === destination); + const native = response ? await response : undefined; + evidence.push({ pattern: row.pattern, mode, destination, status: native?.status() }); + if (native) { + const denied = identity === 'normal' && row.pattern.startsWith('/admin/'); + // Finish the route census even when one native read exposes a defect. + expect.soft(native.status()).toBe(denied ? 403 : row.status ?? 200); + if (row.pattern === '/correspondence/review') { + expect(await native.json()).toMatchObject({ code: 'gmail_not_connected' }); + await expect(page.getByRole('alert').filter({ hasText: 'Connect Gmail before using the Gmail review queue.' })).toBeVisible(); + } + if (denied) { + await expect(page.getByRole('alert').first()).toBeVisible(); + await expect(page.getByText('e2e@example.test', { exact: true })).toHaveCount(0); + } + } + if (protectedAnonymous) { + await publicState(page, census[0]); + await expect(page.getByRole('main')).toHaveCount(0); + } else if (row.public && !(row.pattern === '/' && identity !== 'anonymous')) await publicState(page, row); + else { + await expect(page.getByRole('heading', { level: 1, name: row.pattern === '/' ? t.dashboard : row.heading!, exact: true })).toBeVisible(); + if (['/jobs/:id', '/applications/:id'].includes(row.pattern)) await expect(page.getByRole('heading', { name: 'Acceptance engineer', exact: true })).toBeVisible(); + if (row.pattern === '/career/builder/:id') await expect(page.getByLabel('CV name', { exact: true })).toHaveValue('Acceptance CV'); + if (row.pattern === '/jobs') await expect(page.locator(`[data-job-row-id="${jobId}"]`)).toBeVisible(); + if (row.pattern === '/kanban') await expect(page.locator(`[data-kanban-card-id="${jobId}"]`)).toBeVisible(); + } + } + }); + } + if (identity === 'anonymous') { + await page.goto('/analytics?months=6'); + await expect(page).toHaveURL(/\/$/); + await page.getByRole('button', { name: 'Sign in', exact: true }).first().click(); + await page.getByLabel('Username or email', { exact: true }).fill(email); + await page.getByLabel('Current password', { exact: true }).fill(password); + await page.getByRole('button', { name: 'Sign in', exact: true }).click(); + await expect(page).toHaveURL(/\/analytics\?months=6$/); + await expect(page.getByRole('button', { name: '6 mo', exact: true })).toHaveAttribute('aria-pressed', 'true'); + } else { + await page.goto('/dashboard'); + await page.getByRole('button', { name: t.jobApplications, exact: true }).click(); + await expect(page).toHaveURL(/\/jobs$/); + await page.goBack(); await expect(page).toHaveURL(/\/dashboard$/); + await page.goForward(); await expect(page).toHaveURL(/\/jobs$/); + await expect(page.getByTestId('admin-version-badge')).toHaveCount(identity === 'admin' ? 1 : 0); + } + } finally { + await info.attach(`route-census-${identity}`, { body: JSON.stringify(evidence, null, 2), contentType: 'application/json' }); + await admin.dispose(); + } + }); +} + +test('[P5-15-01 W01 C07 A01] foreign and missing workspace deep links deny data through entry and refresh', async ({ page, playwright }, info) => { + test.setTimeout(90_000); + const admin = await playwright.request.newContext(); + const owner = await playwright.request.newContext(); + const evidence: { kind: string; mode: string; status: number }[] = []; + try { + const adminHeaders = await login(admin, 'e2e@example.test'); + const ownerHeaders = await login(owner, await createAccount(admin, adminHeaders)); + const company = await owner.post(`${apiUrl}/companies`, { headers: ownerHeaders, data: { name: 'Foreign acceptance company' } }); + expect(company.status()).toBe(201); + const job = await owner.post(`${apiUrl}/jobapplications`, { headers: ownerHeaders, data: { companyId: (await company.json()).id, jobTitle: 'Foreign acceptance engineer', status: 'Saved' } }); + expect(job.status()).toBe(201); + const jobId = (await job.json()).id; + const cv = await owner.post(`${apiUrl}/cv/variants`, { headers: ownerHeaders, data: { name: 'Foreign acceptance CV' } }); + expect(cv.status()).toBe(200); + const cvId = (await cv.json()).id; + await login(page.request, await createAccount(admin, adminHeaders)); + for (const [kind, id, editor] of [['foreign-job', jobId, false], ['missing-job', 2147483647, false], ['foreign-cv', cvId, true], ['missing-cv', 2147483647, true]] as const) { + for (const mode of ['entry', 'refresh']) { + const endpoint = editor ? `/cv/variants/${id}` : `/jobapplications/${id}/workspace`; + const response = page.waitForResponse(response => new URL(response.url()).pathname === `/api${endpoint}` && response.request().method() === 'GET'); + if (mode === 'entry') await page.goto(editor ? `/career/builder/${id}` : `/applications/${id}?section=documents&followMode=waiting-update`); else await page.reload(); + const status = (await response).status(); + evidence.push({ kind, mode, status }); + expect(status).toBe(404); + await expect(page.getByRole('alert').first()).toBeVisible(); + await expect(page.getByText('Foreign acceptance engineer', { exact: true })).toHaveCount(0); + if (editor) await expect(page.getByLabel('CV name', { exact: true })).toHaveCount(0); + else { + await expect(page).toHaveURL(new RegExp(`/jobs/${id}\\?section=documents&followMode=waiting-update$`)); + await expect(page.getByRole('button', { name: 'Back to applications', exact: true })).toBeVisible(); + } + } + } + } finally { + await info.attach('foreign-missing-route-denial', { body: JSON.stringify(evidence, null, 2), contentType: 'application/json' }); + await owner.dispose(); await admin.dispose(); + } +}); + +test('[P5-15-01 J05 J07] a finite 250-job Board preserves the complete matching Table population', async ({ page, playwright }, info) => { + test.setTimeout(240_000); + const admin = await playwright.request.newContext(); + try { + const adminHeaders = await login(admin, 'e2e@example.test'); + const headers = await login(page.request, await createAccount(admin, adminHeaders)); + const company = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: 'Finite Board acceptance company' } }); + expect(company.status()).toBe(201); + const companyId = (await company.json()).id; + const seededIds: number[] = []; + for (let index = 0; index < 250; index++) { + const created = await page.request.post(`${apiUrl}/jobapplications`, { headers, data: { companyId, jobTitle: `Finite Board ${String(index).padStart(3, '0')}`, status: 'Saved' } }); + expect(created.status()).toBe(201); + seededIds.push((await created.json()).id); + } + // Reuse the existing Board/Table read contract: full Board vs Table's normal 15-row pages. + const query = 'q=Finite%20Board&status=Saved'; + const tableIds: number[] = []; + for (let number = 1; tableIds.length < seededIds.length; number++) { + const response = await page.request.get(`${apiUrl}/jobapplications?${query}&page=${number}&pageSize=15`); + expect(response.status()).toBe(200); + const table = await response.json(); + expect(table.total).toBe(seededIds.length); + expect(table.items.length).toBeGreaterThan(0); + tableIds.push(...table.items.map((item: { id: number }) => item.id)); + } + expect(new Set(tableIds).size).toBe(250); + expect([...tableIds].sort((a, b) => a - b)).toEqual([...seededIds].sort((a, b) => a - b)); + let boardRequests = 0; + page.on('request', request => { if (new URL(request.url()).pathname === '/api/jobapplications/board' && request.method() === 'GET') boardRequests++; }); + const read = page.waitForResponse(response => new URL(response.url()).pathname === '/api/jobapplications/board' && response.request().method() === 'GET'); + const started = Date.now(); + await page.goto(`/jobs?${query}&view=board`); + const boardResponse = await read; + expect(boardResponse.status()).toBe(200); + const bytes = await boardResponse.body(); + const board = JSON.parse(bytes.toString()) as { id: number }[]; + expect(board.map(item => item.id).sort((a, b) => a - b)).toEqual([...tableIds].sort((a, b) => a - b)); + await expect(page.locator('[data-kanban-card-id]')).toHaveCount(250); + const renderedIds = await page.locator('[data-kanban-card-id]').evaluateAll(cards => cards.map(card => Number(card.getAttribute('data-kanban-card-id')))); + expect(renderedIds.sort((a, b) => a - b)).toEqual([...tableIds].sort((a, b) => a - b)); + await info.attach('finite-250-board-observation', { body: JSON.stringify({ fixtureRows: 250, matchedRows: board.length, tableRows: tableIds.length, + renderedRows: renderedIds.length, boardRequests, responseBytes: bytes.length, cardPopulationReadyMs: Date.now() - started, + limitation: 'Finite functional observation only; no capacity or latency acceptance threshold, and full-population O(N) debt remains.' }, null, 2), contentType: 'application/json' }); + } finally { await admin.dispose(); } +});