diff --git a/frontend/src/components/ScanComparisonSheet.tsx b/frontend/src/components/ScanComparisonSheet.tsx index a96131a5..7b644af1 100644 --- a/frontend/src/components/ScanComparisonSheet.tsx +++ b/frontend/src/components/ScanComparisonSheet.tsx @@ -241,9 +241,14 @@ export function ScanComparisonSheet({ size="sm" className="h-7 text-xs px-2.5" onClick={() => { setFilter('unchanged'); setPage(0); }} + title={ + crossImage + ? 'Same CVE and package name appear in both images. Because the images differ, this does not necessarily mean the finding is literally unchanged.' + : 'Findings present in both scans' + } > - Unchanged ({data.unchanged.length}) + {crossImage ? 'Shared' : 'Unchanged'} ({data.unchanged.length}) {needsPagination && (
@@ -349,7 +354,7 @@ export function ScanComparisonSheet({ {filter === 'unchanged' && ( - Unchanged + {crossImage ? 'Shared' : 'Unchanged'} )} diff --git a/frontend/src/components/__tests__/ScanComparisonSheet.test.tsx b/frontend/src/components/__tests__/ScanComparisonSheet.test.tsx index 9db1300f..2e9849d7 100644 --- a/frontend/src/components/__tests__/ScanComparisonSheet.test.tsx +++ b/frontend/src/components/__tests__/ScanComparisonSheet.test.tsx @@ -171,6 +171,41 @@ describe('ScanComparisonSheet', () => { expect(screen.queryByText(/findings per scan/i)).toBeNull(); }); + it('relabels the unchanged bucket as "Shared" for cross-image comparisons', async () => { + mockedFetch.mockResolvedValueOnce( + jsonResponse(200, result({ + scanB: { id: 2, image_ref: 'alpine:3.19', scanned_at: 1_700_000_010_000 }, + unchanged: [vuln({ vulnerability_id: 'CVE-U', pkg_name: 'pu' })], + })), + ); + + const user = userEvent.setup(); + render( {}} />); + + await waitFor(() => + expect(screen.getByRole('button', { name: /Shared \(1\)/ })).toBeInTheDocument(), + ); + expect(screen.queryByRole('button', { name: /Unchanged/ })).toBeNull(); + + await user.click(screen.getByRole('button', { name: /Shared \(1\)/ })); + expect(screen.getByText('Shared')).toBeInTheDocument(); + }); + + it('keeps the "Unchanged" label when both scans use the same image', async () => { + mockedFetch.mockResolvedValueOnce( + jsonResponse(200, result({ + unchanged: [vuln({ vulnerability_id: 'CVE-U', pkg_name: 'pu' })], + })), + ); + + render( {}} />); + + await waitFor(() => + expect(screen.getByRole('button', { name: /Unchanged \(1\)/ })).toBeInTheDocument(), + ); + expect(screen.queryByRole('button', { name: /Shared/ })).toBeNull(); + }); + it('reloads when the scan ids change', async () => { mockedFetch.mockResolvedValue(jsonResponse(200, result()));