test(qa): qualify bounded revision timeline and notification volume
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# Bounded volume supplement
|
||||
|
||||
**3/3 passed**, zero failures/skips/flaky/retries, `volume-qualified`, exit0 at20:46:29UTC on22October. Full TypeScript checking also passed. The initial runtime pass used an incorrect type-only import; that import was corrected and the complete three-case successor passed on the final source. Neither run changes product code.
|
||||
|
||||
| Scenario | Actual native result | Limit |
|
||||
|---|---|---|
|
||||
| 100 CV revisions | Exact descending1–100 membership; stale99 save rejected409 without appending history; restoring revision2 appends101 with its original headline; canonical Career readback unchanged | Isolated SQLite API. No100-row history UI, MariaDB, long-term growth or real model claim |
|
||||
| 100 long manual correspondence entries | All100 persisted IDs returned exactly; long content retained; second owner's job read404 | Manual timeline records, no external email delivery or100-message browser usability acceptance |
|
||||
| 100 notifications | Existing guarded one-insert inert-operation helper + actual native cancellation generates100 distinct notices; default25/max100 list, limit101 rejected400; repeated Read counts99 once, unread Dismiss counts98, foreign mutation404 | No enabled-worker/provider/inference proof or100-item bell UI acceptance |
|
||||
|
||||
The separate config reuses the existing API fixture on **localhost5304**, with a unique `jobtracker-e2e-*` direct child of system temporary storage and reuse disabled. It does not share the ordinary browser database or start another frontend. The existing helper verifies actual temp path, schema and exact owner before permitting one inert operation insert; all notification creation/read/dismissal goes through native product APIs. No production data or new seeding framework is used.
|
||||
|
||||
[Portable observations](../volume-results.json), [cases](../volume-cases.json) and [validation](../../validation.json) retain outcomes. The two source hashes are separately recorded as `supplementalSources`; the original825-input browser/product manifest remains unchanged. This supplement is additional to the18-case diagnostics and the362-case ordinary suite, never a replacement for either.
|
||||
|
||||
From `job-tracker-ui`, use the existing pinned DOTNET environment and run `rtk proxy node node_modules/@playwright/test/cli.js test --config=../docs/redesign/phase-9/p9-05/volume/playwright.config.ts --retries=0 --reporter=list,json`, with its own `PLAYWRIGHT_JSON_OUTPUT_NAME`.
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from '../../../../../job-tracker-ui/node_modules/@playwright/test';
|
||||
import base from '../../../../../job-tracker-ui/playwright.config';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
const server = Array.isArray(base.webServer) ? base.webServer[0] : base.webServer!;
|
||||
const dataRoot = path.join(os.tmpdir(), `jobtracker-e2e-p9-volume-${process.pid}-${Date.now().toString(36)}`);
|
||||
export default defineConfig({ ...base, testDir: __dirname, testMatch: '*.spec.ts', retries: 0,
|
||||
metadata: { isolatedDataRoot: dataRoot },
|
||||
webServer: { ...server, cwd: path.resolve(__dirname, '../../../../../job-tracker-ui'),
|
||||
command: server.command.replaceAll('5302', '5304'), url: 'http://localhost:5304/health',
|
||||
reuseExistingServer: false, env: { ...server.env, Data__Root: dataRoot } } });
|
||||
@@ -0,0 +1,99 @@
|
||||
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(); }
|
||||
});
|
||||
Reference in New Issue
Block a user