#!/usr/bin/env node import { mkdir, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { chromium } from 'playwright'; import { authenticateFrontdoor, createAuthenticatedContext } from './live-frontdoor-auth.mjs'; const webRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const outputDir = path.join(webRoot, 'output', 'playwright'); const outputPath = path.join(outputDir, 'live-jobs-queues-action-sweep.json'); const authStatePath = path.join(outputDir, 'live-jobs-queues-action-sweep.state.json'); const authReportPath = path.join(outputDir, 'live-jobs-queues-action-sweep.auth.json'); const scopeQuery = 'tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d'; const routePath = '/ops/operations/jobs-queues'; function scopedUrl(route = routePath) { const separator = route.includes('?') ? '&' : '?'; return `https://stella-ops.local${route}${separator}${scopeQuery}`; } async function settle(page) { await page.waitForLoadState('domcontentloaded', { timeout: 20_000 }).catch(() => {}); await page.waitForTimeout(1_200); } async function gotoPage(page) { await page.goto(scopedUrl(), { waitUntil: 'domcontentloaded', timeout: 30_000 }); await settle(page); } async function clickTab(page, tabName) { await page.getByRole('button', { name: tabName }).click({ timeout: 10_000 }); await settle(page); } async function rowCount(page) { return page.locator('tbody tr').count(); } function searchFilter(page) { return page.locator('section.filters input[type="search"]').first(); } function filterSelect(page, index) { return page.locator('section.filters select').nth(index); } function actionBanner(page) { return page.locator('section.jobs-queues .jobs-queues__banner[role="status"]').first(); } async function captureRuntimeIssues(page) { const alerts = await page .locator('[role="alert"], .error-banner, .toast, .notification') .evaluateAll((nodes) => nodes .map((node) => (node.textContent || '').trim().replace(/\s+/g, ' ')) .filter(Boolean), ) .catch(() => []); return alerts; } async function readActionHrefs(page) { return page.locator('tbody tr:first-child .actions a').evaluateAll((links) => links.map((link) => ({ label: (link.textContent || '').trim().replace(/\s+/g, ' '), href: link.getAttribute('href') || '', })), ); } async function assert(condition, message, details = {}) { if (!condition) { throw new Error(`${message}${Object.keys(details).length ? ` ${JSON.stringify(details)}` : ''}`); } } async function run() { await mkdir(outputDir, { recursive: true }); const authReport = await authenticateFrontdoor({ statePath: authStatePath, reportPath: authReportPath, headless: true, }); const browser = await chromium.launch({ headless: true, args: ['--disable-dev-shm-usage'], }); const context = await createAuthenticatedContext(browser, authReport, { statePath: authStatePath }); const page = await context.newPage(); const consoleErrors = []; const responseErrors = []; const requestFailures = []; page.on('console', (message) => { if (message.type() === 'error') { consoleErrors.push(message.text()); } }); page.on('response', (response) => { const url = response.url(); if (/\.(?:css|js|map|png|jpg|jpeg|svg|woff2?)(?:$|\?)/i.test(url)) { return; } if (response.status() >= 400 && !url.includes('/connect/authorize')) { responseErrors.push({ status: response.status(), method: response.request().method(), url, }); } }); page.on('requestfailed', (request) => { const url = request.url(); const failure = request.failure()?.errorText ?? 'unknown'; if (failure === 'net::ERR_ABORTED') { return; } if (/\.(?:css|js|map|png|jpg|jpeg|svg|woff2?)(?:$|\?)/i.test(url)) { return; } requestFailures.push({ method: request.method(), url, error: failure, }); }); const checks = []; try { await gotoPage(page); await assert((await page.locator('h1').first().textContent())?.trim() === 'Jobs & Queues', 'Unexpected page heading.'); let actions = await readActionHrefs(page); await assert(actions.length === 2, 'Jobs tab did not expose the expected two row actions.', { actions }); await assert(actions[0].href.includes('/ops/operations/jobengine'), 'Jobs primary action did not target JobEngine.', { actions }); await assert(actions[1].href.includes('/ops/operations/jobengine/jobs'), 'Jobs secondary action did not target the Job list.', { actions }); await assert(!actions.some((entry) => entry.href.includes('/ops/operations/jobs-queues')), 'Jobs tab still contains self-linking row actions.', { actions }); checks.push({ name: 'jobs-row-actions', ok: true, actions }); await searchFilter(page).fill('Vulnerability'); await settle(page); let rows = await rowCount(page); await assert(rows === 1, 'Jobs search filter did not reduce the table to one row.', { rows }); await page.getByRole('button', { name: 'Clear filters' }).click(); await settle(page); rows = await rowCount(page); await assert(rows === 4, 'Jobs clear filters did not restore all rows.', { rows }); checks.push({ name: 'jobs-filters', ok: true, rowsAfterClear: rows }); await searchFilter(page).fill('Vulnerability'); await settle(page); await clickTab(page, 'Runs'); await assert(await searchFilter(page).inputValue() === '', 'Tab switch did not clear the search filter.'); checks.push({ name: 'tab-switch-clears-filters', ok: true }); await filterSelect(page, 0).selectOption('FAILED'); await filterSelect(page, 1).selectOption('DEGRADED'); await settle(page); rows = await rowCount(page); await assert(rows === 1, 'Runs filters did not isolate the failed degraded row.', { rows }); checks.push({ name: 'runs-filters', ok: true, rows }); await page.getByRole('button', { name: 'Clear filters' }).click(); await settle(page); await page.getByRole('button', { name: 'Copy CorrID' }).first().click(); await settle(page); const statusText = ((await actionBanner(page).textContent()) || '').trim(); await assert(statusText.includes('corr-run-001'), 'Runs copy action did not produce inline feedback.', { statusText }); checks.push({ name: 'runs-copy-feedback', ok: true, statusText }); await filterSelect(page, 0).selectOption('DEAD-LETTER'); await filterSelect(page, 1).selectOption('BLOCKING'); await settle(page); await page.getByRole('link', { name: 'Open Dead-Letter Queue' }).click(); await settle(page); await assert(page.url().includes('/ops/operations/dead-letter/queue'), 'Runs dead-letter handoff did not reach the DLQ.'); checks.push({ name: 'runs-dead-letter-handoff', ok: true, targetUrl: page.url() }); await gotoPage(page); await clickTab(page, 'Schedules'); await filterSelect(page, 0).selectOption('FAIL'); await filterSelect(page, 1).selectOption('Daily'); await settle(page); rows = await rowCount(page); await assert(rows === 1, 'Schedules filters did not isolate the failed daily schedule.', { rows }); await page.getByRole('link', { name: 'Review Dead-Letter Queue' }).click(); await settle(page); await assert(page.url().includes('/ops/operations/dead-letter/queue'), 'Schedules failure handoff did not reach the DLQ.'); checks.push({ name: 'schedules-failure-handoff', ok: true, targetUrl: page.url() }); await gotoPage(page); await clickTab(page, 'Schedules'); await page.getByRole('link', { name: 'Manage Schedules' }).first().click(); await settle(page); await assert(page.url().includes('/ops/operations/scheduler/schedules'), 'Schedules primary handoff did not reach Scheduler.'); checks.push({ name: 'schedules-primary-handoff', ok: true, targetUrl: page.url() }); await gotoPage(page); await clickTab(page, 'Dead Letters'); actions = await readActionHrefs(page); await assert(actions[0].href.includes('/ops/operations/dead-letter/queue'), 'Dead-letter primary action did not target the queue.', { actions }); await assert(actions[1].href.includes('/ops/operations/data-integrity/dlq'), 'Dead-letter recovery action did not target the DLQ recovery surface.', { actions }); await page.getByRole('link', { name: 'Open Replay Recovery' }).first().click(); await settle(page); await assert(page.url().includes('/ops/operations/data-integrity/dlq'), 'Dead-letter recovery handoff did not reach Data Integrity.'); checks.push({ name: 'dead-letter-handoffs', ok: true, targetUrl: page.url() }); await gotoPage(page); await clickTab(page, 'Workers'); await filterSelect(page, 0).selectOption('DEGRADED'); await filterSelect(page, 1).selectOption('feeds'); await settle(page); rows = await rowCount(page); await assert(rows === 1, 'Workers filters did not isolate the degraded feeds worker.', { rows }); await page.getByRole('link', { name: 'Open Worker Fleet' }).click(); await settle(page); await assert(page.url().includes('/ops/operations/scheduler/workers'), 'Workers primary handoff did not reach Worker Fleet.'); checks.push({ name: 'workers-primary-handoff', ok: true, targetUrl: page.url() }); await gotoPage(page); await clickTab(page, 'Workers'); await filterSelect(page, 0).selectOption('DEGRADED'); await filterSelect(page, 1).selectOption('feeds'); await settle(page); await page.getByRole('link', { name: 'Inspect Scheduler Runs' }).click(); await settle(page); await assert(page.url().includes('/ops/operations/scheduler/runs'), 'Workers secondary handoff did not reach Scheduler Runs.'); checks.push({ name: 'workers-secondary-handoff', ok: true, targetUrl: page.url() }); const runtimeIssues = { consoleErrors, responseErrors, requestFailures, alerts: await captureRuntimeIssues(page), }; const summary = { checkedAtUtc: new Date().toISOString(), routePath, checks, runtimeIssues, failedCheckCount: checks.filter((check) => !check.ok).length, runtimeIssueCount: runtimeIssues.consoleErrors.length + runtimeIssues.responseErrors.length + runtimeIssues.requestFailures.length + runtimeIssues.alerts.length, }; await writeFile(outputPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8'); if (summary.failedCheckCount > 0 || summary.runtimeIssueCount > 0) { process.exitCode = 1; } } catch (error) { const summary = { checkedAtUtc: new Date().toISOString(), routePath, checks, error: error instanceof Error ? error.message : String(error), consoleErrors, responseErrors, requestFailures, }; await writeFile(outputPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8'); process.exitCode = 1; } finally { await context.close().catch(() => {}); await browser.close().catch(() => {}); } } await run();