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