diff --git a/.gitea/workflows/ci-deploy.yml b/.gitea/workflows/ci-deploy.yml index 3ac0a66..5e0f5ac 100644 --- a/.gitea/workflows/ci-deploy.yml +++ b/.gitea/workflows/ci-deploy.yml @@ -217,6 +217,11 @@ jobs: echo "Frontend lint process crashed with SIGSEGV — retrying once..." 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 working-directory: job-tracker-ui # Run the WHOLE suite. Never whitelist test files here again: the previous diff --git a/docs/redesign/audit/README.md b/docs/redesign/audit/README.md index 2c8790d..c0cce70 100644 --- a/docs/redesign/audit/README.md +++ b/docs/redesign/audit/README.md @@ -1,5 +1,7 @@ # 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. **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. diff --git a/docs/redesign/phase-2/BASELINE_DEFECTS.md b/docs/redesign/phase-2/BASELINE_DEFECTS.md new file mode 100644 index 0000000..5b97749 --- /dev/null +++ b/docs/redesign/phase-2/BASELINE_DEFECTS.md @@ -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. diff --git a/docs/redesign/phase-2/PRESERVATION_MATRIX.md b/docs/redesign/phase-2/PRESERVATION_MATRIX.md new file mode 100644 index 0000000..5142cee --- /dev/null +++ b/docs/redesign/phase-2/PRESERVATION_MATRIX.md @@ -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 diff --git a/docs/redesign/phase-2/README.md b/docs/redesign/phase-2/README.md new file mode 100644 index 0000000..0baf53c --- /dev/null +++ b/docs/redesign/phase-2/README.md @@ -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. diff --git a/docs/redesign/phase-2/TRACEABILITY.md b/docs/redesign/phase-2/TRACEABILITY.md new file mode 100644 index 0000000..23ce3ff --- /dev/null +++ b/docs/redesign/phase-2/TRACEABILITY.md @@ -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. diff --git a/docs/todo/work.md b/docs/todo/work.md index dbb4b71..464665a 100644 --- a/docs/todo/work.md +++ b/docs/todo/work.md @@ -1,13 +1,15 @@ # 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] 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. -- [ ] 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 -- [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. diff --git a/job-tracker-ui/e2e/redesign-preservation.spec.ts b/job-tracker-ui/e2e/redesign-preservation.spec.ts new file mode 100644 index 0000000..8497ce9 --- /dev/null +++ b/job-tracker-ui/e2e/redesign-preservation.spec.ts @@ -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 }>({ + 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([]); +}); diff --git a/job-tracker-ui/package.json b/job-tracker-ui/package.json index ca4154c..15e402c 100644 --- a/job-tracker-ui/package.json +++ b/job-tracker-ui/package.json @@ -35,6 +35,7 @@ "build": "node --max-old-space-size=4096 ./node_modules/next/dist/bin/next build", "lint": "eslint . --max-warnings=0", "lint:fix": "eslint . --fix", + "typecheck": "tsc --noEmit --incremental false", "test": "jest", "test:pdf-admission": "playwright test --config=playwright.pdf.config.ts", "test:rate-limits": "playwright test --config=playwright.rate-limits.config.ts", diff --git a/job-tracker-ui/src/ai-section-assistant.test.tsx b/job-tracker-ui/src/ai-section-assistant.test.tsx index 102b64c..5259943 100644 --- a/job-tracker-ui/src/ai-section-assistant.test.tsx +++ b/job-tracker-ui/src/ai-section-assistant.test.tsx @@ -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.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(); +}); diff --git a/job-tracker-ui/src/application-assets.test.tsx b/job-tracker-ui/src/application-assets.test.tsx index 34c17f8..29b6d0c 100644 --- a/job-tracker-ui/src/application-assets.test.tsx +++ b/job-tracker-ui/src/application-assets.test.tsx @@ -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(); + 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 () => { mockedApi.get.mockRejectedValue(new Error("boom")); diff --git a/job-tracker-ui/src/cv-builder-deep-link.test.tsx b/job-tracker-ui/src/cv-builder-deep-link.test.tsx index fb80d86..f1b7bd2 100644 --- a/job-tracker-ui/src/cv-builder-deep-link.test.tsx +++ b/job-tracker-ui/src/cv-builder-deep-link.test.tsx @@ -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'); 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: '

Preview

' } } 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(); +}); diff --git a/job-tracker-ui/src/quick-command-preservation.test.tsx b/job-tracker-ui/src/quick-command-preservation.test.tsx new file mode 100644 index 0000000..4895485 --- /dev/null +++ b/job-tracker-ui/src/quick-command-preservation.test.tsx @@ -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; + +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(); + 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) => ; + 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(); +}); diff --git a/job-tracker-ui/src/settings-view.test.tsx b/job-tracker-ui/src/settings-view.test.tsx index faf91cf..96b6f59 100644 --- a/job-tracker-ui/src/settings-view.test.tsx +++ b/job-tracker-ui/src/settings-view.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; 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 SettingsView from './components/SettingsView'; @@ -136,3 +136,30 @@ test('AI privacy settings are server-backed and external processing is local-onl 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, + })); +});