feat(career): wire /career to the relational profile API + completeness overview
CI and Deploy / test (push) Failing after 1m52s
CI and Deploy / deploy (push) Has been skipped

Phase 3, frontend. /career now reads and writes the master profile through the
relational source of truth instead of the legacy blob path.

- CareerProfilePage loads GET /career/profile (structured profile from the
  relational children + cvText + completeness) and saves PUT /career/profile
  ({ profile, cvText }). This keeps the relational store authoritative — the
  previous PUT /auth/profile blob write left it stale after first load.
- Added a "Profile completeness" overview (percent bar + missing sections) at the
  top of /career, from the server scorecard.
- PUT /career/profile now accepts { profile, cvText } so the single /career save
  covers both the structured profile and the raw imported text; GET returns cvText.

Tests: career-save asserts the /career/profile payload; new completeness-overview
test; controller tests updated for the request wrapper. 75/76 frontend pass (the
1 failure is the unrelated pre-existing settings-view suite); prod build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-18 00:53:35 +02:00
parent b203120ab4
commit 2b57d65715
4 changed files with 84 additions and 29 deletions
+24 -8
View File
@@ -96,6 +96,16 @@ void ProfilePage;
beforeEach(() => {
mockedApi.get.mockImplementation((url: string) => {
if (url === '/career/profile') {
// Phase 3: /career reads the structured profile from the relational source of truth.
return Promise.resolve({
data: {
profile: structuredCv,
cvText: 'Professional Summary\nBuilt backend systems',
completeness: { percent: 70, missing: ['Projects'], sections: [] },
},
} as any);
}
if (url === '/auth/me') {
return Promise.resolve({
data: {
@@ -289,9 +299,10 @@ test('profile page rewrite tools use selected template and saved job context', a
await waitFor(() => expect(createObjectURLMock).toHaveBeenCalledTimes(REWRITE_TEMPLATES_COUNT));
});
test('saving the master profile (career) persists structured cv json', async () => {
// Phase 2: the master-profile save lives on /career (CareerProfilePage). It sends only the CV
// fields — identity is saved separately on /profile — so the payload carries profileCvStructureJson.
test('saving the master profile (career) persists the structured profile via /career/profile', async () => {
// Phase 3: /career saves the master profile through the relational API. The payload carries the
// structured profile object (not the legacy flat blob) and never identity fields.
mockedApi.put.mockResolvedValue({ data: { profile: structuredCv, completeness: { percent: 70, missing: [], sections: [] }, cvText: '' } } as any);
renderWith(CareerProfilePage);
expect(await screen.findByText(/cv ready/i)).toBeInTheDocument();
@@ -303,15 +314,20 @@ test('saving the master profile (career) persists structured cv json', async ()
fireEvent.click(saveButton);
await waitFor(() => {
expect(mockedApi.put).toHaveBeenCalled();
expect(mockedApi.put).toHaveBeenCalledWith('/career/profile', expect.anything());
});
const payload = mockedApi.put.mock.calls[0][1] as any;
const parsed = JSON.parse(payload.profileCvStructureJson);
expect(parsed.contact.fullName).toBe('Updated Demo User');
expect(parsed.skills).toEqual(['.NET', 'SQL']);
expect(parsed.jobs[0].title).toBe('System Developer');
expect(payload.profile.contact.fullName).toBe('Updated Demo User');
expect(payload.profile.skills).toEqual(['.NET', 'SQL']);
expect(payload.profile.jobs[0].title).toBe('System Developer');
// The career save must NOT carry identity fields (they belong to /profile).
expect(payload.email).toBeUndefined();
expect(payload.displayName).toBeUndefined();
});
test('/career shows the profile completeness overview', async () => {
renderWith(CareerProfilePage);
expect(await screen.findByText(/profile completeness/i)).toBeInTheDocument();
expect(screen.getByText(/70%/)).toBeInTheDocument();
});