import { expect, test, type APIRequest, type APIRequestContext, type APIResponse, type BrowserContext, type Locator, type Page, type TestInfo } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; import { createHash } from 'node:crypto'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; import type { CvOutline, CvVariant } from '../src/cvBuilder'; import type { CoverLetter, SubmittedPackage, SubmittedPackageDetail } from '../src/applicationWorkspace'; import type { StructuredCvProfile } from '../src/profileCv'; import { translations } from '../src/i18n/translations'; const apiUrl = 'http://localhost:5302/api'; const password = 'E2ePassword123!'; type Language = 'en' | 'nb'; type Headers = Record; type BrowserTools = { request: APIRequest }; type Snapshot = { revision: number; profile: StructuredCvProfile }; type Block = { id: string; text: string; page: number | null }; type Review = { runId: number; status: string; revision: number; sourceBlocks: Block[]; groups: { category: string; fields: { id: string }[] }[] }; type Run = { id: number; status: string; parserVersion: string; operation: { progressStage: string } }; type Timings = Record; type MappedValue = { name: string; value: string; evidence?: string; corrected?: boolean }; type Scenario = { language: Language; mode: 'light' | 'dark'; width: number; file: string; kind: 'job' | 'education' | 'repeated'; theme: 'minimal' | 'technical'; package?: boolean }; async function json(response: APIResponse, status = 200): Promise { expect(response.status(), response.url()).toBe(status); return response.json(); } async function login(api: APIRequestContext, email: string): Promise { await json(await api.post(`${apiUrl}/auth/login`, { data: { email, password } })); const me = await json<{ sessionContext: string }>(await api.get(`${apiUrl}/auth/me`)); const csrf = (await api.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN'); expect(csrf).toBeDefined(); return { 'X-CSRF-TOKEN': csrf!.value, 'X-Session-Context': me.sessionContext }; } async function career(api: APIRequestContext) { return json(await api.get(`${apiUrl}/career/profile`)); } async function versions(api: APIRequestContext) { return json(await api.get(`${apiUrl}/career/profile/versions`)); } async function readVariant(api: APIRequestContext, id: number) { return json(await api.get(`${apiUrl}/cv/variants/${id}`)); } const sha = (bytes: Buffer) => createHash('sha256').update(bytes).digest('hex').toUpperCase(); const clean = (text: string) => text.replace(/\s+/g, ' ').trim(); const numbered = (text: string, number: number) => text.replace('{number}', String(number)); function panel(page: Page, runId: number) { return page.locator(`#cv-import-${runId}-review-panel`); } async function keyboardClick(page: Page, button: Locator) { await button.focus(); await page.keyboard.press('Enter'); } async function owner(page: Page, context: BrowserContext, playwright: BrowserTools, sample: Scenario, info: TestInfo) { const admin = await playwright.request.newContext(); const email = `cv-qa-${Date.now()}-${info.testId.replace(/[^a-z0-9]/gi, '').slice(-14)}@example.test`; try { const adminHeaders = await login(admin, 'e2e@example.test'); await json(await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: ['Premium'] } })); } finally { await admin.dispose(); } const headers = await login(page.request, email); expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers, data: { language: sample.language === 'nb' ? 'nb-NO' : 'en' } })).status()).toBe(204); await context.addInitScript(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, sample); await context.route('**/*', route => { const url = new URL(route.request().url()); return ['localhost', '127.0.0.1'].includes(url.hostname) || ['data:', 'blob:', 'about:'].includes(url.protocol) ? route.continue() : route.abort(); }); await page.setViewportSize({ width: sample.width, height: 900 }); await page.emulateMedia({ reducedMotion: 'reduce' }); return { email, headers }; } async function upload(page: Page, playwright: BrowserTools, language: Language, fileName: string, info: TestInfo, timings: Timings, stageName: string) { const t = translations[language]; const before = await career(page.request); const historyBefore = await versions(page.request); const existing = new Set((await json(await page.request.get(`${apiUrl}/profile-cv/runs`))).map(run => run.id)); const fixture = path.resolve(process.env.CV_QA_FIXTURE_ROOT!, fileName); const fixtureHash = sha(await readFile(fixture)); await page.goto('/career?section=import'); await expect(page.getByRole('button', { name: t.profileUploadCv, exact: true })).toBeEnabled(); const started = Date.now(); const accepted = page.waitForResponse(response => response.url() === `${apiUrl}/profile-cv/import` && response.request().method() === 'POST'); await page.locator('input[type=file][accept*=".pdf"]').setInputFiles(fixture); expect((await accepted).status()).toBe(202); const observed: { status: string; stage: string | null; elapsedMs: number }[] = []; let pending: Run | undefined; await expect.poll(async () => { pending = (await json(await page.request.get(`${apiUrl}/profile-cv/runs`))).find(run => !existing.has(run.id)); if (pending) observed.push({ status: pending.status, stage: pending.operation?.progressStage ?? null, elapsedMs: Date.now() - started }); return pending?.status; }, { timeout: 90_000, message: 'A real owned Docling run must reach persisted pending_review' }).toBe('pending_review'); timings[`${stageName}.uploadToPendingReviewMs`] = Date.now() - started; expect(pending!.parserVersion).toBe('docling-v1'); expect(pending!.operation.progressStage).toBe('cv_review_ready'); expect(await career(page.request)).toEqual(before); expect(await versions(page.request)).toEqual(historyBefore); await page.reload(); await expect(page.locator('html')).toHaveAttribute('lang', language === 'nb' ? 'nb-NO' : 'en'); const review = await json(await page.request.get(`${apiUrl}/profile-cv/runs/${pending!.id}/review`)); expect(review.status).toBe('pending_review'); expect(review.revision).toBe(before.revision); expect(review.sourceBlocks.length).toBeGreaterThan(0); await expect(panel(page, pending!.id).getByRole('button', { name: t.cvImportApplyReviewed, exact: true })).toBeDisabled(); expect(await career(page.request)).toEqual(before); expect(await versions(page.request)).toEqual(historyBefore); const source = await page.request.get(`${apiUrl}/profile-cv/runs/${pending!.id}/source`); expect(source.status()).toBe(200); expect(source.headers()['cache-control']).toContain('no-store'); expect(sha(await source.body())).toBe(fixtureHash); const stranger = await playwright.request.newContext(); try { await login(stranger, 'e2e@example.test'); for (const suffix of ['source', 'review']) expect((await stranger.get(`${apiUrl}/profile-cv/runs/${pending!.id}/${suffix}`)).status()).toBe(404); } finally { await stranger.dispose(); } await info.attach(`${stageName}-persisted-stages`, { body: JSON.stringify({ runId: pending!.id, parserVersion: pending!.parserVersion, observed, fixtureHash, boundary: 'Only sampled persisted stages are claimed; intermediate stages can complete between polls. Real local Docling, no extraction/model mocks.' }), contentType: 'application/json' }); return { runId: pending!.id, review, before, fixtureHash }; } async function rejectOriginal(page: Page, runId: number) { // Mapping editor selectors have cv-mapping IDs, so this addresses only extracted decisions. for (const select of await panel(page, runId).locator('select[id^="cv-import-"]').all()) await select.selectOption('keep'); } async function approveExtracted(page: Page, runId: number, language: Language) { const buttons = panel(page, runId).getByRole('button', { name: translations[language].cvImportUseGroup, exact: true }); expect(await buttons.count()).toBeGreaterThanOrEqual(3); for (const button of await buttons.all()) await button.click(); } function evidence(review: Review, literal: string) { const block = review.sourceBlocks.find(item => clean(item.text).includes(clean(literal))); expect(block, `Original source block for literal ${JSON.stringify(literal)}`).toBeDefined(); return block!; } async function author(page: Page, runId: number, review: Review, language: Language, category: 'jobs' | 'education', fields: MappedValue[], target: number | null = null) { const t = translations[language]; const reviewPanel = panel(page, runId); await keyboardClick(page, reviewPanel.getByRole('button', { name: t.cvImportMapMissing, exact: true })); const mapping = reviewPanel.getByRole('group', { name: numbered(t.cvImportMappingRecord, 1), exact: true }); await mapping.getByRole('combobox', { name: t.cvImportMappingCategory, exact: true }).selectOption(category); await mapping.getByRole('combobox', { name: t.cvImportMappingTarget, exact: true }).selectOption(target === null ? '' : String(target)); const selected: { name: string; value: string; block: Block; corrected: boolean }[] = []; for (const [index, field] of fields.entries()) { const number = index + 1; if (index >= 2) await mapping.getByRole('button', { name: t.cvImportMappingAddField, exact: true }).click(); await mapping.getByRole('combobox', { name: numbered(t.cvImportMappingField, number), exact: true }).selectOption(field.name); const block = evidence(review, field.evidence ?? field.value); const value = mapping.getByRole('textbox', { name: numbered(t.cvImportMappingValue, number), exact: true }); await mapping.getByRole('combobox', { name: numbered(t.cvImportMappingSource, number), exact: true }).selectOption(block.id); await expect(value).toHaveValue(block.text); await value.fill(field.value); if (field.corrected) await mapping.getByRole('checkbox', { name: t.cvImportMappingCorrection, exact: true }).nth(index).check(); selected.push({ name: field.name, value: field.value, block, corrected: field.corrected ?? false }); } await keyboardClick(page, mapping.getByRole('button', { name: t.cvImportMappingConfirm, exact: true })); await expect(mapping.getByText(t.cvImportMappingConfirmed, { exact: true })).toBeVisible(); return { mapping, selected }; } async function accessibility(page: Page, runId: number, info: TestInfo, name: string) { const result = await new AxeBuilder({ page }).include(`#cv-import-${runId}-review-panel`).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze(); await info.attach(`${name}-axe`, { body: JSON.stringify({ scope: `#cv-import-${runId}-review-panel`, violations: result.violations, incomplete: result.incomplete }), contentType: 'application/json' }); expect(result.violations).toEqual([]); await page.screenshot({ path: info.outputPath(`${name}.png`), fullPage: true, animations: 'disabled' }); } async function apply(page: Page, headers: Headers, runId: number, language: Language, before: Snapshot, timings: Timings, name: string) { const endpoint = `${apiUrl}/profile-cv/runs/${runId}/apply-reviewed`; const started = Date.now(); const response = page.waitForResponse(result => result.url() === endpoint && result.request().method() === 'POST'); await keyboardClick(page, panel(page, runId).getByRole('button', { name: translations[language].cvImportApplyReviewed, exact: true })); const applied = await response; expect(applied.status()).toBe(200); await expect(page.locator(`#career-import-run-${runId}`).getByText(translations[language].cvImportSaved, { exact: true })).toBeVisible(); await expect.poll(async () => (await career(page.request)).revision).toBe(before.revision + 1); const snapshot = await career(page.request); timings[`${name}.applyToDurableReadbackMs`] = Date.now() - started; const history = await versions(page.request); const replay = await page.request.post(endpoint, { headers, data: applied.request().postDataJSON() }); expect(replay.status()).toBe(200); expect(await career(page.request)).toEqual(snapshot); expect(await versions(page.request)).toEqual(history); return snapshot; } async function createVariant(api: APIRequestContext, headers: Headers, name: string, theme = 'minimal') { return json(await api.post(`${apiUrl}/cv/variants`, { headers, data: { name, settings: { themeId: theme } } })); } async function editAndExport(page: Page, headers: Headers, sample: Scenario, sibling: CvVariant, info: TestInfo, timings: Timings) { const t = translations[sample.language]; const source = await career(page.request); const sectionKey = sample.kind === 'education' ? 'education' : 'experience'; const sectionName = sample.kind === 'education' ? t.cvSectionEducation : t.cvSectionExperience; const outline = await json(await page.request.get(`${apiUrl}/cv/outline`)); const entries = outline.sections.find(section => section.key === sectionKey)!.entries; expect(entries.length).toBeGreaterThan(0); const first = entries[0]; const canonical = sample.kind === 'education' ? source.profile.education[0] : source.profile.jobs[0]; expect(canonical.id).toBeTruthy(); expect(first.key).toBe(canonical.id); const cv = await json(await page.request.post(`${apiUrl}/cv/variants`, { headers, data: { name: `QA ${sample.language} ${sample.kind} CV`, settings: { themeId: sample.theme, sections: [{ key: sectionKey, itemOrder: entries.map(entry => entry.key!) }] }, } })); const started = Date.now(); await page.goto(`/career/builder/${cv.id}`); await expect(page.getByLabel(t.cvEditorName, { exact: true })).toHaveValue(cv.name); await page.getByRole('button', { name: t.cvEditorExpandSection.replace('{section}', sectionName), exact: true }).click(); const section = page.getByRole('group', { name: sectionName, exact: true }); const entry = section.getByRole('group', { name: first.title!, exact: true }).first(); const tailoredTitle = `CV-only ${first.title}`; await entry.getByLabel(t.cvEditorEntryTitle, { exact: true }).fill(tailoredTitle); await expect.poll(async () => (await readVariant(page.request, cv.id)).settings.overrides[first.key!]?.title).toBe(tailoredTitle); await expect(page.getByText(t.cvEditorSaved, { exact: true })).toBeVisible(); expect((await readVariant(page.request, cv.id)).version).toBeGreaterThan(cv.version); expect(await career(page.request)).toEqual(source); expect(await readVariant(page.request, sibling.id)).toEqual(sibling); if (sample.width < 900) await page.getByRole('button', { name: t.cvEditorPreview, exact: true }).click(); const title = t.cvEditorPreviewPage.replace('{page}', '1'); await expect(page.getByTitle(title, { exact: true })).toBeVisible(); await expect(page.frameLocator(`iframe[title=${JSON.stringify(title)}]`).locator('body')).toContainText(tailoredTitle); timings.editorOpenEditToPreviewMs = Date.now() - started; await page.screenshot({ path: info.outputPath('tailored-preview.png'), fullPage: true, animations: 'disabled' }); const exportStarted = Date.now(); const download = page.waitForEvent('download'); await page.getByRole('button', { name: t.cvEditorDownloadPdf, exact: true }).click(); const pdf = await download; expect(pdf.suggestedFilename()).toMatch(/\.pdf$/); const pdfPath = info.outputPath(`cv-qa-${sample.language}-${sample.kind}-${sample.theme}.pdf`); await pdf.saveAs(pdfPath); expect(await pdf.failure()).toBeNull(); const bytes = await readFile(pdfPath); expect(bytes.subarray(0, 5).toString()).toBe('%PDF-'); timings.pdfExportToSavedBytesMs = Date.now() - exportStarted; expect(await career(page.request)).toEqual(source); expect(await readVariant(page.request, sibling.id)).toEqual(sibling); const markers = sample.kind === 'education' ? [tailoredTitle, 'Example Learning Centre', '2015', '2017'] : sample.kind === 'repeated' ? [tailoredTitle, 'Repeat AS', 'Built first service.', 'Built second service.', 'Reviewed third service.', '2018', '2020', '2022', '2023'] : [tailoredTitle, 'Ambiguous Labs', '2019', '2022']; await info.attach('pdf-semantic-oracle-input', { body: JSON.stringify({ pdfPath, sha256: sha(bytes), expectedLiteralMarkers: markers, canonicalEntryKey: first.key, canonicalTitle: first.title, cvOnlyTitle: tailoredTitle, theme: sample.theme, boundary: 'Actual downloaded PDF bytes; semantic extraction and physical/render oracle are separate root-run gates.' }), contentType: 'application/json' }); await info.attach('actual-cv-pdf', { path: pdfPath, contentType: 'application/pdf' }); return { cv: await readVariant(page.request, cv.id), tailoredTitle, source }; } async function freezeApplication(page: Page, headers: Headers, cv: CvVariant, tailoredTitle: string) { const company = await json<{ id: number }>(await page.request.post(`${apiUrl}/companies`, { headers, data: { name: 'CV QA synthetic application company', source: 'User' } }), 201); const job = await json<{ id: number }>(await page.request.post(`${apiUrl}/jobapplications`, { headers, data: { companyId: company.id, jobTitle: 'Reviewed QA engineering application', status: 'Saved', description: 'Synthetic source-backed qualification. No external delivery.', } }), 201); const url = `${apiUrl}/jobapplications/${job.id}`; await json(await page.request.put(`${url}/cv`, { headers, data: { variantId: cv.id } })); const initial = await json(await page.request.get(`${url}/cover-letter`)); const text = 'Manually reviewed frozen application letter — æøå. Preserve this exact text across later import and CV edits.'; await json(await page.request.put(`${url}/cover-letter`, { headers, data: { expectedVersion: initial.revision, text, source: 'manual' } })); const captured = await json(await page.request.post(`${url}/submitted-packages`, { headers })); const detailUrl = `${url}/submitted-packages/${captured.id}`; const detail = await json(await page.request.get(detailUrl)); expect(detail.coverLetterText).toBe(text); expect(detail.renderedCvHtml).toContain(tailoredTitle); expect(captured.cvVariantId).toBe(cv.id); return { jobId: job.id, url, detailUrl, detail, text, letter: await json(await page.request.get(`${url}/cover-letter`)) }; } const scenarios: Scenario[] = [ { language: 'en', mode: 'light', width: 1440, file: 'frozen-ambiguous-employer-first.docx', kind: 'job', theme: 'minimal', package: true }, { language: 'nb', mode: 'dark', width: 390, file: 'frozen-ambiguous-title-first.pdf', kind: 'job', theme: 'technical' }, { language: 'en', mode: 'light', width: 768, file: 'frozen-ambiguous-education.docx', kind: 'education', theme: 'minimal' }, { language: 'en', mode: 'dark', width: 768, file: 'frozen-repeated-overlapping-roles.docx', kind: 'repeated', theme: 'technical' }, ]; for (const sample of scenarios) { test(`[CV-QA] real Docling ${sample.file} ${sample.language}/${sample.mode}/${sample.width}: source, explicit review, canonical persistence, scoped CV and PDF`, async ({ page, context, playwright }, info) => { const timings: Timings = {}; try { const { email, headers } = await owner(page, context, playwright, sample, info); const t = translations[sample.language]; const sibling = await createVariant(page.request, headers, 'Untouched pre-import sibling'); const first = await upload(page, playwright, sample.language, sample.file, info, timings, 'first'); let current: Snapshot; if (sample.kind === 'repeated') { await approveExtracted(page, first.runId, sample.language); await accessibility(page, first.runId, info, 'extracted-review'); current = await apply(page, headers, first.runId, sample.language, first.before, timings, 'first'); expect(current.revision).toBe(1); expect(current.profile.jobs).toHaveLength(3); expect(current.profile.jobs.map(job => [job.company, job.title, job.start, job.end])).toEqual([ ['Repeat AS', 'Engineer', '2018', '2021'], ['Repeat AS', 'Engineer', '2020', '2023'], ['Repeat AS', 'Lead', '2022', 'Present'], ]); expect(current.profile.jobs.map(job => job.bullets)).toEqual([['Built first service.'], ['Built second service.'], ['Reviewed third service.']]); expect(current.profile.jobs[2].isCurrent).toBe(true); const origins = current.profile.jobs.map((_, index) => current.profile.metadata.fields[`jobs.${index}.company`]); expect(new Set(origins.map(origin => origin.sourceBlockId)).size).toBe(3); for (const [index, origin] of origins.entries()) { expect(origin.sourceExtractionRunId).toBe(first.runId); expect(origin.sourceArtifactId).toBeGreaterThan(0); expect(origin.sourceFileSha256).toBe(first.fixtureHash); expect(clean(first.review.sourceBlocks.find(block => block.id === origin.sourceBlockId)!.text)).toContain('Repeat AS'); expect(first.review.sourceBlocks.find(block => block.id === current.profile.metadata.fields[`jobs.${index}.bullets.0`].sourceBlockId)!.text).toContain(current.profile.jobs[index].bullets[0]); } } else { expect(first.review.groups.some(group => group.category === 'jobs' || group.category === 'education')).toBe(false); await rejectOriginal(page, first.runId); const category = sample.kind === 'education' ? 'education' : 'jobs'; const fields: MappedValue[] = sample.kind === 'education' ? [{ name: 'qualification', value: 'Advanced Computing' }, { name: 'institution', value: 'Example Learning Centre' }, { name: 'start', value: '2015', evidence: '2015 - 2017' }, { name: 'end', value: '2017', evidence: '2015 - 2017' }] : [{ name: 'title', value: 'Principal Engineer' }, { name: 'company', value: 'Ambiguous Labs' }, { name: 'start', value: '2019', evidence: '2019 - 2022' }, { name: 'end', value: '2022', evidence: '2019 - 2022' }]; const mapped = await author(page, first.runId, first.review, sample.language, category, fields); await accessibility(page, first.runId, info, 'mapped-review'); current = await apply(page, headers, first.runId, sample.language, first.before, timings, 'first'); expect(current.revision).toBe(1); expect(current.profile.otherSections).toEqual([]); expect(current.profile.jobs).toHaveLength(sample.kind === 'education' ? 0 : 1); expect(current.profile.education).toHaveLength(sample.kind === 'education' ? 1 : 0); const record = sample.kind === 'education' ? current.profile.education[0] : current.profile.jobs[0]; expect(record.id).toBeTruthy(); for (const field of mapped.selected) { expect(record).toHaveProperty(field.name, field.value); expect(current.profile.metadata.fields[`${category}.0.${field.name}`]).toMatchObject({ method: 'user-source-mapping', sourceBlockId: field.block.id, sourceExtractionRunId: first.runId, sourceFileSha256: first.fixtureHash, reviewState: 'accepted' }); expect(current.profile.metadata.fields[`${category}.0.${field.name}`].sourceArtifactId).toBeGreaterThan(0); } expect((await versions(page.request))).toHaveLength(1); } const stableIds = [...current.profile.jobs, ...current.profile.education].map(record => record.id); expect(stableIds.every(Boolean)).toBe(true); expect(new Set(stableIds).size).toBe(stableIds.length); expect(await readVariant(page.request, sibling.id)).toEqual(sibling); const edited = await editAndExport(page, headers, sample, sibling, info, timings); if (sample.kind === 'repeated') { const repeated = await upload(page, playwright, sample.language, sample.file, info, timings, 'same-source'); await approveExtracted(page, repeated.runId, sample.language); const reapplied = await apply(page, headers, repeated.runId, sample.language, repeated.before, timings, 'same-source'); expect(reapplied.revision).toBe(2); expect(reapplied.profile.jobs).toHaveLength(3); expect(reapplied.profile.jobs.map(job => job.id)).toEqual(stableIds); expect(reapplied.profile.jobs.map(job => job.bullets)).toEqual(current.profile.jobs.map(job => job.bullets)); expect(new Set(reapplied.profile.jobs.map((_, index) => reapplied.profile.metadata.fields[`jobs.${index}.company`].sourceBlockId)).size).toBe(3); for (const index of [0, 1, 2]) expect(reapplied.profile.metadata.fields[`jobs.${index}.company`].sourceExtractionRunId).toBe(repeated.runId); expect(await versions(page.request)).toHaveLength(2); } if (sample.package) { const frozen = await freezeApplication(page, headers, edited.cv, edited.tailoredTitle); const next = await upload(page, playwright, sample.language, sample.file, info, timings, 'target-conflict'); await rejectOriginal(page, next.runId); const draft = await author(page, next.runId, next.review, sample.language, 'jobs', [{ name: 'title', value: 'Principal Engineer' }, { name: 'company', value: 'Ambiguous Labs' }], 0); await expect(draft.mapping.getByText(t.careerImportCurrentValue.replace('{value}', 'Principal Engineer'), { exact: true })).toBeVisible(); const concurrent = await playwright.request.newContext(); let changed: Snapshot; try { const concurrentHeaders = await login(concurrent, email); const latest = await career(concurrent); const profile = { ...latest.profile, jobs: latest.profile.jobs.map((job, index) => index === 0 ? { ...job, title: 'Concurrent owner-reviewed Engineer' } : job) }; await json(await concurrent.put(`${apiUrl}/career/profile`, { headers: concurrentHeaders, data: { expectedVersion: latest.revision, profile } })); changed = await career(concurrent); } finally { await concurrent.dispose(); } const conflict = page.waitForResponse(response => response.url() === `${apiUrl}/profile-cv/runs/${next.runId}/apply-reviewed` && response.request().method() === 'POST'); await keyboardClick(page, panel(page, next.runId).getByRole('button', { name: t.cvImportApplyReviewed, exact: true })); expect((await conflict).status()).toBe(409); await expect(page.getByText(t.cvImportStaleReview, { exact: true })).toBeVisible(); expect(await career(page.request)).toEqual(changed!); await expect(draft.mapping.getByRole('textbox', { name: numbered(t.cvImportMappingValue, 1), exact: true })).toHaveValue('Principal Engineer'); const target = draft.mapping.getByRole('combobox', { name: t.cvImportMappingTarget, exact: true }); await expect(target).toHaveValue(''); await expect(panel(page, next.runId).getByRole('button', { name: t.cvImportApplyReviewed, exact: true })).toBeDisabled(); await target.selectOption('0'); await expect(draft.mapping.getByText(t.careerImportCurrentValue.replace('{value}', 'Concurrent owner-reviewed Engineer'), { exact: true })).toBeVisible(); await rejectOriginal(page, next.runId); await draft.mapping.getByRole('button', { name: t.cvImportMappingConfirm, exact: true }).click(); const final = await apply(page, headers, next.runId, sample.language, changed!, timings, 'target-reconfirmed'); expect(final.revision).toBe(3); expect(final.profile.jobs).toHaveLength(1); expect(final.profile.jobs[0].id).toBe(stableIds[0]); expect(final.profile.jobs[0]).toMatchObject({ title: 'Principal Engineer', company: 'Ambiguous Labs', start: '2019', end: '2022' }); expect(final.profile.metadata.fields['jobs.0.start'].sourceExtractionRunId).toBe(first.runId); expect(final.profile.metadata.fields['jobs.0.title'].sourceExtractionRunId).toBe(next.runId); const latestCv = await readVariant(page.request, edited.cv.id); await json(await page.request.put(`${apiUrl}/cv/variants/${latestCv.id}`, { headers, data: { name: latestCv.name, settings: { ...latestCv.settings, headline: 'Later CV-only headline' }, expectedVersion: latestCv.version, source: 'manual' } })); expect(await json(await page.request.get(frozen.detailUrl))).toEqual(frozen.detail); expect(await json(await page.request.get(`${frozen.url}/cover-letter`))).toEqual(frozen.letter); expect(await versions(page.request)).toHaveLength(3); await page.goto(`/jobs/${frozen.jobId}?section=application&sub=letter`); await expect(page.getByRole('textbox', { name: t.jobDetailsCoverLetter, exact: true })).toHaveValue(frozen.text); await info.attach('frozen-application-preservation', { body: JSON.stringify({ packageId: frozen.detail.summary.id, renderedCvSha256: createHash('sha256').update(frozen.detail.renderedCvHtml!).digest('hex'), sourceRevisions: [1, changed!.revision, final.revision], letter: 'Exact DTO/text/history unchanged', packageDetail: 'Exact immutable detail unchanged', delivery: 'No send or external email endpoint called' }), contentType: 'application/json' }); } expect(await readVariant(page.request, sibling.id)).toEqual(sibling); const sourceAfter = await page.request.get(`${apiUrl}/profile-cv/runs/${first.runId}/source`); expect(sourceAfter.status()).toBe(200); expect(sha(await sourceAfter.body())).toBe(first.fixtureHash); } finally { await info.attach('actual-latencies', { body: JSON.stringify({ measurementsMs: timings, boundary: 'Measured local scenario samples, not an SLA, production load claim, live model result or human/device acceptance.' }), contentType: 'application/json' }); } }); }