diff --git a/job-tracker-ui/src/application-assets.test.tsx b/job-tracker-ui/src/application-assets.test.tsx
index c0d8322..9f98b48 100644
--- a/job-tracker-ui/src/application-assets.test.tsx
+++ b/job-tracker-ui/src/application-assets.test.tsx
@@ -74,8 +74,10 @@ test("cv section shows the attached variant and the ones available to attach", a
expect(await screen.findByText("Backend CV")).toBeInTheDocument();
expect(screen.getByText(/Theme nordic ยท version 4/)).toBeInTheDocument();
+ // Must be the builder's real route (/career/builder/:id). An href the router does not serve
+ // silently dead-ends the user, which is exactly what shipped before this test existed.
expect(screen.getByRole("link", { name: /Edit, preview and export/i })).toHaveAttribute(
- "href", "/cv-builder?variant=3");
+ "href", "/career/builder/3");
});
test("attaching a different variant only re-points the application", async () => {
diff --git a/job-tracker-ui/src/components/ApplicationAssets.tsx b/job-tracker-ui/src/components/ApplicationAssets.tsx
index 295fb80..c12a40e 100644
--- a/job-tracker-ui/src/components/ApplicationAssets.tsx
+++ b/job-tracker-ui/src/components/ApplicationAssets.tsx
@@ -148,7 +148,9 @@ export function ApplicationCvSection({ jobId }: { jobId: number }) {
size="small"
variant="outlined"
endIcon={}
- href={`/cv-builder?variant=${data.attachedVariantId}`}
+ // The builder's real route. It loads the variant from :id and enforces ownership
+ // server-side, so there is no second CV loading path here.
+ href={`/career/builder/${data.attachedVariantId}`}
>
Edit, preview and export
diff --git a/job-tracker-ui/src/cv-builder-deep-link.test.tsx b/job-tracker-ui/src/cv-builder-deep-link.test.tsx
new file mode 100644
index 0000000..90d6ac0
--- /dev/null
+++ b/job-tracker-ui/src/cv-builder-deep-link.test.tsx
@@ -0,0 +1,93 @@
+import React from 'react';
+import '@testing-library/jest-dom';
+import { render, screen } from '@testing-library/react';
+import { MemoryRouter, Route, Routes } from 'react-router-dom';
+
+import CvBuilderEditor from './views/CvBuilderEditor';
+import { I18nProvider } from './i18n/I18nProvider';
+import { ToastProvider } from './toast';
+import { api } from './api';
+import { emptyCvVariantSettings } from './cvBuilder';
+
+// The Application Workspace CV section deep-links straight into the builder at
+// /career/builder/:id. These tests cover that entry point: the variant loads, and a variant the
+// server refuses (missing, or someone else's) lands on a safe error state rather than a blank editor.
+jest.mock('./api', () => ({
+ api: {
+ get: jest.fn(),
+ post: jest.fn(),
+ put: jest.fn(),
+ delete: jest.fn(),
+ interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } },
+ },
+ getApiErrorMessage: (_e: any, fallback?: string) => fallback || 'Request failed.',
+}));
+
+const mockedApi = api as jest.Mocked;
+
+const variant = {
+ id: 3,
+ name: 'Backend CV',
+ settings: emptyCvVariantSettings(),
+ isPublic: false,
+ publicSlug: 'abc123',
+ version: 4,
+ jobApplicationId: 7,
+ updatedAtUtc: '2026-07-19T10:00:00Z',
+};
+
+function renderAt(id: number) {
+ return render(
+
+
+
+
+ } />
+
+
+
+ ,
+ );
+}
+
+beforeEach(() => {
+ jest.clearAllMocks();
+ mockedApi.post.mockResolvedValue({ data: { themeId: 'nordic', html: 'cv
', suggestedFileName: 'cv.pdf' } } as any);
+});
+
+function routeGet(onVariant: () => Promise) {
+ mockedApi.get.mockImplementation((url: string) => {
+ if (url === '/cv/variants/3') return onVariant();
+ if (url === '/cv/themes') return Promise.resolve({ data: [] } as any);
+ if (url === '/cv/outline') return Promise.resolve({ data: { sections: [] } } as any);
+ return Promise.resolve({ data: [] } as any);
+ });
+}
+
+test('deep link loads the exact variant named in the route', async () => {
+ routeGet(() => Promise.resolve({ data: variant } as any));
+
+ renderAt(3);
+
+ expect(await screen.findByDisplayValue('Backend CV')).toBeInTheDocument();
+ expect(mockedApi.get).toHaveBeenCalledWith('/cv/variants/3');
+});
+
+test('a variant that does not exist shows a safe error instead of an empty editor', async () => {
+ routeGet(() => Promise.reject(new Error('404')));
+
+ renderAt(3);
+
+ expect(await screen.findByText(/Could not open this CV/i)).toBeInTheDocument();
+ expect(screen.queryByDisplayValue('Backend CV')).not.toBeInTheDocument();
+});
+
+test("another user's variant is refused by the server and never rendered", async () => {
+ // Ownership is enforced server-side: CvVariantService scopes every read to the owner and the
+ // controller returns 404, so the client only ever sees the same safe failure.
+ routeGet(() => Promise.reject({ response: { status: 404 } }));
+
+ renderAt(3);
+
+ expect(await screen.findByText(/Could not open this CV/i)).toBeInTheDocument();
+});