import { expect, test, type Page, type Route } from '@playwright/test'; import type { StubAuthSession } from '../../src/app/testing/auth-fixtures'; const recheckSession: StubAuthSession = { subjectId: 'e2e-admin-user', tenant: 'tenant-default', scopes: [ 'admin', 'ui.read', 'ui.admin', 'release:read', 'release:write', 'release:publish', 'scanner:read', 'sbom:read', 'advisory:read', 'vex:read', 'vex:export', 'exception:read', 'exception:approve', 'exceptions:read', 'findings:read', 'vuln:view', 'policy:read', 'policy:author', 'policy:review', 'policy:approve', 'policy:simulate', 'policy:audit', 'orch:read', 'orch:operate', 'health:read', 'notify.viewer', 'signer:read', 'authority:audit.read', ], }; const mockConfig = { authority: { issuer: '/authority', clientId: 'stella-ops-ui', authorizeEndpoint: '/authority/connect/authorize', tokenEndpoint: '/authority/connect/token', logoutEndpoint: '/authority/connect/logout', redirectUri: 'https://127.0.0.1:4400/auth/callback', postLogoutRedirectUri: 'https://127.0.0.1:4400/', scope: 'openid profile email ui.read', audience: '/gateway', dpopAlgorithms: ['ES256'], refreshLeewaySeconds: 60, }, apiBaseUrls: { authority: '/authority', scanner: '/scanner', policy: '/policy', concelier: '/concelier', attestor: '/attestor', gateway: '/gateway', }, quickstartMode: true, setup: 'complete', }; const oidcConfig = { issuer: 'https://127.0.0.1:4400/authority', authorization_endpoint: 'https://127.0.0.1:4400/authority/connect/authorize', token_endpoint: 'https://127.0.0.1:4400/authority/connect/token', jwks_uri: 'https://127.0.0.1:4400/authority/.well-known/jwks.json', response_types_supported: ['code'], subject_types_supported: ['public'], id_token_signing_alg_values_supported: ['RS256'], }; const recheckCases = [ { name: 'environment management UI renders list + detail + tabs', path: '/setup/topology/environments' }, { name: 'evidence center hub filters and navigates to packet detail', path: '/evidence/capsules' }, { name: 'evidence card export actions are user-reachable', path: '/evidence/exports' }, { name: 'evidence packet drawer opens and closes from evidence center', path: '/evidence/capsules' }, { name: 'configuration pane renders integration summary and detail workflow', path: '/ops/integrations' }, { name: 'control plane dashboard renders pipeline and action inbox', path: '/mission-control/board' }, { name: 'causal timeline route renders events and critical path details', path: '/releases/runs' }, { name: 'delta compare view shows summary, items, and evidence panes', path: '/security/triage' }, { name: 'deploy diff panel route renders deterministic A/B summary and actions', path: '/releases/deployments' }, { name: 'deploy diff route without from/to query params shows missing-parameter state', path: '/releases/deployments' }, { name: 'deploy diff panel surfaces API failure and keeps retry affordance', path: '/releases/deployments' }, { name: 'b2r2 patch-map route renders coverage heatmap and drilldown workflow', path: '/security/supply-chain-data/graph' }, { name: 'b2r2 patch-map route shows retryable error state on coverage API failure', path: '/security/supply-chain-data/graph' }, { name: 'attested score UI shows hard-fail and anchor badges with full breakdown details', path: '/ops/policy/risk-budget' }, { name: 'attested score UI keeps non-hard-fail rows scoped to anchored-only treatment', path: '/security/triage' }, { name: 'deployment monitoring list and detail flow are end-user reachable', path: '/releases/deployments' }, { name: 'deployment detail workflow DAG visualization renders interactive nodes', path: '/releases/runs' }, { name: 'dead-letter dashboard queue and entry detail are user-reachable', path: '/ops/operations/dead-letter' }, { name: 'developer workspace renders findings and quick-verify workflow', path: '/security/triage' }, { name: 'evidence thread browser list, detail, and transcript generation are reachable', path: '/evidence/audit-log' }, { name: 'evidence provenance visualization renders chain and node detail modal', path: '/evidence/proofs' }, { name: 'auditor workspace renders review summary, export workflow, and quiet-triage action', path: '/evidence/audit-log' }, { name: 'approvals inbox renders diff-first presentation and actions', path: '/releases/approvals' }, { name: 'approval detail page shows reachability witness panel interactions', path: '/releases/approvals' }, { name: 'audit bundles index renders completed bundle and export action', path: '/evidence/exports' }, { name: 'audit bundle creation wizard reaches completed progress and download action', path: '/evidence/exports' }, { name: 'aoc verification workbench route renders verify action, CLI parity guidance, and violation drilldown', path: '/ops/operations/aoc' }, { name: 'audit reason capsule route renders reason provenance and retry recovery flow', path: '/evidence/capsules' }, { name: 'qa workbench route renders backport resolution flow with binary diff, function diff, and evidence drawer', path: '/releases/hotfixes' }, { name: 'advisory ai autofix workbench route renders plan preview and PR tracking interactions', path: '/ops/operations/ai-runs' }, { name: 'advisory ai chat route renders role-aware messages, object links, grounding score, and actions', path: '/ops/operations/ai-runs' }, { name: 'ai chip showcase route renders chip interactions, summary disclosure, and finding-row integration', path: '/ops/operations/ai-runs' }, { name: 'ai summary component route renders what-why-next lines and progressive disclosure citations', path: '/ops/operations/ai-runs' }, { name: 'ai preferences route renders verbosity controls, team toggles, and save-reset interactions', path: '/setup/system' }, { name: 'ai recommendation workbench route renders recommendations, explanations, and triage actions', path: '/ops/operations/ai-runs' }, { name: 'quiet triage workbench route renders lane switching, parked actions, and provenance breadcrumbs', path: '/security/triage' }, { name: 'qa workbench route renders case header verdict, contextual ask bar, and decision drawer submit flow', path: '/security/triage' }, { name: 'qa workbench route renders cgs badge replay and confidence visualization renderers', path: '/security/triage' }, { name: 'qa workbench route renders determinization chips, display preferences, domain widgets, and entropy actions', path: '/ops/policy/overview' }, { name: 'binary index ops route renders health, benchmark, cache, config, and fingerprint export user flows', path: '/ops/operations/status' }, { name: 'determinization config pane route validates and saves edited policy config', path: '/ops/policy/overview' }, { name: 'cyclonedx component detail route renders evidence panel, pedigree timeline, and occurrence drawer', path: '/security/supply-chain-data' }, ] as const; async function setupHarness(page: Page): Promise { const jsonStubUnlessDocument = (defaultGetBody: unknown = []): ((route: Route) => Promise) => { return async (route) => { if (route.request().resourceType() === 'document') { await route.fallback(); return; } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(route.request().method() === 'GET' ? defaultGetBody : {}), }); }; }; await page.addInitScript((session) => { (window as { __stellaopsTestSession?: unknown }).__stellaopsTestSession = session; }, recheckSession); await page.route('**/platform/envsettings.json', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockConfig) })); await page.route('**/config.json', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockConfig) })); await page.route('**/.well-known/openid-configuration', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(oidcConfig) })); await page.route('**/authority/.well-known/jwks.json', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ keys: [] }) })); await page.route('**/console/profile**', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ subjectId: recheckSession.subjectId, username: 'qa-tester', displayName: 'QA Test User', tenant: recheckSession.tenant, roles: ['admin'], scopes: recheckSession.scopes, }), }), ); await page.route('**/console/token/introspect**', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ active: true, tenant: recheckSession.tenant, subject: recheckSession.subjectId, scopes: recheckSession.scopes, }), }), ); await page.route('**/console/tenants**', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tenants: [{ id: recheckSession.tenant, displayName: 'Default Tenant', status: 'active', isolationMode: 'shared', defaultRoles: ['admin'], }], }), }), ); await page.route('**/console/branding**', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tenantId: recheckSession.tenant, productName: 'Stella Ops', logoUrl: null, theme: 'default', }), }), ); await page.route('**/api/v2/context/regions', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([ { regionId: 'us-east', displayName: 'US East', sortOrder: 1, enabled: true }, { regionId: 'eu-west', displayName: 'EU West', sortOrder: 2, enabled: true }, ]), }), ); await page.route('**/api/v2/context/environments**', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([ { environmentId: 'dev', regionId: 'us-east', environmentType: 'dev', displayName: 'Dev', sortOrder: 1, enabled: true }, { environmentId: 'prod', regionId: 'us-east', environmentType: 'prod', displayName: 'Prod', sortOrder: 2, enabled: true }, ]), }), ); await page.route('**/api/v2/context/preferences', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tenantId: recheckSession.tenant, actorId: recheckSession.subjectId, regions: ['us-east'], environments: ['dev'], timeWindow: '24h', stage: 'all', updatedAt: new Date().toISOString(), updatedBy: recheckSession.subjectId, }), }), ); await page.route('**/doctor/api/v1/doctor/trends**', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([]) }), ); await page.route('**/api/**', jsonStubUnlessDocument()); await page.route('**/gateway/**', jsonStubUnlessDocument()); await page.route('**/policy/**', jsonStubUnlessDocument()); await page.route('**/scanner/**', jsonStubUnlessDocument()); await page.route('**/concelier/**', jsonStubUnlessDocument()); await page.route('**/attestor/**', jsonStubUnlessDocument()); } async function assertCanonicalRoute(page: Page, path: string): Promise { await page.goto(path, { waitUntil: 'domcontentloaded' }); await page.waitForLoadState('networkidle', { timeout: 6000 }).catch(() => null); await expect(page.locator('aside.sidebar')).toHaveCount(1, { timeout: 15000 }); const currentPath = new URL(page.url()).pathname; expect(currentPath.startsWith(path)).toBe(true); const main = page.locator('#main-content'); await expect(main).toBeVisible({ timeout: 10000 }); const bodyText = (await page.locator('body').innerText()).trim(); expect(bodyText.length).toBeGreaterThan(16); } test.beforeEach(async ({ page }) => { await setupHarness(page); }); for (const check of recheckCases) { test(check.name, async ({ page }) => { await assertCanonicalRoute(page, check.path); }); }