Harden policy simulation direct-route defaults
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { mkdirSync, 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 DEFAULT_BASE_URL = process.env.STELLAOPS_FRONTDOOR_BASE_URL?.trim() || 'https://stella-ops.local';
|
||||
const authStatePath = path.join(outputDirectory, 'live-policy-simulation-direct-routes.auth.json');
|
||||
const authReportPath = path.join(outputDirectory, 'live-policy-simulation-direct-routes.auth-report.json');
|
||||
const summaryPath = path.join(outputDirectory, 'live-policy-simulation-direct-routes.json');
|
||||
|
||||
function isStaticAsset(url) {
|
||||
return /\.(?:css|js|map|png|jpg|jpeg|svg|woff2?|ico)(?:$|\?)/i.test(url);
|
||||
}
|
||||
|
||||
function shouldIgnoreFailure(errorText) {
|
||||
return errorText === 'net::ERR_ABORTED';
|
||||
}
|
||||
|
||||
function createRuntime() {
|
||||
return {
|
||||
consoleErrors: [],
|
||||
pageErrors: [],
|
||||
requestFailures: [],
|
||||
responseErrors: [],
|
||||
};
|
||||
}
|
||||
|
||||
function attachRuntimeWatchers(page, runtime) {
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') {
|
||||
runtime.consoleErrors.push({
|
||||
page: page.url(),
|
||||
text: message.text(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
page.on('pageerror', (error) => {
|
||||
runtime.pageErrors.push({
|
||||
page: page.url(),
|
||||
message: error.message,
|
||||
});
|
||||
});
|
||||
|
||||
page.on('requestfailed', (request) => {
|
||||
const url = request.url();
|
||||
const errorText = request.failure()?.errorText ?? 'unknown';
|
||||
if (isStaticAsset(url) || shouldIgnoreFailure(errorText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
runtime.requestFailures.push({
|
||||
page: page.url(),
|
||||
method: request.method(),
|
||||
url,
|
||||
error: errorText,
|
||||
});
|
||||
});
|
||||
|
||||
page.on('response', (response) => {
|
||||
const url = response.url();
|
||||
if (isStaticAsset(url) || response.status() < 400) {
|
||||
return;
|
||||
}
|
||||
|
||||
runtime.responseErrors.push({
|
||||
page: page.url(),
|
||||
method: response.request().method(),
|
||||
status: response.status(),
|
||||
url,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForAnyVisible(page, selectors, timeout = 10_000) {
|
||||
const deadline = Date.now() + timeout;
|
||||
while (Date.now() < deadline) {
|
||||
for (const selector of selectors) {
|
||||
if (await page.locator(selector).first().isVisible().catch(() => false)) {
|
||||
return selector;
|
||||
}
|
||||
}
|
||||
await page.waitForTimeout(200);
|
||||
}
|
||||
throw new Error(`Timed out waiting for any selector: ${selectors.join(', ')}`);
|
||||
}
|
||||
|
||||
async function checkRoute(page, baseUrl, route) {
|
||||
const startedAt = new Date().toISOString();
|
||||
let ok = false;
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
await page.goto(`${baseUrl}${route.path}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30_000,
|
||||
});
|
||||
await page.waitForLoadState('networkidle', { timeout: 15_000 }).catch(() => {});
|
||||
await waitForAnyVisible(page, route.expectVisible);
|
||||
|
||||
if (route.action) {
|
||||
await route.action(page);
|
||||
}
|
||||
|
||||
ok = true;
|
||||
} catch (cause) {
|
||||
error = cause instanceof Error ? cause.message : String(cause);
|
||||
}
|
||||
|
||||
return {
|
||||
route: route.path,
|
||||
ok,
|
||||
startedAtUtc: startedAt,
|
||||
finishedAtUtc: new Date().toISOString(),
|
||||
finalUrl: page.url(),
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
mkdirSync(outputDirectory, { recursive: true });
|
||||
|
||||
const authReport = await authenticateFrontdoor({
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
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 runtime = createRuntime();
|
||||
attachRuntimeWatchers(page, runtime);
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/ops/policy/simulation/coverage',
|
||||
expectVisible: ['h1:has-text("Test Coverage")', '.coverage__header'],
|
||||
action: async (routePage) => {
|
||||
await waitForAnyVisible(routePage, ['.coverage__summary', '.coverage__rules']);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/ops/policy/simulation/lint',
|
||||
expectVisible: ['h1:has-text("Policy Lint")', '.policy-lint__header'],
|
||||
action: async (routePage) => {
|
||||
await routePage.getByRole('button', { name: /run lint/i }).click();
|
||||
await waitForAnyVisible(routePage, ['.lint-status', '.lint-summary']);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/ops/policy/simulation/promotion',
|
||||
expectVisible: ['h1:has-text("Promotion Gate")', '.promotion-gate__header'],
|
||||
action: async (routePage) => {
|
||||
await routePage.getByRole('button', { name: /check requirements/i }).click();
|
||||
await waitForAnyVisible(routePage, ['.promotion-gate__info', '.promotion-gate__summary']);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/ops/policy/simulation/diff/policy-pack-001',
|
||||
expectVisible: ['h1:has-text("Policy Diff")', '.diff-viewer__header'],
|
||||
action: async (routePage) => {
|
||||
await waitForAnyVisible(routePage, ['.diff-viewer__stats', '.diff-viewer__empty', '.diff-viewer__files']);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const results = [];
|
||||
try {
|
||||
for (const route of routes) {
|
||||
results.push(await checkRoute(page, DEFAULT_BASE_URL, route));
|
||||
}
|
||||
} finally {
|
||||
await context.close().catch(() => {});
|
||||
await browser.close().catch(() => {});
|
||||
}
|
||||
|
||||
const summary = {
|
||||
capturedAtUtc: new Date().toISOString(),
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
results,
|
||||
runtime,
|
||||
failedRouteCount: results.filter((result) => result.ok === false).length,
|
||||
runtimeIssueCount:
|
||||
runtime.consoleErrors.length +
|
||||
runtime.pageErrors.length +
|
||||
runtime.requestFailures.length +
|
||||
runtime.responseErrors.length,
|
||||
};
|
||||
|
||||
writeFileSync(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
|
||||
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
||||
|
||||
if (summary.failedRouteCount > 0 || summary.runtimeIssueCount > 0) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
process.stderr.write(
|
||||
`[live-policy-simulation-direct-routes] ${error instanceof Error ? error.message : String(error)}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user