From bd036ef0f0a11f3bec7f89f078d054036fc564d0 Mon Sep 17 00:00:00 2001
From: Christopher Sellers <1@christophersellers.com>
Date: Mon, 26 Oct 2026 07:29:06 -0700
Subject: [PATCH] fix(ingest): replace DEMO-DOC-ID fallback with empty state
Signed-off-by: Christopher Sellers <1@christophersellers.com>
---
.../pages/ingest-normalized-viewer.test.tsx | 93 +++++++++++++++++++
frontend/src/app/ingest/page.tsx | 13 ++-
2 files changed, 103 insertions(+), 3 deletions(-)
create mode 100644 frontend/src/__tests__/pages/ingest-normalized-viewer.test.tsx
diff --git a/frontend/src/__tests__/pages/ingest-normalized-viewer.test.tsx b/frontend/src/__tests__/pages/ingest-normalized-viewer.test.tsx
new file mode 100644
index 000000000..19d91e38e
--- /dev/null
+++ b/frontend/src/__tests__/pages/ingest-normalized-viewer.test.tsx
@@ -0,0 +1,93 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { fireEvent, render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import IngestPage from '@/app/ingest/page';
+
+const mocks = vi.hoisted(() => ({
+ ingestURL: vi.fn(),
+ ingestFile: vi.fn(),
+ ingestEpcisEvent: vi.fn(),
+ ingestEpcisBatch: vi.fn(),
+ ingestEpcisXml: vi.fn(),
+ validateEpcis: vi.fn(),
+ getIngestionJob: vi.fn(),
+ refresh: vi.fn(),
+ storeApiKey: vi.fn(),
+ urlData: undefined as { job_id?: string; document_id?: string } | undefined,
+}));
+
+vi.mock('@/lib/auth-context', () => ({
+ useAuth: () => ({ apiKey: 'cookie-managed', setApiKey: mocks.storeApiKey }), // pragma: allowlist secret
+}));
+vi.mock('@/hooks/use-api', () => ({
+ useIngestURL: () => ({ mutateAsync: mocks.ingestURL, data: mocks.urlData, reset: vi.fn(), isPending: false, isError: false }),
+ useIngestFile: () => ({ mutateAsync: mocks.ingestFile, reset: vi.fn(), isPending: false, isError: false }),
+ useIngestEpcisEvent: () => ({ mutateAsync: mocks.ingestEpcisEvent, reset: vi.fn(), isPending: false }),
+ useIngestEpcisBatch: () => ({ mutateAsync: mocks.ingestEpcisBatch, reset: vi.fn(), isPending: false }),
+ useIngestEpcisXml: () => ({ mutateAsync: mocks.ingestEpcisXml, reset: vi.fn(), isPending: false }),
+ useValidateEpcisEvent: () => ({ mutateAsync: mocks.validateEpcis, reset: vi.fn(), isPending: false }),
+}));
+vi.mock('@/lib/api-client', () => ({
+ apiClient: { getIngestionJob: mocks.getIngestionJob },
+}));
+vi.mock('@/hooks/use-dashboard-refresh', () => ({
+ notifyDashboardRefresh: mocks.refresh,
+}));
+vi.mock('@/app/ingest/NormalizedDocumentViewer', () => ({
+ NormalizedDocumentViewer: ({ documentId }: { documentId: string }) => (
+
{documentId}
+ ),
+}));
+
+const EMPTY_STATE = 'No normalized document is available for this ingest.';
+
+function mockCompletedUrlIngest(response: { job_id: string; document_id?: string }) {
+ mocks.ingestURL.mockImplementation(async () => {
+ mocks.urlData = response;
+ return response;
+ });
+ mocks.getIngestionJob.mockResolvedValue({ status: 'completed' });
+}
+
+async function submitUrlIngest() {
+ render();
+ fireEvent.change(screen.getByPlaceholderText(/https:\/\//i), {
+ target: { value: `https://example.org/document-${crypto.randomUUID()}.pdf` },
+ });
+ await userEvent.click(screen.getByRole('button', { name: /^ingest document$/i }));
+ // The page polls the job every 2s; the mocked poll resolves completed.
+ expect(await screen.findByText('Ingestion request completed', {}, { timeout: 5000 })).toBeInTheDocument();
+}
+
+describe('completed URL ingest on /ingest', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mocks.urlData = undefined;
+ mocks.storeApiKey.mockResolvedValue(undefined);
+ });
+
+ it('shows an empty state and no viewer when document_id is absent', async () => {
+ mockCompletedUrlIngest({ job_id: crypto.randomUUID() });
+ await submitUrlIngest();
+
+ expect(screen.getByText(EMPTY_STATE)).toBeInTheDocument();
+ expect(screen.queryByTestId('normalized-viewer')).not.toBeInTheDocument();
+ });
+
+ it('shows an empty state and no viewer when document_id is an empty string', async () => {
+ mockCompletedUrlIngest({ job_id: crypto.randomUUID(), document_id: '' });
+ await submitUrlIngest();
+
+ expect(screen.getByText(EMPTY_STATE)).toBeInTheDocument();
+ expect(screen.queryByTestId('normalized-viewer')).not.toBeInTheDocument();
+ });
+
+ it('passes the real document id to the viewer when present', async () => {
+ const documentId = crypto.randomUUID();
+ mockCompletedUrlIngest({ job_id: crypto.randomUUID(), document_id: documentId });
+ await submitUrlIngest();
+
+ expect(screen.queryByText(EMPTY_STATE)).not.toBeInTheDocument();
+ expect(screen.getByTestId('normalized-viewer')).toHaveTextContent(documentId);
+ });
+});
diff --git a/frontend/src/app/ingest/page.tsx b/frontend/src/app/ingest/page.tsx
index b30ade7ea..0fb1d4b45 100644
--- a/frontend/src/app/ingest/page.tsx
+++ b/frontend/src/app/ingest/page.tsx
@@ -411,6 +411,9 @@ export default function IngestPage() {
? 'success'
: 'warning'
: 'neutral';
+ // No fallback id: without a real document_id the completed panel shows an
+ // empty state instead of the viewer.
+ const completedDocumentId = ingestMutation.data?.document_id;
return (
@@ -893,9 +896,13 @@ export default function IngestPage() {
-
+ {typeof completedDocumentId === 'string' && completedDocumentId.length > 0 ? (
+
+ ) : (
+
+ No normalized document is available for this ingest.
+
+ )}
)}