From fd06fbbb5d2ca49388a6de89625060df262b2eb9 Mon Sep 17 00:00:00 2001 From: Daniel Noulet Date: Sat, 19 Sep 2026 21:34:12 +0200 Subject: [PATCH] fix: handle empty candidate trait lists Return empty JSON arrays from the API and normalize nullable values from older servers before rendering candidate match counts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- backend/internal/web/mapper/mapper.go | 10 +++++++++- backend/internal/web/mapper/mapper_test.go | 7 +++++++ frontend/src/App.test.tsx | 13 ++++++++++++- frontend/src/api.ts | 9 ++++++++- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/backend/internal/web/mapper/mapper.go b/backend/internal/web/mapper/mapper.go index 803ae27..67fad0e 100644 --- a/backend/internal/web/mapper/mapper.go +++ b/backend/internal/web/mapper/mapper.go @@ -66,9 +66,17 @@ func ParticipantToWeb(value servicemodel.Participant) webmodel.ParticipantRespon } func CandidateToWeb(value servicemodel.CandidateMatch) webmodel.CandidateResponse { + matched := value.Matched + if matched == nil { + matched = []string{} + } + missing := value.Missing + if missing == nil { + missing = []string{} + } return webmodel.CandidateResponse{ Participant: ParticipantToWeb(value.Participant), Score: value.Score, - Matched: value.Matched, Missing: value.Missing, + Matched: matched, Missing: missing, } } diff --git a/backend/internal/web/mapper/mapper_test.go b/backend/internal/web/mapper/mapper_test.go index d25d429..0c1b70a 100644 --- a/backend/internal/web/mapper/mapper_test.go +++ b/backend/internal/web/mapper/mapper_test.go @@ -19,3 +19,10 @@ func TestInterviewOmitsReferenceAnswer(t *testing.T) { t.Fatalf("expected participant-safe code question, got %#v", response) } } + +func TestCandidateAlwaysUsesJSONArrays(t *testing.T) { + response := CandidateToWeb(servicemodel.CandidateMatch{}) + if response.Matched == nil || response.Missing == nil { + t.Fatalf("candidate trait collections must not be nil: %#v", response) + } +} diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 682bc54..59b0037 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -69,7 +69,7 @@ function mockApi() { participant, score: 90, matchedTraits: ['Go'], - missingTraits: [], + missingTraits: null, }, ]) if (path === '/api/jobs/7/invitations' && method === 'GET') return response([]) @@ -129,6 +129,17 @@ describe('Go Guess frontend', () => { mockApi() }) + it('renders candidate matches when an older API returns null trait arrays', async () => { + authenticate() + const user = userEvent.setup() + renderApp('/jobs/7') + + await user.click(await screen.findByRole('tab', { name: /candidate match/i })) + + expect(await screen.findByText('90%')).toBeInTheDocument() + expect(screen.getByText('Missing').nextElementSibling).toHaveTextContent('0') + }) + it('persists login, navigates from the login URL, and shows API jobs', async () => { const user = userEvent.setup() window.history.replaceState({}, '', '/login') diff --git a/frontend/src/api.ts b/frontend/src/api.ts index f613e2f..e8dcdae 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -115,7 +115,14 @@ export const api = { }), detachQuestion: (jobId: number, questionId: number) => request(`/jobs/${jobId}/questions/${questionId}`, { method: 'DELETE' }), - candidates: (jobId: number) => request(`/jobs/${jobId}/candidates`), + candidates: async (jobId: number) => { + const matches = await request(`/jobs/${jobId}/candidates`) + return matches.map((match) => ({ + ...match, + matchedTraits: match.matchedTraits ?? [], + missingTraits: match.missingTraits ?? [], + })) + }, invitations: (jobId: number) => request(`/jobs/${jobId}/invitations`), invitation: (jobId: number, invitationId: number) => request(`/jobs/${jobId}/invitations/${invitationId}`),