Files
jobtrackingapp/docs/redesign/phase-9/p9-05/volume/volume.spec.ts
T

100 lines
8.4 KiB
TypeScript

import { test, expect, type APIRequestContext, type PlaywrightWorkerArgs } from '../../../../../job-tracker-ui/node_modules/@playwright/test';
import { spawnSync } from 'node:child_process';
import path from 'node:path';
const base = 'http://localhost:5304/api';
const password = 'E2ePassword123!';
async function login(api: APIRequestContext, email: string) {
expect((await api.post(`${base}/auth/login`, { data: { email, password } })).status()).toBe(200);
const me = await (await api.get(`${base}/auth/me`)).json();
return { 'X-CSRF-TOKEN': (await api.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value, 'X-Session-Context': me.sessionContext as string };
}
async function owner(playwright: PlaywrightWorkerArgs['playwright']) {
const admin = await playwright.request.newContext();
const api = await playwright.request.newContext();
try {
const headers = await login(admin, 'e2e@example.test');
const email = `p9-volume-${Date.now()}@example.test`;
const response = await admin.post(`${base}/users`, { headers, data: { email, password, roles: [] } });
expect(response.status()).toBe(200);
return { api, email, id: (await response.json()).id as string, headers: await login(api, email) };
} finally { await admin.dispose(); }
}
test('P9 volume: 100 CV revisions retain exact history, stale rejection and additive restore', async ({ playwright }, info) => {
test.setTimeout(180000);
const account = await owner(playwright);
try {
const { api, headers } = account;
expect((await api.put(`${base}/career/profile`, { headers, data: { profile: { version: '1', contact: { fullName: 'Volume fixture' }, summary: ['Canonical unchanged'] } } })).status()).toBe(200);
const source = await (await api.get(`${base}/career/profile`)).json();
const created = await api.post(`${base}/cv/variants`, { headers, data: { name: 'Volume CV', settings: { themeId: 'minimal' } } });
expect(created.status()).toBe(200); let cv = await created.json(); const id = cv.id;
for (let version = 2; version <= 100; version++) {
const response = await api.put(`${base}/cv/variants/${id}`, { headers, data: { expectedVersion: cv.version, name: cv.name, settings: { ...cv.settings, headline: `Revision ${version}` }, source: 'manual' } });
expect(response.status()).toBe(200); cv = await response.json(); expect(cv.version).toBe(version);
}
const history = await (await api.get(`${base}/cv/variants/${id}/versions`)).json();
expect(history.map((row: { version: number }) => row.version)).toEqual(Array.from({ length: 100 }, (_, index) => 100 - index));
expect((await api.put(`${base}/cv/variants/${id}`, { headers, data: { expectedVersion: 99, settings: cv.settings } })).status()).toBe(409);
expect(await (await api.get(`${base}/cv/variants/${id}/versions`)).json()).toEqual(history);
const restored = await api.post(`${base}/cv/variants/${id}/versions/2/restore`, { headers, data: { expectedVersion: 100 } });
expect(restored.status()).toBe(200); expect(await restored.json()).toMatchObject({ version: 101, settings: { headline: 'Revision 2' } });
expect((await (await api.get(`${base}/cv/variants/${id}/versions`)).json()).length).toBe(101);
expect(await (await api.get(`${base}/career/profile`)).json()).toEqual(source);
await info.attach('phase9-volume-cv', { body: JSON.stringify({ savedRevisions: 100, finalVersions: 101, staleStatus: 409, canonicalUnchanged: true, limits: 'Native isolated SQLite API; no100-row history UI or MariaDB/long-term capacity claim.' }), contentType: 'application/json' });
} finally { await account.api.dispose(); }
});
test('P9 volume: 100 manual timeline messages retain exact membership and foreign-owner denial', async ({ playwright }, info) => {
test.setTimeout(180000);
const account = await owner(playwright); const other = await owner(playwright);
try {
const { api, headers } = account;
const company = await api.post(`${base}/companies`, { headers, data: { name: 'Synthetic volume timeline' } });
expect(company.status()).toBe(201);
const job = await api.post(`${base}/jobapplications`, { headers, data: { companyId: (await company.json()).id, jobTitle: 'Volume timeline fixture', status: 'Saved' } });
expect(job.status()).toBe(201); const jobId = (await job.json()).id;
const ids: number[] = [];
for (let index = 0; index < 100; index++) {
const response = await api.post(`${base}/correspondence`, { headers, data: { jobApplicationId: jobId, from: 'Synthetic manual note', content: `Timeline evidence ${index} ${'Documented progress. '.repeat(20)}`, channel: 'manual', direction: 'incoming' } });
expect(response.status()).toBe(201); ids.push((await response.json()).id);
}
const response = await api.get(`${base}/correspondence/${jobId}`); expect(response.status()).toBe(200);
const rows = await response.json(); expect(rows.map((row: { id: number }) => row.id).sort((a: number, b: number) => a - b)).toEqual([...ids].sort((a, b) => a - b));
expect(rows.every((row: { content: string }) => row.content.startsWith('Timeline evidence '))).toBe(true);
expect((await other.api.get(`${base}/correspondence/${jobId}`)).status()).toBe(404);
await info.attach('phase9-volume-timeline', { body: JSON.stringify({ messages: rows.length, exactMembership: true, foreignRead: 404, limits: 'Native persisted manual correspondence; no external mail delivery or100-message browser rendering claim.' }), contentType: 'application/json' });
} finally { await account.api.dispose(); await other.api.dispose(); }
});
test('P9 volume: 100 native cancellation notices preserve bounded lists, unread counts and ownership', async ({ playwright }, info) => {
test.setTimeout(300000);
const account = await owner(playwright); const other = await owner(playwright);
try {
const dataRoot = info.config.metadata.isolatedDataRoot;
expect(typeof dataRoot).toBe('string'); expect(process.env.PLAYWRIGHT_REUSE_SERVERS).not.toBe('true');
for (let index = 0; index < 100; index++) {
// Existing helper admits one inert insert only after temp-path/schema/owner verification.
const seeded = spawnSync(process.env.PLAYWRIGHT_PYTHON_PATH || 'python', [path.resolve('e2e/helpers/seed-operation.py')], { input: JSON.stringify({ dataRoot, ownerId: account.id, ownerEmail: account.email }), encoding: 'utf8', timeout: 15000 });
expect(seeded.status, seeded.stderr).toBe(0);
const id = JSON.parse(seeded.stdout).operationId;
expect((await account.api.post(`${base}/operations/${id}/cancel`, { headers: account.headers })).status()).toBe(200);
}
const list = await account.api.get(`${base}/notifications?limit=100`); expect(list.status()).toBe(200);
const rows = await list.json(); expect(rows.length).toBe(100); expect(new Set(rows.map((row: { id: string }) => row.id)).size).toBe(100);
expect((await (await account.api.get(`${base}/notifications`)).json()).length).toBe(25);
expect(await (await account.api.get(`${base}/notifications/unread-count`)).json()).toEqual({ count: 100 });
expect((await account.api.get(`${base}/notifications?limit=101`)).status()).toBe(400);
expect(await (await other.api.get(`${base}/notifications`)).json()).toEqual([]);
expect((await other.api.post(`${base}/notifications/${rows[0].id}/read`, { headers: other.headers })).status()).toBe(404);
expect((await account.api.post(`${base}/notifications/${rows[0].id}/read`, { headers: account.headers })).status()).toBe(204);
expect((await account.api.post(`${base}/notifications/${rows[0].id}/read`, { headers: account.headers })).status()).toBe(204);
expect(await (await account.api.get(`${base}/notifications/unread-count`)).json()).toEqual({ count: 99 });
expect((await account.api.delete(`${base}/notifications/${rows[1].id}`, { headers: account.headers })).status()).toBe(204);
expect(await (await account.api.get(`${base}/notifications/unread-count`)).json()).toEqual({ count: 98 });
expect((await (await account.api.get(`${base}/notifications?limit=100`)).json()).length).toBe(99);
await info.attach('phase9-volume-notifications', { body: JSON.stringify({ notices: 100, defaultList: 25, maxList: 100, repeatedReadCount: 99, afterUnreadDismiss: 98, foreignMutation: 404, limits: 'Existing guarded inert operation seed + native cancel/read/dismiss. No enabled worker/provider/model or100-item bell UI claim.' }), contentType: 'application/json' });
} finally { await account.api.dispose(); await other.api.dispose(); }
});