Files
jobtrackingapp/job-tracker-ui/import-tests/cv-integration-qa.spec.ts
T

388 lines
29 KiB
TypeScript

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<string, string>;
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<string, number>;
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<T>(response: APIResponse, status = 200): Promise<T> {
expect(response.status(), response.url()).toBe(status);
return response.json();
}
async function login(api: APIRequestContext, email: string): Promise<Headers> {
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<Snapshot>(await api.get(`${apiUrl}/career/profile`)); }
async function versions(api: APIRequestContext) { return json<unknown[]>(await api.get(`${apiUrl}/career/profile/versions`)); }
async function readVariant(api: APIRequestContext, id: number) { return json<CvVariant>(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<Run[]>(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<Run[]>(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<Review>(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<CvVariant>(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<CvOutline>(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<CvVariant>(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<CoverLetter>(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<SubmittedPackage>(await page.request.post(`${url}/submitted-packages`, { headers }));
const detailUrl = `${url}/submitted-packages/${captured.id}`;
const detail = await json<SubmittedPackageDetail>(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<CoverLetter>(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' });
}
});
}