test(redesign): establish preservation baseline

Map 57 capability groups before restructuring. Add behavioral regression coverage and a standalone TypeScript gate; retain the native SQLite package failure as separate baseline debt.
This commit is contained in:
cesnimda
2026-09-10 12:53:06 +02:00
parent 2d0f131a99
commit d6d3dd977d
14 changed files with 835 additions and 3 deletions
+5
View File
@@ -217,6 +217,11 @@ jobs:
echo "Frontend lint process crashed with SIGSEGV — retrying once..." echo "Frontend lint process crashed with SIGSEGV — retrying once..."
npm run lint npm run lint
- name: Typecheck frontend
working-directory: job-tracker-ui
# Include application and test sources; Jest transpilation is not type checking.
run: npm run typecheck
- name: Test frontend - name: Test frontend
working-directory: job-tracker-ui working-directory: job-tracker-ui
# Run the WHOLE suite. Never whitelist test files here again: the previous # Run the WHOLE suite. Never whitelist test files here again: the previous
+2
View File
@@ -1,5 +1,7 @@
# Phase 1 — Frontend audit # Phase 1 — Frontend audit
Successor checkpoint: [Phase 2 preservation matrix, regression results and traceability](../phase-2/README.md), based on the completed Phase 1 commit `2d0f131`. The historical audit below is retained; no visual redesign has started.
Date: 2026-09-09. Source checkpoint: `97fce89c002fe86e737ca7387170321d464b80d7` (application changes through `0954876`; subsequent commits contain the brief and CI prerequisite corrections). Audit branch: `codex/ui-redesign-audit`. Test-only packaging correction `23cc250` leaves the frontend census unchanged; its fresh 22 PDF and 915 broader backend passes are recorded in the test assessment. Date: 2026-09-09. Source checkpoint: `97fce89c002fe86e737ca7387170321d464b80d7` (application changes through `0954876`; subsequent commits contain the brief and CI prerequisite corrections). Audit branch: `codex/ui-redesign-audit`. Test-only packaging correction `23cc250` leaves the frontend census unchanged; its fresh 22 PDF and 915 broader backend passes are recorded in the test assessment.
**Phase 1 source audit complete; redesign implementation has not started.** The repository already implements much of the proposed workflow. Retain its MUI/React foundation and domain contracts, improve shared semantics, then migrate representative screens in small steps. A replacement frontend framework or component library is not justified by the observed problems. **Phase 1 source audit complete; redesign implementation has not started.** The repository already implements much of the proposed workflow. Retain its MUI/React foundation and domain contracts, improve shared semantics, then migrate representative screens in small steps. A replacement frontend framework or component library is not justified by the observed problems.
+68
View File
@@ -0,0 +1,68 @@
# Phase 2 baseline defects and intentional gaps
Application source is unchanged from `2d0f131`. No defect below was fixed as part of the preservation checkpoint.
## P2-D01 — SQLite tailoring and submitted-package capture fail
**Confirmed in the disposable running local API, not production.** The new [browser regression](../../../job-tracker-ui/e2e/redesign-preservation.spec.ts) creates a real non-admin account, canonical profile, company/job, attached CV, cover letter and UTF-8 attachment. It opens the existing legacy job URL, then clicks **Freeze current package** in the **CV section** (not the cover-letter section).
Observed:
| Request | Result |
|---|---|
| Profile/job/CV/letter/file fixture writes | Successful, including B02 session preconditions |
| GET job submitted-packages | 200, empty list |
| GET job tailoring | 500 |
| POST job submitted-packages | 500; no successful package row shown |
| API exception | `System.NotSupportedException: SQLite does not support expressions of type 'DateTimeOffset' in ORDER BY clauses.` |
Stack evidence points to [ApplicationAssetsService](../../../JobTrackerApi/Services/ApplicationAssetsService.cs), `GetTailoringPlanAsync` (ordering verified CareerEvidence by UpdatedAtUtc at line 182), and [SubmittedApplicationPackageService](../../../JobTrackerApi/Services/SubmittedApplicationPackageService.cs), `CaptureAsync` (ordering variants by UpdatedAtUtc at line 67). These paths do not use the SQLite-safe ordering used in some nearby services. Existing [SqliteDateTimeOffsetCompatibilityTests](../../../JobTrackerApi.Tests/SqliteDateTimeOffsetCompatibilityTests.cs) exercise other ordering paths; they do not certify these two. Existing [SubmittedApplicationPackageTests](../../../JobTrackerApi.Tests/SubmittedApplicationPackageTests.cs) prove snapshot/owner/failure contracts with test seams, not successful native SQLite execution of this query.
The new desired success test is retained with an explicit `test.fail` annotation activated **after fixture and navigation setup, only for the observed capture HTTP 500**. Playwright reports an expected failure as expected execution; this is **not** a successfully frozen package. Post-capture checks for unchanged archived HTML/letter and exact attachment bytes after deleting the current source are present but **not reached**. Do not count them as achieved coverage. A successful capture runs all later assertions as ordinary tests; setup, non-500 responses and subsequent archive failures cannot be masked by the annotation. Remove the narrow annotation and update this classification only with the separately scoped fix and successful native evidence.
Initial fixture error: the first draft looked for package capture under the cover-letter tab; inspection showed the existing capture lives under CV, and the test was corrected. That locator error is not an application defect.
Scope/next owner: a separate bounded provider-compatibility repair and native storage verification before any package/tailoring redesign. Preserve timestamps/ownership and MariaDB behavior; no schema change is presumed. Phase 2 neither changes ordering nor claims the same failure in MariaDB/production. All original 916 backend and 298 frontend cases remain retained.
Local diagnostic command (PowerShell, from job-tracker-ui, after the documented build):
```powershell
$env:DEBUG='pw:webserver'
node node_modules/@playwright/test/cli.js test e2e/redesign-preservation.spec.ts
```
The full debug log is local ignored evidence, `.artifacts/phase2-browser-focused.log`; it is not committed wholesale. It contains synthetic accounts only. The exception/request facts above are the durable evidence. Use the normal commands in [README](README.md) to reproduce with an isolated data directory; never substitute production URLs.
## P2-O01 — one rules-save HTTP 500, not independently reproduced
The first browser run sent the valid rule object with appliedFollowUpDays=5 and received 500; a subsequent GET still returned 14. The visible draft retained 5. Response contained a generic problem and trace ID; that run did not retain the server exception, so the cause is unknown.
An isolated authenticated direct PUT then returned 204 and persisted 5. A repeat of the original mobile→desktop browser save with explicit response observation also returned 204 and persisted 5. **No rules regression is expected-failed or skipped.** Both successful browser/direct saves remain ordinary permanent tests. Do not claim a confirmed root cause from one transient result. If it recurs, collect the server exception/trace before proposing a fix. Failed notification-save UI retention is independently covered with a deterministic transport failure.
## Audit precision: workspace overlay and admin route
The current source has a reusable ApplicationWorkspace with override/onClose/full-page-link props, but the production call-site search finds only its normal full-page wrapper. Preserve the reusable contract; do not claim the test named application-workspace-overlay exercises a mounted production quick-preview host. A future overlay needs its own close/dirty/focus tests before adoption.
Non-admin navigation hides admin destinations. A crafted admin URL can render the page shell; the API returns 403 and no user list is exposed. The browser test verifies this established boundary, not a nonexistent frontend route-denial screen.
## Existing debt remains separate
The [Phase 1 risks](../audit/RISKS_AND_MIGRATION.md), [baseline summary](../audit/PHASE1_SUMMARY.md) and [validated queue](../../audits/2026-09-07/validation/implementation-queue.md) remain authoritative for prior findings:
- Stale CV revision overwrites, creation idempotency, calendar/timezone correctness, restore/rollback readiness, billing/deletion and operational failures are not resolved by redesign tests.
- Shared contrast/focus, builder preview scrolling and incomplete loaded accessibility/manual testing remain open. No axe/WCAG certification is added.
- Historical TS2571 was corrected before Phase 2; it does not recur. This checkpoint adds the explicit full src/app standalone type gate without relaxing strictness. Its existing tsconfig scope includes Jest files, not all Playwright/config files.
- Main CI run 743's worker SIGSEGV/retry timeout/closed-browser failures and skipped deployment remain unresolved runner evidence. A local passing suite and feature-branch push do not close this release blocker.
- **B06 implementation complete; F04 remains open.** Actual Traefik peer, nginx/API runtime listeners, firewall/NAT/IPv6 exposure, forwarded client IP, DNS/TLS/redirect and restart behavior still need the [operator handoff](../../audits/2026-09-07/validation/b06-completion.md). No production credentials accessed or live tests run here.
## Intentional gaps by deadline
| Deadline | Evidence still required |
|---|---|
| Before shell/Jobs restructuring | Mobile drawer focus/return, saved view CRUD, bulk partial failures/trash restore, capture/legacy entries; matrix E07/J04/J05/J06/W01 |
| Before CV/package restructuring | Native capture/tailoring success, item-specific override reset/source edit, browser history restore, loaded preview keyboard and template/control combinations; C07–C10/W05/W07 |
| Before career import restructuring | Low-confidence selection/discard, real unusual document extraction and error recovery; C03 |
| Before mail/settings/admin restructuring | Provider sandbox reconnect/delivery; persisted draft conflict/uncertain-send retained; session list UI, audit undo/restore, dashboard toggle ownership; E05/M01–M06/S02/A02 |
| Before public release | Manual assistive technology/touch/zoom/reflow; EN/NB and both themes; provider/DNS/TLS/firewall/backup restoration; original CI runner remediation |
These gaps permit Phase 3 planning, not unguarded production restructuring of the affected capability.
@@ -0,0 +1,223 @@
# Phase 2 preservation matrix
Baseline: **`2d0f131`**, the [57-group Phase 1 ledger](../audit/FUNCTIONALITY_INVENTORY.md). `PRES-E01` is a stable alias of `E01`, and similarly for all rows; original IDs retain their meaning. No capability has been removed, merged or deprecated. Each row includes all sub-capabilities in its linked baseline group, even when its short label abbreviates them.
## Measurement and limits
**57/57 groups (100%) have mapped automated behavioral evidence for at least one core behavior.** This is group-level preservation coverage, not statement coverage, exhaustive sub-workflow coverage, visual parity or 100% readiness. Every row's remaining gaps are explicit. A passing mock/service test does not prove a real provider, actual browser persistence, or production. In particular W07 retains passing service/component evidence but its new native SQLite capture test is an **expected failure**, not successful end-to-end preservation proof. See [baseline defects](BASELINE_DEFECTS.md).
`H` = identity/data loss/irreversible action or a high-risk shared navigation/editing surface; `M` = important reversible presentation/workflow risk. These are redesign regression risks, not security severity labels. “Touch” is expected later-phase impact, not implementation authorization. `F` links are Jest component/helper tests; `B` links are backend tests. Named behaviors describe assertions, not mere file existence. The existing App.test harness is deliberately not counted as route proof.
New browser tests live in [redesign-preservation.spec.ts](../../../job-tracker-ui/e2e/redesign-preservation.spec.ts); original [smoke](../../../job-tracker-ui/e2e/smoke.spec.ts), [account isolation](../../../job-tracker-ui/e2e/account-isolation.spec.ts), and special B01–B06 suites stay unchanged. The [full original test census](../audit/TEST_FILES.md) supplies secondary coverage beyond the representative tests mapped below.
| Preservation ID / capability | Route / entry | Frontend; backend owners | Automated tests and protected behavior | Missing evidence / deliberate gap | Risk | Expected redesign touch |
|---|---|---|---|---|---|---|
| **PRES-E01** — Public product/plans; honest unavailable billing | `/` | LandingPage, planCatalog; AccountPlans | [F:landing-page][F:landing-page.test.tsx], [F:planCatalog][F:planCatalog.test.ts], [F:LandingPage.authRedirect][F:LandingPage.authRedirect.test.tsx]; [B:AccountPlans][B:AccountPlansTests]. Free/Pro copy, unavailable checkout and authenticated redirect | Actual checkout/provider availability; public mobile/keyboard review | M | Yes — public entry |
| **PRES-E02** — Local login/register/remember, challenge, reset and verification | `/login; /register; /forgot-password; /reset-password; /verify-email` | LoginPage, TurnstileWidget, VerifyEmailPage; AuthController | [F:login-page][F:login-page.test.tsx], [F:verify-email-page][F:verify-email-page.test.tsx], [F:turnstile-widget][F:turnstile-widget.test.tsx]; [B:LocalAuthIdentity][B:LocalAuthIdentityTests], [B:AuthAndSystemController][B:AuthAndSystemControllerTests]. Configuration gates, two-factor branch, safe redirect, verification outcomes; smoke login | Real reset-email delivery, Turnstile widget/browser configuration, complete reset submit journey | H | Yes — entry forms |
| **PRES-E03** — Google/Microsoft login, link, legacy relink and popup results | `/login; /microsoft-legacy-relink; provider callbacks` | GoogleAuthCard, MicrosoftAuthCard, MicrosoftLegacyRelinkPage; token validators | [F:login-page][F:login-page.test.tsx], [F:microsoft-legacy-relink-page][F:microsoft-legacy-relink-page.test.tsx]; [B:GoogleTokenValidator][B:GoogleTokenValidatorTests], [B:MicrosoftTokenValidator][B:MicrosoftTokenValidatorTests], [B:LocalAuthIdentity][B:LocalAuthIdentityTests]. Provider failure/relink presentation and token validation | Real consent/popups, revoked grants, provider sandbox; callbacks must retain origin/state | H | Yes — account entry |
| **PRES-E04** — 2FA challenge/recovery/setup/disable; trusted-device revocation | `/profile; /login` | TwoFactorSettingsCard, TwoFactorChallenge; TwoFactorController | [F:two-factor-settings][F:two-factor-settings.test.tsx], [F:login-page][F:login-page.test.tsx]; [B:TwoFactorController][B:TwoFactorControllerTests], [B:SessionsController][B:SessionsControllerTests]. Setup/recovery presentation, disable failure/retry; server trusted-device/session checks | Real mobile authenticator/recovery and trusted-device UI traversal | H | Yes — security forms |
| **PRES-E05** — Session list/current device, revoke one/others, logout | `/profile; shell user menu` | SessionsSettingsCard, AppShell, auth; SessionsController | [F:session-boundary][F:session-boundary.test.ts], [F:account-scoped-state][F:account-scoped-state.test.tsx]; [B:SessionsController][B:SessionsControllerTests], [B:AuthSessionRevocation][B:AuthSessionRevocationTests]. Own active sessions only, revoke ownership and next-request invalidation; B02 browser logout | Session-list card UI selection/refresh after remote revoke | H | Yes — shell/account |
| **PRES-E06** — Avatar, account names/headline, email change and password | `/profile; /confirm-email-change` | ProfilePage; AuthController, AvatarStorage | [F:profile-page][F:profile-page.test.tsx], [F:verify-email-page][F:verify-email-page.test.tsx]; [B:AvatarStorage][B:AvatarStorageTests], [B:LocalAuthIdentity][B:LocalAuthIdentityTests]. Email change password/confirmation contract; safe avatar storage and identity endpoints | Avatar upload/remove and password-change browser journeys | H | Yes — account/profile separation |
| **PRES-E07** — Desktop collapse/mobile drawer, active nav and command search/actions | `Shell; Ctrl/Cmd+K` | AppShell, QuickCommandDialog; job/company search APIs | [F:sidebar-active-nav][F:sidebar-active-nav.test.ts], [F:app-shell-notifications][F:app-shell-notifications.test.tsx], [F:quick-command-preservation][F:quick-command-preservation.test.tsx]; [B:JobApplicationsAuthorization][B:JobApplicationsAuthorizationTests], [B:CompanyRelationship][B:CompanyRelationshipTests]. Active matching and notification entry; NEW job/company destinations, reviewed add and search reset; NEW browser keyboard navigation | Drawer focus return and collapse persistence in EN/NB; all command destinations before shell migration | H | Yes — first restructuring target |
| **PRES-E08** — Theme/system, EN/NB hydration, toast/confirm/prompt and errors | `Shell; preferences; fallback routes` | themePrefs, I18nProvider, confirm, prompt; auth preference | [F:theme-state][F:theme-state.test.tsx], [F:i18n-provider][F:i18n-provider.test.tsx], [F:confirm][F:confirm.test.tsx], [F:prompt][F:prompt.test.tsx], [F:dialog-flow][F:dialog-flow.test.tsx]; [B:LocalAuthIdentity][B:LocalAuthIdentityTests]. Hydrated theme/language, confirmation/cancellation and dialog behavior | Not-found/error-boundary retry route; full locale/theme/state cross-product and OS theme changes | M | Yes — shared foundations |
| **PRES-E09** — Account-switch private-state disposal and stale request rejection | `All private routes; tabs/BFCache/focus` | App, api, sessionBoundary; SessionContextMiddleware | [F:session-boundary][F:session-boundary.test.ts], [F:account-scoped-state][F:account-scoped-state.test.tsx]; [B:SessionContextMiddleware][B:SessionContextMiddlewareTests], [B:CurrentUserIdLiveEvaluation][B:CurrentUserIdLiveEvaluationTests], [B:OwnershipGuard][B:OwnershipGuardTests]. B02 stale mutation/read abort, account-scoped state; four real-API/two-tab account-isolation browser cases | Real BFCache variations and long offline retry; preserve server precondition even with old bundles | H | Yes — all shared state |
| **PRES-J01** — NAV discovery/filter/sort/source/deadline/details | `/discover` | JobDiscoveryPage; JobDiscoveryController | [F:job-discovery][F:job-discovery.test.tsx]; [B:JobDiscoveryController][B:JobDiscoveryControllerTests], [B:JobOpportunitySync][B:JobOpportunitySyncTests]. Search/sort/source details, empty versus failure/retry; responsive discovery browser | Live NAV freshness/API availability; long result keyboard traversal | M | Yes — jobs entry |
| **PRES-J02** — Saved searches, run/delete/alerts, result changes/dismissal | `/discover` | JobDiscoveryPage; saved-search/alert services | [F:job-discovery][F:job-discovery.test.tsx]; [B:SavedJobSearchAlert][B:SavedJobSearchAlertTests], [B:JobDiscoveryController][B:JobDiscoveryControllerTests]. Saved search run, pause and dismiss controls; owner-scoped alert results | Scheduler-to-notification browser journey, delete confirmation on mobile | M | Yes — discovery workspace |
| **PRES-J03** — Reviewed manual/URL/paste job create, company/tags/dates, duplicates | `Shell AddJobModal` | AddJobModal; JobImportService, JobApplicationsController | [F:quick-capture][F:quick-capture.test.tsx]; [B:JobImportService][B:JobImportServiceTests], [B:JobImportHttpTransport][B:JobImportHttpTransportTests], [B:JobApplicationsController][B:JobApplicationsControllerTests]. Preview before write, URL prefills, manual browser create; B01 real connection boundary | Duplicate warning and company/date edge UI; creation-intent idempotency remains separate unresolved contract | H | Yes — capture/navigation |
| **PRES-J04** — Extension/bookmarklet/share capture; post-create attachment/CV | `?add= / ?addtext=; QuickCaptureCard; browser-extension` | App, captureUrl, QuickCaptureCard; job/CV APIs | [F:captureUrl][F:captureUrl.test.ts], [F:quick-capture][F:quick-capture.test.tsx], [F:quick-command-preservation][F:quick-command-preservation.test.tsx]; [B:JobImportService][B:JobImportServiceTests], [B:CvBuilder][B:CvBuilderTests]. Capture URL encoding/review/no auto-create, post-create CV; extension node tests; NEW keyboard add opens review only | Installed extension permissions/browser-share flow and optional attachment failure recovery | H | Yes — entry contracts |
| **PRES-J05** — Job filters/sort/page/columns, saved views and row/mobile actions | `/jobs` | JobTable, SavedViewsMenu; JobApplicationsController | [F:application-workspace-overlay][F:application-workspace-overlay.test.tsx], [F:account-scoped-state][F:account-scoped-state.test.tsx], [F:daily-control-loop][F:daily-control-loop.test.tsx]; [B:JobApplicationsController][B:JobApplicationsControllerTests], [B:JobApplicationsEndpointBehavior][B:JobApplicationsEndpointBehaviorTests]. URL hydration, selection does not open row, list return/focus; browser dirty/full workspace | Saved-view CRUD browser journey, page-size changes with dirty nested state | H | Yes — jobs view consolidation |
| **PRES-J06** — Bulk status/delete, soft delete/trash/restore and job edit | `/jobs; /trash; EditJobDialog` | JobTable, EditJobDialog; job lifecycle/controller | [F:application-workspace-overlay][F:application-workspace-overlay.test.tsx], [F:confirm-flow][F:confirm-flow.test.tsx]; [B:JobApplicationsController][B:JobApplicationsControllerTests], [B:JobApplicationsAuthorization][B:JobApplicationsAuthorizationTests], [B:JobLifecycleEvent][B:JobLifecycleEventTests]. Selection and destructive confirmation primitive; server CRUD/ownership/lifecycle behavior | Bulk partial failure and trash→restore UI journey; shared confirm tests alone do not prove bulk flow | H | Yes — table/actions |
| **PRES-J07** — Kanban grouped statuses, pointer/keyboard move and precise menu | `/kanban` | KanbanBoard, pipeline; status endpoint | [F:kanban-grouped-board][F:kanban-grouped-board.test.tsx], [F:pipeline][F:pipeline.test.ts]; [B:JobPipeline][B:JobPipelineTests]. Keyboard pickup/drop, failed move keeps card, custom group and precise stages; dark browser test | Touch drag, keyboard cancel/focus after responsive view switching | H | Yes — table/board unified entry |
| **PRES-J08** — Company/contact edits, pipeline dates and linked-job history | `/companies; ?edit=id` | CompaniesTable; company relationship APIs | [F:companies-relationship][F:companies-relationship.test.tsx], [F:quick-command-preservation][F:quick-command-preservation.test.tsx]; [B:CompanyRelationship][B:CompanyRelationshipTests]. Job history/context and NEW command company destination | All contact/date edits persisted through browser; source URL safety | M | Yes — directory/navigation |
| **PRES-J09** — Recorded offers/compensation/deadlines/notes and source links | `/offers` | OfferComparisonPage; job API | [F:offer-comparison][F:offer-comparison.test.tsx]; [B:AnalyticsSalary][B:AnalyticsSalaryTests], [B:JobApplicationsEndpointBehavior][B:JobApplicationsEndpointBehaviorTests]. Recorded values/empty states and salary normalization | Mobile many-offer comparison; localized currency/date visual review | M | Yes — jobs/analytics placement |
| **PRES-J10** — CSV/JSON export, JSON import and CSV-to-JSON conversion | `/settings; ImportExportJobs` | ImportExportJobs; CSV/job controller | [F:csv-export][F:csv-export.test.tsx]; [B:CsvExport][B:CsvExportTests], [B:JobApplicationsController][B:JobApplicationsControllerTests]. B05 formula/quotes/UTF-8/typed dates and numeric behavior; CSV browser byte download | Full browser JSON import/count/roundtrip; Excel/LibreOffice compatibility remains separately bounded | H | Yes — settings/download placement |
| **PRES-W01** — Full workspace, sections, legacy links and optional reusable overlay | `/jobs/:id; /applications/:id; section/followMode` | ApplicationWorkspacePage, jobWorkspaceRoute; workspace API | [F:application-workspace-overlay][F:application-workspace-overlay.test.tsx], [F:applicationWorkspace][F:applicationWorkspace.test.ts], [F:application-route-contracts][F:application-route-contracts.test.ts]; [B:ApplicationWorkspace][B:ApplicationWorkspaceTests]. Deep links, dirty cancel/discard, missing job and back/filter/focus; NEW actual App command/legacy entry | No active production caller of override/onClose overlay props found; full overlay parity needs a host fixture before adoption | H | Yes — primary workflow |
| **PRES-W02** — Detail/status/progression, recruiter/dates/description/timeline | `/jobs/:id overview` | WorkspaceHeader, JobDetailsSection, ApplicationProgress/Timeline; workflow APIs | [F:application-intelligence][F:application-intelligence.test.tsx], [F:workflow-trust-signals][F:workflow-trust-signals.test.tsx], [F:daily-control-loop][F:daily-control-loop.test.tsx]; [B:JobApplicationsWorkflowSignals][B:JobApplicationsWorkflowSignalsTests], [B:JobApplicationsAppliedDateHistory][B:JobApplicationsAppliedDateHistoryTests]. Timeline source labels, next-action routing and applied-date history | Edit/status→timeline real-browser sequence across locale/timezone | H | Yes — workspace |
| **PRES-W03** — Analysis, match/gaps, strategy and reviewed status/workflow suggestions | `/jobs/:id?section=analysis` | ApplicationAnalysis/Match/StrategySnapshot/StatusSuggestion; intelligence/AI services | [F:application-intelligence][F:application-intelligence.test.tsx], [F:application-workflow-assist][F:application-workflow-assist.test.tsx]; [B:ApplicationIntelligence][B:ApplicationIntelligenceTests], [B:StrategySnapshotOperation][B:StrategySnapshotOperationTests], [B:JobApplicationsEndpointBehavior][B:JobApplicationsEndpointBehaviorTests]. Loading/error/empty states, match and explicit suggested action behavior | Actual model truthfulness/evaluation; source freshness and cancelled generation browser states | H | Yes — contextual AI |
| **PRES-W04** — Checklist create/edit/complete/delete/order and next action | `Job overview/checklist entry` | ApplicationChecklist; checklist service/controller | [F:application-checklist][F:application-checklist.test.tsx]; [B:ApplicationChecklist][B:ApplicationChecklistTests]. Persisted item actions and keyboard reorder; localized readiness | Real-API mobile edit/delete and interruption recovery | H | Yes — workspace detail |
| **PRES-W05** — Attach/select/create/tailor CV and source context | `Job CV section; builder deep link` | ApplicationCvSection/TailoringSection; ApplicationAssetsService | [F:application-assets][F:application-assets.test.tsx], [F:cv-builder-deep-link][F:cv-builder-deep-link.test.tsx]; [B:ApplicationAssets][B:ApplicationAssetsTests], [B:CvBuilder][B:CvBuilderTests]. Attach re-points only, detach keeps variant, foreign owner denied, tailoring never writes source; B02 API browser | P2-D01 SQLite tailoring ordering blocks successful real-browser guidance; actual attach→render relationship beyond existing service coverage | H | Yes — document relationships |
| **PRES-W06** — Cover-letter manual/AI review, versions/restore and dirty protection | `Job cover-letter section` | ApplicationCoverLetterSection, CoverLetterAiAssistant; assets/history | [F:application-assets][F:application-assets.test.tsx], [F:application-workspace-overlay][F:application-workspace-overlay.test.tsx]; [B:ApplicationAssets][B:ApplicationAssetsTests]. Manual save/discard/restore, linked-CV AI explicit apply then save, version history owner scope | Whole-browser AI reject/provider success and cover-letter history persistence; no actual model called here | H | Yes — workspace/content editing |
| **PRES-W07** — Application drafts and immutable submitted package/archived binaries | `Job cover-letter drafts; CV section package history` | ApplicationPackageDraftsSection, SubmittedPackagesSection; package service/storage | [F:application-assets][F:application-assets.test.tsx], [F:application-drafts][F:application-drafts.test.ts]; [B:SubmittedApplicationPackage][B:SubmittedApplicationPackageTests], [B:JobApplicationsApplicationPackage][B:JobApplicationsApplicationPackageTests], [B:JobApplicationsEndpointBehavior][B:JobApplicationsEndpointBehaviorTests]. Drafts keep ordinary notes; service frozen document/hash/copy-failure/tenant assertions; NEW native browser capture oracle | P2-D01 expected failure: SQLite capture 500. NEW post-capture mutation/archive assertions are NOT reached. Require real storage/provider success before package restructuring | H | Yes — documents/submission |
| **PRES-W08** — Attachments upload/category/name/AI consent/preview/download/delete | `Job overview Documents accordion; package archive` | Attachments; AttachmentsController, AttachmentStorage | [F:attachments][F:attachments.test.tsx]; [B:AttachmentsController][B:AttachmentsControllerTests], [B:AttachmentConsistency][B:AttachmentConsistencyTests], [B:AttachmentFlagsRecompute][B:AttachmentFlagsRecomputeTests]. Empty/flag controls, storage consistency and owned download; NEW browser fixture uploads real UTF-8 binary | Archived bytes after live delete blocked by W07; drag/drop, rename and download browser journey | H | Yes — document surfaces |
| **PRES-W09** — Interview questions/answers/ready/manual removal/AI review | `Job interview section` | InterviewPrep; interview persistence/AI endpoints | [F:interview-prep][F:interview-prep.test.tsx]; [B:InterviewPrepPersistence][B:InterviewPrepPersistenceTests], [B:InterviewAiContext][B:InterviewAiContextTests], [B:InterviewPrep][B:InterviewPrepTests]. Explicit AI add, manual/answer readiness, relevant prompt context without invented defaults | Populated mobile keyboard editing and real reload; actual model quality separate | H | Yes — workspace AI |
| **PRES-W10** — Debrief/follow-up draft/date/readiness | `Job interview section; follow-up entry` | InterviewPrep; follow-up draft services | [F:interview-prep][F:interview-prep.test.tsx]; [B:JobApplicationsFollowUpDraft][B:JobApplicationsFollowUpDraftTests], [B:InterviewPrep][B:InterviewPrepTests]. Neutral debrief template and follow-up content handling | Timezone/reload browser chain; calendar defects remain outside Phase 2 | M | Yes — workspace workflow |
| **PRES-W11** — Reminder rules/actions/notifications, ICS and Google/Outlook events | `/reminders; job calendar actions; /settings` | RemindersView, RulesSettingsCard, ApplicationCalendarActions; calendar/rules APIs | [F:daily-control-loop][F:daily-control-loop.test.tsx], [F:application-calendar][F:application-calendar.test.tsx], [F:settings-view][F:settings-view.test.tsx]; [B:ApplicationCalendarController][B:ApplicationCalendarControllerTests], [B:RulesEngineProspect][B:RulesEngineProspectTests], [B:NotificationSettingsController][B:NotificationSettingsControllerTests]. Provider write confirmation, ICS and permission; NEW per-scenario rule payload and real saved preference checks | DST/date-only correctness and real provider timezone must be resolved separately; initial rules 500 not reproduced | H | Yes — scheduling/navigation |
| **PRES-W12** — Async progress/failure/retry/cancel/notifications | `/operations; profile processing` | OperationsPage, CareerProfilePage; operation store/worker | [F:operations-page][F:operations-page.test.tsx], [F:profile-page][F:profile-page.test.tsx]; [B:OperationsController][B:OperationsControllerTests], [B:UserOperationStore][B:UserOperationStoreTests], [B:CvProcessingOperation][B:CvProcessingOperationTests]. Failure retry/cancel, durable operation IDs; extraction polling keeps dirty fields | Long-lived disconnect/reconnect and worker fault/recovery operational gaps | H | Yes — notifications/import |
| **PRES-C01** — Canonical structured career data and long-tail/custom sections | `/career?section=profile` | CareerProfilePage, career views; CareerProfileService/controller | [F:profile-page][F:profile-page.test.tsx]; [B:CareerProfileController][B:CareerProfileControllerTests], [B:CareerProfileService][B:CareerProfileServiceTests]. Structured field changes propagate to saved singleton, stable child IDs and relational roundtrip; NEW CV edit leaves master unchanged | Every long-tail section/mobile keyboard save; concurrent canonical saves outside scope | H | Yes — career workflow |
| **PRES-C02** — Profile overview/completeness/import guidance/raw copy/history/restore | `/career` | CareerWorkspacePage, CareerProfilePage; version service | [F:career-workspace-page][F:career-workspace-page.test.tsx], [F:profile-page][F:profile-page.test.tsx]; [B:CareerProfileService][B:CareerProfileServiceTests]. First/returning state, completeness/errors and non-destructive history restore | Clipboard/avatar and full browser historical restore; real extraction quality separate | M | Yes — career entry |
| **PRES-C03** — Upload/extract/rebuild/reprocess/diff/select/accept/discard/retry/cancel | `Career import/review` | CareerProfilePage; profile-CV/processing/diff services | [F:profile-page][F:profile-page.test.tsx]; [B:ProfileCvController][B:ProfileCvControllerTests], [B:CvProfileDiffService][B:CvProfileDiffServiceTests], [B:CvProcessingOperation][B:CvProcessingOperationTests], [B:CvExtractionCoverage][B:CvExtractionCoverageTests]. Queue/upload, operation retry, pending explicit apply and polling retains edits; extraction/diff service checks | Manual low-confidence deselection/discard UI; OCR/scanned/unusual real-document quality | H | Yes — career import |
| **PRES-C04** — Evidence bank source/category/tags/add/list/delete | `Career overview/evidence` | CareerEvidenceBank; CareerEvidenceController | [F:career-evidence][F:career-evidence.test.tsx]; [B:CareerEvidenceController][B:CareerEvidenceControllerTests]. Sourced evidence creation, tags and owner boundaries | Delete and many-item search/keyboard browser flow | M | Yes — canonical evidence |
| **PRES-C05** — CV library create/copy/delete/templates/share/publication/PDF | `/career/builder` | CvBuilderPage, thumbnails; CvVariantService/PublicCvController | [F:cv-builder-page][F:cv-builder-page.test.tsx], [F:cv-template-thumbnail][F:cv-template-thumbnail.test.tsx]; [B:CvBuilder][B:CvBuilderTests], [B:PublicCvController][B:PublicCvControllerTests]. General/job-specific creation, delete cancel, duplication fresh slug, public revoke/republish; existing public browser PDF | Complete library→publish/unpublish/revoke old link browser chain | H | Yes — CV workspace |
| **PRES-C06** — Studio tabs, preview debounce/pages/fit/save state/mobile panes | `/career/builder/:id` | CvBuilderEditor, CvPreviewPane; preview API | [F:cv-builder-deep-link][F:cv-builder-deep-link.test.tsx]; [B:CvBuilder][B:CvBuilderTests]. Five tabs, deep-link/error/save state and preview retry; NEW real mobile Edit/Preview, reload and desktop split | Keyboard preview scroll/focus (known F15), all theme/page interactions | H | Yes — representative editor |
| **PRES-C07** — Visibility/order/custom sections and item/document overrides | `Builder Content` | CvBuilderEditor, CV controls/useDragReorder; resolver/settings | [F:cv-builder-deep-link][F:cv-builder-deep-link.test.tsx], [F:cvBuilder][F:cvBuilder.test.ts]; [B:CvBuilder][B:CvBuilderTests]. Add/edit/delete/order/hide, override renders without master change; NEW persisted headline and sibling isolation | Native drag/touch and override reset-to-source browser flow; item-level real saved overrides beyond headline | H | Yes — editing controls |
| **PRES-C08** — Capability-aware templates/typography/color/header/skills/sidebar/photo | `Builder Template/Customize` | CvCustomizeTab, registry; ThemedCvRenderer | [F:cv-builder-deep-link][F:cv-builder-deep-link.test.tsx], [F:cvBuilder][F:cvBuilder.test.ts], [F:cv-template-thumbnail][F:cv-template-thumbnail.test.tsx]; [B:CvBuilder][B:CvBuilderTests]. Template change clears incompatible defaults; normalized overrides reach shared renderer, safe color/markdown | Complete template×advanced control visual/render parity; inaccessible color controls baseline | M | Yes — component foundation |
| **PRES-C09** — A4/Letter/density/margins/dates/document locale/section presentation | `Builder Customize/Content` | cvBuilder, CvCustomizeTab; settings normalization/render model | [F:cvBuilder][F:cvBuilder.test.ts], [F:cv-builder-deep-link][F:cv-builder-deep-link.test.tsx]; [B:CvBuilder][B:CvBuilderTests]. Page sizes, NB controls/default labels/dates, section grid/columns; existing long searchable PDF browser | All advanced combinations, print margins and long localized text manual review | M | Yes — controls must remain |
| **PRES-C10** — Undo/redo, save queue, dirty guard, history restore/preview lifecycle | `Builder editor/history` | CvBuilderEditor; CvVariantService version history | [F:cv-builder-deep-link][F:cv-builder-deep-link.test.tsx]; [B:CvBuilder][B:CvBuilderTests]. Undo/redo, failed save retry, guard cancel, restore-as-new-version; NEW saved override reload | Multi-browser stale-CV revision remains separate defect; history-tab browser restore and offline navigation | H | Yes — do not alter persistence contract |
| **PRES-C11** — Section and whole-document AI improve/translate/instruction/grammar review | `Builder AI Tools/content assistant` | AiSectionAssistant, DocumentAiTab; CV AI assist | [F:ai-section-assistant][F:ai-section-assistant.test.tsx], [F:cv-builder-deep-link][F:cv-builder-deep-link.test.tsx]; [B:AiWorkspace][B:AiWorkspaceTests], [B:AiPrivacyPolicy][B:AiPrivacyPolicyTests], [B:ProEntitlementAuthorization][B:ProEntitlementAuthorizationTests]. NEW reject/close, edited acceptance, failure retry, whole-document apply/reject and malformed/missing/duplicate IDs rejected; no implicit PUT | Real model factual validation and section→whole review interactions; Free-disabled controls proven in new browser | H | Yes — AI interaction pattern |
| **PRES-C12** — Private PDF and published anonymous CV/PDF, long searchable pages | `Builder export; /cv/:slug` | PublicCvPage, cvBuilder; HtmlPdfGenerator/shared admission | [F:public-cv-page][F:public-cv-page.test.tsx]; [B:PublicCvController][B:PublicCvControllerTests], [B:PdfRenderAdmission][B:PdfRenderAdmissionTests], [B:CvBuilder][B:CvBuilderTests]. Anonymous public rendering versus private refusal; B03 shared capacity/failure; real multipage/public PDF browser | All templates/print engines; deployed Chromium/font behavior and PDF manual inspection | H | Yes — export/preview links |
| **PRES-M01** — Job-scoped manual correspondence/search/delete/paste import | `Workspace communication accordion` | Correspondence; CorrespondenceController | [F:end-to-end-trust-loop][F:end-to-end-trust-loop.test.tsx], [F:correspondence-search-context][F:correspondence-search-context.test.ts]; [B:CorrespondenceController][B:CorrespondenceControllerTests]. Canonical communication entry does not send mail, job context and CRUD ownership | Manual note edit/delete/paste-import real browser; preserve send/log distinction | H | Yes — workspace/correspondence |
| **PRES-M02** — Gmail candidate/thread import/refresh/sync/unlink/relink/move | `Job communication Gmail tools` | Correspondence; GmailController/provider | [F:correspondence-gmail-import][F:correspondence-gmail-import.test.tsx]; [B:GmailController][B:GmailControllerTests], [B:EmailOwnershipIntegration][B:EmailOwnershipIntegrationTests]. Candidates, selected thread import/move/refresh and diagnostics; owner-bound relationships | Real provider/sandbox threads, multi-account reconnect and sync failure | H | Yes — relationship controls |
| **PRES-M03** — Global inbox filters/page/provider/link/direction/detail/source links | `/correspondence` | CorrespondenceInboxPage; EmailController | [F:correspondence-inbox-page][F:correspondence-inbox-page.test.tsx]; [B:EmailController][B:EmailControllerTests], [B:EmailOwnershipIntegration][B:EmailOwnershipIntegrationTests]. Filter/pagination/detail/provider read failure distinct from empty | Real large mailbox and attachment binary browser flows | H | Yes — inbox/navigation |
| **PRES-M04** — Compose/reply/provider/recipient/job; draft conflicts; confirm send/uncertainty | `/correspondence drafts/compose` | CorrespondenceInboxPage; EmailDrafts/Send/attempt store | [F:correspondence-inbox-page][F:correspondence-inbox-page.test.tsx]; [B:EmailDraftsController][B:EmailDraftsControllerTests], [B:EmailDraftPersistence][B:EmailDraftPersistenceTests], [B:EmailSendController][B:EmailSendControllerTests], [B:EmailSendAttemptStore][B:EmailSendAttemptStoreTests], [B:EmailProviderDelivery][B:EmailProviderDeliveryTests]. Revision conflicts, explicit send confirm, uncertain state has no automatic retry, deliberate new attempt | Actual sandbox mail delivery and browser reconnect; do not loosen idempotency/conflict guards | H | Yes — high-risk drafts/navigation |
| **PRES-M05** — Review proposed jobs/link/reject/keep/notes/sync/create | `/correspondence?view=review; /correspondence/review` | GmailReviewPage, CorrespondenceInboxPage; Gmail review | [F:gmail-review-page][F:gmail-review-page.test.tsx], [F:correspondence-inbox-page][F:correspondence-inbox-page.test.tsx]; [B:GmailController][B:GmailControllerTests]. Review decisions/localization and inbox review entry | Real App legacy alias and suggested-job create browser workflow | H | Yes — merged review entry |
| **PRES-M06** — Gmail/Graph/IMAP connect/status/disconnect separate from sign-in | `/settings/connected-accounts` | EmailProviderConnections; Google/Graph/IMAP controllers | [F:email-provider-connections][F:email-provider-connections.test.tsx]; [B:MicrosoftGraphController][B:MicrosoftGraphControllerTests], [B:ImapController][B:ImapControllerTests], [B:ImapServiceSsrfGuard][B:ImapServiceSsrfGuardTests], [B:GmailController][B:GmailControllerTests]. Connection state/NB/IMAP form and provider ownership; outbound IMAP safety | Actual provider authorization/revocation/token expiry; no credentials contacted in new tests | H | Yes — settings/account |
| **PRES-S01** — Personal metrics/activity/funnel/company/skills/stage/salary/next actions | `/dashboard` | DashboardView; analytics/controllers | [F:daily-control-loop][F:daily-control-loop.test.tsx], [F:workflow-trust-signals][F:workflow-trust-signals.test.tsx]; [B:StageAnalytics][B:StageAnalyticsTests], [B:AnalyticsSalary][B:AnalyticsSalaryTests]. Empty dashboard prioritizes onboarding, attention deep links; server analytics facts | Chart transformation fidelity and full populated/timezone browser dashboard | M | Yes — dashboard/analytics |
| **PRES-S02** — Owner-local five visibility preferences/onboarding dismiss/notifications | `/dashboard; notification bell` | DashboardView, OnboardingChecklist, NotificationsPopover; notification APIs | [F:onboarding-checklist][F:onboarding-checklist.test.tsx], [F:notifications-popover][F:notifications-popover.test.tsx], [F:app-shell-notifications][F:app-shell-notifications.test.tsx], [F:account-scoped-state][F:account-scoped-state.test.tsx]; [B:JobLifecycleEvent][B:JobLifecycleEventTests]. Onboarding actions; unread/read/dismiss UI and owner-scoped storage primitive | Five dashboard toggles/reload per account not fully tested at mounted dashboard; ordering/resizing is new work | M | Yes — preserve current preferences |
| **PRES-S03** — Rules/reminder/table/theme/language preferences and account links | `/settings tabs/mobile selector` | SettingsView, RulesSettingsCard, theme/i18n; rules/notification/auth | [F:settings-view][F:settings-view.test.tsx], [F:theme-state][F:theme-state.test.tsx], [F:i18n-provider][F:i18n-provider.test.tsx]; [B:NotificationSettingsController][B:NotificationSettingsControllerTests], [B:RulesEngineProspect][B:RulesEngineProspectTests]. NEW failed notification retry, rule scenario preservation; NEW mobile→desktop edit + native notification/rule persistence | Initial single rules 500 unconfirmed; unsaved rule drafts are not persisted across tab unmount; table preference/router identity change gap | H | Yes — shared preferences/navigation |
| **PRES-S04** — AI privacy/usage/limits/Free-Pro/server entitlement/conditional billing | `/settings; locked AI controls; plans` | AiPrivacySettingsCard, AiUsageCard, AccountPlanProvider; billing/entitlement | [F:settings-view][F:settings-view.test.tsx], [F:ai-usage-card][F:ai-usage-card.test.tsx], [F:pro-feature-notice][F:pro-feature-notice.test.tsx], [F:planCatalog][F:planCatalog.test.ts]; [B:AiPrivacyPolicy][B:AiPrivacyPolicyTests], [B:AiUsageMeter][B:AiUsageMeterTests], [B:ProEntitlementAuthorization][B:ProEntitlementAuthorizationTests], [B:BillingController][B:BillingControllerTests]. Local-only AI setting, limits/locks, billing readiness; NEW real Free CV AI denied UI and API | Real checkout/webhooks/portal; billing/account deletion issues retain independent queue | H | Yes — settings/AI states |
| **PRES-S05** — Account ZIP/encrypted backup/lifecycle/deletion confirmations | `Settings Backup tab` | BackupCard; export/backup/deletion services | [F:account-data-export][F:account-data-export.test.tsx]; [B:AccountDataExport][B:AccountDataExportTests], [B:BackupController][B:BackupControllerTests], [B:AccountDeletion][B:AccountDeletionTests], [B:DatabaseBackupRunner][B:DatabaseBackupRunnerTests]. ZIP protected download, recent auth, server enable flag and exact deletion phrase; service deletion/backup | Real restored account/database and paid deletion/provider cancellation operational proof; not safe to run against production | H | Yes — account lifecycle |
| **PRES-A01** — Admin user/create/roles/reset/delete/last-admin/self guards | `/admin/users` | AdminUsersPage; UsersController | [F:admin-users-page][F:admin-users-page.test.tsx]; [B:UsersController][B:UsersControllerTests], [B:ApiAuthorizationPosture][B:ApiAuthorizationPostureTests]. Demotion/delete confirm, last-admin server checks; NEW non-admin crafted create/read denied | Role revocation during mounted admin view and actual reset delivery; route can render empty shell without granting privilege | H | Yes — separate admin area |
| **PRES-A02** — Audit list/detail/supported undo/job restore | `/admin/audit` | AdminAuditPage; AdminAuditController | [F:admin-audit-page][F:admin-audit-page.test.tsx]; [B:AuthAndSystemController][B:AuthAndSystemControllerTests], [B:ApiAuthorizationPosture][B:ApiAuthorizationPostureTests]. Unavailable-versus-empty retry and NB stored event text; server admin-only posture | Successful undo and job-restore UI against persisted API; role posture test is not undo behavior proof | H | Yes — admin navigation |
| **PRES-A03** — System/build/DB/AI/mail/SMTP/test/probe/benchmark/maintenance | `/admin/system` | AdminSystemPage; AdminSystemController/maintenance/benchmark | [F:admin-system-page][F:admin-system-page.test.tsx]; [B:AuthAndSystemController][B:AuthAndSystemControllerTests], [B:MaintenanceJobDispatcher][B:MaintenanceJobDispatcherTests], [B:CvCorpusHarness][B:CvCorpusHarnessTests]. Health/error/NB, SMTP submit and benchmark controls; dispatcher authorization and modes | Production SMTP/model/corpus/maintenance and sensitive-data presentation; no live jobs launched | H | Yes — admin hierarchy |
## Before changing a mapped surface
Run its mapped tests and the shared identity/route regressions. Add the missing behavioral fixture needed by the proposed change before editing that workflow. A gap can remain intentional for Phase 3 documentation; it cannot silently become permission to restructure an unprotected workflow. Keep [traceability](TRACEABILITY.md) updated per preservation ID and require real EN/NB, light/dark, desktop/mobile and keyboard evidence for each later migration.
[F:landing-page.test.tsx]: ../../../job-tracker-ui/src/landing-page.test.tsx
[F:planCatalog.test.ts]: ../../../job-tracker-ui/src/planCatalog.test.ts
[F:LandingPage.authRedirect.test.tsx]: ../../../job-tracker-ui/src/LandingPage.authRedirect.test.tsx
[B:AccountPlansTests]: ../../../JobTrackerApi.Tests/AccountPlansTests.cs
[F:login-page.test.tsx]: ../../../job-tracker-ui/src/login-page.test.tsx
[F:verify-email-page.test.tsx]: ../../../job-tracker-ui/src/verify-email-page.test.tsx
[F:turnstile-widget.test.tsx]: ../../../job-tracker-ui/src/turnstile-widget.test.tsx
[B:LocalAuthIdentityTests]: ../../../JobTrackerApi.Tests/LocalAuthIdentityTests.cs
[B:AuthAndSystemControllerTests]: ../../../JobTrackerApi.Tests/AuthAndSystemControllerTests.cs
[F:microsoft-legacy-relink-page.test.tsx]: ../../../job-tracker-ui/src/microsoft-legacy-relink-page.test.tsx
[B:GoogleTokenValidatorTests]: ../../../JobTrackerApi.Tests/GoogleTokenValidatorTests.cs
[B:MicrosoftTokenValidatorTests]: ../../../JobTrackerApi.Tests/MicrosoftTokenValidatorTests.cs
[F:two-factor-settings.test.tsx]: ../../../job-tracker-ui/src/two-factor-settings.test.tsx
[B:TwoFactorControllerTests]: ../../../JobTrackerApi.Tests/TwoFactorControllerTests.cs
[B:SessionsControllerTests]: ../../../JobTrackerApi.Tests/SessionsControllerTests.cs
[F:session-boundary.test.ts]: ../../../job-tracker-ui/src/session-boundary.test.ts
[F:account-scoped-state.test.tsx]: ../../../job-tracker-ui/src/account-scoped-state.test.tsx
[B:AuthSessionRevocationTests]: ../../../JobTrackerApi.Tests/AuthSessionRevocationTests.cs
[F:profile-page.test.tsx]: ../../../job-tracker-ui/src/profile-page.test.tsx
[B:AvatarStorageTests]: ../../../JobTrackerApi.Tests/AvatarStorageTests.cs
[F:sidebar-active-nav.test.ts]: ../../../job-tracker-ui/src/sidebar-active-nav.test.ts
[F:app-shell-notifications.test.tsx]: ../../../job-tracker-ui/src/app-shell-notifications.test.tsx
[F:quick-command-preservation.test.tsx]: ../../../job-tracker-ui/src/quick-command-preservation.test.tsx
[B:JobApplicationsAuthorizationTests]: ../../../JobTrackerApi.Tests/JobApplicationsAuthorizationTests.cs
[B:CompanyRelationshipTests]: ../../../JobTrackerApi.Tests/CompanyRelationshipTests.cs
[F:theme-state.test.tsx]: ../../../job-tracker-ui/src/theme-state.test.tsx
[F:i18n-provider.test.tsx]: ../../../job-tracker-ui/src/i18n-provider.test.tsx
[F:confirm.test.tsx]: ../../../job-tracker-ui/src/confirm.test.tsx
[F:prompt.test.tsx]: ../../../job-tracker-ui/src/prompt.test.tsx
[F:dialog-flow.test.tsx]: ../../../job-tracker-ui/src/dialog-flow.test.tsx
[B:SessionContextMiddlewareTests]: ../../../JobTrackerApi.Tests/SessionContextMiddlewareTests.cs
[B:CurrentUserIdLiveEvaluationTests]: ../../../JobTrackerApi.Tests/CurrentUserIdLiveEvaluationTests.cs
[B:OwnershipGuardTests]: ../../../JobTrackerApi.Tests/OwnershipGuardTests.cs
[F:job-discovery.test.tsx]: ../../../job-tracker-ui/src/job-discovery.test.tsx
[B:JobDiscoveryControllerTests]: ../../../JobTrackerApi.Tests/JobDiscoveryControllerTests.cs
[B:JobOpportunitySyncTests]: ../../../JobTrackerApi.Tests/JobOpportunitySyncTests.cs
[B:SavedJobSearchAlertTests]: ../../../JobTrackerApi.Tests/SavedJobSearchAlertTests.cs
[F:quick-capture.test.tsx]: ../../../job-tracker-ui/src/quick-capture.test.tsx
[B:JobImportServiceTests]: ../../../JobTrackerApi.Tests/JobImportServiceTests.cs
[B:JobImportHttpTransportTests]: ../../../JobTrackerApi.Tests/JobImportHttpTransportTests.cs
[B:JobApplicationsControllerTests]: ../../../JobTrackerApi.Tests/JobApplicationsControllerTests.cs
[F:captureUrl.test.ts]: ../../../job-tracker-ui/src/captureUrl.test.ts
[B:CvBuilderTests]: ../../../JobTrackerApi.Tests/CvBuilderTests.cs
[F:application-workspace-overlay.test.tsx]: ../../../job-tracker-ui/src/application-workspace-overlay.test.tsx
[F:daily-control-loop.test.tsx]: ../../../job-tracker-ui/src/daily-control-loop.test.tsx
[B:JobApplicationsEndpointBehaviorTests]: ../../../JobTrackerApi.Tests/JobApplicationsEndpointBehaviorTests.cs
[F:confirm-flow.test.tsx]: ../../../job-tracker-ui/src/confirm-flow.test.tsx
[B:JobLifecycleEventTests]: ../../../JobTrackerApi.Tests/JobLifecycleEventTests.cs
[F:kanban-grouped-board.test.tsx]: ../../../job-tracker-ui/src/kanban-grouped-board.test.tsx
[F:pipeline.test.ts]: ../../../job-tracker-ui/src/pipeline.test.ts
[B:JobPipelineTests]: ../../../JobTrackerApi.Tests/JobPipelineTests.cs
[F:companies-relationship.test.tsx]: ../../../job-tracker-ui/src/companies-relationship.test.tsx
[F:offer-comparison.test.tsx]: ../../../job-tracker-ui/src/offer-comparison.test.tsx
[B:AnalyticsSalaryTests]: ../../../JobTrackerApi.Tests/AnalyticsSalaryTests.cs
[F:csv-export.test.tsx]: ../../../job-tracker-ui/src/csv-export.test.tsx
[B:CsvExportTests]: ../../../JobTrackerApi.Tests/CsvExportTests.cs
[F:applicationWorkspace.test.ts]: ../../../job-tracker-ui/src/applicationWorkspace.test.ts
[F:application-route-contracts.test.ts]: ../../../job-tracker-ui/src/application-route-contracts.test.ts
[B:ApplicationWorkspaceTests]: ../../../JobTrackerApi.Tests/ApplicationWorkspaceTests.cs
[F:application-intelligence.test.tsx]: ../../../job-tracker-ui/src/application-intelligence.test.tsx
[F:workflow-trust-signals.test.tsx]: ../../../job-tracker-ui/src/workflow-trust-signals.test.tsx
[B:JobApplicationsWorkflowSignalsTests]: ../../../JobTrackerApi.Tests/JobApplicationsWorkflowSignalsTests.cs
[B:JobApplicationsAppliedDateHistoryTests]: ../../../JobTrackerApi.Tests/JobApplicationsAppliedDateHistoryTests.cs
[F:application-workflow-assist.test.tsx]: ../../../job-tracker-ui/src/application-workflow-assist.test.tsx
[B:ApplicationIntelligenceTests]: ../../../JobTrackerApi.Tests/ApplicationIntelligenceTests.cs
[B:StrategySnapshotOperationTests]: ../../../JobTrackerApi.Tests/StrategySnapshotOperationTests.cs
[F:application-checklist.test.tsx]: ../../../job-tracker-ui/src/application-checklist.test.tsx
[B:ApplicationChecklistTests]: ../../../JobTrackerApi.Tests/ApplicationChecklistTests.cs
[F:application-assets.test.tsx]: ../../../job-tracker-ui/src/application-assets.test.tsx
[F:cv-builder-deep-link.test.tsx]: ../../../job-tracker-ui/src/cv-builder-deep-link.test.tsx
[B:ApplicationAssetsTests]: ../../../JobTrackerApi.Tests/ApplicationAssetsTests.cs
[F:application-drafts.test.ts]: ../../../job-tracker-ui/src/application-drafts.test.ts
[B:SubmittedApplicationPackageTests]: ../../../JobTrackerApi.Tests/SubmittedApplicationPackageTests.cs
[B:JobApplicationsApplicationPackageTests]: ../../../JobTrackerApi.Tests/JobApplicationsApplicationPackageTests.cs
[F:attachments.test.tsx]: ../../../job-tracker-ui/src/attachments.test.tsx
[B:AttachmentsControllerTests]: ../../../JobTrackerApi.Tests/AttachmentsControllerTests.cs
[B:AttachmentConsistencyTests]: ../../../JobTrackerApi.Tests/AttachmentConsistencyTests.cs
[B:AttachmentFlagsRecomputeTests]: ../../../JobTrackerApi.Tests/AttachmentFlagsRecomputeTests.cs
[F:interview-prep.test.tsx]: ../../../job-tracker-ui/src/interview-prep.test.tsx
[B:InterviewPrepPersistenceTests]: ../../../JobTrackerApi.Tests/InterviewPrepPersistenceTests.cs
[B:InterviewAiContextTests]: ../../../JobTrackerApi.Tests/InterviewAiContextTests.cs
[B:InterviewPrepTests]: ../../../JobTrackerApi.Tests/InterviewPrepTests.cs
[B:JobApplicationsFollowUpDraftTests]: ../../../JobTrackerApi.Tests/JobApplicationsFollowUpDraftTests.cs
[F:application-calendar.test.tsx]: ../../../job-tracker-ui/src/application-calendar.test.tsx
[F:settings-view.test.tsx]: ../../../job-tracker-ui/src/settings-view.test.tsx
[B:ApplicationCalendarControllerTests]: ../../../JobTrackerApi.Tests/ApplicationCalendarControllerTests.cs
[B:RulesEngineProspectTests]: ../../../JobTrackerApi.Tests/RulesEngineProspectTests.cs
[B:NotificationSettingsControllerTests]: ../../../JobTrackerApi.Tests/NotificationSettingsControllerTests.cs
[F:operations-page.test.tsx]: ../../../job-tracker-ui/src/operations-page.test.tsx
[B:OperationsControllerTests]: ../../../JobTrackerApi.Tests/OperationsControllerTests.cs
[B:UserOperationStoreTests]: ../../../JobTrackerApi.Tests/UserOperationStoreTests.cs
[B:CvProcessingOperationTests]: ../../../JobTrackerApi.Tests/CvProcessingOperationTests.cs
[B:CareerProfileControllerTests]: ../../../JobTrackerApi.Tests/CareerProfileControllerTests.cs
[B:CareerProfileServiceTests]: ../../../JobTrackerApi.Tests/CareerProfileServiceTests.cs
[F:career-workspace-page.test.tsx]: ../../../job-tracker-ui/src/career-workspace-page.test.tsx
[B:ProfileCvControllerTests]: ../../../JobTrackerApi.Tests/ProfileCvControllerTests.cs
[B:CvProfileDiffServiceTests]: ../../../JobTrackerApi.Tests/CvProfileDiffServiceTests.cs
[B:CvExtractionCoverageTests]: ../../../JobTrackerApi.Tests/CvExtractionCoverageTests.cs
[F:career-evidence.test.tsx]: ../../../job-tracker-ui/src/career-evidence.test.tsx
[B:CareerEvidenceControllerTests]: ../../../JobTrackerApi.Tests/CareerEvidenceControllerTests.cs
[F:cv-builder-page.test.tsx]: ../../../job-tracker-ui/src/cv-builder-page.test.tsx
[F:cv-template-thumbnail.test.tsx]: ../../../job-tracker-ui/src/cv-template-thumbnail.test.tsx
[B:PublicCvControllerTests]: ../../../JobTrackerApi.Tests/PublicCvControllerTests.cs
[F:cvBuilder.test.ts]: ../../../job-tracker-ui/src/cvBuilder.test.ts
[F:ai-section-assistant.test.tsx]: ../../../job-tracker-ui/src/ai-section-assistant.test.tsx
[B:AiWorkspaceTests]: ../../../JobTrackerApi.Tests/AiWorkspaceTests.cs
[B:AiPrivacyPolicyTests]: ../../../JobTrackerApi.Tests/AiPrivacyPolicyTests.cs
[B:ProEntitlementAuthorizationTests]: ../../../JobTrackerApi.Tests/ProEntitlementAuthorizationTests.cs
[F:public-cv-page.test.tsx]: ../../../job-tracker-ui/src/public-cv-page.test.tsx
[B:PdfRenderAdmissionTests]: ../../../JobTrackerApi.Tests/PdfRenderAdmissionTests.cs
[F:end-to-end-trust-loop.test.tsx]: ../../../job-tracker-ui/src/end-to-end-trust-loop.test.tsx
[F:correspondence-search-context.test.ts]: ../../../job-tracker-ui/src/correspondence-search-context.test.ts
[B:CorrespondenceControllerTests]: ../../../JobTrackerApi.Tests/CorrespondenceControllerTests.cs
[F:correspondence-gmail-import.test.tsx]: ../../../job-tracker-ui/src/correspondence-gmail-import.test.tsx
[B:GmailControllerTests]: ../../../JobTrackerApi.Tests/GmailControllerTests.cs
[B:EmailOwnershipIntegrationTests]: ../../../JobTrackerApi.Tests/EmailOwnershipIntegrationTests.cs
[F:correspondence-inbox-page.test.tsx]: ../../../job-tracker-ui/src/correspondence-inbox-page.test.tsx
[B:EmailControllerTests]: ../../../JobTrackerApi.Tests/EmailControllerTests.cs
[B:EmailDraftsControllerTests]: ../../../JobTrackerApi.Tests/EmailDraftsControllerTests.cs
[B:EmailDraftPersistenceTests]: ../../../JobTrackerApi.Tests/EmailDraftPersistenceTests.cs
[B:EmailSendControllerTests]: ../../../JobTrackerApi.Tests/EmailSendControllerTests.cs
[B:EmailSendAttemptStoreTests]: ../../../JobTrackerApi.Tests/EmailSendAttemptStoreTests.cs
[B:EmailProviderDeliveryTests]: ../../../JobTrackerApi.Tests/EmailProviderDeliveryTests.cs
[F:gmail-review-page.test.tsx]: ../../../job-tracker-ui/src/gmail-review-page.test.tsx
[F:email-provider-connections.test.tsx]: ../../../job-tracker-ui/src/email-provider-connections.test.tsx
[B:MicrosoftGraphControllerTests]: ../../../JobTrackerApi.Tests/MicrosoftGraphControllerTests.cs
[B:ImapControllerTests]: ../../../JobTrackerApi.Tests/ImapControllerTests.cs
[B:ImapServiceSsrfGuardTests]: ../../../JobTrackerApi.Tests/ImapServiceSsrfGuardTests.cs
[B:StageAnalyticsTests]: ../../../JobTrackerApi.Tests/StageAnalyticsTests.cs
[F:onboarding-checklist.test.tsx]: ../../../job-tracker-ui/src/onboarding-checklist.test.tsx
[F:notifications-popover.test.tsx]: ../../../job-tracker-ui/src/notifications-popover.test.tsx
[F:ai-usage-card.test.tsx]: ../../../job-tracker-ui/src/ai-usage-card.test.tsx
[F:pro-feature-notice.test.tsx]: ../../../job-tracker-ui/src/pro-feature-notice.test.tsx
[B:AiUsageMeterTests]: ../../../JobTrackerApi.Tests/AiUsageMeterTests.cs
[B:BillingControllerTests]: ../../../JobTrackerApi.Tests/BillingControllerTests.cs
[F:account-data-export.test.tsx]: ../../../job-tracker-ui/src/account-data-export.test.tsx
[B:AccountDataExportTests]: ../../../JobTrackerApi.Tests/AccountDataExportTests.cs
[B:BackupControllerTests]: ../../../JobTrackerApi.Tests/BackupControllerTests.cs
[B:AccountDeletionTests]: ../../../JobTrackerApi.Tests/AccountDeletionTests.cs
[B:DatabaseBackupRunnerTests]: ../../../JobTrackerApi.Tests/DatabaseBackupRunnerTests.cs
[F:admin-users-page.test.tsx]: ../../../job-tracker-ui/src/admin-users-page.test.tsx
[B:UsersControllerTests]: ../../../JobTrackerApi.Tests/UsersControllerTests.cs
[B:ApiAuthorizationPostureTests]: ../../../JobTrackerApi.Tests/ApiAuthorizationPostureTests.cs
[F:admin-audit-page.test.tsx]: ../../../job-tracker-ui/src/admin-audit-page.test.tsx
[F:admin-system-page.test.tsx]: ../../../job-tracker-ui/src/admin-system-page.test.tsx
[B:MaintenanceJobDispatcherTests]: ../../../JobTrackerApi.Tests/MaintenanceJobDispatcherTests.cs
[B:CvCorpusHarnessTests]: ../../../JobTrackerApi.Tests/CvCorpusHarnessTests.cs
+107
View File
@@ -0,0 +1,107 @@
# Phase 2 — Preservation regression checkpoint
**Completed 2026-09-10 from `2d0f131` on `codex/ui-redesign-phase2`.** This checkpoint adds regression protection and an explicit TypeScript CI gate. **No visual redesign, application behavior change, route/layout/component restructuring, schema change or B07 work.** The Phase 1 source/audit remains intact.
## Preservation coverage
- **57/57 protected capability groups mapped (100%)**, with stable `PRES-` aliases of the original E/J/W/C/M/S/A IDs.
- **44 high-risk groups**, all mapped to existing behavioral assertions. High risk includes private identity, data edits/history/relationships, irreversible actions and shared navigation likely to be restructured.
- **147 referenced existing/new automated test files** across frontend and backend. Each row records route/entry, component/service owners, protected behavior, missing proof and expected later redesign impact.
- This percentage means **at least one core automated behavioral assertion per capability group**. It is not exhaustive sub-workflow, code, provider, mobile/accessibility or visual coverage. The matrix explicitly retains partial coverage and one native package-capture blocker.
Read the [complete preservation matrix](PRESERVATION_MATRIX.md), [traceability contract and Phase 2 change ledger](TRACEABILITY.md), and [baseline defects/intentional gaps](BASELINE_DEFECTS.md).
## High-risk additions and preserved evidence
| Workflow | New meaningful regression |
|---|---|
| Command/navigation/capture | Three component cases: job/company destination, closing clears query, add opens review without creating; real browser Ctrl+K search, legacy match link, reviewed add |
| CV section AI | Four cases: Reject/Close clear proposal without applying, edited proposal applies only on acceptance, failure retry leaves content untouched |
| Whole-CV AI | Five cases: explicit Apply/Reject; invalid JSON, missing IDs and duplicate IDs cannot become edits; save targets only selected CV |
| Cover-letter AI | One case: Reject preserves saved draft, disabled Save/no dirty state/no PUT |
| Settings | Two component cases: retry failed notification save with retained draft; changing one rule keeps other scenarios |
| Canonical profile → derived CV | Browser mobile edit/preview → reload → desktop preview; stored headline changes only selected CV, complete canonical profile and sibling remain unchanged |
| Documents/submission | Native profile/job/CV/letter/file fixture + legacy entry and capture; **expected failure P2-D01**, with desired immutable HTML/letter/archive-byte checks retained but not reached |
| Responsive preferences | Browser mobile→desktop rule draft stays, native save persists; notification false survives reload; separate direct API rule save |
| Permissions | Non-admin admin-menu entries absent, direct users GET/POST forbidden, normal CV create allowed, Free AI actions disabled and API 403 |
Existing B01 connection-time outbound protections, B02 ownership/session/cache checks, B03 rendering admission, B04 trusted principal/IP limits, B05 CSV bytes, and B06 application/configuration tests are unchanged. Mail draft revisions/uncertain sends, profile extraction acceptance, CV history, long PDF, job dirty navigation and other existing tests were retained.
## Baseline and fresh verification
Fresh local execution uses installed .NET **9.0.317**, Node dependencies already locked in the checkout, Windows/Chromium and Playwright's disposable SQLite API. There were no production requests or provider credentials. Test fixtures use only synthetic accounts and local URLs.
| Gate | Recorded Phase 1 baseline | Fresh Phase 2 result |
|---|---|---|
| Backend Release suite | 916 passing | **916 passed, 0 failed, 0 skipped**; no exclusions, count unchanged |
| Frontend Jest | 298 tests / 67 suites | **313 passed / 68 suites**; +15 cases, +1 test file; no existing test removed |
| General browser | 18 passing | **23 actually passed + 1 expected failure**, 0 unexpected failures/skips/flaky; original 18 still pass, +6 cases |
| B04 real HTTP | 9 cases | **9 passed** |
| B06 production-mode API/forwarding HTTP | 6 cases | **6 passed**; not nginx/Traefik runtime proof |
| B03 PDF burst/admission | 1 case | **1 passed**, multi-route capacity/recovery oracle unchanged |
| Compose ingress/configuration | 3 tests/17 hostile mutations | **3 passed** |
| Backend build | Release | **Pass, 0 warnings/errors**, warnings-as-errors |
| Frontend build | Next production static export | **Pass** |
| Standalone type check | Historical TS2571 repaired before Phase 2 | **Pass**; new explicit CI command, unchanged strictness |
| New browser-spec TypeScript | Previously no whole-Playwright type gate | **Pass** with temporary Node-aware config for this new spec only |
| ESLint | Zero-warning gate | **Pass**, no suppressions added |
| Tracked secret scan; diff whitespace | Required | **Pass** |
Playwright's list reporter says “24 passed” because expected failures count as expected outcomes. Its JSON records **23 `expectedStatus=passed/actual=passed` and one `expectedStatus=failed/actual=failed`**. Do not present this as 24 successful workflows. The failed native package test is not a regression introduced by application changes: there are no such changes in this branch. See P2-D01.
Intermediate test-authoring corrections: one missing test-fixture brace, Testing Library's unsupported `exact` role options, a Playwright callback named `use` misidentified as a React hook, and package capture's actual CV-tab location. Corrected test code rather than weakening lint/types or changing UI. The temporary isolated TypeScript config initially lacked Node type roots; setting the existing installed Node declarations resolved Buffer. No new dependency installed.
One initial rules-save 500 did not reproduce in the direct or repeated browser path; retained as observation P2-O01, not asserted as a diagnosed defect. The reproducible SQLite package/tailoring failure is recorded separately. No blanket skipping or gate bypass was added.
## Exact commands
From the repository root (PowerShell):
```powershell
$env:DOTNET_ROOT='C:/Users/Connor/.dotnet'
& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --logger 'trx;LogFileName=phase2.trx' --results-directory .artifacts/redesign-phase2 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
& C:/Users/Connor/.dotnet/dotnet.exe build JobTrackerApi/JobTrackerApi.csproj --configuration Release --no-restore -warnaserror
python scripts/test-ingress.py
python scripts/supply-chain.py scan-secrets
git diff --check
```
From `job-tracker-ui`:
```powershell
npm test -- --watchAll=false --runInBand
npm run typecheck
npm run lint
$env:NEXT_PUBLIC_API_BASE_URL='http://localhost:5302/api'
$env:NEXT_TELEMETRY_DISABLED='1'
npm run build
$env:DOTNET_ROOT='C:/Users/Connor/.dotnet'
$env:DOTNET_HOST_PATH='C:/Users/Connor/.dotnet/dotnet.exe'
$env:PLAYWRIGHT_STATIC_EXPORT='true'
$env:PLAYWRIGHT_PREBUILT_EXPORT='true'
$env:PLAYWRIGHT_JSON_OUTPUT_FILE='../.artifacts/phase2-browser-results.json'
node node_modules/@playwright/test/cli.js test --reporter=list,json
node node_modules/@playwright/test/cli.js test --config=playwright.rate-limits.config.ts
node node_modules/@playwright/test/cli.js test --config=playwright.ingress.config.ts
node node_modules/@playwright/test/cli.js test --config=playwright.pdf.config.ts
```
Initial focused command: `npm test -- --watchAll=false --runInBand --runTestsByPath src/quick-command-preservation.test.tsx src/ai-section-assistant.test.tsx src/cv-builder-deep-link.test.tsx src/settings-view.test.tsx` → 31 passed. Final full Jest includes the additional cover-letter reject case.
Final browser rerun after narrowing the known-failure annotation: `node node_modules/@playwright/test/cli.js test e2e/redesign-preservation.spec.ts` → five actual passes and the same one expected HTTP-500 capture failure (23.8s); no unexpected failures. Final lint and isolated browser-spec typing also pass.
Additional isolated browser-spec typing used `node node_modules/typescript/bin/tsc --project ../.artifacts/phase2-browser-tsconfig.json --noEmit`. Temporary config extends the frontend tsconfig, includes only `../job-tracker-ui/e2e/redesign-preservation.spec.ts`, sets `incremental:false`, `typeRoots:["../job-tracker-ui/node_modules/@types"]`, `types:["node"]`. This is a local check, not a claim that every pre-existing Playwright/config source is covered by the new CI type gate.
Local logs: `.artifacts/phase2-{backend,jest,build,api-build,typecheck,browser-typecheck,lint,browser,rate-limits,ingress,pdf,compose,secrets}.log`; backend TRX under `.artifacts/redesign-phase2`; browser JSON and sanitized outcome summary under `.artifacts`. Durable conclusions and reproduction remain in these committed documents/tests; raw traces with synthetic session material are not published.
The matrix was checked against the original ledger: 57 rows, 57 unique IDs, no missing/extra IDs, and all 147 test-reference paths exist. No production source file changed. Full baseline execution was not repeated for documentation-only edits.
## Remaining gaps and next phase
**Phase 2 is complete as a regression/planning checkpoint, with its documented baseline failure.** Phase 3 may define UX architecture: grouped sidebar, page/route destination and backwards compatibility, one Jobs table/board entry, canonical Career Profile/derived-document workflows, contextual AI review, and explicit mobile task rules. Keep the existing MUI/Emotion/React Router/API stack while planning. Do not implement those layouts or components in this checkpoint.
Before a later code migration, close that surface's matrix gaps or establish a deliberately scoped additional fixture. W07 package/tailoring needs native-provider success first; shared shell needs drawer/focus and bulk/saved-view cases; career/mail/admin need their specified interactive and provider proof. Screen-reader/touch/reflow and the full EN/NB × light/dark × desktop/mobile matrix remain visual/UX gates.
**F04 remains open.** The B06 repository/application changes are complete, but actual deployed proxy peer/listeners, firewall/NAT/IPv6, client IP, DNS/TLS/redirect and restart behavior remain unverified; follow the existing [operator commands](../../audits/2026-09-07/validation/b06-completion.md). Prior main CI run 743 failed in the browser worker and did not deploy; this branch's local success does not repair or certify that runner. Pushing this feature branch is a preservation checkpoint, not a production deployment or a release-readiness claim.
No Phase 3 implementation, visual redesign, component-library migration, B07 or later security ticket has begun.
+56
View File
@@ -0,0 +1,56 @@
# Redesign traceability
Phase 2 starts at `2d0f131`. The [preservation matrix](PRESERVATION_MATRIX.md) is the stable baseline: **PRES-E01…PRES-A03**, in the original ledger's groups (not one contiguous numeric range). Old references such as C07 remain valid aliases. Subdivide a later workflow with a suffix, for example `PRES-C07.override-reset`; do not reuse an ID or silently change its meaning.
## Required chain for each later change
`PRES-ID → requirement/source contract → implementation commit + changed owner → regression test + executed result → visual/UX evidence`
The requirement for every current PRES-ID is its **complete original row** in [FUNCTIONALITY_INVENTORY.md](../audit/FUNCTIONALITY_INVENTORY.md), including the source contract. A proposed new requirement uses a separate `NEW-xxx` ID referencing the product brief; it cannot overwrite a PRES requirement. Defects use their original audit IDs or the separate P2-D IDs below, not “preserve the broken behavior.”
For every implementation PR/session, add a row to this file (or link its scoped trace sheet here) with:
| Field | Required evidence |
|---|---|
| PRES-ID / requirement | Original ledger group plus the precise sub-workflow being changed; target-brief requirement if applicable |
| Baseline → destination | Existing route, capture/query/alias/keyboard entry and intended location; preserved/relocated/merged/explicitly deprecated |
| Implementation | Commit SHA, affected components/API owners, reason for the change; “unchanged” is valid but not “done” |
| Automated regression | Named behavioral test, file, command/result and provider/test layer; include negative/dirty/error cases |
| Visual/UX validation | Screenshot/video or review artifact with route, populated/empty/error/locked state, viewport, theme, locale, keyboard/focus result and date |
| Gaps / decision / reviewer | Known baseline defects and intentional missing proof; approval reference only when semantics/deprecation need it |
No screenshot alone proves data preservation. No mocked API test alone proves ownership. No successful localhost run proves F04. A shifted feature remains protected after moving to another page.
## Phase 2 changes (test/CI checkpoint only)
All application implementations and routes remain at their Phase 1 behavior. The commit containing this document identifies the Phase 2 implementation of **regression protection**, not a product migration. Use `git log -- docs/redesign/phase-2` to resolve its SHA without a self-referential commit placeholder.
| PRES IDs → existing requirement | Phase 2 change → automated oracle | Visual/UX evidence |
|---|---|---|
| E07, J04 → command job/company/add entry and reviewed capture | [quick-command-preservation](../../../job-tracker-ui/src/quick-command-preservation.test.tsx): three cases; destinations, callback and no-create boundary | No visual redesign. Native browser keyboard search/add evidence below |
| C11 → per-section review never implicitly applies | [ai-section-assistant](../../../job-tracker-ui/src/ai-section-assistant.test.tsx): reject/close, edit then accept, failed suggestion retry | jsdom interaction only; no model or visual certification |
| C07, C11 → whole-document review changes only accepted CV settings | [cv-builder-deep-link](../../../job-tracker-ui/src/cv-builder-deep-link.test.tsx): apply/reject and invalid/missing/duplicate block responses | jsdom current/proposed comparison; real canonical isolation below |
| W06 → rejected letter does not replace saved content | [application-assets](../../../job-tracker-ui/src/application-assets.test.tsx): reject clears proposal, original remains, no dirty/save | No provider contacted; real delivery remains outside this workflow |
| S03, W11 → scenario-specific rules and retryable preferences | [settings-view](../../../job-tracker-ui/src/settings-view.test.tsx): unchanged scenarios in payload; failed notification save retains draft and retries | Responsive browser preference evidence below |
| C01, C06, C07, C10 → canonical/sibling isolation and responsive editor persistence | [redesign-preservation browser](../../../job-tracker-ui/e2e/redesign-preservation.spec.ts): mobile override, preview/edit, reload, desktop preview; API compares master and sibling before/after | Current Chromium UI at 390×844 and 1440×900, EN/default theme. Behavioral viewport check, not visual approval |
| W01, W05–W08 → source relationships and frozen submission | Same browser spec: fixture creates actual job/CV/letter/file; captures via legacy CV workspace entry | **Expected failure P2-D01:** capture fails on SQLite. Later source-mutation/archive checks not reached; do not mark this migration ready |
| E07, J04, W01 → real command launcher and old links | Same browser spec: Ctrl+K job selection, actual App legacy match link, reviewed add leaves job count unchanged | Current desktop route/keyboard behavior only |
| S03, W11 → responsive rules/notification controls | Same browser spec: edit rule mobile→desktop, save, native API check; notification false survives reload; independent direct rule save | 390×844/1440×900 EN/default theme; no screenshot-based redesign |
| A01, S04, E07 → hidden admin navigation is not authorization; Free AI locks | Same browser spec: hidden admin menu, direct users read/create 403; ordinary CV creation succeeds, all three AI actions disabled, API AI 403 | Current non-admin browser + real API. Admin route shell may render, but cannot retrieve privileged data |
| All 57 → reliable test type baseline | [package script](../../../job-tracker-ui/package.json), [CI gate](../../../.gitea/workflows/ci-deploy.yml): `npm run typecheck` before Jest | Static TypeScript for existing src/app scope; no visual or app change |
## Phase 3 handoff and later migration gates
Phase 3 is **UX architecture documentation**, not a new implementation batch: grouped sidebar/page hierarchy; Jobs table/board view entry; canonical Career Profile versus derived CV/document relationships; full-page versus optional preview; contextual AI review pattern; mobile task flows. Map all 31 existing routes, both aliases, capture/notification/provider entries and all 57 PRES rows to intended destinations.
Keep current MUI/Emotion/React Router/API and ownership contracts while designing. Brand/name/accent and cross-device dashboard customization remain genuine product decisions for the dependent work; do not ask about facts already in the audit.
Before Phase 4/5 code, require an approved route/preservation plan, targeted regression fixtures for that surface's gaps, a shared accessibility/state contract, and coordinated integrity/recovery dependencies. In particular:
- Do not restructure package/tailoring workflows while P2-D01 blocks native success proof.
- Do not conflate B02 identity, stale-CV revision and create-intent idempotency. Those queue items remain distinct; B07 was not started.
- Do not drop mail draft revisions, explicit send/new-attempt controls or owner-bound references.
- Before migrating shared controls, validate EN/NB × light/dark × desktop/mobile, keyboard focus, error/empty/dirty states and representative loaded data. Real screen reader/touch/reflow tests remain manual.
- Do not label a release healthy until the previously failed CI browser runner is diagnosed and the required pipeline passes. F04 requires actual production ingress evidence.
Future trace sheets must link here. There are **no Phase 3+ implementation commits or visual sign-offs** in this checkpoint.
+4 -2
View File
@@ -1,13 +1,15 @@
# Redesign — 2026-09-09 # Redesign — 2026-09-09
- [x] Phase 2 from `2d0f131`, on `codex/ui-redesign-phase2`: [preservation checkpoint](../redesign/phase-2/README.md), 57 stable PRES groups mapped, 15 added component cases, six added browser cases, explicit standalone CI type gate and traceability. Fresh 916 backend / 313 frontend pass; browser 23 actual passes + one documented SQLite expected failure, B04/B06/PDF/config regressions pass. No application/visual redesign, B07 or F04 closure.
- [ ] P2-D01 separate baseline repair before package/tailoring restructuring: SQLite DateTimeOffset ordering fails in native tailoring/capture. Retain the expected-failure browser oracle; prove native capture and unchanged historical binaries after source edits/deletion before removing annotation. P2-O01 initial rules 500 remains unconfirmed; direct and browser save controls pass. See [baseline evidence](../redesign/phase-2/BASELINE_DEFECTS.md).
- [x] Read the Astra handoff and required product documents; complete Phase 1 source audit with 31 concrete route patterns, functionality preservation ledger, 266 API call sites, component/test inventory, identity/storage map, responsive/localisation assessment and migration gates. See `docs/redesign/audit/README.md`. No redesign implementation or B07 started. - [x] Read the Astra handoff and required product documents; complete Phase 1 source audit with 31 concrete route patterns, functionality preservation ledger, 266 API call sites, component/test inventory, identity/storage map, responsive/localisation assessment and migration gates. See `docs/redesign/audit/README.md`. No redesign implementation or B07 started.
- [x] Finalize Phase 1 separately, 2026-09-10: classify all 57 preservation groups, UX improvements, genuinely new/conditional requirements and baseline defects; retain already-implemented CV drag/fit, custom status values, calendar and other protected workflows. Final summary/contract/test limits/Phase 2 starting point: `docs/redesign/audit/PHASE1_SUMMARY.md`. Documentation only; B06/F04 closure remains open. - [x] Finalize Phase 1 separately, 2026-09-10: classify all 57 preservation groups, UX improvements, genuinely new/conditional requirements and baseline defects; retain already-implemented CV drag/fit, custom status values, calendar and other protected workflows. Final summary/contract/test limits/Phase 2 starting point: `docs/redesign/audit/PHASE1_SUMMARY.md`. Documentation only; B06/F04 closure remains open.
- [ ] Complete the authorized `main` release through required CI/deploy gates. Main `27657f3` is pushed; run 743 passed 916 backend/298 frontend tests, lint, dependency audits and production builds, then the browser gate failed (worker SIGSEGV, retry timeout and closed-browser errors; 6 passed/12 failed). Deploy skipped; live remains version 299. Stop further release retries pending runner-host diagnostics; exact read-only handoff and evidence are in `docs/audits/2026-09-07/validation/b06-completion.md`. No gate was bypassed. - [ ] Complete the authorized `main` release through required CI/deploy gates. Main `27657f3` is pushed; run 743 passed 916 backend/298 frontend tests, lint, dependency audits and production builds, then the browser gate failed (worker SIGSEGV, retry timeout and closed-browser errors; 6 passed/12 failed). Deploy skipped; live remains version 299. Stop further release retries pending runner-host diagnostics; exact read-only handoff and evidence are in `docs/audits/2026-09-07/validation/b06-completion.md`. No gate was bypassed.
- [ ] Next redesign work: Phase 2 functional baseline, subject to the audit's integrity/recovery and release gates. No automatic implementation in Phase 1. - [ ] Next redesign work: Phase 3 UX architecture/route and workflow plan using the [traceability contract](../redesign/phase-2/TRACEABILITY.md), subject to documented gaps and integrity/recovery/release gates. Not started automatically; no visual implementation authorized by this checkpoint.
# Expanded production audit — 2026-09-07 # Expanded production audit — 2026-09-07
- [x] Authorized release repair, 2026-09-10: patch Next/sharp/js-yaml dependency advisories; preserve deliberate React Router full navigation and correct the historical TS2571 test assertion when the patched Next build makes it release-blocking. Local standalone tsc/build and 13 interview tests pass. R01a's explicit standalone CI command remains outstanding; deployment and F04 closure are separate gates in the B06 report. - [x] Authorized release repair, 2026-09-10: patch Next/sharp/js-yaml dependency advisories; preserve deliberate React Router full navigation and correct the historical TS2571 test assertion when the patched Next build makes it release-blocking. Local standalone tsc/build and 13 interview tests pass. R01a's explicit standalone CI command is now added in the separate Phase 2 checkpoint; deployed CI execution and F04 closure remain separate gates in the B06 report.
- [ ] B06 implementation complete; **F04 open, runtime/production verification incomplete**. Fresh 166 focused backend, 3 Compose, 6 Production HTTP and 9 B04 HTTP checks pass; prior broader/build evidence retained. Public DNS/valid HTTPS observed; ports 3000/5202/8080 unreachable from this workstation. HTTP redirects to unreachable HTTPS port 444. Docker inference-socket failure blocks real Compose/Traefik/nginx tests; actual peers/backend client IP and production host/firewall evidence require operator handoff (no documented SSH alias). See `docs/audits/2026-09-07/validation/b06-completion.md`. B01–B05 preserved; B07 not started. - [ ] B06 implementation complete; **F04 open, runtime/production verification incomplete**. Fresh 166 focused backend, 3 Compose, 6 Production HTTP and 9 B04 HTTP checks pass; prior broader/build evidence retained. Public DNS/valid HTTPS observed; ports 3000/5202/8080 unreachable from this workstation. HTTP redirects to unreachable HTTPS port 444. Docker inference-socket failure blocks real Compose/Traefik/nginx tests; actual peers/backend client IP and production host/firewall evidence require operator handoff (no documented SSH alias). See `docs/audits/2026-09-07/validation/b06-completion.md`. B01–B05 preserved; B07 not started.
@@ -0,0 +1,188 @@
import { expect, test as base, type APIRequestContext } from "@playwright/test";
const apiUrl = "http://localhost:5302/api";
const password = "E2ePassword123!";
const canonical = {
version: "1", contact: { fullName: "Åse Eksempel", headline: "Canonical engineer" },
jobs: [{ title: "Engineer", company: "Nordlys AS", bullets: ["Built reliable APIs"] }],
skills: ["C#", "Norsk"],
};
async function login(api: APIRequestContext, email: string) {
expect((await api.post(`${apiUrl}/auth/login`, { data: { email, password } })).status()).toBe(200);
const me = await (await api.get(`${apiUrl}/auth/me`)).json();
const csrf = (await api.storageState()).cookies.find(cookie => cookie.name === "XSRF-TOKEN")!.value;
return { "X-CSRF-TOKEN": csrf, "X-Session-Context": me.sessionContext as string };
}
// Every case gets a real non-admin account in Playwright's disposable local API database.
// No API responses are mocked; browser traffic cannot leave the local test environment.
const test = base.extend<{ headers: Record<string, string> }>({
headers: async ({ playwright, page, context }, provide) => {
const admin = await playwright.request.newContext();
try {
const adminHeaders = await login(admin, "e2e@example.test");
const email = `pres-${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);
const headers = await login(page.request, email);
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 context.addInitScript(() => localStorage.setItem("uiLanguage", "en"));
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile: canonical } })).status()).toBe(200);
await provide(headers);
} finally { await admin.dispose(); }
},
});
test("[PRES-C01 PRES-C06 PRES-C07 PRES-C10] mobile CV override persists without changing the canonical profile or sibling CV", async ({ page, headers }) => {
const create = async (name: string) => {
const response = await page.request.post(`${apiUrl}/cv/variants`, { headers, data: { name } });
expect(response.status()).toBe(200);
return response.json();
};
const selected = await create("Selected CV");
const sibling = await create("Sibling CV");
const profileBefore = await (await page.request.get(`${apiUrl}/career/profile`)).json();
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(`/career/builder/${selected.id}`);
await page.getByRole("tab", { name: "Content", exact: true }).click();
await page.getByLabel("Headline override", { exact: true }).fill("Reviewed CV-only headline");
await page.getByRole("button", { name: "Save now", exact: true }).click();
await expect(page.getByText("Saved", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Preview", exact: true }).click();
await expect(page.getByTitle("CV preview page 1")).toBeVisible();
await page.getByRole("button", { name: "Edit", exact: true }).click();
await expect(page.getByLabel("Headline override", { exact: true })).toHaveValue("Reviewed CV-only headline");
await page.reload();
await page.getByRole("tab", { name: "Content", exact: true }).click();
await expect(page.getByLabel("Headline override", { exact: true })).toHaveValue("Reviewed CV-only headline");
await page.setViewportSize({ width: 1440, height: 900 });
await expect(page.getByTitle("CV preview page 1")).toBeVisible();
expect((await (await page.request.get(`${apiUrl}/cv/variants/${selected.id}`)).json()).settings.headline).toBe("Reviewed CV-only headline");
expect(await (await page.request.get(`${apiUrl}/cv/variants/${sibling.id}`)).json()).toEqual(sibling);
expect((await (await page.request.get(`${apiUrl}/career/profile`)).json()).profile).toEqual(profileBefore.profile);
});
test("[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves frozen documents after current sources change", async ({ page, headers }) => {
const companyResponse = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: "Preservation AS" } });
expect(companyResponse.status()).toBe(201);
const company = await companyResponse.json();
const jobResponse = await page.request.post(`${apiUrl}/jobapplications`, { headers, data: { companyId: company.id, jobTitle: "Preserved workspace", status: "Saved" } });
expect(jobResponse.ok()).toBe(true);
const job = await jobResponse.json();
const variantResponse = await page.request.post(`${apiUrl}/cv/variants`, { headers, data: { name: "Submitted CV", jobApplicationId: job.id } });
expect(variantResponse.status()).toBe(200);
const variant = await variantResponse.json();
const jobUrl = `${apiUrl}/jobapplications/${job.id}`;
expect((await page.request.put(`${jobUrl}/cv`, { headers, data: { variantId: variant.id } })).status()).toBe(200);
expect((await page.request.put(`${jobUrl}/cover-letter`, { headers, data: { text: "Original reviewed letter", source: "manual" } })).status()).toBe(200);
const bytes = Buffer.from("Original attachment — æøå\n", "utf8");
const upload = await page.request.post(`${apiUrl}/attachments`, { headers, multipart: { jobId: String(job.id), files: { name: "evidence.txt", mimeType: "text/plain", buffer: bytes } } });
expect(upload.ok()).toBe(true);
const [attachment] = await (await page.request.get(`${apiUrl}/attachments/${job.id}`)).json();
await page.goto(`/applications/${job.id}?section=cv`);
await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}\\?section=cv$`));
const captured = page.waitForResponse(response => response.url() === `${jobUrl}/submitted-packages` && response.request().method() === 'POST');
await page.getByRole("button", { name: "Freeze current package", exact: true }).click();
const captureResponse = await captured;
// P2-D01: native SQLite rejects the service's DateTimeOffset ORDER BY. Only
// this known response is expected; setup failures and later archive failures
// after a successful capture must remain ordinary failing assertions.
if (captureResponse.status() === 500) {
test.fail(true, 'P2-D01: SQLite submitted-package capture; phase-2/BASELINE_DEFECTS.md');
}
expect(captureResponse.status()).toBe(200);
await expect(page.getByText("Submission package v1", { exact: true })).toBeVisible();
const [snapshot] = await (await page.request.get(`${jobUrl}/submitted-packages`)).json();
const before = await (await page.request.get(`${jobUrl}/submitted-packages/${snapshot.id}`)).json();
expect(before.coverLetterText).toBe("Original reviewed letter");
expect(before.renderedCvHtml).toContain("Canonical engineer");
expect(snapshot.cvVariantId).toBe(variant.id);
expect(snapshot.attachments).toHaveLength(1);
expect((await page.request.put(`${jobUrl}/cover-letter`, { headers, data: { text: "Later letter", source: "manual" } })).status()).toBe(200);
expect((await page.request.put(`${apiUrl}/cv/variants/${variant.id}`, { headers, data: { name: "Later CV", settings: { ...variant.settings, headline: "Later headline" } } })).status()).toBe(200);
expect((await page.request.delete(`${apiUrl}/attachments/${attachment.id}`, { headers })).status()).toBe(204);
expect(await (await page.request.get(`${jobUrl}/submitted-packages/${snapshot.id}`)).json()).toEqual(before);
const archived = await page.request.get(`${jobUrl}/submitted-packages/${snapshot.id}/attachments/${snapshot.attachments[0].id}`);
expect(archived.status()).toBe(200);
expect(await archived.body()).toEqual(bytes);
await page.reload();
await page.getByRole("button", { name: "View preserved package", exact: true }).click();
await expect(page.getByRole("dialog").getByText("Original reviewed letter", { exact: true })).toBeVisible();
});
test("[PRES-S03 PRES-W11] responsive settings navigation keeps rule edits and persists notification preferences", async ({ page, headers }) => {
expect((await page.request.get(`${apiUrl}/auth/me`, { headers })).status()).toBe(200);
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/settings");
await page.getByLabel("Settings section", { exact: true }).click();
await page.getByRole("option", { name: "Follow-ups", exact: true }).click();
const field = page.getByLabel("Applied: follow-up days", { exact: true });
await field.fill("5");
await page.setViewportSize({ width: 1440, height: 900 });
await expect(field).toHaveValue("5");
const saveRules = page.waitForResponse(response => response.url() === `${apiUrl}/rules` && response.request().method() === 'PUT');
await page.getByRole("button", { name: "Save Rules", exact: true }).click();
expect((await saveRules).status()).toBe(204);
await expect.poll(async () => (await (await page.request.get(`${apiUrl}/rules`)).json()).appliedFollowUpDays).toBe(5);
await page.getByRole("tab", { name: "Notifications", exact: true }).click();
await page.getByLabel("Email reminders for follow-ups", { exact: true }).uncheck();
await page.getByRole("button", { name: "Save notification settings", exact: true }).click();
await expect.poll(async () => (await (await page.request.get(`${apiUrl}/notification-settings`)).json()).emailFollowUpRemindersEnabled).toBe(false);
await page.reload();
await page.getByRole("tab", { name: "Notifications", exact: true }).click();
await expect(page.getByLabel("Email reminders for follow-ups", { exact: true })).not.toBeChecked();
});
test("[PRES-S03] rule changes persist through the real API", async ({ page, headers }) => {
const settings = await (await page.request.get(`${apiUrl}/rules`)).json();
const saved = await page.request.put(`${apiUrl}/rules`, { headers, data: { ...settings, appliedFollowUpDays: 5 } });
expect(saved.status()).toBe(204);
expect((await (await page.request.get(`${apiUrl}/rules`)).json()).appliedFollowUpDays).toBe(5);
});
test("[PRES-E07 PRES-W01 PRES-J04] keyboard command navigation and legacy workspace entry preserve the selected job", async ({ page, headers }) => {
const companyResponse = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: "Command AS" } });
expect(companyResponse.status()).toBe(201);
const jobResponse = await page.request.post(`${apiUrl}/jobapplications`, { headers,
data: { companyId: (await companyResponse.json()).id, jobTitle: "Command engineer", status: "Saved" } });
expect(jobResponse.ok()).toBe(true);
const job = await jobResponse.json();
await page.goto("/dashboard");
await expect(page.getByRole("heading", { name: "Dashboard", exact: true })).toBeVisible();
await page.keyboard.press("Control+k");
await page.getByRole("dialog").getByRole("textbox").fill("Command engineer");
await page.getByText("Command AS - Command engineer", { exact: true }).click();
await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}$`));
await expect(page.getByRole("heading", { name: "Command engineer", exact: true })).toBeVisible();
await page.goto(`/applications/${job.id}?section=match`);
await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}\\?section=match$`));
await expect(page.getByRole("tab", { name: "Analysis", exact: true })).toHaveAttribute("aria-selected", "true");
await page.keyboard.press("Control+k");
await page.getByRole("dialog").getByText("Add new job", { exact: true }).click();
await expect(page.getByRole("dialog")).toBeVisible();
expect((await (await page.request.get(`${apiUrl}/jobapplications`)).json()).total).toBe(1);
});
test("[PRES-A01 PRES-S04 PRES-E07] non-admin navigation cannot grant API privileges or unlock CV AI", async ({ page, headers }) => {
await page.goto("/dashboard");
await expect(page.getByRole("heading", { name: "Dashboard", exact: true })).toBeVisible();
for (const name of ["Users", "Audit log", "System"]) await expect(page.getByRole("button", { name, exact: true })).toHaveCount(0);
const denied = page.waitForResponse(response => response.url() === `${apiUrl}/users`);
await page.goto("/admin/users");
expect((await denied).status()).toBe(403);
await expect(page.getByText("e2e@example.test", { exact: true })).toHaveCount(0);
expect((await page.request.post(`${apiUrl}/users`, { headers, data: { email: "must-not-exist@example.test", password, roles: ["Admin"] } })).status()).toBe(403);
const created = await page.request.post(`${apiUrl}/cv/variants`, { headers, data: { name: "Free CV" } });
expect(created.status()).toBe(200);
await page.goto(`/career/builder/${(await created.json()).id}`);
await page.getByRole("tab", { name: "AI Tools", exact: true }).click();
const actions = page.getByRole("button", { name: "Review suggestion", exact: true });
await expect(actions).toHaveCount(3);
for (const action of await actions.all()) await expect(action).toBeDisabled();
expect((await page.request.post(`${apiUrl}/cv/ai/assist`, { headers, data: { action: "improve", text: "Safe test text" } })).status()).toBe(403);
expect((await (await page.request.get(`${apiUrl}/auth/me`)).json()).roles).toEqual([]);
});
+1
View File
@@ -35,6 +35,7 @@
"build": "node --max-old-space-size=4096 ./node_modules/next/dist/bin/next build", "build": "node --max-old-space-size=4096 ./node_modules/next/dist/bin/next build",
"lint": "eslint . --max-warnings=0", "lint": "eslint . --max-warnings=0",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
"typecheck": "tsc --noEmit --incremental false",
"test": "jest", "test": "jest",
"test:pdf-admission": "playwright test --config=playwright.pdf.config.ts", "test:pdf-admission": "playwright test --config=playwright.pdf.config.ts",
"test:rate-limits": "playwright test --config=playwright.rate-limits.config.ts", "test:rate-limits": "playwright test --config=playwright.rate-limits.config.ts",
@@ -69,3 +69,44 @@ test("section AI controls render in Bokmål without changing the document langua
expect(screen.getByLabelText("Språk for resultat")).toHaveTextContent("Norsk bokmål"); expect(screen.getByLabelText("Språk for resultat")).toHaveTextContent("Norsk bokmål");
expect(screen.getByRole("button", { name: "Foreslå" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Foreslå" })).toBeInTheDocument();
}); });
test.each(["Reject", "Close"])("[PRES-C11] %s leaves the source unchanged and removes the suggestion", async (action) => {
mockedApi.post.mockResolvedValue({ data: { result: "Unaccepted draft" } } as never);
const onApply = renderAssistant();
fireEvent.click(screen.getByRole("button", { name: "AI suggestions" }));
fireEvent.click(screen.getByRole("button", { name: "Suggest" }));
expect(await screen.findByDisplayValue("Unaccepted draft")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: action }));
expect(screen.queryByDisplayValue("Unaccepted draft")).not.toBeInTheDocument();
expect(onApply).not.toHaveBeenCalled();
expect(mockedApi.put).not.toHaveBeenCalled();
if (action === "Close") fireEvent.click(screen.getByRole("button", { name: "AI suggestions" }));
expect(screen.queryByRole("button", { name: "Apply" })).not.toBeInTheDocument();
});
test("[PRES-C11] applies the user's edited suggestion only after acceptance", async () => {
mockedApi.post.mockResolvedValue({ data: { result: "Proposed wording" } } as never);
const onApply = renderAssistant();
fireEvent.click(screen.getByRole("button", { name: "AI suggestions" }));
fireEvent.click(screen.getByRole("button", { name: "Suggest" }));
fireEvent.change(await screen.findByDisplayValue("Proposed wording"), { target: { value: "My reviewed wording" } });
expect(onApply).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole("button", { name: "Apply" }));
expect(onApply).toHaveBeenCalledTimes(1);
expect(onApply).toHaveBeenCalledWith("My reviewed wording");
});
test("[PRES-C11] a failed suggestion can be retried without applying or saving content", async () => {
mockedApi.post.mockRejectedValueOnce(new Error("offline"))
.mockResolvedValueOnce({ data: { result: "Recovered suggestion" } } as never);
const onApply = renderAssistant();
fireEvent.click(screen.getByRole("button", { name: "AI suggestions" }));
fireEvent.click(screen.getByRole("button", { name: "Suggest" }));
expect(await screen.findByRole("alert")).toBeInTheDocument();
await waitFor(() => expect(screen.getByRole("button", { name: "Suggest" })).toBeEnabled());
expect(onApply).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole("button", { name: "Suggest" }));
expect(await screen.findByDisplayValue("Recovered suggestion")).toBeInTheDocument();
expect(onApply).not.toHaveBeenCalled();
expect(mockedApi.put).not.toHaveBeenCalled();
});
@@ -261,6 +261,23 @@ test("AI cover letter suggestions use the linked CV and require explicit apply",
)); ));
}); });
test("[PRES-W06] rejecting an AI cover letter preserves the saved draft without a write", async () => {
routeGet();
mockedApi.post.mockResolvedValue({ data: { result: { text: "Unaccepted letter" } } } as never);
const onDirtyChange = jest.fn();
render(<ApplicationCoverLetterSection jobId={7} onDirtyChange={onDirtyChange} />);
await screen.findByText(/application's full job advert and analysis/i);
fireEvent.click(screen.getByRole("button", { name: "Generate" }));
await screen.findByText("Unaccepted letter");
expect(screen.getByLabelText("Cover letter")).toHaveValue("Dear team");
fireEvent.click(screen.getByRole("button", { name: "Reject" }));
expect(screen.queryByText("Unaccepted letter")).not.toBeInTheDocument();
expect(screen.getByLabelText("Cover letter")).toHaveValue("Dear team");
expect(screen.getByRole("button", { name: "Save" })).toBeDisabled();
expect(onDirtyChange).not.toHaveBeenCalledWith(true);
expect(mockedApi.put).not.toHaveBeenCalled();
});
test("a failed load surfaces an error", async () => { test("a failed load surfaces an error", async () => {
mockedApi.get.mockRejectedValue(new Error("boom")); mockedApi.get.mockRejectedValue(new Error("boom"));
@@ -345,3 +345,52 @@ test('preview failure is visible and retryable without leaving the editor', asyn
expect(screen.getByTitle('CV preview')).toHaveAttribute('sandbox', 'allow-same-origin allow-scripts'); expect(screen.getByTitle('CV preview')).toHaveAttribute('sandbox', 'allow-same-origin allow-scripts');
await waitFor(() => expect(screen.queryByText('Preview unavailable')).not.toBeInTheDocument()); await waitFor(() => expect(screen.queryByText('Preview unavailable')).not.toBeInTheDocument());
}); });
async function reviewDocumentSuggestion(result: string) {
routeGet(() => Promise.resolve({ data: variant }), { headline: 'Canonical headline', sections: [] });
mockedApi.post.mockImplementation((url: string) => Promise.resolve({ data: url === '/cv/ai/assist'
? { result } : { html: '<p>Preview</p>' } } as never));
mockedApi.put.mockImplementation((_url, body) => Promise.resolve({ data: { ...variant, ...(body as object) } } as never));
renderAt(3);
await screen.findByDisplayValue('Backend CV');
fireEvent.click(screen.getByRole('tab', { name: 'AI Tools' }));
fireEvent.click(screen.getAllByRole('button', { name: 'Review suggestion' })[0]);
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/cv/ai/assist', expect.objectContaining({
action: 'document-translate', text: JSON.stringify([{ id: 'headline', items: ['Canonical headline'] }]),
})));
}
test.each(['Apply', 'Reject'])('[PRES-C11 PRES-C07] whole-document AI %s changes only an explicitly accepted CV override', async action => {
await reviewDocumentSuggestion(JSON.stringify([{ id: 'headline', items: ['Reviewed headline'] }]));
expect(await screen.findByText('Review 1 proposed change(s)')).toBeInTheDocument();
expect(screen.getByText('Canonical headline')).toBeInTheDocument();
expect(screen.getByText('Reviewed headline')).toBeInTheDocument();
expect(mockedApi.put).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: action }));
expect(screen.queryByText('Review 1 proposed change(s)')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('tab', { name: 'Content' }));
expect(await screen.findByLabelText('Headline override')).toHaveValue(action === 'Apply' ? 'Reviewed headline' : '');
if (action === 'Apply') {
fireEvent.click(screen.getByRole('button', { name: 'Save now' }));
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith('/cv/variants/3', expect.objectContaining({
settings: expect.objectContaining({ headline: 'Reviewed headline' }),
})));
expect(mockedApi.put.mock.calls.every(([url]) => url === '/cv/variants/3')).toBe(true);
} else {
expect(screen.getByText('Saved')).toBeInTheDocument();
expect(mockedApi.put).not.toHaveBeenCalled();
}
});
test.each([
['invalid JSON', 'not JSON'],
['missing original ID', JSON.stringify([{ id: 'unknown', items: ['Wrong document'] }])],
['duplicate original ID', JSON.stringify([{ id: 'headline', items: ['First'] }, { id: 'headline', items: ['Second'] }])],
])('[PRES-C11] unsafe whole-document response (%s) cannot be applied', async (_case, result) => {
await reviewDocumentSuggestion(result);
expect(await screen.findByText('AI suggestion failed.')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Apply' })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('tab', { name: 'Content' }));
expect(await screen.findByLabelText('Headline override')).toHaveValue('');
expect(mockedApi.put).not.toHaveBeenCalled();
});
@@ -0,0 +1,46 @@
import React from "react";
import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { api } from "./api";
import { I18nProvider } from "./i18n/I18nProvider";
import QuickCommandDialog from "./components/QuickCommandDialog";
const mockedApi = api as jest.Mocked<typeof api>;
beforeEach(() => {
jest.clearAllMocks();
localStorage.removeItem("uiLanguage");
mockedApi.get.mockImplementation((url: string) => Promise.resolve({ data: url === "/companies"
? [{ id: 7, name: "Nordlys AS" }]
: { items: [{ id: 42, jobTitle: "Platform engineer", company: { name: "Nordlys AS" } }] } } as never));
});
test.each([
["Nordlys AS - Platform engineer", "/jobs/42"],
["Nordlys AS", "/companies?edit=7"],
])("[PRES-E07] command search opens %s at its existing destination", async (label, destination) => {
const onNavigate = jest.fn();
const onClose = jest.fn();
render(<I18nProvider><QuickCommandDialog open onClose={onClose} onNavigate={onNavigate} onOpenAddJob={jest.fn()} /></I18nProvider>);
fireEvent.change(screen.getByRole("textbox"), { target: { value: "Nordlys" } });
fireEvent.click(await screen.findByText(label, { exact: true }));
expect(onNavigate).toHaveBeenCalledWith(destination);
expect(onClose).toHaveBeenCalledTimes(1);
expect(mockedApi.post).not.toHaveBeenCalled();
});
test("[PRES-E07 PRES-J04] the capture command opens review without creating a job, and closing clears search", async () => {
const onOpenAddJob = jest.fn();
const props = { onClose: jest.fn(), onNavigate: jest.fn(), onOpenAddJob };
const view = (open: boolean) => <I18nProvider><QuickCommandDialog open={open} {...props} /></I18nProvider>;
const { rerender } = render(view(true));
fireEvent.click(screen.getByRole("button", { name: /add new job/i }));
expect(onOpenAddJob).toHaveBeenCalledTimes(1);
expect(mockedApi.post).not.toHaveBeenCalled();
fireEvent.change(screen.getByRole("textbox"), { target: { value: "Nordlys" } });
await screen.findByText("Nordlys AS - Platform engineer");
rerender(view(false));
rerender(view(true));
await waitFor(() => expect(screen.getByRole("textbox")).toHaveValue(""));
expect(screen.queryByText("Nordlys AS - Platform engineer")).not.toBeInTheDocument();
});
+28 -1
View File
@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { fireEvent, render, screen } from '@testing-library/react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import SettingsView from './components/SettingsView'; import SettingsView from './components/SettingsView';
@@ -136,3 +136,30 @@ test('AI privacy settings are server-backed and external processing is local-onl
externalProcessingAllowed: false, externalProcessingAllowed: false,
}); });
}); });
test('[PRES-S03] failed notification save preserves the draft for an explicit retry', async () => {
mockedApi.put.mockRejectedValueOnce(new Error('offline'));
renderView();
fireEvent.click(screen.getByRole('tab', { name: /notifications/i }));
const preference = await screen.findByLabelText(/email reminders for follow-ups/i);
await waitFor(() => expect(preference).toBeChecked());
fireEvent.click(preference);
fireEvent.click(screen.getByRole('button', { name: /save notification settings/i }));
expect(await screen.findByRole('alert')).toHaveTextContent(/could not|failed/i);
expect(preference).not.toBeChecked();
fireEvent.click(screen.getByRole('button', { name: /save notification settings/i }));
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledTimes(2));
expect(mockedApi.put).toHaveBeenLastCalledWith('/notification-settings', { emailFollowUpRemindersEnabled: false });
await waitFor(() => expect(screen.queryByRole('alert')).not.toBeInTheDocument());
});
test('[PRES-S03 PRES-W11] saving one follow-up rule preserves the other scenarios', async () => {
renderView();
fireEvent.click(screen.getByRole('tab', { name: /follow-ups/i }));
fireEvent.change(await screen.findByLabelText('Applied: follow-up days'), { target: { value: '5' } });
fireEvent.click(screen.getByRole('button', { name: /save rules/i }));
await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith('/rules', {
id: 1, appliedFollowUpDays: 5, appliedGhostDays: 30,
offerFollowUpDays: 7, offerGhostDays: 14, feedbackFollowUpDays: 7, feedbackGhostDays: 14,
}));
});