import { test, expect, type APIRequestContext } from '../../../../../job-tracker-ui/node_modules/@playwright/test'; const api = 'http://localhost:5302/api'; const password = 'E2ePassword123!'; async function login(request: APIRequestContext, email: string) { expect((await request.post(`${api}/auth/login`, { data: { email, password } })).status()).toBe(200); const me = await (await request.get(`${api}/auth/me`)).json(); const csrf = (await request.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value; return { 'X-CSRF-TOKEN': csrf, 'X-Session-Context': me.sessionContext as string }; } test('P9 RD21 source revisions reject stale writes and preserve acknowledged winners', async ({ playwright }, info) => { const admin = await playwright.request.newContext(); const first = await playwright.request.newContext(); const second = await playwright.request.newContext(); try { const adminHeaders = await login(admin, 'e2e@example.test'); const email = `p9-stale-${Date.now()}@example.test`; expect((await admin.post(`${api}/users`, { headers: adminHeaders, data: { email, password, roles: [] } })).status()).toBe(200); const a = await login(first, email); const b = await login(second, email); const submitted = { version: '1', contact: { fullName: 'Synthetic concurrent owner' }, summary: ['Initial summary'] }; const initialCareer = await (await first.get(`${api}/career/profile`)).json(); expect((await first.put(`${api}/career/profile`, { headers: a, data: { expectedVersion: initialCareer.revision, profile: submitted } })).status()).toBe(200); const sourceA = await (await first.get(`${api}/career/profile`)).json(); const sourceB = await (await second.get(`${api}/career/profile`)).json(); expect(sourceB).toEqual(sourceA); const careerA = sourceA.profile; const careerB = sourceB.profile; careerA.summary = ['First editor committed summary']; careerB.contact.headline = 'Second editor headline'; expect((await first.put(`${api}/career/profile`, { headers: a, data: { expectedVersion: sourceA.revision, profile: careerA } })).status()).toBe(200); const careerWinner = await (await first.get(`${api}/career/profile`)).json(); expect(careerWinner.revision).toBe(sourceA.revision + 1); expect(careerWinner.profile.summary).toEqual(careerA.summary); const careerHistory = await (await first.get(`${api}/career/profile/versions`)).json(); const staleCareer = await second.put(`${api}/career/profile`, { headers: b, data: { expectedVersion: sourceB.revision, profile: careerB } }); expect(staleCareer.status()).toBe(409); const careerReadback = await (await second.get(`${api}/career/profile`)).json(); expect(careerReadback).toEqual(careerWinner); expect(await (await first.get(`${api}/career/profile/versions`)).json()).toEqual(careerHistory); const careerAfter = careerReadback.profile; const company = await first.post(`${api}/companies`, { headers: a, data: { name: 'Synthetic stale job fixture' } }); expect(company.status()).toBe(201); const created = await first.post(`${api}/jobapplications`, { headers: a, data: { companyId: (await company.json()).id, jobTitle: 'Original job title', status: 'Saved' } }); expect(created.status()).toBe(201); const jobId = (await created.json()).id; const jobA = await (await first.get(`${api}/jobapplications/${jobId}`)).json(); const jobB = await (await second.get(`${api}/jobapplications/${jobId}`)).json(); expect(jobB).toEqual(jobA); jobA.jobTitle = 'First editor committed title'; jobB.notes = 'Second editor notes'; expect((await first.put(`${api}/jobapplications/${jobId}`, { headers: a, data: { ...jobA, expectedVersion: jobA.version } })).status()).toBe(204); const jobWinner = await (await first.get(`${api}/jobapplications/${jobId}`)).json(); expect(jobWinner.version).toBe(jobA.version + 1); expect(jobWinner.jobTitle).toBe(jobA.jobTitle); const staleJob = await second.put(`${api}/jobapplications/${jobId}`, { headers: b, data: { ...jobB, expectedVersion: jobB.version } }); expect(staleJob.status()).toBe(409); const jobAfter = await (await second.get(`${api}/jobapplications/${jobId}`)).json(); expect(jobAfter).toEqual(jobWinner); const careerLostFirstEdit = careerAfter.summary[0] !== careerA.summary[0]; const jobLostFirstEdit = jobAfter.jobTitle !== jobA.jobTitle; expect(careerLostFirstEdit).toBe(false); expect(jobLostFirstEdit).toBe(false); await info.attach('phase9-rd21-characterization', { body: JSON.stringify({ id: 'RD21', career: { staleStatus: staleCareer.status(), expectedFirstSummary: careerA.summary, actualSummary: careerAfter.summary, lostFirstEdit: careerLostFirstEdit }, job: { staleStatus: staleJob.status(), expectedFirstTitle: jobA.jobTitle, actualTitle: jobAfter.jobTitle, lostFirstEdit: jobLostFirstEdit }, safetyQualification: 'Both stale replacements rejected with 409; exact winner state and Career history retained', limitation: 'Finite sequential A/B scenario on the configured isolated API; not simultaneous provider contention or creation-intent idempotency proof.' }, null, 2), contentType: 'application/json' }); } finally { await second.dispose(); await first.dispose(); await admin.dispose(); } });