test(pdf-parse): add unit tests for client lifecycle and worker route flows
Introduce new unit tests covering the PDF parse client lifecycle and worker-based API routes. Tests verify client behavior for not-ready and ready parse states, operation initiation, SSE event handling, and route validation. Also add coverage for worker event proxy and worker flow routes. Remove all legacy parseStatus and parsedJsonKey fields from document types, API, and gallery view components to align with the new worker-owned PDF parse model.
This commit is contained in:
parent
7a86ce6a94
commit
9db30742f8
6 changed files with 171 additions and 22 deletions
|
|
@ -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',
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 <PDFIcon className={className ?? 'w-4 h-4 text-danger'} />;
|
||||
if (doc.type === 'epub') return <EPUBIcon className={className ?? 'w-4 h-4 text-accent'} />;
|
||||
|
|
@ -244,12 +236,6 @@ export function GalleryView({
|
|||
</dd>
|
||||
</>
|
||||
)}
|
||||
{activeDoc.type === 'pdf' && (
|
||||
<>
|
||||
<dt className="text-soft">Parse status</dt>
|
||||
<dd className="text-foreground text-right">{formatParseStatus(activeDoc.parseStatus)}</dd>
|
||||
</>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
169
tests/unit/pdf-parse-client-lifecycle.vitest.spec.ts
Normal file
169
tests/unit/pdf-parse-client-lifecycle.vitest.spec.ts
Normal file
|
|
@ -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<string, SnapshotListener[]>();
|
||||
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<typeof ParsedPdfNotReadyError>).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' });
|
||||
});
|
||||
});
|
||||
|
|
@ -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(() => {
|
||||
|
|
@ -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(() => ({
|
||||
Loading…
Reference in a new issue