diff --git a/frontend/src/__tests__/lib/api-hooks.test.ts b/frontend/src/__tests__/lib/api-hooks.test.ts
index 26017d541..3fd63c52c 100644
--- a/frontend/src/__tests__/lib/api-hooks.test.ts
+++ b/frontend/src/__tests__/lib/api-hooks.test.ts
@@ -75,6 +75,25 @@ describe('internal api hooks', () => {
expect(new Headers(scoreInit.headers).get('X-RegEngine-API-Key')).toBeNull();
});
+ it('rejects a 401 on workbench readiness once, with a machine-readable auth status (SEL-616)', async () => {
+ fetchWithCsrfMock.mockResolvedValue({
+ ok: false,
+ status: 401,
+ statusText: 'Unauthorized',
+ headers: new Headers(),
+ });
+
+ const { fetchWorkbenchReadinessSummary } = await import('@/lib/api-hooks');
+ const { getErrorStatus, isAuthError } = await import('@/lib/api-error');
+ const error = await fetchWorkbenchReadinessSummary('tenant-123').catch((err: unknown) => err);
+
+ expect(error).toBeInstanceOf(Error);
+ expect(getErrorStatus(error)).toBe(401);
+ expect(isAuthError(error)).toBe(true);
+ // 401 is final: no retry/backoff on the readiness path.
+ expect(fetchWithCsrfMock).toHaveBeenCalledTimes(1);
+ });
+
it('throws client errors immediately without retrying them', async () => {
fetchWithCsrfMock.mockResolvedValue({
ok: false,
diff --git a/frontend/src/__tests__/pages/dashboard-load-errors.test.tsx b/frontend/src/__tests__/pages/dashboard-load-errors.test.tsx
index e7e3d297a..f8dc86ae3 100644
--- a/frontend/src/__tests__/pages/dashboard-load-errors.test.tsx
+++ b/frontend/src/__tests__/pages/dashboard-load-errors.test.tsx
@@ -53,7 +53,11 @@ vi.mock('@/hooks/use-agent-reviews', () => ({
vi.mock('@/hooks/use-dashboard-refresh', () => ({ useDashboardRefresh: vi.fn() }));
vi.mock('next/link', () => ({ default: ({ children, href }: { children: React.ReactNode; href: string }) => {children} }));
vi.mock('framer-motion', () => ({
- motion: { div: ({ children, ...props }: React.PropsWithChildren>) => {children}
},
+ motion: {
+ div: ({ children }: React.PropsWithChildren>) => {children}
,
+ li: ({ children }: React.PropsWithChildren>) => {children},
+ circle: () => ,
+ },
AnimatePresence: ({ children }: React.PropsWithChildren) => <>{children}>,
}));
@@ -223,11 +227,47 @@ describe('shared dashboard load errors', () => {
const user = userEvent.setup();
renderWithQuery();
- expect(await screen.findByRole('alert')).toHaveTextContent('We could not load this page.');
+ expect(await screen.findByRole('alert')).toHaveTextContent('We could not load the compliance score.');
+ // SEL-616: score failure must not blank Import readiness / setup paths
+ expect(await screen.findByText('Import readiness')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Retry' }));
await waitFor(() => expect(fetchComplianceScoreMock).toHaveBeenCalledTimes(2));
});
+ it('keeps Import readiness visible when score and readiness both fail (SEL-616)', async () => {
+ fetchComplianceScoreMock.mockRejectedValue(apiError(401, 'Unauthorized', crypto.randomUUID()));
+ fetchWorkbenchSummaryMock.mockRejectedValue(apiError(503, 'Service Unavailable', crypto.randomUUID()));
+ renderWithQuery();
+
+ expect(await screen.findByRole('alert')).toHaveTextContent('We could not load the compliance score.');
+ expect(await screen.findByText('Import readiness')).toBeInTheDocument();
+ expect(screen.getByText(/Import readiness is unavailable right now/i)).toBeInTheDocument();
+ expect(screen.queryByText('We could not load this page.')).not.toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Retry import readiness' })).toBeInTheDocument();
+ });
+
+ it('rejects a 401 on readiness without showing placeholder readiness data (SEL-616)', async () => {
+ fetchComplianceScoreMock.mockResolvedValue({
+ tenant_id: 't',
+ overall_score: 82,
+ grade: 'B',
+ breakdown: {},
+ next_actions: [],
+ events_analyzed: 10,
+ last_chain_hash: null,
+ });
+ fetchWorkbenchSummaryMock.mockRejectedValue(apiError(401, 'Unauthorized', crypto.randomUUID()));
+ renderWithQuery();
+
+ expect(await screen.findByText(/Import readiness is unavailable right now/i)).toBeInTheDocument();
+ // An unauthorized readiness request must not render as an empty-but-healthy workspace.
+ expect(screen.queryByText('No saved preflight run yet')).not.toBeInTheDocument();
+ expect(screen.queryByText(/open fix/)).not.toBeInTheDocument();
+ expect(screen.queryByText('We could not load this page.')).not.toBeInTheDocument();
+ expect(screen.queryByText('We could not load the compliance score.')).not.toBeInTheDocument();
+ expect(fetchWorkbenchSummaryMock).toHaveBeenCalledTimes(1);
+ });
+
it('lets recall report retry a failed request in place', async () => {
fetchRecallReportMock.mockRejectedValue(new Error('server error'));
const user = userEvent.setup();
diff --git a/frontend/src/app/dashboard/compliance/page.tsx b/frontend/src/app/dashboard/compliance/page.tsx
index d439be603..2cd1f241a 100644
--- a/frontend/src/app/dashboard/compliance/page.tsx
+++ b/frontend/src/app/dashboard/compliance/page.tsx
@@ -321,6 +321,7 @@ export default function ComplianceDashboardPage() {
const {
data: workbenchSummary = null,
+ isError: readinessFailed,
refetch: fetchWorkbenchSummary,
} = useQuery({
queryKey: ['inflow-workbench-readiness', tenantId],
@@ -330,7 +331,8 @@ export default function ComplianceDashboardPage() {
retry: false,
});
- const error = scoreError;
+ // Score and workbench failures stay sectional so one outage cannot blank the page
+ // (SEL-616: readiness/summary 503 must not look like a full Compliance crash).
const lastFetched = dataUpdatedAt ? new Date(dataUpdatedAt) : null;
// Ordered breakdown keys by weight (highest first)
@@ -381,12 +383,12 @@ export default function ComplianceDashboardPage() {
- {lastFetched && !error && (
+ {lastFetched && !scoreError && (
Updated {lastFetched.toLocaleTimeString()}
)}
- {score && !error && (
+ {score && !scoreError && (