Preserve canonical policy and reachability QA routes

This commit is contained in:
master
2026-03-08 10:23:34 +02:00
parent 56143d12b7
commit c797bd9f46
5 changed files with 224 additions and 15 deletions

View File

@@ -401,16 +401,33 @@ function collectRouteParams(snapshot: ActivatedRouteSnapshot | null): Record<str
}
function resolvePrimaryTab(currentUrl: string): DecisioningPrimaryTab {
if (currentUrl.includes('/ops/policy/packs')) {
if (currentUrl.includes('/ops/policy/packs') || currentUrl.includes('/ops/policy/baselines')) {
return 'packs';
}
if (currentUrl.includes('/ops/policy/governance')) {
if (
currentUrl.includes('/ops/policy/governance') ||
currentUrl.includes('/ops/policy/risk-budget') ||
currentUrl.includes('/ops/policy/budget') ||
currentUrl.includes('/ops/policy/trust-weights') ||
currentUrl.includes('/ops/policy/staleness') ||
currentUrl.includes('/ops/policy/sealed-mode') ||
currentUrl.includes('/ops/policy/profiles') ||
currentUrl.includes('/ops/policy/validator') ||
currentUrl.includes('/ops/policy/conflicts') ||
currentUrl.includes('/ops/policy/impact-preview') ||
currentUrl.includes('/ops/policy/schema-playground') ||
currentUrl.includes('/ops/policy/schema-docs')
) {
return 'governance';
}
if (currentUrl.includes('/ops/policy/simulation')) {
return 'simulation';
}
if (currentUrl.includes('/ops/policy/vex')) {
if (
currentUrl.includes('/ops/policy/vex') ||
currentUrl.includes('/ops/policy/waivers') ||
currentUrl.includes('/ops/policy/exceptions')
) {
return 'vex';
}
if (currentUrl.includes('/ops/policy/gates')) {

View File

@@ -22,20 +22,165 @@ export const policyDecisioningRoutes: Routes = [
(m) => m.PolicyDecisioningOverviewPageComponent,
),
},
{
path: 'risk-budget',
title: 'Policy Governance',
loadComponent: () =>
import('../policy-governance/risk-budget-dashboard.component').then(
(m) => m.RiskBudgetDashboardComponent,
),
},
{
path: 'budget',
title: 'Policy Governance',
loadComponent: () =>
import('../policy-governance/risk-budget-dashboard.component').then(
(m) => m.RiskBudgetDashboardComponent,
),
},
{
path: 'risk-budget/config',
title: 'Policy Governance',
loadComponent: () =>
import('../policy-governance/risk-budget-config.component').then(
(m) => m.RiskBudgetConfigComponent,
),
},
{
path: 'budget/config',
title: 'Policy Governance',
loadComponent: () =>
import('../policy-governance/risk-budget-config.component').then(
(m) => m.RiskBudgetConfigComponent,
),
},
{
path: 'trust-weights',
title: 'Policy Governance',
loadComponent: () =>
import('../policy-governance/trust-weighting.component').then(
(m) => m.TrustWeightingComponent,
),
},
{
path: 'staleness',
title: 'Policy Governance',
loadComponent: () =>
import('../policy-governance/staleness-config.component').then(
(m) => m.StalenessConfigComponent,
),
},
{
path: 'sealed-mode',
title: 'Policy Governance',
loadComponent: () =>
import('../policy-governance/sealed-mode-control.component').then(
(m) => m.SealedModeControlComponent,
),
},
{
path: 'sealed-mode/overrides',
title: 'Policy Governance',
loadComponent: () =>
import('../policy-governance/sealed-mode-overrides.component').then(
(m) => m.SealedModeOverridesComponent,
),
},
{
path: 'profiles',
title: 'Policy Governance',
loadComponent: () =>
import('../policy-governance/risk-profile-list.component').then(
(m) => m.RiskProfileListComponent,
),
},
{
path: 'profiles/new',
title: 'Policy Governance',
loadComponent: () =>
import('../policy-governance/risk-profile-editor.component').then(
(m) => m.RiskProfileEditorComponent,
),
},
{
path: 'profiles/:profileId',
title: 'Policy Governance',
loadComponent: () =>
import('../policy-governance/risk-profile-editor.component').then(
(m) => m.RiskProfileEditorComponent,
),
},
{
path: 'validator',
title: 'Policy Governance',
loadComponent: () =>
import('../policy-governance/policy-validator.component').then(
(m) => m.PolicyValidatorComponent,
),
},
{
path: 'conflicts',
title: 'Policy Governance',
loadComponent: () =>
import('../policy-governance/policy-conflict-dashboard.component').then(
(m) => m.PolicyConflictDashboardComponent,
),
},
{
path: 'conflicts/:conflictId/resolve',
title: 'Policy Governance',
loadComponent: () =>
import('../policy-governance/conflict-resolution-wizard.component').then(
(m) => m.ConflictResolutionWizardComponent,
),
},
{
path: 'impact-preview',
title: 'Policy Governance',
loadComponent: () =>
import('../policy-governance/impact-preview.component').then(
(m) => m.ImpactPreviewComponent,
),
},
{
path: 'schema-playground',
title: 'Policy Governance',
loadComponent: () =>
import('../policy-governance/schema-playground.component').then(
(m) => m.SchemaPlaygroundComponent,
),
},
{
path: 'schema-docs',
title: 'Policy Governance',
loadComponent: () =>
import('../policy-governance/schema-docs.component').then(
(m) => m.SchemaDocsComponent,
),
},
{
path: 'baselines',
pathMatch: 'full',
redirectTo: 'overview',
title: 'Policy Packs',
loadComponent: () =>
import('../policy-studio/workspace/policy-workspace.component').then(
(m) => m.PolicyWorkspaceComponent,
),
},
{
path: 'waivers',
pathMatch: 'full',
redirectTo: 'vex/exceptions',
title: 'VEX & Exceptions',
loadComponent: () =>
import('../exceptions/exception-dashboard.component').then(
(m) => m.ExceptionDashboardComponent,
),
},
{
path: 'exceptions',
pathMatch: 'full',
redirectTo: 'vex/exceptions',
title: 'VEX & Exceptions',
loadComponent: () =>
import('../exceptions/exception-dashboard.component').then(
(m) => m.ExceptionDashboardComponent,
),
},
{
path: 'packs',

View File

@@ -19,6 +19,23 @@ describe('policyDecisioningRoutes', () => {
);
});
it('preserves governance aliases as canonical top-level policy routes', () => {
const routeByPath = new Map(children.map((route) => [route.path, route] as const));
expect(routeByPath.get('risk-budget')?.loadComponent).toEqual(jasmine.any(Function));
expect(routeByPath.get('baselines')?.loadComponent).toEqual(jasmine.any(Function));
expect(routeByPath.get('budget')?.loadComponent).toEqual(jasmine.any(Function));
expect(routeByPath.get('risk-budget/config')?.loadComponent).toEqual(jasmine.any(Function));
expect(routeByPath.get('budget/config')?.loadComponent).toEqual(jasmine.any(Function));
expect(routeByPath.get('trust-weights')?.loadComponent).toEqual(jasmine.any(Function));
expect(routeByPath.get('staleness')?.loadComponent).toEqual(jasmine.any(Function));
expect(routeByPath.get('sealed-mode')?.loadComponent).toEqual(jasmine.any(Function));
expect(routeByPath.get('profiles')?.loadComponent).toEqual(jasmine.any(Function));
expect(routeByPath.get('validator')?.loadComponent).toEqual(jasmine.any(Function));
expect(routeByPath.get('waivers')?.loadComponent).toEqual(jasmine.any(Function));
expect(routeByPath.get('exceptions')?.loadComponent).toEqual(jasmine.any(Function));
});
it('keeps pack authoring subviews inside the packs shell', () => {
const packsRoute = children.find((route) => route.path === 'packs');
const packPaths = packsRoute?.children?.map((route) => route.path) ?? [];

View File

@@ -100,6 +100,25 @@ describe('PolicyDecisioningShellComponent', () => {
expect(navigateByUrlSpy).toHaveBeenCalledWith('/releases/rel-42');
});
it('treats preserved governance aliases as governance tab routes', () => {
const fixture = createShell('/ops/policy/risk-budget');
const component = fixture.componentInstance;
expect(component.shellState().kind).toBe('global');
expect(component.shellState().activeTab).toBe('governance');
expect(component.primaryTabs().find((tab) => tab.id === 'governance')?.route).toEqual([
'/ops/policy/governance',
]);
});
it('treats preserved baseline and waiver aliases as first-class shell tabs', () => {
const packsFixture = createShell('/ops/policy/baselines');
expect(packsFixture.componentInstance.shellState().activeTab).toBe('packs');
const vexFixture = createShell('/ops/policy/waivers');
expect(vexFixture.componentInstance.shellState().activeTab).toBe('vex');
});
});
function buildSnapshot(

View File

@@ -193,7 +193,10 @@ const strictRouteExpectations: Partial<Record<(typeof canonicalRoutes)[number],
},
'/security/reachability': {
title: /Reachability/i,
texts: ['Reachability Center'],
texts: [
'Coverage, witnesses, proof-of-exposure artifacts, and sensor gaps stay in one investigation shell.',
'Healthy assets',
],
},
'/setup/trust-signing': {
title: /Trust/i,
@@ -213,15 +216,21 @@ const strictRouteExpectations: Partial<Record<(typeof canonicalRoutes)[number],
},
'/ops/policy': {
title: /Policy/i,
texts: ['Policy Governance', 'Risk Budget Overview'],
texts: [
'Policy Decisioning Studio',
'Decisioning Studio now owns policy packs, governance, simulation, VEX conflicts, exceptions, gate review, and audit.',
],
},
'/ops/policy/overview': {
title: /Policy/i,
texts: ['Policy Governance', 'Risk Budget Overview'],
texts: [
'Policy Decisioning Studio',
'Decisioning Studio now owns policy packs, governance, simulation, VEX conflicts, exceptions, gate review, and audit.',
],
},
'/ops/policy/risk-budget': {
title: /Policy/i,
texts: ['Policy Governance', 'Risk Budget Overview'],
texts: ['Policy Decisioning Studio', 'Risk Budget Overview'],
},
};
@@ -1602,13 +1611,15 @@ test.describe('Pre-alpha key end-user interactions', () => {
await expect(page.locator('#main-content')).toContainText('Attestation Coverage Metrics');
});
test('mission board reachability card opens Reachability Center', async ({ page }) => {
test('mission board reachability card opens Reachability workspace', async ({ page }) => {
await page.goto('/mission-control/board', { waitUntil: 'domcontentloaded' });
await page.locator('#main-content a[href="/security/reachability"]').first().click();
await expect(page).toHaveURL(/\/security\/reachability$/);
await expect(page).toHaveTitle(/Reachability/i);
await expect(page.locator('#main-content')).toContainText('Reachability Center');
await expect(page.locator('#main-content')).toContainText(
'Coverage, witnesses, proof-of-exposure artifacts, and sensor gaps stay in one investigation shell.'
);
});
test('setup trust-signing tabs stay under setup routes', async ({ page }) => {