import { beforeEach, describe, expect, test, vi } from 'vitest'; import { NextRequest } from 'next/server'; const hoisted = vi.hoisted(() => ({ db: null as { select: ReturnType; } | null, row: { id: 'doc-1', type: 'pdf', }, requireAuthContext: vi.fn(), readCurrentParsedPdfArtifact: vi.fn(), readParsedPdfArtifactByKey: vi.fn(), lookupCurrentPdfParseOperation: vi.fn(), createOrReuseCurrentPdfParseOperation: vi.fn(), checkJobRate: vi.fn(), getPdfLayoutRateConfig: vi.fn(), getResolvedRuntimeConfig: vi.fn(), buildComputeRateLimitedResponse: vi.fn(), })); vi.mock('@openreader/database', () => ({ get db() { return hoisted.db; }, })); vi.mock('@/lib/server/auth/auth', () => ({ requireAuthContext: hoisted.requireAuthContext, })); vi.mock('@/lib/server/pdf-parse/artifact', () => ({ readCurrentParsedPdfArtifact: hoisted.readCurrentParsedPdfArtifact, readParsedPdfArtifactByKey: hoisted.readParsedPdfArtifactByKey, })); vi.mock('@/lib/server/pdf-parse/operation', () => ({ lookupCurrentPdfParseOperation: hoisted.lookupCurrentPdfParseOperation, createOrReuseCurrentPdfParseOperation: hoisted.createOrReuseCurrentPdfParseOperation, })); vi.mock('@/lib/server/rate-limit/job-rate-limiter', () => ({ checkJobRate: hoisted.checkJobRate, getPdfLayoutRateConfig: hoisted.getPdfLayoutRateConfig, })); vi.mock('@/lib/server/rate-limit/problem-response', () => ({ buildComputeRateLimitedResponse: hoisted.buildComputeRateLimitedResponse, })); vi.mock('@/lib/server/runtime-config', () => ({ getResolvedRuntimeConfig: hoisted.getResolvedRuntimeConfig, })); vi.mock('@/lib/server/documents/blobstore', () => ({ isValidDocumentId: vi.fn(() => true), })); vi.mock('@/lib/server/storage/s3', () => ({ isS3Configured: vi.fn(() => true), })); vi.mock('@/lib/server/testing/test-namespace', () => ({ getOpenReaderTestNamespace: vi.fn(() => null), })); vi.mock('@/lib/server/logger', () => ({ createRequestLogger: vi.fn(() => ({ logger: { warn: vi.fn(), info: vi.fn(), error: vi.fn(), }, requestId: 'req-test', })), })); describe('GET/POST /api/documents/[id]/parsed worker flow', () => { beforeEach(() => { hoisted.db = { select: vi.fn(() => ({ from: vi.fn(() => ({ where: vi.fn(() => ({ limit: vi.fn(async () => [{ ...hoisted.row }]), })), })), })), }; hoisted.requireAuthContext.mockReset(); hoisted.requireAuthContext.mockResolvedValue({ userId: 'user-1' }); hoisted.readCurrentParsedPdfArtifact.mockReset(); hoisted.readCurrentParsedPdfArtifact.mockResolvedValue(null); hoisted.readParsedPdfArtifactByKey.mockReset(); hoisted.readParsedPdfArtifactByKey.mockResolvedValue(null); hoisted.lookupCurrentPdfParseOperation.mockReset(); hoisted.lookupCurrentPdfParseOperation.mockResolvedValue(null); hoisted.createOrReuseCurrentPdfParseOperation.mockReset(); hoisted.checkJobRate.mockReset(); hoisted.checkJobRate.mockResolvedValue({ allowed: true }); hoisted.getPdfLayoutRateConfig.mockReset(); hoisted.getPdfLayoutRateConfig.mockReturnValue({}); hoisted.getResolvedRuntimeConfig.mockReset(); hoisted.getResolvedRuntimeConfig.mockResolvedValue({}); hoisted.buildComputeRateLimitedResponse.mockReset(); }); test('GET returns pending when no current artifact or worker op exists', async () => { const { GET } = await import('../../src/app/api/documents/[id]/parsed/route'); const response = await GET(new NextRequest('http://localhost/api/documents/doc-1/parsed'), { params: Promise.resolve({ id: 'doc-1' }), }); expect(response.status).toBe(409); await expect(response.json()).resolves.toMatchObject({ parseStatus: 'pending', parseProgress: null, opId: null, }); }); test('GET reads the artifact referenced by a succeeded worker op', async () => { hoisted.lookupCurrentPdfParseOperation.mockResolvedValue({ opId: 'op-1', opKey: 'pdf_layout|v1|parser|doc-1||doc-key|', kind: 'pdf_layout', jobId: 'job-1', status: 'succeeded', queuedAt: Date.now() - 1000, updatedAt: Date.now(), result: { parsedObjectKey: 'parsed-key.json' }, }); hoisted.readParsedPdfArtifactByKey.mockResolvedValue({ key: 'parsed-key.json', bytes: Buffer.from(JSON.stringify({ documentId: 'doc-1', pages: [] })), parsed: { documentId: 'doc-1', pages: [] }, }); const { GET } = await import('../../src/app/api/documents/[id]/parsed/route'); const response = await GET(new NextRequest('http://localhost/api/documents/doc-1/parsed'), { params: Promise.resolve({ id: 'doc-1' }), }); expect(response.status).toBe(200); await expect(response.json()).resolves.toMatchObject({ documentId: 'doc-1' }); expect(hoisted.readParsedPdfArtifactByKey).toHaveBeenCalledWith('parsed-key.json'); }); test('POST creates a worker op when replace is requested', async () => { hoisted.createOrReuseCurrentPdfParseOperation.mockResolvedValue({ opId: 'op-force-1', opKey: 'pdf_layout|v1|parser|doc-1||doc-key|force', kind: 'pdf_layout', jobId: 'job-force-1', status: 'queued', queuedAt: Date.now(), updatedAt: Date.now(), }); const { POST } = await import('../../src/app/api/documents/[id]/parsed/route'); const request = new NextRequest('http://localhost/api/documents/doc-1/parsed', { method: 'POST', body: JSON.stringify({ replace: true }), headers: { 'Content-Type': 'application/json' }, }); const response = await POST(request, { params: Promise.resolve({ id: 'doc-1' }), }); expect(response.status).toBe(202); await expect(response.json()).resolves.toMatchObject({ parseStatus: 'pending', opId: 'op-force-1', }); expect(hoisted.createOrReuseCurrentPdfParseOperation).toHaveBeenCalled(); }); test('POST does not report ready for a succeeded op until its artifact is readable', async () => { hoisted.lookupCurrentPdfParseOperation.mockResolvedValue({ opId: 'op-ready-1', opKey: 'pdf_layout|v1|parser|doc-1||doc-key|', kind: 'pdf_layout', jobId: 'job-ready-1', status: 'succeeded', queuedAt: Date.now() - 1000, updatedAt: Date.now(), result: { parsedObjectKey: 'missing-parsed-key.json' }, }); hoisted.readParsedPdfArtifactByKey.mockResolvedValue(null); const { POST } = await import('../../src/app/api/documents/[id]/parsed/route'); const request = new NextRequest('http://localhost/api/documents/doc-1/parsed', { method: 'POST', body: JSON.stringify({ replace: false }), headers: { 'Content-Type': 'application/json' }, }); const response = await POST(request, { params: Promise.resolve({ id: 'doc-1' }), }); expect(response.status).toBe(202); await expect(response.json()).resolves.toMatchObject({ parseStatus: 'running', opId: 'op-ready-1', }); expect(hoisted.readParsedPdfArtifactByKey).toHaveBeenCalledWith('missing-parsed-key.json'); expect(hoisted.createOrReuseCurrentPdfParseOperation).not.toHaveBeenCalled(); }); test('POST returns the rate-limited response without creating a worker op', async () => { const sentinel = new Response('rate limited', { status: 429 }); hoisted.checkJobRate.mockResolvedValue({ allowed: false }); hoisted.buildComputeRateLimitedResponse.mockReturnValue(sentinel); const { POST } = await import('../../src/app/api/documents/[id]/parsed/route'); const request = new NextRequest('http://localhost/api/documents/doc-1/parsed', { method: 'POST', body: JSON.stringify({ replace: true }), headers: { 'Content-Type': 'application/json' }, }); const response = await POST(request, { params: Promise.resolve({ id: 'doc-1' }), }); expect(response).toBe(sentinel); expect(hoisted.createOrReuseCurrentPdfParseOperation).not.toHaveBeenCalled(); }); });