diff --git a/src/app/api/documents/route.ts b/src/app/api/documents/route.ts index 1cdc2f6..fa34474 100644 --- a/src/app/api/documents/route.ts +++ b/src/app/api/documents/route.ts @@ -67,8 +67,6 @@ export async function GET(req: NextRequest) { size: number; lastModified: number; filePath: string; - parseState: string | null; - parsedJsonKey: string | null; }>; const results: BaseDocument[] = rows.map((doc) => { @@ -79,8 +77,6 @@ export async function GET(req: NextRequest) { size: Number(doc.size), lastModified: Number(doc.lastModified), type, - parseStatus: null, - parsedJsonKey: null, scope: 'user', }; }); diff --git a/src/components/doclist/views/GalleryView.tsx b/src/components/doclist/views/GalleryView.tsx index 0bc609e..6d2bf6a 100644 --- a/src/components/doclist/views/GalleryView.tsx +++ b/src/components/doclist/views/GalleryView.tsx @@ -28,14 +28,6 @@ function formatDateTime(value: number | undefined): string { }); } -function formatParseStatus(status: DocumentListDocument['parseStatus']): string { - if (!status) return 'N/A'; - if (status === 'pending') return 'Pending'; - if (status === 'running') return 'Running'; - if (status === 'ready') return 'Ready'; - return 'Failed'; -} - function KindIcon({ doc, className }: { doc: DocumentListDocument; className?: string }) { if (doc.type === 'pdf') return ; if (doc.type === 'epub') return ; @@ -244,12 +236,6 @@ export function GalleryView({ )} - {activeDoc.type === 'pdf' && ( - <> -
Parse status
-
{formatParseStatus(activeDoc.parseStatus)}
- - )} ) : ( diff --git a/src/types/documents.ts b/src/types/documents.ts index ac870d5..98802b5 100644 --- a/src/types/documents.ts +++ b/src/types/documents.ts @@ -7,8 +7,6 @@ export interface BaseDocument { lastModified: number; recentlyOpenedAt?: number; type: DocumentType; - parseStatus?: 'pending' | 'running' | 'ready' | 'failed' | null; - parsedJsonKey?: string | null; scope?: 'user'; folderId?: string; } diff --git a/tests/unit/pdf-parse-client-lifecycle.vitest.spec.ts b/tests/unit/pdf-parse-client-lifecycle.vitest.spec.ts new file mode 100644 index 0000000..f661970 --- /dev/null +++ b/tests/unit/pdf-parse-client-lifecycle.vitest.spec.ts @@ -0,0 +1,169 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; + +type SnapshotListener = (event: Event) => void; + +class MockMessageEvent extends Event { + data: string; + + constructor(type: string, init: { data: string }) { + super(type); + this.data = init.data; + } +} + +class MockEventSource { + static instances: MockEventSource[] = []; + + readonly url: string; + readonly listeners = new Map(); + closed = false; + + constructor(url: string) { + this.url = url; + MockEventSource.instances.push(this); + } + + addEventListener(type: string, listener: SnapshotListener): void { + const arr = this.listeners.get(type) ?? []; + arr.push(listener); + this.listeners.set(type, arr); + } + + close(): void { + this.closed = true; + } + + emit(type: string, payload: unknown): void { + const event = new MockMessageEvent(type, { + data: JSON.stringify(payload), + }); + for (const listener of this.listeners.get(type) ?? []) { + listener(event); + } + } +} + +describe('PDF parse client lifecycle', () => { + const originalFetch = global.fetch; + const originalEventSource = global.EventSource; + const originalMessageEvent = global.MessageEvent; + + beforeEach(() => { + MockEventSource.instances = []; + + const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input); + const method = init?.method ?? 'GET'; + + if (url.endsWith('/api/documents/doc-1/parsed') && method === 'GET') { + fetchMock.getCount = (fetchMock.getCount ?? 0) + 1; + if (fetchMock.getCount === 1) { + return new Response(JSON.stringify({ + parseStatus: 'pending', + parseProgress: null, + opId: null, + }), { + status: 409, + headers: { 'Content-Type': 'application/json' }, + }); + } + + return new Response(JSON.stringify({ + documentId: 'doc-1', + pages: [], + }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + + if (url.endsWith('/api/documents/doc-1/parsed') && method === 'POST') { + return new Response(JSON.stringify({ + parseStatus: 'pending', + parseProgress: null, + opId: 'op-1', + }), { + status: 202, + headers: { 'Content-Type': 'application/json' }, + }); + } + + throw new Error(`Unexpected fetch: ${method} ${url}`); + }) as typeof fetch & { getCount?: number }; + + global.fetch = fetchMock; + global.EventSource = MockEventSource as unknown as typeof EventSource; + global.MessageEvent = MockMessageEvent as unknown as typeof MessageEvent; + }); + + afterEach(() => { + global.fetch = originalFetch; + global.EventSource = originalEventSource; + global.MessageEvent = originalMessageEvent; + }); + + test('follows not-ready -> ensure op -> SSE snapshots -> ready artifact', async () => { + const { + ParsedPdfNotReadyError, + ensureParsedPdfDocumentOperation, + getParsedPdfDocument, + subscribeParsedPdfDocumentEvents, + } = await import('../../src/lib/client/api/documents'); + + let initialError: unknown; + try { + await getParsedPdfDocument('doc-1'); + } catch (error) { + initialError = error; + } + + expect(initialError).toBeInstanceOf(ParsedPdfNotReadyError); + expect((initialError as InstanceType).parseStatus).toBe('pending'); + + const ensured = await ensureParsedPdfDocumentOperation('doc-1'); + expect(ensured).toMatchObject({ + parseStatus: 'pending', + opId: 'op-1', + }); + + const snapshots: Array<{ parseStatus: string; opId?: string | null }> = []; + const unsubscribe = subscribeParsedPdfDocumentEvents('doc-1', { opId: 'op-1' }, { + onSnapshot: (snapshot) => { + snapshots.push({ + parseStatus: snapshot.parseStatus, + opId: snapshot.opId, + }); + }, + }); + + expect(MockEventSource.instances).toHaveLength(1); + expect(MockEventSource.instances[0]?.url).toBe('/api/documents/doc-1/parsed/events?opId=op-1'); + + MockEventSource.instances[0]?.emit('snapshot', { + eventId: 1, + snapshot: { + opId: 'op-1', + status: 'running', + progress: { totalPages: 5, pagesParsed: 2, currentPage: 3, phase: 'infer' }, + }, + }); + MockEventSource.instances[0]?.emit('snapshot', { + eventId: 2, + snapshot: { + opId: 'op-1', + status: 'succeeded', + }, + }); + + expect(snapshots).toEqual([ + { parseStatus: 'running', opId: 'op-1' }, + { parseStatus: 'ready', opId: 'op-1' }, + ]); + + unsubscribe(); + expect(MockEventSource.instances[0]?.closed).toBe(true); + + const parsed = await getParsedPdfDocument('doc-1'); + expect(parsed).toMatchObject({ documentId: 'doc-1' }); + }); +}); diff --git a/tests/unit/pdf-parsed-events-route-legacy-backfill.vitest.spec.ts b/tests/unit/pdf-parsed-events-route-worker-proxy.vitest.spec.ts similarity index 98% rename from tests/unit/pdf-parsed-events-route-legacy-backfill.vitest.spec.ts rename to tests/unit/pdf-parsed-events-route-worker-proxy.vitest.spec.ts index c86f679..87fd852 100644 --- a/tests/unit/pdf-parsed-events-route-legacy-backfill.vitest.spec.ts +++ b/tests/unit/pdf-parsed-events-route-worker-proxy.vitest.spec.ts @@ -57,7 +57,7 @@ vi.mock('@/lib/server/logger', () => ({ })), })); -describe('GET /api/documents/[id]/parsed/events worker proxy', () => { +describe('GET /api/documents/[id]/parsed/events worker event proxy', () => { const originalFetch = global.fetch; beforeEach(() => { diff --git a/tests/unit/pdf-parsed-route-legacy-backfill.vitest.spec.ts b/tests/unit/pdf-parsed-route-worker-flow.vitest.spec.ts similarity index 98% rename from tests/unit/pdf-parsed-route-legacy-backfill.vitest.spec.ts rename to tests/unit/pdf-parsed-route-worker-flow.vitest.spec.ts index ae1e6d3..13fa85b 100644 --- a/tests/unit/pdf-parsed-route-legacy-backfill.vitest.spec.ts +++ b/tests/unit/pdf-parsed-route-worker-flow.vitest.spec.ts @@ -76,7 +76,7 @@ vi.mock('@/lib/server/logger', () => ({ })), })); -describe('GET/POST /api/documents/[id]/parsed worker-owned flow', () => { +describe('GET/POST /api/documents/[id]/parsed worker flow', () => { beforeEach(() => { hoisted.db = { select: vi.fn(() => ({