// Local-only observational audit. Product failures are recorded, not blessed as regression expectations. // Run from the repository root after Release API and static UI builds (see verification.md). // Browser requests are restricted to loopback; integrations exercised here use local synthetic fixtures. import { createRequire } from 'node:module'; import { spawn } from 'node:child_process'; import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { createWriteStream } from 'node:fs'; import path from 'node:path'; import os from 'node:os'; const root = process.cwd(); const ui = path.join(root, 'job-tracker-ui'); const require = createRequire(path.join(ui, 'package.json')); const { chromium, request } = require('@playwright/test'); const AxeBuilder = require('@axe-core/playwright').default; const evidence = path.join(root, '.artifacts', 'expanded-audit'); await mkdir(evidence, { recursive: true }); const dataRoot = await mkdtemp(path.join(os.tmpdir(), 'jobjakt-audit-')); const apiOrigin = 'http://localhost:5402'; const uiOrigin = 'http://localhost:3400'; const password = 'SyntheticAudit123!'; const findings = []; const consoleErrors = []; const networkErrors = []; const timings = []; const blockedExternalHosts = new Set(); const children = []; let browser; const record = (scenario, meetsExpectation, observation) => findings.push({ scenario, meetsExpectation, observation }); async function waitReady(url) { for (let i = 0; i < 120; i++) { try { if ((await fetch(url, { signal: AbortSignal.timeout(1000) })).ok) return; } catch {} await new Promise(resolve => setTimeout(resolve, 500)); } throw new Error(`Local audit server did not start: ${url}`); } function start(command, args, cwd, env, logName) { const child = spawn(command, args, { cwd, env: { ...process.env, ...env }, windowsHide: true }); const log = createWriteStream(path.join(evidence, logName)); child.stdout.pipe(log); child.stderr.pipe(log); children.push(child); return child; } async function stop(child) { if (child.exitCode !== null) return; if (process.platform === 'win32') { await new Promise(resolve => { const killer = spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }); killer.on('exit', resolve); }); } else child.kill('SIGTERM'); } async function client(email) { const client = await request.newContext({ baseURL: apiOrigin }); const login = await client.post('/api/auth/login', { data: { email, password } }); if (!login.ok()) throw new Error(`Synthetic login failed: ${login.status()}`); const cookie = (await client.storageState()).cookies.find(c => c.name === 'XSRF-TOKEN'); return { client, headers: { 'X-CSRF-TOKEN': cookie.value } }; } async function body(response) { if (!response.ok()) throw new Error(`Fixture request failed: ${response.status()} ${response.url()}`); return response.json(); } try { // Override outbound integrations and storage even if the developer shell has provider settings. start(process.env.DOTNET_HOST_PATH || path.join(os.homedir(), '.dotnet', 'dotnet.exe'), [path.join(root, 'JobTrackerApi/bin/Release/net9.0/JobTrackerApi.dll'), '--urls', apiOrigin], path.join(root, 'JobTrackerApi'), { ASPNETCORE_ENVIRONMENT: 'Development', DOTNET_CLI_TELEMETRY_OPTOUT: '1', Data__Root: dataRoot, Database__Provider: 'sqlite', ConnectionStrings__JobTracker: `Data Source=${path.join(dataRoot, 'jobtracker.db')}`, AccountLifecycle__TombstonesRoot: path.join(dataRoot, 'tombstones'), Auth__Require: 'true', Auth__JwtKey: 'audit-synthetic-signing-key-only-32-chars-minimum', Auth__AdminEmail: 'audit-admin@example.test', Auth__AdminPassword: password, Auth__TurnstileSecretKey: '', Auth__GoogleClientId: '', Auth__MicrosoftClientId: '', Auth__AllowRegistration: 'false', Auth__MicrosoftTenant: 'common', App__PublicBaseUrl: uiOrigin, Cors__Origins__0: uiOrigin, HttpsRedirection__Enabled: 'false', Ai__BaseUrl: 'http://127.0.0.1:1', Email__Enabled: 'false', Stripe__SecretKey: '', Workers__RulesEnabled: 'false', Workers__FollowUpRemindersEnabled: 'false', Workers__DailyExportEnabled: 'false', Workers__JobEnrichmentEnabled: 'false', Backups__Enabled: 'false', CV_PDF_BROWSER_PATH: chromium.executablePath(), }, 'break-api.log'); await waitReady(`${apiOrigin}/health`); const admin = await client('audit-admin@example.test'); for (const email of ['audit-a@example.test', 'audit-b@example.test']) { await body(await admin.client.post('/api/users', { headers: admin.headers, data: { email, password, roles: [] } })); } const a = await client('audit-a@example.test'); const b = await client('audit-b@example.test'); await body(await a.client.put('/api/career/profile', { headers: a.headers, data: { profile: { version: '1', contact: { fullName: 'Åse Audit 🧪', email: 'audit-a@example.test', headline: 'Software developer' }, summary: ['Synthetic profile for local audit only.'], jobs: [{ company: 'Fjord Example', title: 'Developer', start: '2021', end: '2025', bullets: ['Built reliable APIs.'] }], skills: ['C#', 'TypeScript', 'Norsk'], } } })); const cv = await body(await a.client.post('/api/cv/variants', { headers: a.headers, data: { name: 'Audit CV' } })); const stale = await body(await a.client.get(`/api/cv/variants/${cv.id}`)); const first = await a.client.put(`/api/cv/variants/${cv.id}`, { headers: a.headers, data: { name: 'Newer edit', settings: cv.settings, version: cv.version } }); const second = await a.client.put(`/api/cv/variants/${cv.id}`, { headers: a.headers, data: { name: 'Stale tab edit', settings: stale.settings, version: stale.version } }); const final = await body(await a.client.get(`/api/cv/variants/${cv.id}`)); record('stale CV save', second.status() === 409 || second.status() === 412, { first: first.status(), staleSave: second.status(), overwritten: final.name === 'Stale tab edit', version: final.version }); const concurrent = await Promise.all(['Concurrent A', 'Concurrent B'].map(name => a.client.put(`/api/cv/variants/${cv.id}`, { headers: a.headers, data: { name, settings: cv.settings } }))); record('simultaneous CV saves detect conflict', concurrent.some(r => [409, 412].includes(r.status())), { statuses: concurrent.map(r => r.status()) }); for (const method of ['get', 'put', 'delete']) { const response = await b.client[method](`/api/cv/variants/${cv.id}`, { headers: b.headers, ...(method === 'put' ? { data: { name: 'Cross-owner', settings: cv.settings } } : {}) }); record(`CV cross-owner ${method}`, [403, 404].includes(response.status()), { status: response.status() }); } const company = await body(await a.client.post('/api/companies', { headers: a.headers, data: { name: 'Åse & Sons 🧪', location: 'Oslo' } })); const input = { companyId: company.id, jobTitle: '=1+1', status: 'Saved', notes: '' }; const created = await Promise.all([0, 1].map(() => a.client.post('/api/jobapplications', { headers: a.headers, data: input }))); const jobs = await Promise.all(created.map(body)); record('duplicate job creation deduplicated', jobs[0].id === jobs[1].id, { statuses: created.map(r => r.status()), distinctIds: jobs[0].id !== jobs[1].id }); for (const [stamp, expectedUtc] of [ ['2026-03-28T09:00:00+01:00', '20260328T080000Z'], ['2026-03-29T09:00:00+02:00', '20260329T070000Z'], ['2026-10-25T02:30:00+02:00', '20261025T003000Z'], ['2026-10-25T02:30:00+01:00', '20261025T013000Z'], ]) { const dated = await body(await a.client.post('/api/jobapplications', { headers: a.headers, data: { companyId: company.id, jobTitle: 'Synthetic DST probe', status: 'Saved', followUpAt: stamp } })); const ics = await a.client.get(`/api/calendar/jobs/${dated.id}/events.ics?kind=follow-up`); const content = await ics.text(); const actual = content.match(/^DTSTART:(.+)$/m)?.[1]?.trim(); record(`DST calendar instant ${stamp}`, actual === expectedUtc, { status: ics.status(), expectedUtc, actual }); } const csv = await a.client.get('/api/export/jobs?format=csv'); const csvText = await csv.text(); record('CSV formula neutralized', !csvText.includes(',=1+1,'), { formulaCellPresent: csvText.includes(',=1+1,') }); for (const route of ['/api/auth/me', `/api/cv/variants/${cv.id}`, '/api/export/jobs?format=csv']) { const response = await a.client.get(route); record(`sensitive response cache ${route.split('?')[0]}`, /no-store/i.test(response.headers()['cache-control'] || ''), { cacheControl: response.headers()['cache-control'] || null }); } const adminAccess = await b.client.get('/api/admin/audit'); record('standard user admin denial', adminAccess.status() === 403, { status: adminAccess.status() }); for (let i = 0; i < 2; i++) await a.client.post('/api/export/account', { headers: a.headers, data: {} }); const otherExport = await b.client.post('/api/export/account', { headers: b.headers, data: {} }); record('account export limiter isolation', otherExport.status() === 200, { otherAccountStatusAfterTwoExports: otherExport.status() }); start(process.execPath, [path.join(ui, 'scripts/serve-export.mjs')], ui, { PORT: '3400' }, 'break-ui.log'); await waitReady(uiOrigin); browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: { width: 1440, height: 900 } }); await context.route('**/*', route => { const url = new URL(route.request().url()); if (['localhost', '127.0.0.1'].includes(url.hostname) || ['data:', 'about:', 'blob:'].includes(url.protocol)) return route.continue(); blockedExternalHosts.add(url.hostname); return route.abort(); }); await context.addInitScript(() => { if (!localStorage.getItem('uiLanguage')) localStorage.setItem('uiLanguage', 'en'); }); const page = await context.newPage(); page.on('pageerror', error => consoleErrors.push(error.message)); page.on('console', message => { if (message.type() === 'error' || message.type() === 'warning') consoleErrors.push(message.text().slice(0, 500)); }); page.on('response', response => { if (response.status() >= 400) networkErrors.push({ path: new URL(response.url()).pathname, status: response.status() }); }); await page.goto(`${uiOrigin}/login`); await page.getByLabel('Email').fill('audit-a@example.test'); await page.getByLabel('Current password').fill(password); await page.getByRole('button', { name: 'Sign in', exact: true }).click(); await page.waitForURL('**/dashboard'); for (const route of ['/jobs', '/career', `/career/builder/${cv.id}`]) { const started = Date.now(); await page.goto(`${uiOrigin}${route}`); await page.locator('main').waitFor(); timings.push({ route, navigationToMainMs: Date.now() - started }); } await page.getByLabel('CV name', { exact: true }).waitFor(); for (const language of ['en', 'nb']) { await a.client.put('/api/auth/preferences/language', { headers: a.headers, data: { language } }); for (const theme of ['light', 'dark']) { await page.evaluate(({ language, theme }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', theme); }, { language, theme }); await page.reload(); await page.locator('main').waitFor(); await page.waitForFunction(() => document.querySelector('[data-cv-page] iframe')?.getAttribute('srcdoc')?.includes('Synthetic profile')); await page.waitForTimeout(400); for (const width of [375, 1440]) { await page.setViewportSize({ width, height: 900 }); await page.screenshot({ path: path.join(evidence, `builder-${language}-${theme}-${width}.png`), fullPage: true }); const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth); const axe = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa']).analyze(); record(`builder layout ${language}/${theme}/${width}`, overflow <= 1 && axe.violations.length === 0, { overflow, actualLanguage: await page.locator('html').getAttribute('lang'), actualTheme: await page.locator('html').getAttribute('data-color-scheme'), violations: axe.violations.map(v => ({ id: v.id, impact: v.impact, nodes: v.nodes.map(n => ({ target: n.target, failureSummary: n.failureSummary })) })) }); } } } await a.client.put('/api/auth/preferences/language', { headers: a.headers, data: { language: 'en' } }); await page.evaluate(() => { localStorage.setItem('uiLanguage', 'en'); localStorage.setItem('jobtracker.themeMode', 'light'); }); await page.reload(); await page.getByLabel('CV name', { exact: true }).waitFor(); await context.setOffline(true); await page.getByLabel('CV name', { exact: true }).fill('Offline synthetic draft'); await page.waitForTimeout(1600); record('offline save reports failure', await page.getByText(/save failed|not saved|could not save/i).count() > 0, { matchingFailureMessages: await page.getByText(/save failed|not saved|could not save/i).count() }); await context.setOffline(false); page.once('dialog', dialog => dialog.accept()); await page.reload(); const recoveredName = await page.getByLabel('CV name', { exact: true }).inputValue(); record('offline draft survives confirmed refresh', recoveredName === 'Offline synthetic draft', { draftRecovered: recoveredName === 'Offline synthetic draft' }); const secondTab = await context.newPage(); await secondTab.goto(`${uiOrigin}/career/builder/${cv.id}`); await secondTab.getByLabel('CV name', { exact: true }).waitFor(); await secondTab.waitForTimeout(1000); await page.evaluate(async () => { await fetch('http://localhost:5402/api/auth/logout', { method: 'POST', credentials: 'include' }); localStorage.removeItem('authUserKey'); window.dispatchEvent(new Event('auth-changed')); }); await secondTab.waitForTimeout(1000); record('logout clears other tab immediately', !(await secondTab.getByLabel('CV name', { exact: true }).count()), { privateEditorStillMounted: (await secondTab.getByLabel('CV name', { exact: true }).count()) > 0 }); await context.close(); } catch (error) { if (browser) for (const context of browser.contexts()) for (const page of context.pages()) { await page.screenshot({ path: path.join(evidence, 'harness-failure.png'), fullPage: true }).catch(() => {}); } findings.push({ scenario: 'harness', meetsExpectation: false, observation: String(error.stack || error) }); process.exitCode = 2; } finally { if (browser) await browser.close(); for (const child of children.reverse()) await stop(child); await writeFile(path.join(evidence, 'break-results.json'), JSON.stringify({ generatedAt: new Date().toISOString(), scope: 'isolated SQLite and local Chromium; synthetic data; outbound browser requests blocked', findings, consoleErrors, networkErrors, blockedExternalHosts: [...blockedExternalHosts], timings }, null, 2)); console.log(JSON.stringify({ observations: findings.length, unmet: findings.filter(f => !f.meetsExpectation).length, evidence: '.artifacts/expanded-audit/break-results.json' })); }