First-time user experience fixes and platform contract repairs
FTUX fixes (Sprint 316-001): - Remove all hardcoded fake data from dashboard — fresh installs show honest setup guide instead of fake crisis data (5 fake criticals gone) - Curate advisory source defaults: 32 sources disabled by default (ecosystem, geo-restricted, exploit, hardware, mirror). ~43 core sources remain enabled. StellaOps Mirror no longer enabled at priority 1. - Filter Mirror-category sources from Create Domain wizard to prevent circular mirror-from-mirror chains - Add 404 catch-all route — unknown URLs show "Page Not Found" instead of silently rendering the dashboard - Fix arrow characters in release target path dropdown (? → →) - Add login credentials to quickstart documentation - Update Feature Matrix: 14 release orchestration features marked as shipped (was marked planned) Platform contract repairs (from prior session): - Add /api/v1/jobengine/quotas/summary endpoint on Platform - Fix gateway route prefix matching for /policy/shadow/* and /policy/simulations/* (regex routes instead of exact match) - Fix VexHub PostgresVexSourceRepository missing interface method - Fix advisory-vex-sources sweep text expectation - Fix mirror operator journey auth (session storage token extraction) Verified: 110/111 canonical routes passing (1 unrelated stale approval ref) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
650
src/Web/StellaOps.Web/scripts/live-mirror-operator-journey.mjs
Normal file
650
src/Web/StellaOps.Web/scripts/live-mirror-operator-journey.mjs
Normal file
@@ -0,0 +1,650 @@
|
||||
#!/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 __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const webRoot = path.resolve(__dirname, '..');
|
||||
const outputDir = path.join(webRoot, 'output', 'playwright');
|
||||
const resultPath = path.join(outputDir, 'live-mirror-operator-journey.json');
|
||||
const authStatePath = path.join(outputDir, 'live-mirror-operator-journey.state.json');
|
||||
const authReportPath = path.join(outputDir, 'live-mirror-operator-journey.auth.json');
|
||||
const screenshotDir = path.join(outputDir, 'mirror-operator-journey');
|
||||
|
||||
const baseUrl = process.env.STELLAOPS_FRONTDOOR_BASE_URL?.trim() || 'https://stella-ops.local';
|
||||
const tenantId = 'demo-prod';
|
||||
const scopeQuery = `tenant=${tenantId}®ions=us-east&environments=stage&timeWindow=7d`;
|
||||
const sourceApiBasePath = '/api/v1/advisory-sources';
|
||||
const mirrorApiBasePath = '/api/v1/advisory-sources/mirror';
|
||||
const setupCatalogRoute = '/setup/integrations/advisory-vex-sources';
|
||||
const opsCatalogRoute = '/ops/integrations/advisory-vex-sources';
|
||||
const setupMirrorRoute = '/setup/integrations/advisory-vex-sources/mirror';
|
||||
const opsMirrorRoute = '/ops/integrations/advisory-vex-sources/mirror';
|
||||
const setupMirrorNewRoute = '/setup/integrations/advisory-vex-sources/mirror/new';
|
||||
const opsMirrorNewRoute = '/ops/integrations/advisory-vex-sources/mirror/new';
|
||||
const setupMirrorClientRoute = '/setup/integrations/advisory-vex-sources/mirror/client-setup';
|
||||
const opsMirrorClientRoute = '/ops/integrations/advisory-vex-sources/mirror/client-setup';
|
||||
const mirrorJourneySuffix = Date.now().toString(36);
|
||||
const mirrorJourneyDomainId = `qa-mirror-${mirrorJourneySuffix}`;
|
||||
const mirrorJourneyDisplayName = `QA Mirror ${mirrorJourneySuffix}`;
|
||||
|
||||
function buildUrl(route) {
|
||||
const separator = route.includes('?') ? '&' : '?';
|
||||
return `${baseUrl}${route}${separator}${scopeQuery}`;
|
||||
}
|
||||
|
||||
function cleanText(value) {
|
||||
return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
|
||||
}
|
||||
|
||||
function isStaticAsset(url) {
|
||||
return /\.(?:css|js|map|png|jpg|jpeg|svg|woff2?|ico)(?:$|\?)/i.test(url);
|
||||
}
|
||||
|
||||
function isIgnorableFailure(request) {
|
||||
const url = request.url();
|
||||
const error = request.failure()?.errorText ?? '';
|
||||
return isStaticAsset(url) || /net::ERR_ABORTED|aborted/i.test(error);
|
||||
}
|
||||
|
||||
function attachRuntime(page, runtime) {
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') {
|
||||
runtime.consoleErrors.push(cleanText(message.text()));
|
||||
}
|
||||
});
|
||||
|
||||
page.on('pageerror', (error) => {
|
||||
runtime.pageErrors.push(cleanText(error instanceof Error ? error.message : String(error)));
|
||||
});
|
||||
|
||||
page.on('requestfailed', (request) => {
|
||||
if (isIgnorableFailure(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
runtime.requestFailures.push({
|
||||
method: request.method(),
|
||||
url: request.url(),
|
||||
error: request.failure()?.errorText ?? 'request failed',
|
||||
page: page.url(),
|
||||
});
|
||||
});
|
||||
|
||||
page.on('response', (response) => {
|
||||
if (isStaticAsset(response.url())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status() >= 400) {
|
||||
runtime.responseErrors.push({
|
||||
status: response.status(),
|
||||
method: response.request().method(),
|
||||
url: response.url(),
|
||||
page: page.url(),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function settle(page, ms = 1200) {
|
||||
await page.waitForLoadState('domcontentloaded', { timeout: 20_000 }).catch(() => {});
|
||||
await page.waitForFunction(() => {
|
||||
const nodes = Array.from(document.querySelectorAll('[role="alert"], .banner, .banner--error'));
|
||||
return !nodes.some((node) => ((node.textContent || '').replace(/\s+/g, ' ').trim().toLowerCase().startsWith('loading ')));
|
||||
}, { timeout: 10_000 }).catch(() => {});
|
||||
await page.waitForTimeout(ms);
|
||||
}
|
||||
|
||||
async function navigate(page, route, ms = 1400) {
|
||||
await page.goto(buildUrl(route), { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
||||
await settle(page, ms);
|
||||
}
|
||||
|
||||
async function heading(page) {
|
||||
const selectors = [
|
||||
'.source-catalog h1',
|
||||
'.mirror-dashboard h1',
|
||||
'.wizard h1',
|
||||
'[data-testid="page-title"]',
|
||||
'main h1',
|
||||
'h1',
|
||||
];
|
||||
|
||||
for (const selector of selectors) {
|
||||
const locator = page.locator(selector).first();
|
||||
const text = cleanText(await locator.textContent().catch(() => ''));
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
async function bannerTexts(page) {
|
||||
return page.locator('[role="alert"], .banner, .banner--error, .error-banner, .toast')
|
||||
.evaluateAll((nodes) => nodes.map((node) => (node.textContent || '').replace(/\s+/g, ' ').trim()).filter(Boolean))
|
||||
.catch(() => []);
|
||||
}
|
||||
|
||||
async function bodyText(page) {
|
||||
return cleanText(await page.locator('body').textContent().catch(() => ''));
|
||||
}
|
||||
|
||||
async function capture(page, key, extra = {}) {
|
||||
const screenshotPath = path.join(screenshotDir, `${key}.png`);
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true }).catch(() => {});
|
||||
|
||||
return {
|
||||
key,
|
||||
url: page.url(),
|
||||
heading: await heading(page),
|
||||
banners: await bannerTexts(page),
|
||||
screenshotPath,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
function pathPresent(url, expectedPath) {
|
||||
return typeof url === 'string' && url.includes(expectedPath);
|
||||
}
|
||||
|
||||
async function attemptClick(page, locator, label, waitMs = 1400) {
|
||||
const visible = await locator.isVisible().catch(() => false);
|
||||
if (!visible) {
|
||||
return { clicked: false, reason: `${label} not visible` };
|
||||
}
|
||||
|
||||
try {
|
||||
await locator.click({ timeout: 15_000 });
|
||||
await settle(page, waitMs);
|
||||
return { clicked: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
clicked: false,
|
||||
reason: cleanText(error instanceof Error ? error.message : String(error)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function invokeApi(page, endpoint, init = {}) {
|
||||
return page.evaluate(async ({ endpointPath, tenant, method, requestBody }) => {
|
||||
try {
|
||||
// Extract access token from Angular session storage
|
||||
let authHeader = {};
|
||||
try {
|
||||
const raw = sessionStorage.getItem('stellaops.auth.session.full');
|
||||
if (raw) {
|
||||
const session = JSON.parse(raw);
|
||||
const token = session?.tokens?.accessToken;
|
||||
if (token) {
|
||||
authHeader = { 'Authorization': `Bearer ${token}` };
|
||||
}
|
||||
}
|
||||
} catch { /* token extraction failed; proceed without auth */ }
|
||||
|
||||
const response = await fetch(endpointPath, {
|
||||
method,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'X-Stella-Ops-Tenant': tenant,
|
||||
...authHeader,
|
||||
...(requestBody ? { 'Content-Type': 'application/json' } : {}),
|
||||
},
|
||||
body: requestBody ? JSON.stringify(requestBody) : undefined,
|
||||
});
|
||||
|
||||
const bodyText = await response.text();
|
||||
let responseBody;
|
||||
try {
|
||||
responseBody = JSON.parse(bodyText);
|
||||
} catch {
|
||||
responseBody = bodyText;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
body: responseBody,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
status: 0,
|
||||
body: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}, {
|
||||
endpointPath: endpoint,
|
||||
tenant: tenantId,
|
||||
method: init.method ?? 'GET',
|
||||
requestBody: init.body ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
await mkdir(screenshotDir, { recursive: true });
|
||||
|
||||
const runtime = {
|
||||
consoleErrors: [],
|
||||
pageErrors: [],
|
||||
requestFailures: [],
|
||||
responseErrors: [],
|
||||
};
|
||||
|
||||
const browser = await chromium.launch({ headless: process.env.PLAYWRIGHT_HEADLESS !== 'false' });
|
||||
const auth = await authenticateFrontdoor({ statePath: authStatePath, reportPath: authReportPath });
|
||||
const context = await createAuthenticatedContext(browser, auth, { statePath: auth.statePath });
|
||||
const page = await context.newPage();
|
||||
page.setDefaultTimeout(20_000);
|
||||
page.setDefaultNavigationTimeout(30_000);
|
||||
attachRuntime(page, runtime);
|
||||
|
||||
const results = [];
|
||||
const failures = [];
|
||||
|
||||
try {
|
||||
await navigate(page, setupCatalogRoute);
|
||||
const mirrorConfigApi = await invokeApi(page, `${mirrorApiBasePath}/config`);
|
||||
const mirrorHealthApi = await invokeApi(page, `${mirrorApiBasePath}/health`);
|
||||
const mirrorDomainsApi = await invokeApi(page, `${mirrorApiBasePath}/domains`);
|
||||
results.push(await capture(page, 'catalog-direct', {
|
||||
hasConfigureMirrorLink: await page.getByRole('link', { name: 'Configure Mirror', exact: true }).isVisible().catch(() => false),
|
||||
hasConnectMirrorLink: await page.getByRole('link', { name: 'Connect to Mirror', exact: true }).isVisible().catch(() => false),
|
||||
hasCreateMirrorDomainButton: await page.getByRole('button', { name: 'Create Mirror Domain', exact: true }).isVisible().catch(() => false),
|
||||
mirrorConfigApi,
|
||||
mirrorHealthApi,
|
||||
mirrorDomainsApi,
|
||||
}));
|
||||
|
||||
await navigate(page, opsCatalogRoute);
|
||||
results.push(await capture(page, 'ops-catalog-direct', {
|
||||
hasConfigureMirrorLink: await page.getByRole('link', { name: 'Configure Mirror', exact: true }).isVisible().catch(() => false),
|
||||
hasConnectMirrorLink: await page.getByRole('link', { name: 'Connect to Mirror', exact: true }).isVisible().catch(() => false),
|
||||
hasCreateMirrorDomainButton: await page.getByRole('button', { name: 'Create Mirror Domain', exact: true }).isVisible().catch(() => false),
|
||||
}));
|
||||
|
||||
await navigate(page, setupCatalogRoute);
|
||||
const catalogConfigureResult = await attemptClick(
|
||||
page,
|
||||
page.getByRole('link', { name: 'Configure Mirror', exact: true }),
|
||||
'Catalog Configure Mirror',
|
||||
);
|
||||
results.push(await capture(page, 'catalog-configure-mirror-handoff', { clickResult: catalogConfigureResult }));
|
||||
|
||||
await navigate(page, setupCatalogRoute);
|
||||
const catalogCreateResult = await attemptClick(
|
||||
page,
|
||||
page.getByRole('button', { name: 'Create Mirror Domain', exact: true }),
|
||||
'Catalog Create Mirror Domain',
|
||||
);
|
||||
results.push(await capture(page, 'catalog-create-domain-handoff', { clickResult: catalogCreateResult }));
|
||||
|
||||
await navigate(page, setupMirrorRoute);
|
||||
const mirrorDashboardConfigApi = await invokeApi(page, `${mirrorApiBasePath}/config`);
|
||||
results.push(await capture(page, 'dashboard-direct', {
|
||||
hasCreateDomainButton: await page.getByRole('button', { name: 'Create Domain', exact: true }).isVisible().catch(() => false),
|
||||
hasSetupMirrorButton: await page.getByRole('button', { name: /Set Up Mirror Connection|Configure/i }).first().isVisible().catch(() => false),
|
||||
mirrorConfigApi: mirrorDashboardConfigApi,
|
||||
}));
|
||||
|
||||
await navigate(page, opsMirrorRoute);
|
||||
results.push(await capture(page, 'ops-dashboard-direct', {
|
||||
hasCreateDomainButton: await page.getByRole('button', { name: 'Create Domain', exact: true }).isVisible().catch(() => false),
|
||||
hasSetupMirrorButton: await page.getByRole('button', { name: /Set Up Mirror Connection|Configure/i }).first().isVisible().catch(() => false),
|
||||
}));
|
||||
|
||||
await navigate(page, setupMirrorRoute);
|
||||
const dashboardCreateResult = await attemptClick(
|
||||
page,
|
||||
page.getByRole('button', { name: 'Create Domain', exact: true }),
|
||||
'Dashboard Create Domain',
|
||||
);
|
||||
results.push(await capture(page, 'dashboard-create-domain-handoff', { clickResult: dashboardCreateResult }));
|
||||
|
||||
await navigate(page, setupMirrorRoute);
|
||||
const setupButton = page.getByRole('button', { name: /Set Up Mirror Connection|Configure/i }).first();
|
||||
const dashboardConsumerResult = await attemptClick(
|
||||
page,
|
||||
setupButton,
|
||||
'Dashboard Configure Consumer',
|
||||
);
|
||||
results.push(await capture(page, 'dashboard-configure-consumer-handoff', { clickResult: dashboardConsumerResult }));
|
||||
|
||||
await navigate(page, setupMirrorNewRoute);
|
||||
const sourceCheckboxes = page.locator('.source-row-label input[type="checkbox"]');
|
||||
const sourceCount = await sourceCheckboxes.count().catch(() => 0);
|
||||
if (sourceCount >= 2) {
|
||||
await sourceCheckboxes.nth(0).check({ force: true });
|
||||
await sourceCheckboxes.nth(1).check({ force: true });
|
||||
}
|
||||
const builderNextToConfig = await attemptClick(
|
||||
page,
|
||||
page.getByRole('button', { name: 'Next: Configure Domain', exact: true }),
|
||||
'Builder next to config',
|
||||
800,
|
||||
);
|
||||
if (builderNextToConfig.clicked) {
|
||||
await page.locator('#domainId').fill(mirrorJourneyDomainId).catch(() => {});
|
||||
await page.locator('#displayName').fill(mirrorJourneyDisplayName).catch(() => {});
|
||||
const signingKeyInput = page.locator('#signingKeyId');
|
||||
if (await signingKeyInput.isVisible().catch(() => false)) {
|
||||
await signingKeyInput.fill('operator-mirror-signing-key').catch(() => {});
|
||||
}
|
||||
}
|
||||
const builderNextToReview = await attemptClick(
|
||||
page,
|
||||
page.getByRole('button', { name: 'Next: Review', exact: true }),
|
||||
'Builder next to review',
|
||||
800,
|
||||
);
|
||||
const generateToggle = page.getByLabel('Generate mirror output immediately after creation');
|
||||
if (await generateToggle.isVisible().catch(() => false)) {
|
||||
await generateToggle.check({ force: true }).catch(() => {});
|
||||
}
|
||||
const builderCreateResult = await attemptClick(
|
||||
page,
|
||||
page.getByRole('button', { name: 'Create Domain', exact: true }),
|
||||
'Builder create domain',
|
||||
2400,
|
||||
);
|
||||
const createdDomainConfigApi = await invokeApi(page, `${mirrorApiBasePath}/domains/${encodeURIComponent(mirrorJourneyDomainId)}/config`);
|
||||
const createdDomainEndpointsApi = await invokeApi(page, `${mirrorApiBasePath}/domains/${encodeURIComponent(mirrorJourneyDomainId)}/endpoints`);
|
||||
const createdDomainStatusApi = await invokeApi(page, `${mirrorApiBasePath}/domains/${encodeURIComponent(mirrorJourneyDomainId)}/status`);
|
||||
const publicMirrorIndexApi = await invokeApi(page, '/concelier/exports/index.json');
|
||||
const publicDomainManifestApi = await invokeApi(page, `/concelier/exports/mirror/${mirrorJourneyDomainId}/manifest.json`);
|
||||
const publicDomainBundleApi = await invokeApi(page, `/concelier/exports/mirror/${mirrorJourneyDomainId}/bundle.json`);
|
||||
results.push(await capture(page, 'builder-create-attempt', {
|
||||
sourceCount,
|
||||
nextToConfig: builderNextToConfig,
|
||||
nextToReview: builderNextToReview,
|
||||
createClick: builderCreateResult,
|
||||
createErrorBanner: cleanText(await page.locator('.banner--error').first().textContent().catch(() => '')),
|
||||
domainConfigApi: createdDomainConfigApi,
|
||||
domainEndpointsApi: createdDomainEndpointsApi,
|
||||
domainStatusApi: createdDomainStatusApi,
|
||||
publicMirrorIndexApi,
|
||||
publicDomainManifestApi,
|
||||
publicDomainBundleApi,
|
||||
}));
|
||||
|
||||
await navigate(page, opsMirrorNewRoute);
|
||||
results.push(await capture(page, 'ops-builder-direct', {
|
||||
sourceCount: await page.locator('.source-row-label input[type="checkbox"]').count().catch(() => 0),
|
||||
}));
|
||||
|
||||
await navigate(page, setupMirrorRoute);
|
||||
const viewEndpointsResult = await attemptClick(
|
||||
page,
|
||||
page.getByRole('button', { name: 'View Endpoints', exact: true }).first(),
|
||||
'Dashboard view endpoints',
|
||||
1200,
|
||||
);
|
||||
const viewConfigResult = await attemptClick(
|
||||
page,
|
||||
page.getByRole('button', { name: 'View Config', exact: true }).first(),
|
||||
'Dashboard view config',
|
||||
1200,
|
||||
);
|
||||
results.push(await capture(page, 'dashboard-domain-panels', {
|
||||
viewEndpointsResult,
|
||||
viewConfigResult,
|
||||
endpointsPanelText: cleanText(await page.locator('.card-endpoints').first().textContent().catch(() => '')),
|
||||
configPanelText: cleanText(await page.locator('.card-config').first().textContent().catch(() => '')),
|
||||
}));
|
||||
|
||||
await navigate(page, setupMirrorClientRoute);
|
||||
const clientConfigApi = await invokeApi(page, `${mirrorApiBasePath}/config`);
|
||||
const clientDomainsApi = await invokeApi(page, `${mirrorApiBasePath}/domains`);
|
||||
await page.locator('#mirrorAddress').fill(baseUrl).catch(() => {});
|
||||
const clientTestConnection = await attemptClick(
|
||||
page,
|
||||
page.getByRole('button', { name: 'Test Connection', exact: true }),
|
||||
'Mirror client test connection',
|
||||
2200,
|
||||
);
|
||||
const discoveredDomainOptions = await page.locator('#domainSelect option').count().catch(() => 0);
|
||||
const nextToSignatureVisible = await page.getByRole('button', { name: 'Next: Signature Verification', exact: true }).isVisible().catch(() => false);
|
||||
const nextToSignatureEnabled = nextToSignatureVisible
|
||||
? await page.getByRole('button', { name: 'Next: Signature Verification', exact: true }).isEnabled().catch(() => false)
|
||||
: false;
|
||||
results.push(await capture(page, 'client-setup-direct', {
|
||||
mirrorConfigApi: clientConfigApi,
|
||||
mirrorDomainsApi: clientDomainsApi,
|
||||
testConnection: clientTestConnection,
|
||||
connectedBanner: cleanText(await page.locator('.result-banner--success').first().textContent().catch(() => '')),
|
||||
connectionErrorBanner: cleanText(await page.locator('.result-banner--error').first().textContent().catch(() => '')),
|
||||
discoveredDomainOptions,
|
||||
nextToSignatureVisible,
|
||||
nextToSignatureEnabled,
|
||||
}));
|
||||
|
||||
await navigate(page, opsMirrorClientRoute);
|
||||
results.push(await capture(page, 'ops-client-setup-direct', {
|
||||
hasMirrorAddress: await page.locator('#mirrorAddress').isVisible().catch(() => false),
|
||||
hasTestConnectionButton: await page.getByRole('button', { name: 'Test Connection', exact: true }).isVisible().catch(() => false),
|
||||
}));
|
||||
|
||||
await navigate(page, '/ops/operations/feeds-airgap');
|
||||
const feedsConfigureResult = await attemptClick(
|
||||
page,
|
||||
page.getByRole('link', { name: 'Configure Sources', exact: true }),
|
||||
'Feeds & Airgap Configure Sources',
|
||||
1000,
|
||||
);
|
||||
results.push(await capture(page, 'feeds-airgap-configure-sources-handoff', { clickResult: feedsConfigureResult }));
|
||||
|
||||
await navigate(page, '/security');
|
||||
const securityConfigureResult = await attemptClick(
|
||||
page,
|
||||
page.getByRole('link', { name: /Configure sources|Configure advisory feeds|Configure VEX sources/i }).first(),
|
||||
'Security Configure sources',
|
||||
1000,
|
||||
);
|
||||
results.push(await capture(page, 'security-configure-sources-handoff', { clickResult: securityConfigureResult }));
|
||||
} finally {
|
||||
await invokeApi(page, `${mirrorApiBasePath}/domains/${encodeURIComponent(mirrorJourneyDomainId)}`, {
|
||||
method: 'DELETE',
|
||||
}).catch(() => {});
|
||||
await page.close().catch(() => {});
|
||||
await context.close().catch(() => {});
|
||||
await browser.close().catch(() => {});
|
||||
}
|
||||
|
||||
const catalogDirect = results.find((entry) => entry.key === 'catalog-direct');
|
||||
if (catalogDirect?.heading !== 'Advisory & VEX Source Catalog') {
|
||||
failures.push(`Catalog direct route heading mismatch: ${catalogDirect?.heading || '<empty>'}`);
|
||||
}
|
||||
if (!catalogDirect?.hasConfigureMirrorLink || !catalogDirect?.hasCreateMirrorDomainButton) {
|
||||
failures.push('Catalog direct route is missing primary mirror actions.');
|
||||
}
|
||||
if (!catalogDirect?.mirrorConfigApi?.ok) {
|
||||
failures.push(`Mirror config API is not healthy for catalog context: status=${catalogDirect?.mirrorConfigApi?.status ?? '<unknown>'}`);
|
||||
}
|
||||
if (!catalogDirect?.mirrorDomainsApi?.ok) {
|
||||
failures.push(`Mirror domains API is not healthy for catalog context: status=${catalogDirect?.mirrorDomainsApi?.status ?? '<unknown>'}`);
|
||||
}
|
||||
|
||||
const opsCatalogDirect = results.find((entry) => entry.key === 'ops-catalog-direct');
|
||||
if (opsCatalogDirect?.heading !== 'Advisory & VEX Source Catalog') {
|
||||
failures.push(`Ops catalog direct route heading mismatch: ${opsCatalogDirect?.heading || '<empty>'}`);
|
||||
}
|
||||
if (!opsCatalogDirect?.hasConfigureMirrorLink || !opsCatalogDirect?.hasCreateMirrorDomainButton) {
|
||||
failures.push('Ops catalog direct route is missing primary mirror actions.');
|
||||
}
|
||||
|
||||
const configureHandoff = results.find((entry) => entry.key === 'catalog-configure-mirror-handoff');
|
||||
if (configureHandoff?.clickResult && !configureHandoff.clickResult.clicked) {
|
||||
failures.push(`Catalog Configure Mirror action failed before navigation: ${configureHandoff.clickResult.reason}`);
|
||||
}
|
||||
if (!pathPresent(configureHandoff?.url, '/advisory-vex-sources/mirror')) {
|
||||
failures.push(`Catalog Configure Mirror did not land on mirror dashboard: ${configureHandoff?.url || '<empty>'}`);
|
||||
}
|
||||
|
||||
const catalogCreate = results.find((entry) => entry.key === 'catalog-create-domain-handoff');
|
||||
if (catalogCreate?.clickResult && !catalogCreate.clickResult.clicked) {
|
||||
failures.push(`Catalog Create Mirror Domain action failed before navigation: ${catalogCreate.clickResult.reason}`);
|
||||
}
|
||||
if (!pathPresent(catalogCreate?.url, '/advisory-vex-sources/mirror/new')) {
|
||||
failures.push(`Catalog Create Mirror Domain did not land on /mirror/new: ${catalogCreate?.url || '<empty>'}`);
|
||||
}
|
||||
|
||||
const dashboardDirect = results.find((entry) => entry.key === 'dashboard-direct');
|
||||
if (dashboardDirect?.heading !== 'Mirror Dashboard') {
|
||||
failures.push(`Mirror dashboard direct route heading mismatch: ${dashboardDirect?.heading || '<empty>'}`);
|
||||
}
|
||||
if (!dashboardDirect?.mirrorConfigApi?.ok) {
|
||||
failures.push(`Mirror config API is not healthy for dashboard context: status=${dashboardDirect?.mirrorConfigApi?.status ?? '<unknown>'}`);
|
||||
}
|
||||
|
||||
const opsDashboard = results.find((entry) => entry.key === 'ops-dashboard-direct');
|
||||
if (opsDashboard?.heading !== 'Mirror Dashboard') {
|
||||
failures.push(`Ops mirror dashboard direct route heading mismatch: ${opsDashboard?.heading || '<empty>'}`);
|
||||
}
|
||||
if (!opsDashboard?.hasCreateDomainButton || !opsDashboard?.hasSetupMirrorButton) {
|
||||
failures.push('Ops mirror dashboard is missing primary actions.');
|
||||
}
|
||||
|
||||
const dashboardCreate = results.find((entry) => entry.key === 'dashboard-create-domain-handoff');
|
||||
if (dashboardCreate?.clickResult && !dashboardCreate.clickResult.clicked) {
|
||||
failures.push(`Dashboard Create Domain action failed before navigation: ${dashboardCreate.clickResult.reason}`);
|
||||
}
|
||||
if (!pathPresent(dashboardCreate?.url, '/advisory-vex-sources/mirror/new')) {
|
||||
failures.push(`Dashboard Create Domain did not land on /mirror/new: ${dashboardCreate?.url || '<empty>'}`);
|
||||
}
|
||||
|
||||
const dashboardConsumer = results.find((entry) => entry.key === 'dashboard-configure-consumer-handoff');
|
||||
if (dashboardConsumer?.clickResult && !dashboardConsumer.clickResult.clicked) {
|
||||
failures.push(`Dashboard Configure Consumer action failed before navigation: ${dashboardConsumer.clickResult.reason}`);
|
||||
}
|
||||
if (!pathPresent(dashboardConsumer?.url, '/advisory-vex-sources/mirror/client-setup')) {
|
||||
failures.push(`Dashboard Configure Consumer did not land on /mirror/client-setup: ${dashboardConsumer?.url || '<empty>'}`);
|
||||
}
|
||||
|
||||
const builderAttempt = results.find((entry) => entry.key === 'builder-create-attempt');
|
||||
const builderText = cleanText(builderAttempt?.createErrorBanner || '');
|
||||
if (!builderAttempt?.nextToConfig?.clicked || !builderAttempt?.nextToReview?.clicked || !builderAttempt?.createClick?.clicked) {
|
||||
failures.push(`Mirror domain builder flow was blocked before submit. nextToConfig=${builderAttempt?.nextToConfig?.reason || 'ok'} nextToReview=${builderAttempt?.nextToReview?.reason || 'ok'} create=${builderAttempt?.createClick?.reason || 'ok'}`);
|
||||
}
|
||||
if (builderText || builderAttempt?.heading !== 'Mirror Domain Created') {
|
||||
failures.push(`Mirror domain create journey did not complete successfully. Banner="${builderText || '<none>'}" finalUrl="${builderAttempt?.url || '<empty>'}"`);
|
||||
}
|
||||
if (!builderAttempt?.domainConfigApi?.ok) {
|
||||
failures.push(`Created mirror domain config API failed: status=${builderAttempt?.domainConfigApi?.status ?? '<unknown>'}`);
|
||||
}
|
||||
if (!builderAttempt?.domainEndpointsApi?.ok) {
|
||||
failures.push(`Created mirror domain endpoints API failed: status=${builderAttempt?.domainEndpointsApi?.status ?? '<unknown>'}`);
|
||||
}
|
||||
if (!builderAttempt?.domainStatusApi?.ok) {
|
||||
failures.push(`Created mirror domain status API failed: status=${builderAttempt?.domainStatusApi?.status ?? '<unknown>'}`);
|
||||
}
|
||||
if (!builderAttempt?.publicMirrorIndexApi?.ok) {
|
||||
failures.push(`Public mirror index was not reachable through the frontdoor: status=${builderAttempt?.publicMirrorIndexApi?.status ?? '<unknown>'}`);
|
||||
}
|
||||
if (!builderAttempt?.publicDomainManifestApi?.ok) {
|
||||
failures.push(`Public mirror manifest was not reachable for ${mirrorJourneyDomainId}: status=${builderAttempt?.publicDomainManifestApi?.status ?? '<unknown>'}`);
|
||||
}
|
||||
if (!builderAttempt?.publicDomainBundleApi?.ok) {
|
||||
failures.push(`Public mirror bundle was not reachable for ${mirrorJourneyDomainId}: status=${builderAttempt?.publicDomainBundleApi?.status ?? '<unknown>'}`);
|
||||
}
|
||||
|
||||
const opsBuilderDirect = results.find((entry) => entry.key === 'ops-builder-direct');
|
||||
if (opsBuilderDirect?.heading !== 'Create Mirror Domain') {
|
||||
failures.push(`Ops mirror builder direct route heading mismatch: ${opsBuilderDirect?.heading || '<empty>'}`);
|
||||
}
|
||||
|
||||
const dashboardPanels = results.find((entry) => entry.key === 'dashboard-domain-panels');
|
||||
if (dashboardPanels?.viewEndpointsResult && !dashboardPanels.viewEndpointsResult.clicked) {
|
||||
failures.push(`Mirror dashboard View Endpoints action failed: ${dashboardPanels.viewEndpointsResult.reason}`);
|
||||
}
|
||||
if (dashboardPanels?.viewConfigResult && !dashboardPanels.viewConfigResult.clicked) {
|
||||
failures.push(`Mirror dashboard View Config action failed: ${dashboardPanels.viewConfigResult.reason}`);
|
||||
}
|
||||
if (!dashboardPanels?.endpointsPanelText?.includes('/concelier/exports')) {
|
||||
failures.push('Mirror dashboard endpoints panel did not render public mirror paths.');
|
||||
}
|
||||
if (!dashboardPanels?.configPanelText?.includes('Index limit')) {
|
||||
failures.push('Mirror dashboard config panel did not render resolved domain configuration.');
|
||||
}
|
||||
|
||||
const clientSetup = results.find((entry) => entry.key === 'client-setup-direct');
|
||||
if (clientSetup?.heading !== 'Mirror Client Setup') {
|
||||
failures.push(`Mirror client setup direct route heading mismatch: ${clientSetup?.heading || '<empty>'}`);
|
||||
}
|
||||
if (!clientSetup?.mirrorConfigApi?.ok) {
|
||||
failures.push(`Mirror config API is not healthy for client setup context: status=${clientSetup?.mirrorConfigApi?.status ?? '<unknown>'}`);
|
||||
}
|
||||
if (!clientSetup?.mirrorDomainsApi?.ok) {
|
||||
failures.push(`Mirror domains API is not healthy for client setup context: status=${clientSetup?.mirrorDomainsApi?.status ?? '<unknown>'}`);
|
||||
}
|
||||
if (clientSetup?.testConnection && !clientSetup.testConnection.clicked) {
|
||||
failures.push(`Mirror client Test Connection action failed before request completed: ${clientSetup.testConnection.reason}`);
|
||||
}
|
||||
if (clientSetup?.connectionErrorBanner) {
|
||||
failures.push(`Mirror client connection preflight failed: ${clientSetup.connectionErrorBanner}`);
|
||||
}
|
||||
if (!clientSetup?.nextToSignatureEnabled) {
|
||||
failures.push('Mirror client setup cannot proceed to signature verification after test connection.');
|
||||
}
|
||||
|
||||
const opsClientSetup = results.find((entry) => entry.key === 'ops-client-setup-direct');
|
||||
if (opsClientSetup?.heading !== 'Mirror Client Setup') {
|
||||
failures.push(`Ops mirror client setup direct route heading mismatch: ${opsClientSetup?.heading || '<empty>'}`);
|
||||
}
|
||||
if (!opsClientSetup?.hasMirrorAddress || !opsClientSetup?.hasTestConnectionButton) {
|
||||
failures.push('Ops mirror client setup is missing connection controls.');
|
||||
}
|
||||
|
||||
const feedsHandoff = results.find((entry) => entry.key === 'feeds-airgap-configure-sources-handoff');
|
||||
if (feedsHandoff?.clickResult && !feedsHandoff.clickResult.clicked) {
|
||||
failures.push(`Feeds & Airgap Configure Sources action failed before navigation: ${feedsHandoff.clickResult.reason}`);
|
||||
}
|
||||
if (!pathPresent(feedsHandoff?.url, '/integrations/advisory-vex-sources')) {
|
||||
failures.push(`Feeds & Airgap Configure Sources did not land on advisory source catalog: ${feedsHandoff?.url || '<empty>'}`);
|
||||
}
|
||||
|
||||
const securityHandoff = results.find((entry) => entry.key === 'security-configure-sources-handoff');
|
||||
if (securityHandoff?.clickResult && !securityHandoff.clickResult.clicked) {
|
||||
failures.push(`Security Configure sources action failed before navigation: ${securityHandoff.clickResult.reason}`);
|
||||
}
|
||||
if (!pathPresent(securityHandoff?.url, '/integrations/advisory-vex-sources')) {
|
||||
failures.push(`Security Configure sources did not land on advisory source catalog: ${securityHandoff?.url || '<empty>'}`);
|
||||
}
|
||||
|
||||
const report = {
|
||||
generatedAtUtc: new Date().toISOString(),
|
||||
baseUrl,
|
||||
failedCheckCount: failures.length,
|
||||
runtimeIssueCount:
|
||||
runtime.consoleErrors.length + runtime.pageErrors.length + runtime.requestFailures.length + runtime.responseErrors.length,
|
||||
results,
|
||||
failures,
|
||||
runtime,
|
||||
};
|
||||
|
||||
await writeFile(resultPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
||||
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||
process.exit(failures.length === 0 ? 0 : 1);
|
||||
}
|
||||
|
||||
main().catch(async (error) => {
|
||||
const report = {
|
||||
generatedAtUtc: new Date().toISOString(),
|
||||
baseUrl,
|
||||
failedCheckCount: 1,
|
||||
runtimeIssueCount: 1,
|
||||
failures: [error instanceof Error ? error.message : String(error)],
|
||||
};
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
await writeFile(resultPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
||||
process.stderr.write(`${report.failures[0]}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user