This commit is contained in:
master
2026-02-21 16:21:33 +02:00
parent 7e36c1f151
commit b911537870
116 changed files with 4365 additions and 5903 deletions

View File

@@ -9,6 +9,7 @@ const shellSession = {
...policyAuthorSession.scopes,
'ui.read',
'admin',
'ui.admin',
'orch:read',
'orch:operate',
'orch:quota',
@@ -23,6 +24,19 @@ const shellSession = {
'exceptions:read',
'exceptions:approve',
'aoc:verify',
'policy:read',
'policy:author',
'policy:review',
'policy:approve',
'policy:simulate',
'policy:audit',
'health:read',
'notify:viewer',
'release:read',
'release:write',
'release:publish',
'sbom:read',
'signer:read',
]),
],
};
@@ -79,42 +93,42 @@ async function setupShell(page: Page): Promise<void> {
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('**/authority/.well-known/openid-configuration', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(oidcConfig),
})
}),
);
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('**/authority/connect/**', (route) =>
route.fulfill({
status: 400,
contentType: 'application/json',
body: JSON.stringify({ error: 'not-used-in-shell-e2e' }),
})
}),
);
}
@@ -136,23 +150,6 @@ async function assertMainHasContent(page: Page): Promise<void> {
expect(text.length > 12 || childNodes > 4).toBe(true);
}
async function openSidebarGroupRoute(
page: Page,
groupLabel: string,
targetHref: string
): Promise<void> {
const sidebar = page.locator('aside.sidebar');
const targetLink = sidebar.locator(`a[href="${targetHref}"]`).first();
const isVisible = await targetLink.isVisible().catch(() => false);
if (!isVisible) {
await sidebar.getByRole('button', { name: groupLabel, exact: true }).click();
}
await expect(targetLink).toBeVisible();
await targetLink.click();
}
function collectConsoleErrors(page: Page): string[] {
const errors: string[] = [];
page.on('console', (msg) => {
@@ -170,18 +167,19 @@ test.beforeEach(async ({ page }) => {
test.describe('Nav shell canonical domains', () => {
test('sidebar renders all canonical root labels', async ({ page }) => {
await go(page, '/dashboard');
await go(page, '/mission-control/board');
await ensureShell(page);
const navText = (await page.locator('aside.sidebar').textContent()) ?? '';
const labels = [
'Dashboard',
'Release Control',
'Security & Risk',
'Evidence & Audit',
'Integrations',
'Platform Ops',
'Administration',
'Mission Board',
'Mission Alerts',
'Mission Activity',
'Releases',
'Security',
'Evidence',
'Ops',
'Setup',
];
for (const label of labels) {
@@ -189,78 +187,50 @@ test.describe('Nav shell canonical domains', () => {
}
});
test('sidebar excludes deprecated v1 labels', async ({ page }) => {
await go(page, '/dashboard');
test('sidebar excludes deprecated root labels', async ({ page }) => {
await go(page, '/mission-control/board');
await ensureShell(page);
const navText = (await page.locator('aside.sidebar').textContent()) ?? '';
expect(navText).not.toContain('Operations');
expect(navText).not.toContain('Security & Risk');
expect(navText).not.toContain('Evidence & Audit');
expect(navText).not.toContain('Platform Ops');
expect(navText).not.toContain('Administration');
expect(navText).not.toContain('Policy Studio');
});
});
test.describe('Nav shell critical legacy redirects', () => {
const redirects: Array<{ from: string; expectedPrefix: string }> = [
{ from: '/findings', expectedPrefix: '/security-risk/findings' },
{ from: '/vulnerabilities', expectedPrefix: '/security-risk/vulnerabilities' },
{ from: '/evidence-packs', expectedPrefix: '/evidence-audit/packs' },
{ from: '/admin/audit', expectedPrefix: '/evidence-audit/audit' },
{ from: '/ops/health', expectedPrefix: '/platform-ops/health' },
{ from: '/admin/notifications', expectedPrefix: '/administration/notifications' },
{ from: '/release-orchestrator/releases', expectedPrefix: '/release-control/releases' },
{ from: '/release-orchestrator/approvals', expectedPrefix: '/release-control/approvals' },
{ from: '/release-orchestrator/environments', expectedPrefix: '/release-control/regions' },
{ from: '/settings/release-control', expectedPrefix: '/release-control/setup' },
test.describe('No redirect contracts', () => {
const legacyPaths = [
'/release-control/releases',
'/security-risk/findings',
'/evidence-audit/packs',
'/administration',
];
for (const redirect of redirects) {
test(`${redirect.from} redirects correctly`, async ({ page }) => {
await go(page, redirect.from);
for (const path of legacyPaths) {
test(`${path} does not rewrite URL`, async ({ page }) => {
await go(page, path);
const finalUrl = new URL(page.url());
expect(finalUrl.pathname.startsWith(redirect.expectedPrefix)).toBe(true);
expect(finalUrl.pathname).toBe(path);
await expect(page.getByRole('heading', { level: 1, name: /dashboard/i })).toBeVisible();
});
}
test('redirect preserves query parameters', async ({ page }) => {
await go(page, '/findings?filter=critical&sort=severity');
const finalUrl = page.url();
expect(finalUrl).toContain('/security-risk/findings');
expect(finalUrl).toContain('filter=critical');
expect(finalUrl).toContain('sort=severity');
});
test('redirect preserves fragments', async ({ page }) => {
await go(page, '/admin/audit#recent');
const finalUrl = page.url();
expect(finalUrl).toContain('/evidence-audit/audit');
expect(finalUrl).toContain('#recent');
});
test('release-orchestrator root redirect does not loop', async ({ page }) => {
await go(page, '/release-orchestrator');
const finalUrl = new URL(page.url());
expect(finalUrl.pathname).not.toBe('/release-orchestrator');
});
});
test.describe('Nav shell breadcrumbs and stability', () => {
const breadcrumbRoutes: Array<{ path: string; expected: string }> = [
{ path: '/release-control/releases', expected: 'Release Control' },
{ path: '/release-control/setup', expected: 'Setup' },
{ path: '/security-risk/advisory-sources', expected: 'Advisory Sources' },
{ path: '/evidence-audit/replay', expected: 'Replay & Verify' },
{ path: '/platform-ops/data-integrity', expected: 'Data Integrity' },
{ path: '/administration/trust-signing', expected: 'Trust & Signing' },
{ path: '/mission-control/board', expected: 'Mission Board' },
{ path: '/releases/versions', expected: 'Release Versions' },
{ path: '/security/triage', expected: 'Triage' },
{ path: '/evidence/verify-replay', expected: 'Verify & Replay' },
{ path: '/ops/operations/data-integrity', expected: 'Data Integrity' },
{ path: '/setup/topology/agents', expected: 'Agent Fleet' },
];
for (const route of breadcrumbRoutes) {
test(`breadcrumb renders on ${route.path}`, async ({ page }) => {
if (route.path === '/platform-ops/data-integrity') {
await go(page, '/dashboard');
await openSidebarGroupRoute(page, 'Platform Ops', '/platform-ops/data-integrity');
} else {
await go(page, route.path);
}
await go(page, route.path);
await ensureShell(page);
const breadcrumb = page.locator('app-breadcrumb nav.breadcrumb');
await expect(breadcrumb).toHaveCount(1);
@@ -270,129 +240,116 @@ test.describe('Nav shell breadcrumbs and stability', () => {
test('canonical roots produce no app runtime errors', async ({ page }) => {
const errors = collectConsoleErrors(page);
const routes = [
'/dashboard',
'/release-control',
'/security-risk',
'/evidence-audit',
'/administration',
];
const routes = ['/mission-control/board', '/releases', '/security', '/evidence', '/ops', '/setup'];
for (const route of routes) {
await go(page, route);
await ensureShell(page);
}
// /platform-ops and /integrations are proxy-captured in dev mode.
// Validate them through client-side navigation instead of direct reload.
await go(page, '/dashboard');
await openSidebarGroupRoute(page, 'Platform Ops', '/platform-ops/data-integrity');
await expect(page).toHaveURL(/\/platform-ops\/data-integrity$/);
await go(page, '/dashboard');
await openSidebarGroupRoute(page, 'Integrations', '/integrations');
await expect(page).toHaveURL(/\/integrations$/);
const appErrors = errors.filter(
(error) =>
!error.includes('ERR_FAILED') &&
!error.includes('ERR_BLOCKED') &&
!error.includes('ERR_CONNECTION_REFUSED') &&
!error.includes('404') &&
error.length > 0
error.length > 0,
);
expect(appErrors).toEqual([]);
});
});
test.describe('Pack route render checks', () => {
test('release-control pack routes render non-blank content', async ({ page }) => {
test('release routes render non-blank content', async ({ page }) => {
test.setTimeout(60_000);
const routes = [
'/release-control/control-plane',
'/release-control/bundles',
'/release-control/regions',
'/release-control/governance',
'/release-control/hotfixes',
'/releases/overview',
'/releases/versions',
'/releases/runs',
'/releases/approvals',
'/releases/hotfixes',
'/releases/promotion-queue',
'/releases/environments',
'/releases/deployments',
'/releases/versions/new',
];
for (const route of routes) {
await go(page, route);
await ensureShell(page);
const finalUrl = new URL(page.url());
expect(finalUrl.pathname.startsWith(route)).toBe(true);
expect(new URL(page.url()).pathname).toBe(route);
await assertMainHasContent(page);
}
});
test('security and evidence pack routes render non-blank content', async ({ page }) => {
test('security and evidence routes render non-blank content', async ({ page }) => {
test.setTimeout(60_000);
const routes = [
'/security-risk/findings',
'/security-risk/vulnerabilities',
'/security-risk/vex',
'/security-risk/sbom-lake',
'/security-risk/exceptions',
'/evidence-audit/packs',
'/evidence-audit/bundles',
'/evidence-audit/evidence/export',
'/evidence-audit/audit-log',
'/evidence-audit/trust-signing',
'/security/posture',
'/security/triage',
'/security/advisories-vex',
'/security/supply-chain-data',
'/security/reachability',
'/security/reports',
'/evidence/overview',
'/evidence/capsules',
'/evidence/verify-replay',
'/evidence/exports',
'/evidence/audit-log',
];
for (const route of routes) {
await go(page, route);
await ensureShell(page);
const finalUrl = new URL(page.url());
expect(finalUrl.pathname.startsWith(route)).toBe(true);
expect(new URL(page.url()).pathname).toBe(route);
await assertMainHasContent(page);
}
});
test('platform-ops and integrations routes render via sidebar navigation', async ({ page }) => {
test('ops and setup routes render non-blank content', async ({ page }) => {
test.setTimeout(60_000);
const platformOpsRoutes = [
'/platform-ops/data-integrity',
'/platform-ops/orchestrator',
'/platform-ops/health',
'/platform-ops/quotas',
'/platform-ops/feeds',
const routes = [
'/ops',
'/ops/operations',
'/ops/operations/data-integrity',
'/ops/operations/orchestrator',
'/ops/integrations',
'/ops/integrations/advisory-vex-sources',
'/ops/policy',
'/ops/platform-setup',
'/setup',
'/setup/topology/overview',
'/setup/topology/targets',
'/setup/topology/hosts',
'/setup/topology/agents',
];
for (const route of platformOpsRoutes) {
await go(page, '/dashboard');
await openSidebarGroupRoute(page, 'Platform Ops', route);
await expect(page).toHaveURL(new RegExp(`${route.replace(/\//g, '\\/')}$`));
const currentPath = new URL(page.url()).pathname;
expect(currentPath).toBe(route);
for (const route of routes) {
await go(page, route);
await ensureShell(page);
expect(new URL(page.url()).pathname).toBe(route);
await assertMainHasContent(page);
}
await go(page, '/dashboard');
await openSidebarGroupRoute(page, 'Integrations', '/integrations');
await expect(page).toHaveURL(/\/integrations$/);
expect(new URL(page.url()).pathname).toBe('/integrations');
await assertMainHasContent(page);
});
});
test.describe('Nav shell responsive layout', () => {
test('desktop viewport shows sidebar', async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await go(page, '/dashboard');
await go(page, '/mission-control/board');
await expect(page.locator('aside.sidebar')).toBeVisible();
});
test('mobile viewport remains usable without horizontal overflow', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await go(page, '/dashboard');
await go(page, '/mission-control/board');
await expect(page.locator('.topbar__menu-toggle')).toBeVisible();
const hasHorizontalScroll = await page.evaluate(
() => document.documentElement.scrollWidth > document.documentElement.clientWidth
() => document.documentElement.scrollWidth > document.documentElement.clientWidth,
);
expect(hasHorizontalScroll).toBe(false);
});