Restore policy frontdoor compatibility and live QA

This commit is contained in:
master
2026-03-10 06:18:30 +02:00
parent 6578c82602
commit ff4cd7e999
7 changed files with 2413 additions and 50 deletions

View File

@@ -0,0 +1,692 @@
#!/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-ops-policy-action-sweep.json');
const authStatePath = path.join(outputDir, 'live-ops-policy-action-sweep.state.json');
const authReportPath = path.join(outputDir, 'live-ops-policy-action-sweep.auth.json');
const scopeQuery = 'tenant=demo-prod&regions=us-east&environments=stage&timeWindow=7d';
const STEP_TIMEOUT_MS = 45_000;
async function settle(page) {
await page.waitForLoadState('domcontentloaded', { timeout: 15_000 }).catch(() => {});
await page.waitForTimeout(1_500);
}
async function headingText(page) {
const headings = page.locator('h1, h2, [data-testid="page-title"], .page-title');
const count = await headings.count();
for (let index = 0; index < Math.min(count, 4); index += 1) {
const text = (await headings.nth(index).innerText().catch(() => '')).trim();
if (text) {
return text;
}
}
return '';
}
async function captureSnapshot(page, label) {
const alerts = await page
.locator('[role="alert"], .mat-mdc-snack-bar-container, .toast, .notification, .error-banner')
.evaluateAll((nodes) =>
nodes
.map((node) => (node.textContent || '').trim().replace(/\s+/g, ' '))
.filter(Boolean)
.slice(0, 5),
)
.catch(() => []);
const dialogs = await page
.locator('[role="dialog"], dialog, .cdk-overlay-pane')
.evaluateAll((nodes) =>
nodes
.map((node) => (node.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 240))
.filter(Boolean)
.slice(0, 3),
)
.catch(() => []);
const visibleInputs = await page
.locator('input, textarea, select')
.evaluateAll((nodes) =>
nodes
.filter((node) => {
const style = globalThis.getComputedStyle(node);
return style.visibility !== 'hidden' && style.display !== 'none';
})
.map((node) => ({
tag: node.tagName,
type: node.getAttribute('type') || '',
name: node.getAttribute('name') || '',
placeholder: node.getAttribute('placeholder') || '',
value: node.value || '',
}))
.slice(0, 10),
)
.catch(() => []);
return {
label,
url: page.url(),
title: await page.title(),
heading: await headingText(page),
alerts,
dialogs,
visibleInputs,
};
}
function slugify(value) {
return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'step';
}
async function persistSummary(summary) {
summary.lastUpdatedAtUtc = new Date().toISOString();
await writeFile(outputPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
}
async function captureFailureSnapshot(page, label) {
return captureSnapshot(page, `failure-${slugify(label)}`).catch((error) => ({
label,
error: error instanceof Error ? error.message : String(error),
}));
}
async function runAction(page, route, label, runner) {
const startedAtUtc = new Date().toISOString();
const stepName = `${route} -> ${label}`;
process.stdout.write(`[live-ops-policy-action-sweep] START ${stepName}\n`);
let timeoutHandle = null;
const startedAt = Date.now();
try {
const result = await Promise.race([
runner(),
new Promise((_, reject) => {
timeoutHandle = setTimeout(() => {
reject(new Error(`Timed out after ${STEP_TIMEOUT_MS}ms.`));
}, STEP_TIMEOUT_MS);
}),
]);
const normalized = result && typeof result === 'object' ? result : { ok: true };
const completed = {
action: normalized.action || label,
...normalized,
ok: typeof normalized.ok === 'boolean' ? normalized.ok : true,
startedAtUtc,
durationMs: Date.now() - startedAt,
};
process.stdout.write(
`[live-ops-policy-action-sweep] DONE ${stepName} ok=${completed.ok} durationMs=${completed.durationMs}\n`,
);
return completed;
} catch (error) {
const failed = {
action: label,
ok: false,
reason: 'exception',
error: error instanceof Error ? error.message : String(error),
startedAtUtc,
durationMs: Date.now() - startedAt,
snapshot: await captureFailureSnapshot(page, stepName),
};
process.stdout.write(
`[live-ops-policy-action-sweep] FAIL ${stepName} error=${failed.error} durationMs=${failed.durationMs}\n`,
);
return failed;
} finally {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
}
}
async function navigate(page, route) {
const separator = route.includes('?') ? '&' : '?';
const url = `https://stella-ops.local${route}${separator}${scopeQuery}`;
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
await settle(page);
return url;
}
async function findNavigationTarget(page, name, index = 0) {
const candidates = [
{ role: 'link', locator: page.getByRole('link', { name }) },
{ role: 'tab', locator: page.getByRole('tab', { name }) },
];
for (const candidate of candidates) {
const count = await candidate.locator.count();
if (count > index) {
return {
matchedRole: candidate.role,
locator: candidate.locator.nth(index),
};
}
}
return null;
}
async function clickLink(context, page, route, name, index = 0) {
await navigate(page, route);
const target = await findNavigationTarget(page, name, index);
if (!target) {
return {
action: `link:${name}`,
ok: false,
reason: 'missing-link',
snapshot: await captureSnapshot(page, `missing-link:${name}`),
};
}
const startUrl = page.url();
const popupPromise = context.waitForEvent('page', { timeout: 5_000 }).catch(() => null);
await target.locator.click({ timeout: 10_000 });
const popup = await popupPromise;
if (popup) {
await popup.waitForLoadState('domcontentloaded', { timeout: 15_000 }).catch(() => {});
const result = {
action: `link:${name}`,
ok: true,
matchedRole: target.matchedRole,
mode: 'popup',
targetUrl: popup.url(),
title: await popup.title().catch(() => ''),
};
await popup.close().catch(() => {});
return result;
}
await page.waitForTimeout(1_000);
return {
action: `link:${name}`,
ok: page.url() !== startUrl,
matchedRole: target.matchedRole,
targetUrl: page.url(),
snapshot: await captureSnapshot(page, `after-link:${name}`),
};
}
async function clickButton(page, route, name, index = 0) {
await navigate(page, route);
const locator = page.getByRole('button', { name }).nth(index);
if ((await locator.count()) === 0) {
return {
action: `button:${name}`,
ok: false,
reason: 'missing-button',
snapshot: await captureSnapshot(page, `missing-button:${name}`),
};
}
const disabledBeforeClick = await locator.isDisabled().catch(() => false);
const startUrl = page.url();
const downloadPromise = page.waitForEvent('download', { timeout: 4_000 }).catch(() => null);
await locator.click({ timeout: 10_000 }).catch((error) => {
throw new Error(`${name}: ${error instanceof Error ? error.message : String(error)}`);
});
const download = await downloadPromise;
if (download) {
return {
action: `button:${name}`,
ok: true,
mode: 'download',
disabledBeforeClick,
suggestedFilename: download.suggestedFilename(),
snapshot: await captureSnapshot(page, `after-download:${name}`),
};
}
await page.waitForTimeout(1_200);
return {
action: `button:${name}`,
ok: true,
disabledBeforeClick,
urlChanged: page.url() !== startUrl,
snapshot: await captureSnapshot(page, `after-button:${name}`),
};
}
async function clickFirstAvailableButton(page, route, names) {
await navigate(page, route);
for (const name of names) {
const locator = page.getByRole('button', { name }).first();
if ((await locator.count()) === 0) {
continue;
}
const disabledBeforeClick = await locator.isDisabled().catch(() => false);
const startUrl = page.url();
const downloadPromise = page.waitForEvent('download', { timeout: 4_000 }).catch(() => null);
await locator.click({ timeout: 10_000 }).catch((error) => {
throw new Error(`${name}: ${error instanceof Error ? error.message : String(error)}`);
});
const download = await downloadPromise;
if (download) {
return {
action: `button:${name}`,
ok: true,
mode: 'download',
disabledBeforeClick,
suggestedFilename: download.suggestedFilename(),
snapshot: await captureSnapshot(page, `after-download:${name}`),
};
}
await page.waitForTimeout(1_200);
return {
action: `button:${name}`,
ok: true,
disabledBeforeClick,
urlChanged: page.url() !== startUrl,
snapshot: await captureSnapshot(page, `after-button:${name}`),
};
}
return {
action: `button:${names.join('|')}`,
ok: false,
reason: 'missing-button',
snapshot: await captureSnapshot(page, `missing-button:${names.join('|')}`),
};
}
async function exerciseShadowResults(page) {
const route = '/ops/policy/simulation';
await navigate(page, route);
const viewButton = page.getByRole('button', { name: 'View Results' }).first();
if ((await viewButton.count()) === 0) {
return {
action: 'button:View Results',
ok: false,
reason: 'missing-button',
snapshot: await captureSnapshot(page, 'policy-simulation:missing-view-results'),
};
}
const steps = [];
const initiallyDisabled = await viewButton.isDisabled().catch(() => false);
let enabledInFlow = false;
let restoredDisabledState = false;
if (initiallyDisabled) {
const enableButton = page.getByRole('button', { name: 'Enable' }).first();
if ((await enableButton.count()) === 0) {
return {
action: 'button:View Results',
ok: false,
reason: 'disabled-without-enable',
initiallyDisabled,
snapshot: await captureSnapshot(page, 'policy-simulation:view-results-disabled'),
};
}
await enableButton.click({ timeout: 10_000 });
enabledInFlow = true;
await Promise.race([
page.waitForFunction(() => {
const buttons = Array.from(document.querySelectorAll('button'));
const button = buttons.find((candidate) => (candidate.textContent || '').trim() === 'View Results');
return button instanceof HTMLButtonElement && !button.disabled;
}, null, { timeout: 12_000 }).catch(() => {}),
page.waitForTimeout(2_000),
]);
steps.push({
step: 'enable-shadow-mode',
snapshot: await captureSnapshot(page, 'policy-simulation:enabled-shadow-mode'),
});
}
const activeViewButton = page.getByRole('button', { name: 'View Results' }).first();
const stillDisabled = await activeViewButton.isDisabled().catch(() => false);
if (stillDisabled) {
return {
action: 'button:View Results',
ok: false,
reason: 'still-disabled-after-enable',
initiallyDisabled,
enabledInFlow,
steps,
snapshot: await captureSnapshot(page, 'policy-simulation:view-results-still-disabled'),
};
}
const startUrl = page.url();
await activeViewButton.click({ timeout: 10_000 });
await page.waitForTimeout(1_200);
const resultsUrl = page.url();
const resultsSnapshot = await captureSnapshot(page, 'policy-simulation:view-results');
if (enabledInFlow) {
await navigate(page, route);
const disableButton = page.getByRole('button', { name: 'Disable' }).first();
if ((await disableButton.count()) > 0 && !(await disableButton.isDisabled().catch(() => false))) {
await disableButton.click({ timeout: 10_000 });
await page.waitForTimeout(1_200);
restoredDisabledState = true;
steps.push({
step: 'restore-shadow-disabled',
snapshot: await captureSnapshot(page, 'policy-simulation:restored-shadow-disabled'),
});
}
}
return {
action: 'button:View Results',
ok: resultsUrl !== startUrl && resultsUrl.includes('/ops/policy/simulation/history'),
initiallyDisabled,
enabledInFlow,
restoredDisabledState,
targetUrl: resultsUrl,
snapshot: resultsSnapshot,
steps,
};
}
async function checkDisabledButton(page, route, name) {
await navigate(page, route);
const locator = page.getByRole('button', { name }).first();
if ((await locator.count()) === 0) {
return { action: `button:${name}`, ok: false, reason: 'missing-button' };
}
return {
action: `button:${name}`,
ok: true,
disabled: await locator.isDisabled().catch(() => false),
snapshot: await captureSnapshot(page, `disabled-check:${name}`),
};
}
async function notificationsFormProbe(context, page) {
const route = '/ops/operations/notifications';
const actions = [];
actions.push(await runAction(page, route, 'flow:New channel', async () => {
await navigate(page, route);
const newChannel = page.getByRole('button', { name: 'New channel' }).first();
if ((await newChannel.count()) === 0) {
return {
action: 'flow:New channel',
ok: false,
reason: 'missing-button',
snapshot: await captureSnapshot(page, 'notifications:missing-new-channel'),
};
}
await newChannel.click({ timeout: 10_000 });
await page.waitForTimeout(800);
const flowSteps = [
{
step: 'button:New channel',
snapshot: await captureSnapshot(page, 'notifications:new-channel'),
},
];
const firstTextInput = page.locator('input[type="text"], input:not([type]), textarea').first();
if ((await firstTextInput.count()) > 0) {
await firstTextInput.fill('Live QA Channel');
flowSteps.push({ step: 'fill:channel-name', value: 'Live QA Channel' });
}
const sendTest = page.getByRole('button', { name: 'Send test' }).first();
if ((await sendTest.count()) > 0) {
await sendTest.click({ timeout: 10_000 }).catch(() => {});
await page.waitForTimeout(1_200);
flowSteps.push({
step: 'button:Send test',
snapshot: await captureSnapshot(page, 'notifications:send-test'),
});
}
const saveChannel = page.getByRole('button', { name: 'Save channel' }).first();
if ((await saveChannel.count()) > 0) {
await saveChannel.click({ timeout: 10_000 }).catch(() => {});
await page.waitForTimeout(1_200);
flowSteps.push({
step: 'button:Save channel',
snapshot: await captureSnapshot(page, 'notifications:save-channel'),
});
}
return {
action: 'flow:New channel',
ok: true,
steps: flowSteps,
snapshot: await captureSnapshot(page, 'notifications:new-channel-finished'),
};
}));
actions.push(await runAction(page, route, 'flow:New rule', async () => {
await navigate(page, route);
const newRule = page.getByRole('button', { name: 'New rule' }).first();
if ((await newRule.count()) === 0) {
return {
action: 'flow:New rule',
ok: false,
reason: 'missing-button',
snapshot: await captureSnapshot(page, 'notifications:missing-new-rule'),
};
}
await newRule.click({ timeout: 10_000 });
await page.waitForTimeout(800);
const flowSteps = [
{
step: 'button:New rule',
snapshot: await captureSnapshot(page, 'notifications:new-rule'),
},
];
const saveRule = page.getByRole('button', { name: 'Save rule' }).first();
if ((await saveRule.count()) > 0) {
await saveRule.click({ timeout: 10_000 }).catch(() => {});
await page.waitForTimeout(1_200);
flowSteps.push({
step: 'button:Save rule',
snapshot: await captureSnapshot(page, 'notifications:save-rule'),
});
}
return {
action: 'flow:New rule',
ok: true,
steps: flowSteps,
snapshot: await captureSnapshot(page, 'notifications:new-rule-finished'),
};
}));
actions.push(await runAction(page, route, 'link:Open watchlist tuning', () => clickLink(context, page, route, 'Open watchlist tuning')));
actions.push(await runAction(page, route, 'link:Review watchlist alerts', () => clickLink(context, page, route, 'Review watchlist alerts')));
return { route, actions };
}
async function main() {
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 });
context.setDefaultTimeout(15_000);
context.setDefaultNavigationTimeout(30_000);
const page = await context.newPage();
page.setDefaultTimeout(15_000);
page.setDefaultNavigationTimeout(30_000);
const runtime = {
consoleErrors: [],
pageErrors: [],
responseErrors: [],
requestFailures: [],
};
const results = [];
const summary = {
generatedAtUtc: new Date().toISOString(),
results,
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();
if (/\.(?:css|js|map|png|jpg|jpeg|svg|woff2?)(?:$|\?)/i.test(url)) {
return;
}
runtime.requestFailures.push({
page: page.url(),
method: request.method(),
url,
error: request.failure()?.errorText ?? 'unknown',
});
});
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) {
runtime.responseErrors.push({
page: page.url(),
method: response.request().method(),
status: response.status(),
url,
});
}
});
try {
results.push({
route: '/ops/operations/quotas',
actions: [
await runAction(page, '/ops/operations/quotas', 'button:Configure Alerts', () =>
clickButton(page, '/ops/operations/quotas', 'Configure Alerts')),
await runAction(page, '/ops/operations/quotas', 'button:Export Report', () =>
clickButton(page, '/ops/operations/quotas', 'Export Report')),
await runAction(page, '/ops/operations/quotas', 'link:View Details', () =>
clickLink(context, page, '/ops/operations/quotas', 'View Details')),
await runAction(page, '/ops/operations/quotas', 'link:Default Tenant', () =>
clickLink(context, page, '/ops/operations/quotas', 'Default Tenant')),
],
});
await persistSummary(summary);
results.push({
route: '/ops/operations/dead-letter',
actions: [
await runAction(page, '/ops/operations/dead-letter', 'button:Export CSV', () =>
clickButton(page, '/ops/operations/dead-letter', 'Export CSV')),
await runAction(page, '/ops/operations/dead-letter', 'button:Replay All Retryable (0)', () =>
checkDisabledButton(page, '/ops/operations/dead-letter', 'Replay All Retryable (0)')),
await runAction(page, '/ops/operations/dead-letter', 'button:Clear', () =>
clickButton(page, '/ops/operations/dead-letter', 'Clear')),
await runAction(page, '/ops/operations/dead-letter', 'link:View Full Queue', () =>
clickLink(context, page, '/ops/operations/dead-letter', 'View Full Queue')),
],
});
await persistSummary(summary);
results.push({
route: '/ops/operations/aoc',
actions: [
await runAction(page, '/ops/operations/aoc', 'button:Refresh', () =>
clickButton(page, '/ops/operations/aoc', 'Refresh')),
await runAction(page, '/ops/operations/aoc', 'button:Validate', () =>
clickButton(page, '/ops/operations/aoc', 'Validate')),
await runAction(page, '/ops/operations/aoc', 'link:Export Report', () =>
clickLink(context, page, '/ops/operations/aoc', 'Export Report')),
await runAction(page, '/ops/operations/aoc', 'link:View All', () =>
clickLink(context, page, '/ops/operations/aoc', 'View All')),
await runAction(page, '/ops/operations/aoc', 'link:Details', () =>
clickLink(context, page, '/ops/operations/aoc', 'Details')),
await runAction(page, '/ops/operations/aoc', 'link:Full Validator', () =>
clickLink(context, page, '/ops/operations/aoc', 'Full Validator')),
],
});
await persistSummary(summary);
results.push(await notificationsFormProbe(context, page));
await persistSummary(summary);
results.push({
route: '/ops/policy/simulation',
actions: [
await runAction(page, '/ops/policy/simulation', 'button:View Results', () =>
exerciseShadowResults(page)),
await runAction(page, '/ops/policy/simulation', 'button:Enable|Disable', () =>
clickFirstAvailableButton(page, '/ops/policy/simulation', ['Enable', 'Disable'])),
await runAction(page, '/ops/policy/simulation', 'link:Simulation Console', () =>
clickLink(context, page, '/ops/policy/simulation', 'Simulation Console')),
await runAction(page, '/ops/policy/simulation', 'link:Coverage', () =>
clickLink(context, page, '/ops/policy/simulation', 'Coverage')),
],
});
await persistSummary(summary);
results.push({
route: '/ops/policy/staleness',
actions: [
await runAction(page, '/ops/policy/staleness', 'button:SBOM', () =>
clickButton(page, '/ops/policy/staleness', 'SBOM')),
await runAction(page, '/ops/policy/staleness', 'button:Vulnerability Data', () =>
clickButton(page, '/ops/policy/staleness', 'Vulnerability Data')),
await runAction(page, '/ops/policy/staleness', 'button:VEX Statements', () =>
clickButton(page, '/ops/policy/staleness', 'VEX Statements')),
await runAction(page, '/ops/policy/staleness', 'button:Save Changes', () =>
clickButton(page, '/ops/policy/staleness', 'Save Changes')),
await runAction(page, '/ops/policy/staleness', 'link:Reset view', () =>
clickLink(context, page, '/ops/policy/staleness', 'Reset view')),
],
});
await persistSummary(summary);
} finally {
summary.failedActionCount = results.flatMap((route) => route.actions ?? []).filter((action) => action?.ok === false).length;
summary.runtimeIssueCount =
runtime.consoleErrors.length + runtime.pageErrors.length + runtime.responseErrors.length + runtime.requestFailures.length;
await persistSummary(summary).catch(() => {});
await browser.close().catch(() => {});
}
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
if (summary.failedActionCount > 0 || summary.runtimeIssueCount > 0) {
process.exitCode = 1;
}
}
main().catch((error) => {
process.stderr.write(`[live-ops-policy-action-sweep] ${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
});