#!/usr/bin/env node import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { chromium } from 'playwright'; import { authenticateFrontdoor, createAuthenticatedContext } from './live-frontdoor-auth.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const webRoot = path.resolve(__dirname, '..'); const outputDirectory = path.join(webRoot, 'output', 'playwright'); const BASE_URL = process.env.STELLAOPS_FRONTDOOR_BASE_URL?.trim() || 'https://stella-ops.local'; const STATE_PATH = path.join(outputDirectory, 'live-frontdoor-auth-state.json'); const REPORT_PATH = path.join(outputDirectory, 'live-frontdoor-auth-report.json'); const RESULT_PATH = path.join(outputDirectory, 'live-frontdoor-canonical-route-sweep.json'); const scopedPrefixes = ['/mission-control', '/releases', '/security', '/evidence', '/ops']; const defaultScopeEntries = [ ['tenant', 'demo-prod'], ['regions', 'us-east'], ['environments', 'stage'], ['timeWindow', '7d'], ]; const canonicalRoutes = [ '/mission-control/board', '/mission-control/alerts', '/mission-control/activity', '/releases', '/releases/overview', '/releases/versions', '/releases/versions/new', '/releases/runs', '/releases/approvals', '/releases/promotion-queue', '/releases/hotfixes', '/releases/hotfixes/new', '/releases/environments', '/releases/deployments', '/releases/investigation/timeline', '/releases/investigation/deploy-diff', '/releases/investigation/change-trace', '/security', '/security/posture', '/security/triage', '/security/advisories-vex', '/security/disposition', '/security/supply-chain-data', '/security/supply-chain-data/graph', '/security/sbom-lake', '/security/reachability', '/security/reports', '/evidence', '/evidence/overview', '/evidence/capsules', '/evidence/threads', '/evidence/verify-replay', '/evidence/proofs', '/evidence/exports', '/evidence/audit-log', '/ops', '/ops/operations', '/ops/operations/jobs-queues', '/ops/operations/feeds-airgap', '/ops/operations/data-integrity', '/ops/operations/system-health', '/ops/operations/health-slo', '/ops/operations/jobengine', '/ops/operations/scheduler', '/ops/operations/quotas', '/ops/operations/offline-kit', '/ops/operations/dead-letter', '/ops/operations/aoc', '/ops/operations/doctor', '/ops/operations/signals', '/ops/operations/packs', '/ops/operations/ai-runs', '/ops/operations/notifications', '/ops/operations/status', '/ops/integrations', '/ops/integrations/onboarding', '/ops/integrations/registries', '/ops/integrations/scm', '/ops/integrations/ci', '/ops/integrations/runtime-hosts', '/ops/integrations/advisory-vex-sources', '/ops/integrations/secrets', '/ops/integrations/notifications', '/ops/integrations/sbom-sources', '/ops/integrations/activity', '/ops/integrations/registry-admin', '/ops/policy', '/ops/policy/overview', '/ops/policy/baselines', '/ops/policy/gates', '/ops/policy/simulation', '/ops/policy/waivers', '/ops/policy/risk-budget', '/ops/policy/trust-weights', '/ops/policy/staleness', '/ops/policy/sealed-mode', '/ops/policy/profiles', '/ops/policy/validator', '/ops/policy/audit', '/ops/platform-setup', '/ops/platform-setup/regions-environments', '/ops/platform-setup/promotion-paths', '/ops/platform-setup/workflows-gates', '/ops/platform-setup/release-templates', '/ops/platform-setup/policy-bindings', '/ops/platform-setup/gate-profiles', '/ops/platform-setup/defaults-guardrails', '/ops/platform-setup/trust-signing', '/setup', '/setup/integrations', '/setup/integrations/advisory-vex-sources', '/setup/integrations/secrets', '/setup/identity-access', '/setup/tenant-branding', '/setup/notifications', '/setup/usage', '/setup/system', '/setup/trust-signing', '/setup/topology', '/setup/topology/overview', '/setup/topology/map', '/setup/topology/regions', '/setup/topology/environments', '/setup/topology/targets', '/setup/topology/hosts', '/setup/topology/agents', '/setup/topology/connectivity', '/setup/topology/runtime-drift', '/setup/topology/promotion-graph', '/setup/topology/workflows', '/setup/topology/gate-profiles', ]; const strictRouteExpectations = { '/security/advisories-vex': { title: /Advisories/i, texts: ['Security / Advisories & VEX', 'Providers'], }, '/security/sbom-lake': { title: /SBOM Lake/i, texts: ['SBOM Lake', 'Attestation Coverage Metrics'], }, '/security/reachability': { title: /Reachability/i, texts: ['Reachability', 'Proof of Exposure'], }, '/setup/trust-signing': { title: /Trust/i, texts: ['Trust Management'], }, '/setup/integrations': { title: /Integrations/i, texts: ['Integrations', 'External system connectors'], }, '/setup/integrations/advisory-vex-sources': { title: /Advisory & VEX Sources/i, texts: ['Integrations', 'FeedMirror Integrations'], }, '/setup/integrations/secrets': { title: /Secrets/i, texts: ['Integrations', 'RepoSource Integrations'], }, '/ops/policy': { title: /Policy/i, texts: ['Policy Decisioning Studio', 'One operator shell for policy, VEX, and release gates'], }, '/ops/policy/overview': { title: /Policy/i, texts: ['Policy Decisioning Studio', 'One operator shell for policy, VEX, and release gates'], }, '/ops/policy/risk-budget': { title: /Policy/i, texts: ['Policy Decisioning Studio', 'Risk Budget Overview'], }, }; const allowedFinalPaths = { '/releases': ['/releases/deployments'], '/releases/promotion-queue': ['/releases/promotions'], '/ops/policy': ['/ops/policy/overview'], '/ops/policy/audit': ['/ops/policy/audit/policy'], '/ops/platform-setup/trust-signing': ['/setup/trust-signing'], '/setup/topology': ['/setup/topology/overview'], }; function buildRouteUrl(routePath) { const url = new URL(routePath, BASE_URL); if (scopedPrefixes.some((prefix) => routePath.startsWith(prefix))) { for (const [key, value] of defaultScopeEntries) { if (!url.searchParams.has(key)) { url.searchParams.set(key, value); } } } return url.toString(); } function trimText(value, maxLength = 300) { const normalized = value.replace(/\s+/g, ' ').trim(); return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized; } function shouldIgnoreUrl(url) { return /\.(?:css|js|map|png|jpg|jpeg|svg|woff2?)(?:$|\?)/i.test(url) || url.startsWith('data:'); } async function collectHeadings(page) { return page.locator('h1, main h1, main h2, h2').evaluateAll((elements) => elements .map((element) => (element.textContent || '').replace(/\s+/g, ' ').trim()) .filter((text, index, values) => text.length > 0 && values.indexOf(text) === index), ).catch(() => []); } async function collectVisibleProblemTexts(page) { return page.locator([ '[role="alert"]', '.alert', '.banner', '.status-banner', '.degraded-banner', '.warning-banner', '.error-banner', '.empty-state', '.error-state', ].join(', ')).evaluateAll((elements) => elements .map((element) => (element.textContent || '').replace(/\s+/g, ' ').trim()) .filter((text) => text.length > 0 && /(failed|unable|error|warning|degraded|unavailable|timed out|timeout|no results)/i.test(text), ), ).catch(() => []); } async function collectVisibleActions(page) { return page.locator('main a[href], main button').evaluateAll((elements) => elements .map((element) => { const tagName = element.tagName.toLowerCase(); const text = (element.textContent || '').replace(/\s+/g, ' ').trim(); const href = tagName === 'a' ? element.getAttribute('href') || '' : ''; return { tagName, text, href, }; }) .filter((entry) => entry.text.length > 0 || entry.href.length > 0) .slice(0, 20), ).catch(() => []); } function collectExpectationFailures(routePath, title, headings, bodyText) { const expectation = strictRouteExpectations[routePath]; if (!expectation) { return []; } const failures = []; if (!expectation.title.test(title)) { failures.push(`title mismatch: expected ${expectation.title}`); } for (const text of expectation.texts) { const present = headings.some((heading) => heading.includes(text)) || bodyText.includes(text); if (!present) { failures.push(`missing text: ${text}`); } } return failures; } function finalPathMatchesRoute(routePath, finalUrl) { const finalPath = new URL(finalUrl).pathname; if (finalPath === routePath) { return true; } return allowedFinalPaths[routePath]?.includes(finalPath) ?? false; } async function inspectRoute(context, routePath) { const page = await context.newPage(); const consoleErrors = []; const requestFailures = []; const responseErrors = []; page.on('console', (message) => { if (message.type() === 'error') { consoleErrors.push(trimText(message.text(), 500)); } }); page.on('requestfailed', (request) => { const url = request.url(); if (shouldIgnoreUrl(url)) { return; } requestFailures.push({ method: request.method(), url, error: request.failure()?.errorText ?? 'unknown', }); }); page.on('response', (response) => { const url = response.url(); if (shouldIgnoreUrl(url)) { return; } if (response.status() >= 400) { responseErrors.push({ status: response.status(), method: response.request().method(), url, }); } }); const targetUrl = buildRouteUrl(routePath); await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 30_000 }); await page.waitForLoadState('networkidle', { timeout: 15_000 }).catch(() => {}); await page.waitForTimeout(1_500); const finalUrl = page.url(); const title = await page.title().catch(() => ''); const headings = await collectHeadings(page); const bodyText = trimText(await page.locator('body').innerText().catch(() => ''), 1_200); const problemTexts = await collectVisibleProblemTexts(page); const visibleActions = await collectVisibleActions(page); const finalPathMatches = finalPathMatchesRoute(routePath, finalUrl); const expectationFailures = collectExpectationFailures(routePath, title, headings, bodyText); const record = { routePath, targetUrl, finalUrl, title, headings, problemTexts, visibleActions, consoleErrors, requestFailures, responseErrors, finalPathMatches, expectationFailures, passed: finalPathMatches && expectationFailures.length === 0 && consoleErrors.length === 0 && requestFailures.length === 0 && responseErrors.length === 0 && problemTexts.length === 0, }; await page.close(); return record; } async function main() { mkdirSync(outputDirectory, { recursive: true }); await authenticateFrontdoor({ baseUrl: BASE_URL, statePath: STATE_PATH, reportPath: REPORT_PATH, headless: true, }); const authReport = JSON.parse(readFileSync(REPORT_PATH, 'utf8')); const browser = await chromium.launch({ headless: true, args: ['--disable-dev-shm-usage'] }); try { const context = await createAuthenticatedContext(browser, authReport, { statePath: STATE_PATH }); const routes = []; for (const routePath of canonicalRoutes) { const record = await inspectRoute(context, routePath); routes.push(record); const status = record.passed ? 'PASS' : 'FAIL'; process.stdout.write(`[live-frontdoor-canonical-route-sweep] ${status} ${routePath} -> ${record.finalUrl}\n`); } await context.close(); const summary = { checkedAtUtc: new Date().toISOString(), baseUrl: BASE_URL, totalRoutes: routes.length, passedRoutes: routes.filter((route) => route.passed).length, failedRoutes: routes.filter((route) => !route.passed).map((route) => route.routePath), routes, }; writeFileSync(RESULT_PATH, `${JSON.stringify(summary, null, 2)}\n`, 'utf8'); process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`); if (summary.failedRoutes.length > 0) { process.exitCode = 1; } } finally { await browser.close(); } } main().catch((error) => { process.stderr.write(`[live-frontdoor-canonical-route-sweep] ${error instanceof Error ? error.message : String(error)}\n`); process.exit(1); });