test(release): strengthen production verification
CI and Deploy / test (pull_request) Successful in 5m35s
CI and Deploy / test (push) Successful in 5m17s
CI and Deploy / deploy (pull_request) Has been skipped
CI and Deploy / deploy (push) Failing after 8s

This commit is contained in:
cesnimda
2026-08-24 20:21:37 +02:00
parent 367b70681a
commit 44dba5d08d
16 changed files with 62 additions and 16 deletions
+39
View File
@@ -1,3 +1,42 @@
# Active implementation checklist — 2026-08-24
## Current
- [ ] Deploy the verified commits through the protected `main` workflow.
## Next
- [ ] Verify the live health/version endpoints and production UI after deployment.
## Completed
- [x] Revoke all sessions and trusted devices after administrator role mutations.
- [x] Isolate saved views, profile headlines and company caches by authenticated account.
- [x] Prevent stale resource requests from overwriting newer account/filter results.
- [x] Stream and cap remote job-import and CV summarizer inputs before parsing.
- [x] Apply workflow-readiness filters before pagination on the server.
- [x] Repair the CV builder unsaved-change navigation regression.
- [x] Restore the correspondence inbox to primary navigation and fix connected-account route metadata.
- [x] Bring recruiter status suggestions and durable strategy snapshots into the live application workspace.
- [x] Replace local-only notification toggles with a persisted per-user email reminder preference enforced by the worker.
- [x] Rebuild the CV editor as a responsive document studio with professional toolbar, autosave retry state, undo/redo and mobile edit/preview panes.
- [x] Add normalized colour, typography, spacing, page, date, language, skills and one/two-column layout controls shared by preview and PDF rendering.
- [x] Add real A4/Letter page stacks, overflow detection, fit/zoom/page navigation and long-content pagination safeguards.
- [x] Expand the structured master profile with GitHub/custom links and English/Norwegian labels/date rendering.
- [x] Upgrade CV management with a guided create flow, rendered template previews, useful job metadata, rename, duplicate, download and confirmed delete actions.
- [x] Add eight distinct data-driven templates, including ATS-safe, compact/two-column and photo-capable designs without duplicating renderer implementations.
- [x] Add explicit-review AI writing suggestions and one-click job-specific CV duplication/association.
- [x] Enforce server-side ownership for CV/job associations and revoke sessions after administrator role changes.
- [x] Make the standalone TypeScript check pass and remove obsolete React Router test flags.
- [x] Pass all 61 frontend suites (246 tests), standalone TypeScript, optimized production build, Python syntax, Compose config and diff checks.
- [x] Install .NET SDK 9.0.317 and configure the repository Git author from its established commit history.
- [x] Pass all 692 backend tests, 24 AI-service tests, 9 Playwright flows, dependency audits and all three production Docker image builds.
- [x] Reduce the frontend Docker build context from roughly 662 MB to about 10 KB by excluding generated artifacts.
- [x] Commit the release in three logical, fully verified batches.
- [x] Update CV architecture and verification notes with the rebuilt workflow and remaining environment gates.
---
Continue working on JobTracker using the completed audit under `docs/audits/`.
This is now an implementation task. Read all audit reports, evidence, repository instructions, architecture documentation, and the current git status before changing code.
+5
View File
@@ -1,5 +1,10 @@
node_modules
build
.next
out
coverage
test-results
playwright-report
.git
.vscode
npm-debug.log
+1
View File
@@ -16,6 +16,7 @@ test.beforeEach(async ({ page }) => {
});
test("discovery remains usable across widths and opens the reviewed import", async ({ page }) => {
test.setTimeout(60_000);
await page.route("**/api/job-discovery/search**", async (route) => {
await route.fulfill({ json: [
{
@@ -31,7 +31,7 @@ describe("LandingPage forwards deep-link intent to /login", () => {
mockedApi.get.mockRejectedValueOnce(new Error("401")); // /auth/me: not signed in
render(
<MemoryRouter initialEntries={[{ pathname: "/", state: { from: "/jobs/42" } }]} future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<MemoryRouter initialEntries={[{ pathname: "/", state: { from: "/jobs/42" } }]}>
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/login" element={<LoginStub />} />
@@ -232,6 +232,7 @@ test("hydrates list filters, sort and page from a shareable URL", async () => {
companyId: 1,
location: "Oslo",
needsFollowUp: true,
readiness: "interview",
includeDeleted: true,
sortBy: "company",
sortDir: "asc",
@@ -27,7 +27,7 @@ function renderPage() {
<ToastProvider>
<ConfirmProvider>
<I18nProvider>
<MemoryRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<MemoryRouter>
<CorrespondenceInboxPage />
</MemoryRouter>
</I18nProvider>
@@ -480,7 +480,7 @@ describe('CorrespondenceInboxPage', () => {
<ToastProvider>
<ConfirmProvider>
<I18nProvider>
<MemoryRouter initialEntries={['/correspondence/review']} future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<MemoryRouter initialEntries={['/correspondence/review']}>
<Routes>
<Route path="/correspondence/review" element={<Navigate to="/correspondence?view=review" replace />} />
<Route path="/correspondence" element={<CorrespondenceInboxPage />} />
@@ -163,7 +163,7 @@ function renderLoop(initialPath: string) {
<I18nProvider>
<ConfirmProvider>
<PromptProvider>
<MemoryRouter initialEntries={[initialPath]} future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<MemoryRouter initialEntries={[initialPath]}>
<LocationIndicator />
<Routes>
<Route path="/dashboard" element={<DashboardView />} />
@@ -65,7 +65,7 @@ function renderLoop(initialPath = '/jobs') {
<I18nProvider>
<ConfirmProvider>
<PromptProvider>
<MemoryRouter initialEntries={[initialPath]} future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<MemoryRouter initialEntries={[initialPath]}>
<LocationIndicator />
<Routes>
<Route path="/jobs" element={<JobTable refreshToken={0} pageSize={15} onPageSizeChange={() => {}} columns={{ status: true, dateApplied: true, daysSince: true, jobUrl: false }} onColumnsChange={() => {}} mode="jobs" />} />
@@ -25,7 +25,7 @@ function renderPage() {
return render(
<ToastProvider>
<I18nProvider>
<MemoryRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<MemoryRouter>
<GmailReviewPage />
</MemoryRouter>
</I18nProvider>
+4 -4
View File
@@ -12,7 +12,7 @@ afterEach(() => jest.clearAllMocks());
test("shows honest source metadata and offers the existing reviewed save flow", async () => {
mockedApi.get.mockResolvedValue({ data: [{ id: "abc", title: "Backend Developer", company: "Acme", location: "OSLO", modifiedAt: "2026-08-01T10:00:00Z", deadline: "2026-08-15T23:59:59Z", url: "https://arbeidsplassen.nav.no/stillinger/stilling/abc", source: "nav", sourceName: "NAV Arbeidsplassen", acquisitionType: "searched", retrievedAt: "2026-08-10T10:00:00Z", countryCode: "NO" }] } as any);
render(<MemoryRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}><JobDiscoveryPage /></MemoryRouter>);
render(<MemoryRouter><JobDiscoveryPage /></MemoryRouter>);
fireEvent.change(screen.getByLabelText("Role or company"), { target: { value: "backend" } });
fireEvent.click(screen.getByRole("button", { name: "Search NAV" }));
@@ -27,7 +27,7 @@ test("shows honest source metadata and offers the existing reviewed save flow",
test("distinguishes the initial guidance from a completed empty search", async () => {
mockedApi.get.mockResolvedValue({ data: [] } as any);
render(<MemoryRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}><JobDiscoveryPage /></MemoryRouter>);
render(<MemoryRouter><JobDiscoveryPage /></MemoryRouter>);
expect(screen.getByText("Search recent active vacancies by role, company, or municipality.")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Search NAV" }));
@@ -38,7 +38,7 @@ test("distinguishes the initial guidance from a completed empty search", async (
test("retries the last submitted search after an error", async () => {
mockedApi.get.mockRejectedValueOnce(new Error("offline")).mockResolvedValueOnce({ data: [] } as any);
render(<MemoryRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}><JobDiscoveryPage /></MemoryRouter>);
render(<MemoryRouter><JobDiscoveryPage /></MemoryRouter>);
fireEvent.change(screen.getByLabelText("Role or company"), { target: { value: " utvikler " } });
fireEvent.change(screen.getByLabelText("Municipality"), { target: { value: " Oslo " } });
@@ -54,7 +54,7 @@ test("sorts returned vacancies by title", async () => {
{ id: "2", title: "Zoologist", url: "https://arbeidsplassen.nav.no/2", source: "nav", countryCode: "NO" },
{ id: "1", title: "Analyst", url: "https://arbeidsplassen.nav.no/1", source: "nav", countryCode: "NO" },
] } as any);
render(<MemoryRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}><JobDiscoveryPage /></MemoryRouter>);
render(<MemoryRouter><JobDiscoveryPage /></MemoryRouter>);
fireEvent.click(screen.getByRole("button", { name: "Search NAV" }));
await screen.findByText("2 vacancies found");
+1 -1
View File
@@ -21,7 +21,7 @@ beforeEach(() => {
test("shows exactly Free and Pro without invented commercial terms", async () => {
render(
<MemoryRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<MemoryRouter>
<LandingPage />
</MemoryRouter>,
);
+1 -1
View File
@@ -33,7 +33,7 @@ let consoleErrorSpy: jest.SpyInstance;
function renderLoginPage(initialMode: "login" | "register" = "login", from?: string) {
return render(
<MemoryRouter initialEntries={[{ pathname: "/login", state: from ? { from } : undefined }]} future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<MemoryRouter initialEntries={[{ pathname: "/login", state: from ? { from } : undefined }]}>
<I18nProvider>
<ToastProvider>
<LoginPage initialMode={initialMode} />
@@ -27,7 +27,7 @@ test("legacy Microsoft recovery requires the emailed proof and a fresh matching
window.history.pushState({}, "", "/microsoft-legacy-relink?userId=user-1&tenantId=tenant-1&objectId=object-1&token=recovery-token");
render(
<MemoryRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<MemoryRouter>
<I18nProvider><MicrosoftLegacyRelinkPage /></I18nProvider>
</MemoryRouter>,
);
+1 -1
View File
@@ -4,5 +4,5 @@ import path from "node:path";
test("canonical-host health reaches the backend instead of the SPA fallback", () => {
const config = fs.readFileSync(path.join(process.cwd(), "nginx.conf.template"), "utf8");
expect(config).toMatch(/location = \/health\s*{[^}]*proxy_pass http:\/\/backend-web:8080\/health;/s);
expect(config).toMatch(/location = \/health\s*{[\s\S]*?proxy_pass http:\/\/backend-web:8080\/health;/);
});
@@ -14,7 +14,7 @@ jest.mock("./api", () => ({
const mockedApi = api as jest.Mocked<typeof api>;
function renderChecklist() {
return render(<MemoryRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}><I18nProvider><OnboardingChecklist hasJobs={false} /></I18nProvider></MemoryRouter>);
return render(<MemoryRouter><I18nProvider><OnboardingChecklist hasJobs={false} /></I18nProvider></MemoryRouter>);
}
test("uses structured career data and connected inbox status for onboarding progress", async () => {
@@ -16,7 +16,7 @@ function renderVerifyEmailPage(search: string, emailChange = false) {
const path = emailChange ? '/confirm-email-change' : '/verify-email';
window.history.pushState({}, '', `${path}${search}`);
return render(
<MemoryRouter initialEntries={[`${path}${search}`]} future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<MemoryRouter initialEntries={[`${path}${search}`]}>
<I18nProvider>
<VerifyEmailPage emailChange={emailChange} />
</I18nProvider>