import { expect, test, type APIRequestContext } from '@playwright/test'; import path from 'node:path'; const api = 'http://localhost:5302/api'; const password = 'E2ePassword123!'; async function login(client: APIRequestContext, email: string) { expect((await client.post(`${api}/auth/login`, { data: { email, password } })).status()).toBe(200); const me = await (await client.get(`${api}/auth/me`)).json(); return { id: me.id as string, headers: { 'X-CSRF-TOKEN': (await client.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value, 'X-Session-Context': me.sessionContext as string } }; } test('real Docling produces owner-scoped administrator history without exposing source text', async ({ page, playwright }, info) => { await page.context().route('**/*', route => ['localhost', '127.0.0.1'].includes(new URL(route.request().url()).hostname) ? route.continue() : route.abort()); const admin = await playwright.request.newContext(); try { const administrator = await login(admin, 'e2e@example.test'); const email = `processing-docling-${Date.now()}@example.test`; expect((await admin.post(`${api}/users`, { headers: administrator.headers, data: { email, password, roles: ['Premium'] } })).status()).toBe(200); const owner = await login(page.request, email); await page.goto('/career?section=import'); await page.locator('input[type=file][accept*=".pdf"]').setInputFiles(path.resolve(process.env.CV_IMPORT_FIXTURE_ROOT || '../.artifacts/cv-import/corpus-02', 'dev-en-simple.pdf')); await expect.poll(async () => (await (await page.request.get(`${api}/profile-cv/runs`)).json())[0]?.operation?.status, { timeout: 90000 }).toBe('succeeded'); const run = (await (await page.request.get(`${api}/profile-cv/runs`)).json())[0]; const id = run.operation.id; expect(run.status).toBe('pending_review'); expect((await admin.get(`${api}/profile-cv/runs/${run.id}/source`)).status()).toBe(404); const history = await (await admin.get(`${api}/admin/processing/${id}/events`)).json(); expect(history.items.map((item: { kind: string }) => item.kind)).toEqual(['Created', 'Claimed', ...Array(7).fill('StageChanged'), 'Completed']); expect(history.items.filter((item: { kind: string }) => item.kind === 'StageChanged').map((item: { stage: string }) => item.stage)).toEqual(['cv_validating_file', 'cv_extracting', 'cv_identifying_sections', 'cv_analysing', 'cv_validating_details', 'cv_preparing_review', 'cv_review_ready']); const detail = await (await admin.get(`${api}/admin/processing/${id}`)).json(); expect(detail.owner.id).toBe(owner.id); expect(detail.document.name).toBe('dev-en-simple.pdf'); expect(detail.document.importId).toBe(run.id); expect(detail.document.pages).toBeGreaterThan(0); expect(detail.worker).toBe('document'); expect(detail.workerEnabled).toBe(true); for (const field of ['rawExtractedText', 'documentJson', 'structuredProfileJson', 'leaseToken', 'failureMessage', 'resultReference']) expect(detail).not.toHaveProperty(field); await login(page.request, 'e2e@example.test'); await page.goto('/admin/processing'); await page.getByLabel('Owner ID', { exact: true }).fill(owner.id); await page.getByRole('button', { name: 'Apply filters', exact: true }).click(); await page.getByRole('button', { name: `View processing details ${id}`, exact: true }).click(); await expect(page.getByText(/^Processing completed/)).toBeVisible(); await expect(page.getByLabel('Correlation ID', { exact: true })).toHaveValue(id); await expect(page.getByLabel('Submitting user ID', { exact: true })).toHaveValue(owner.id); await expect(page.getByText(`Import ID: ${run.id}`, { exact: true })).toBeVisible(); await expect(page.getByText('No linked job', { exact: true })).toBeVisible(); await page.screenshot({ path: info.outputPath('real-docling-processing-history.png'), fullPage: true }); await info.attach('real-parser-evidence-limit', { body: 'Actual pinned Docling conversion and deterministic interpretation; no generative model inference or model accuracy acceptance. Administrator source-download access remains denied by the ordinary ownership API.', contentType: 'text/plain' }); } finally { await admin.dispose(); } });