Harden policy simulation direct-route defaults
This commit is contained in:
@@ -0,0 +1,50 @@
|
|||||||
|
# Sprint 20260310-006 - FE Policy Simulation Direct Route Defaults and Hydration
|
||||||
|
|
||||||
|
## Topic & Scope
|
||||||
|
- Harden revived Policy Simulation surfaces so direct entry and partially wired host routes still render usable defaults instead of blank inputs.
|
||||||
|
- Normalize missing policy pack, version, and target environment inputs across the revived lint, diff, coverage, and promotion-gate components.
|
||||||
|
- Ensure the read-only coverage route hydrates on first render instead of waiting for a second interaction.
|
||||||
|
- Working directory: `src/Web/StellaOps.Web/src/app/features/policy-simulation`.
|
||||||
|
- Allowed coordination edits: `src/Web/StellaOps.Web/tsconfig.spec.features.json`, `docs/modules/ui/README.md`.
|
||||||
|
- Expected evidence: focused component spec pass, authenticated live Playwright evidence for `/ops/policy/simulation` policy actions, updated UI docs if behavior changes.
|
||||||
|
|
||||||
|
## Dependencies & Concurrency
|
||||||
|
- Follows `SPRINT_20260309_019_FE_policy_simulation_active_tenant_runtime.md`; this sprint hardens the revived component surfaces after the tenant seam repair.
|
||||||
|
- Safe parallelism: do not touch router readiness or unrelated search work while closing this slice.
|
||||||
|
|
||||||
|
## Documentation Prerequisites
|
||||||
|
- `AGENTS.md`
|
||||||
|
- `docs/code-of-conduct/CODE_OF_CONDUCT.md`
|
||||||
|
- `docs/qa/feature-checks/FLOW.md`
|
||||||
|
- `docs/modules/ui/README.md`
|
||||||
|
|
||||||
|
## Delivery Tracker
|
||||||
|
|
||||||
|
### FE-POLICY-SIM-006-001 - Normalize revived policy simulation inputs and direct hydration
|
||||||
|
Status: DONE
|
||||||
|
Dependency: none
|
||||||
|
Owners: Developer, QA
|
||||||
|
Task description:
|
||||||
|
- Centralize the fallback defaults used by revived Policy Simulation components so direct-entry routes and partially restored host callers do not pass through blank pack IDs, unusable versions, or empty target environments.
|
||||||
|
- Cover the normalization behavior in focused specs for the shared defaults helper and the affected components, then re-run live authenticated policy actions with Playwright to prove the repaired runtime still behaves correctly.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Coverage, diff, lint, and promotion-gate components normalize missing/blank inputs to stable defaults instead of rendering unusable state.
|
||||||
|
- [x] Coverage auto-loads on first render so the direct route is hydrated without a second click.
|
||||||
|
- [x] Focused Angular specs prove the normalization and hydration behaviors.
|
||||||
|
- [x] Authenticated Playwright completes the policy action sweep on `https://stella-ops.local` without new route or runtime failures.
|
||||||
|
|
||||||
|
## Execution Log
|
||||||
|
| Date (UTC) | Update | Owner |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 2026-03-10 | Sprint created after the remaining dirty `policy-simulation` slice was identified as a follow-on hardening pass for revived direct-route component defaults and coverage hydration. | Developer |
|
||||||
|
| 2026-03-10 | Added shared policy simulation defaults plus a Vitest-compatible direct-route regression harness, rebuilt the web bundle, synced `dist/stellaops-web/browser` into `compose_console-dist`, and verified `/ops/policy/simulation/coverage`, `/lint`, `/promotion`, and `/diff/policy-pack-001` live with zero runtime errors via Playwright. | Developer |
|
||||||
|
|
||||||
|
## Decisions & Risks
|
||||||
|
- Decision: keep default restoration local to the revived Policy Simulation feature cluster through a shared helper instead of reintroducing per-component literals.
|
||||||
|
- Decision: use a focused Vitest-compatible regression spec for the revived direct-route behaviors instead of widening the unsupported legacy ProxyZone/Karma component suite.
|
||||||
|
- Risk: defaulting a missing pack/version can hide a wiring regression; mitigate with focused specs and live Playwright verification on the real shell.
|
||||||
|
|
||||||
|
## Next Checkpoints
|
||||||
|
- 2026-03-10: finish the focused spec coverage and live policy Playwright recheck.
|
||||||
|
- 2026-03-10: commit the isolated policy-simulation hardening slice.
|
||||||
@@ -8,6 +8,11 @@
|
|||||||
|
|
||||||
The Console presents operator dashboards for scans, policies, VEX evidence, runtime posture, and admin workflows.
|
The Console presents operator dashboards for scans, policies, VEX evidence, runtime posture, and admin workflows.
|
||||||
|
|
||||||
|
## Latest updates (2026-03-10)
|
||||||
|
- Hardened revived `Ops > Policy > Simulation` direct-entry surfaces so coverage, lint, promotion-gate, and diff routes restore stable defaults when host wiring omits pack/version/environment inputs.
|
||||||
|
- Coverage now hydrates on first render instead of waiting for a second interaction, preventing blank direct-route states on `/ops/policy/simulation/coverage`.
|
||||||
|
- Added focused frontend verification for the policy simulation defaults helper and direct-route behaviors, plus a live Playwright sweep for `/ops/policy/simulation/coverage`, `/lint`, `/promotion`, and `/diff/policy-pack-001`.
|
||||||
|
|
||||||
## Latest updates (2026-03-08)
|
## Latest updates (2026-03-08)
|
||||||
- Shipped the canonical `Releases > Promotions` cutover, including repaired `/release-control/promotions*` and `/releases/promotion-queue*` aliases, release-context promotion wizard handoff, and a usable create-to-detail flow.
|
- Shipped the canonical `Releases > Promotions` cutover, including repaired `/release-control/promotions*` and `/releases/promotion-queue*` aliases, release-context promotion wizard handoff, and a usable create-to-detail flow.
|
||||||
- Added checked-feature verification for release promotions at `../../features/checked/web/release-promotions-cutover-ui.md`.
|
- Added checked-feature verification for release promotions at `../../features/checked/web/release-promotions-cutover-ui.md`.
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||||
|
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 outputDirectory = path.join(webRoot, 'output', 'playwright');
|
||||||
|
|
||||||
|
const DEFAULT_BASE_URL = process.env.STELLAOPS_FRONTDOOR_BASE_URL?.trim() || 'https://stella-ops.local';
|
||||||
|
const authStatePath = path.join(outputDirectory, 'live-policy-simulation-direct-routes.auth.json');
|
||||||
|
const authReportPath = path.join(outputDirectory, 'live-policy-simulation-direct-routes.auth-report.json');
|
||||||
|
const summaryPath = path.join(outputDirectory, 'live-policy-simulation-direct-routes.json');
|
||||||
|
|
||||||
|
function isStaticAsset(url) {
|
||||||
|
return /\.(?:css|js|map|png|jpg|jpeg|svg|woff2?|ico)(?:$|\?)/i.test(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldIgnoreFailure(errorText) {
|
||||||
|
return errorText === 'net::ERR_ABORTED';
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRuntime() {
|
||||||
|
return {
|
||||||
|
consoleErrors: [],
|
||||||
|
pageErrors: [],
|
||||||
|
requestFailures: [],
|
||||||
|
responseErrors: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachRuntimeWatchers(page, 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();
|
||||||
|
const errorText = request.failure()?.errorText ?? 'unknown';
|
||||||
|
if (isStaticAsset(url) || shouldIgnoreFailure(errorText)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.requestFailures.push({
|
||||||
|
page: page.url(),
|
||||||
|
method: request.method(),
|
||||||
|
url,
|
||||||
|
error: errorText,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
page.on('response', (response) => {
|
||||||
|
const url = response.url();
|
||||||
|
if (isStaticAsset(url) || response.status() < 400) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.responseErrors.push({
|
||||||
|
page: page.url(),
|
||||||
|
method: response.request().method(),
|
||||||
|
status: response.status(),
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForAnyVisible(page, selectors, timeout = 10_000) {
|
||||||
|
const deadline = Date.now() + timeout;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
for (const selector of selectors) {
|
||||||
|
if (await page.locator(selector).first().isVisible().catch(() => false)) {
|
||||||
|
return selector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
}
|
||||||
|
throw new Error(`Timed out waiting for any selector: ${selectors.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkRoute(page, baseUrl, route) {
|
||||||
|
const startedAt = new Date().toISOString();
|
||||||
|
let ok = false;
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.goto(`${baseUrl}${route.path}`, {
|
||||||
|
waitUntil: 'domcontentloaded',
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
await page.waitForLoadState('networkidle', { timeout: 15_000 }).catch(() => {});
|
||||||
|
await waitForAnyVisible(page, route.expectVisible);
|
||||||
|
|
||||||
|
if (route.action) {
|
||||||
|
await route.action(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = true;
|
||||||
|
} catch (cause) {
|
||||||
|
error = cause instanceof Error ? cause.message : String(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
route: route.path,
|
||||||
|
ok,
|
||||||
|
startedAtUtc: startedAt,
|
||||||
|
finishedAtUtc: new Date().toISOString(),
|
||||||
|
finalUrl: page.url(),
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
mkdirSync(outputDirectory, { recursive: true });
|
||||||
|
|
||||||
|
const authReport = await authenticateFrontdoor({
|
||||||
|
baseUrl: DEFAULT_BASE_URL,
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
const page = await context.newPage();
|
||||||
|
const runtime = createRuntime();
|
||||||
|
attachRuntimeWatchers(page, runtime);
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/ops/policy/simulation/coverage',
|
||||||
|
expectVisible: ['h1:has-text("Test Coverage")', '.coverage__header'],
|
||||||
|
action: async (routePage) => {
|
||||||
|
await waitForAnyVisible(routePage, ['.coverage__summary', '.coverage__rules']);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ops/policy/simulation/lint',
|
||||||
|
expectVisible: ['h1:has-text("Policy Lint")', '.policy-lint__header'],
|
||||||
|
action: async (routePage) => {
|
||||||
|
await routePage.getByRole('button', { name: /run lint/i }).click();
|
||||||
|
await waitForAnyVisible(routePage, ['.lint-status', '.lint-summary']);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ops/policy/simulation/promotion',
|
||||||
|
expectVisible: ['h1:has-text("Promotion Gate")', '.promotion-gate__header'],
|
||||||
|
action: async (routePage) => {
|
||||||
|
await routePage.getByRole('button', { name: /check requirements/i }).click();
|
||||||
|
await waitForAnyVisible(routePage, ['.promotion-gate__info', '.promotion-gate__summary']);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ops/policy/simulation/diff/policy-pack-001',
|
||||||
|
expectVisible: ['h1:has-text("Policy Diff")', '.diff-viewer__header'],
|
||||||
|
action: async (routePage) => {
|
||||||
|
await waitForAnyVisible(routePage, ['.diff-viewer__stats', '.diff-viewer__empty', '.diff-viewer__files']);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
try {
|
||||||
|
for (const route of routes) {
|
||||||
|
results.push(await checkRoute(page, DEFAULT_BASE_URL, route));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await context.close().catch(() => {});
|
||||||
|
await browser.close().catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = {
|
||||||
|
capturedAtUtc: new Date().toISOString(),
|
||||||
|
baseUrl: DEFAULT_BASE_URL,
|
||||||
|
results,
|
||||||
|
runtime,
|
||||||
|
failedRouteCount: results.filter((result) => result.ok === false).length,
|
||||||
|
runtimeIssueCount:
|
||||||
|
runtime.consoleErrors.length +
|
||||||
|
runtime.pageErrors.length +
|
||||||
|
runtime.requestFailures.length +
|
||||||
|
runtime.responseErrors.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
writeFileSync(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8');
|
||||||
|
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
||||||
|
|
||||||
|
if (summary.failedRouteCount > 0 || summary.runtimeIssueCount > 0) {
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
process.stderr.write(
|
||||||
|
`[live-policy-simulation-direct-routes] ${error instanceof Error ? error.message : String(error)}\n`,
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
@@ -80,7 +80,7 @@ describe('CoverageFixtureComponent', () => {
|
|||||||
mockApi = jasmine.createSpyObj('PolicySimulationApi', [
|
mockApi = jasmine.createSpyObj('PolicySimulationApi', [
|
||||||
'getCoverage',
|
'getCoverage',
|
||||||
'runCoverageTests',
|
'runCoverageTests',
|
||||||
]);
|
]) as jasmine.SpyObj<PolicySimulationApi>;
|
||||||
mockApi.getCoverage.and.returnValue(of(mockCoverageResult));
|
mockApi.getCoverage.and.returnValue(of(mockCoverageResult));
|
||||||
mockApi.runCoverageTests.and.returnValue(of(mockCoverageResult));
|
mockApi.runCoverageTests.and.returnValue(of(mockCoverageResult));
|
||||||
|
|
||||||
@@ -127,6 +127,11 @@ describe('CoverageFixtureComponent', () => {
|
|||||||
expect(component.policyPackId).toBe('custom-pack');
|
expect(component.policyPackId).toBe('custom-pack');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should restore the default policy pack id when routing supplies undefined', () => {
|
||||||
|
component.policyPackId = undefined as unknown as string;
|
||||||
|
expect(component.policyPackId).toBe('policy-pack-001');
|
||||||
|
});
|
||||||
|
|
||||||
it('should accept policyVersion input', () => {
|
it('should accept policyVersion input', () => {
|
||||||
component.policyVersion = 2;
|
component.policyVersion = 2;
|
||||||
expect(component.policyVersion).toBe(2);
|
expect(component.policyVersion).toBe(2);
|
||||||
@@ -158,6 +163,13 @@ describe('CoverageFixtureComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Service Interaction', () => {
|
describe('Service Interaction', () => {
|
||||||
|
it('should load coverage on init so direct routes are hydrated', fakeAsync(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(mockApi.getCoverage).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
it('should call getCoverage on loadCoverage', fakeAsync(() => {
|
it('should call getCoverage on loadCoverage', fakeAsync(() => {
|
||||||
component.loadCoverage();
|
component.loadCoverage();
|
||||||
tick();
|
tick();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, ChangeDetectionStrategy, inject, signal, computed, Input, OnChanges, SimpleChanges } from '@angular/core';
|
import { Component, ChangeDetectionStrategy, inject, signal, computed, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
||||||
|
|
||||||
import { finalize } from 'rxjs/operators';
|
import { finalize } from 'rxjs/operators';
|
||||||
|
|
||||||
@@ -13,6 +13,11 @@ import {
|
|||||||
PolicyTestCase,
|
PolicyTestCase,
|
||||||
CoverageStatus,
|
CoverageStatus,
|
||||||
} from '../../core/api/policy-simulation.models';
|
} from '../../core/api/policy-simulation.models';
|
||||||
|
import {
|
||||||
|
DEFAULT_POLICY_PACK_ID,
|
||||||
|
normalizeOptionalPolicyVersion,
|
||||||
|
normalizePolicyPackId,
|
||||||
|
} from './policy-simulation-defaults';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coverage fixture component showing coverage percentage per rule and missing test cases.
|
* Coverage fixture component showing coverage percentage per rule and missing test cases.
|
||||||
@@ -724,11 +729,28 @@ import {
|
|||||||
`,
|
`,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CoverageFixtureComponent implements OnChanges {
|
export class CoverageFixtureComponent implements OnChanges, OnInit {
|
||||||
private readonly api = inject(POLICY_SIMULATION_API);
|
private readonly api = inject(POLICY_SIMULATION_API);
|
||||||
|
private _policyPackId = DEFAULT_POLICY_PACK_ID;
|
||||||
|
private _policyVersion: number | undefined;
|
||||||
|
|
||||||
@Input() policyPackId = 'policy-pack-001';
|
@Input()
|
||||||
@Input() policyVersion?: number;
|
set policyPackId(value: string | null | undefined) {
|
||||||
|
this._policyPackId = normalizePolicyPackId(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get policyPackId(): string {
|
||||||
|
return this._policyPackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set policyVersion(value: number | string | null | undefined) {
|
||||||
|
this._policyVersion = normalizeOptionalPolicyVersion(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get policyVersion(): number | undefined {
|
||||||
|
return this._policyVersion;
|
||||||
|
}
|
||||||
|
|
||||||
readonly loading = signal(false);
|
readonly loading = signal(false);
|
||||||
readonly result = signal<CoverageResult | undefined>(undefined);
|
readonly result = signal<CoverageResult | undefined>(undefined);
|
||||||
@@ -755,6 +777,12 @@ export class CoverageFixtureComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (!this.loading() && !this.result()) {
|
||||||
|
this.loadCoverage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ringDashArray(): string {
|
ringDashArray(): string {
|
||||||
const circumference = 2 * Math.PI * 54; // ~339.3
|
const circumference = 2 * Math.PI * 54; // ~339.3
|
||||||
const percent = this.result()?.summary.overallCoveragePercent ?? 0;
|
const percent = this.result()?.summary.overallCoveragePercent ?? 0;
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ describe('PolicyDiffViewerComponent', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockApi = jasmine.createSpyObj('PolicySimulationApi', ['getDiff']);
|
mockApi = jasmine.createSpyObj('PolicySimulationApi', ['getDiff']) as jasmine.SpyObj<PolicySimulationApi>;
|
||||||
mockApi.getDiff.and.returnValue(of(mockDiffResult));
|
mockApi.getDiff.and.returnValue(of(mockDiffResult));
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
@@ -93,6 +93,11 @@ describe('PolicyDiffViewerComponent', () => {
|
|||||||
expect(component.policyPackId).toBe('custom-pack');
|
expect(component.policyPackId).toBe('custom-pack');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should restore the default policy pack id when routing supplies undefined', () => {
|
||||||
|
(component as any).policyPackId = undefined;
|
||||||
|
expect(component.policyPackId).toBe('policy-pack-001');
|
||||||
|
});
|
||||||
|
|
||||||
it('should accept fromVersion input', () => {
|
it('should accept fromVersion input', () => {
|
||||||
component.fromVersion = 1;
|
component.fromVersion = 1;
|
||||||
expect(component.fromVersion).toBe(1);
|
expect(component.fromVersion).toBe(1);
|
||||||
@@ -103,6 +108,14 @@ describe('PolicyDiffViewerComponent', () => {
|
|||||||
expect(component.toVersion).toBe(3);
|
expect(component.toVersion).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should restore version fallbacks when routing supplies unusable values', () => {
|
||||||
|
(component as any).fromVersion = '';
|
||||||
|
(component as any).toVersion = '0';
|
||||||
|
|
||||||
|
expect(component.fromVersion).toBe(1);
|
||||||
|
expect(component.toVersion).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
it('should load diff on policyPackId change', fakeAsync(() => {
|
it('should load diff on policyPackId change', fakeAsync(() => {
|
||||||
component.ngOnChanges({
|
component.ngOnChanges({
|
||||||
policyPackId: new SimpleChange(null, 'new-pack', true),
|
policyPackId: new SimpleChange(null, 'new-pack', true),
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ import {
|
|||||||
PolicyDiffHunk,
|
PolicyDiffHunk,
|
||||||
PolicyDiffLine,
|
PolicyDiffLine,
|
||||||
} from '../../core/api/policy-simulation.models';
|
} from '../../core/api/policy-simulation.models';
|
||||||
|
import {
|
||||||
|
DEFAULT_POLICY_PACK_ID,
|
||||||
|
normalizePolicyPackId,
|
||||||
|
normalizePolicyVersion,
|
||||||
|
} from './policy-simulation-defaults';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Policy diff viewer showing before/after comparison for rule changes.
|
* Policy diff viewer showing before/after comparison for rule changes.
|
||||||
@@ -477,10 +482,36 @@ import {
|
|||||||
})
|
})
|
||||||
export class PolicyDiffViewerComponent implements OnChanges {
|
export class PolicyDiffViewerComponent implements OnChanges {
|
||||||
private readonly api = inject(POLICY_SIMULATION_API);
|
private readonly api = inject(POLICY_SIMULATION_API);
|
||||||
|
private _policyPackId = DEFAULT_POLICY_PACK_ID;
|
||||||
|
private _fromVersion = 1;
|
||||||
|
private _toVersion = 2;
|
||||||
|
|
||||||
@Input() policyPackId = 'policy-pack-001';
|
@Input()
|
||||||
@Input() fromVersion = 1;
|
set policyPackId(value: string | null | undefined) {
|
||||||
@Input() toVersion = 2;
|
this._policyPackId = normalizePolicyPackId(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get policyPackId(): string {
|
||||||
|
return this._policyPackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set fromVersion(value: number | string | null | undefined) {
|
||||||
|
this._fromVersion = normalizePolicyVersion(value, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
get fromVersion(): number {
|
||||||
|
return this._fromVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set toVersion(value: number | string | null | undefined) {
|
||||||
|
this._toVersion = normalizePolicyVersion(value, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
get toVersion(): number {
|
||||||
|
return this._toVersion;
|
||||||
|
}
|
||||||
|
|
||||||
readonly loading = signal(false);
|
readonly loading = signal(false);
|
||||||
readonly result = signal<PolicyDiffResult | undefined>(undefined);
|
readonly result = signal<PolicyDiffResult | undefined>(undefined);
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ describe('PolicyLintComponent', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockApi = jasmine.createSpyObj('PolicySimulationApi', ['lintPolicy']);
|
mockApi = jasmine.createSpyObj('PolicySimulationApi', ['lintPolicy']) as jasmine.SpyObj<PolicySimulationApi>;
|
||||||
mockApi.lintPolicy.and.returnValue(of(mockLintResult));
|
mockApi.lintPolicy.and.returnValue(of(mockLintResult));
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
@@ -125,11 +125,21 @@ describe('PolicyLintComponent', () => {
|
|||||||
expect(component.policyPackId).toBe('custom-pack');
|
expect(component.policyPackId).toBe('custom-pack');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should restore the default policy pack id when routing supplies undefined', () => {
|
||||||
|
(component as any).policyPackId = undefined;
|
||||||
|
expect(component.policyPackId).toBe('policy-pack-001');
|
||||||
|
});
|
||||||
|
|
||||||
it('should accept policyVersion input', () => {
|
it('should accept policyVersion input', () => {
|
||||||
component.policyVersion = 2;
|
component.policyVersion = 2;
|
||||||
expect(component.policyVersion).toBe(2);
|
expect(component.policyVersion).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should clear policyVersion when routing supplies an unusable value', () => {
|
||||||
|
(component as any).policyVersion = '0';
|
||||||
|
expect(component.policyVersion).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('should clear result on input change', fakeAsync(() => {
|
it('should clear result on input change', fakeAsync(() => {
|
||||||
component['result'].set(mockLintResult);
|
component['result'].set(mockLintResult);
|
||||||
component.ngOnChanges({
|
component.ngOnChanges({
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ import {
|
|||||||
LintSeverity,
|
LintSeverity,
|
||||||
LintCategory,
|
LintCategory,
|
||||||
} from '../../core/api/policy-simulation.models';
|
} from '../../core/api/policy-simulation.models';
|
||||||
|
import {
|
||||||
|
DEFAULT_POLICY_PACK_ID,
|
||||||
|
normalizeOptionalPolicyVersion,
|
||||||
|
normalizePolicyPackId,
|
||||||
|
} from './policy-simulation-defaults';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Policy lint component showing errors, warnings, and compilation status.
|
* Policy lint component showing errors, warnings, and compilation status.
|
||||||
@@ -593,9 +598,26 @@ import {
|
|||||||
})
|
})
|
||||||
export class PolicyLintComponent implements OnChanges {
|
export class PolicyLintComponent implements OnChanges {
|
||||||
private readonly api = inject(POLICY_SIMULATION_API);
|
private readonly api = inject(POLICY_SIMULATION_API);
|
||||||
|
private _policyPackId = DEFAULT_POLICY_PACK_ID;
|
||||||
|
private _policyVersion: number | undefined;
|
||||||
|
|
||||||
@Input() policyPackId = 'policy-pack-001';
|
@Input()
|
||||||
@Input() policyVersion?: number;
|
set policyPackId(value: string | null | undefined) {
|
||||||
|
this._policyPackId = normalizePolicyPackId(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get policyPackId(): string {
|
||||||
|
return this._policyPackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set policyVersion(value: number | string | null | undefined) {
|
||||||
|
this._policyVersion = normalizeOptionalPolicyVersion(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get policyVersion(): number | undefined {
|
||||||
|
return this._policyVersion;
|
||||||
|
}
|
||||||
|
|
||||||
readonly loading = signal(false);
|
readonly loading = signal(false);
|
||||||
readonly result = signal<PolicyLintResult | undefined>(undefined);
|
readonly result = signal<PolicyLintResult | undefined>(undefined);
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
DEFAULT_POLICY_PACK_ID,
|
||||||
|
DEFAULT_POLICY_VERSION,
|
||||||
|
DEFAULT_TARGET_ENVIRONMENT,
|
||||||
|
normalizeOptionalPolicyVersion,
|
||||||
|
normalizePolicyPackId,
|
||||||
|
normalizePolicyVersion,
|
||||||
|
normalizeTargetEnvironment,
|
||||||
|
} from './policy-simulation-defaults';
|
||||||
|
|
||||||
|
describe('policy simulation input defaults', () => {
|
||||||
|
it('falls back to the default policy pack id when the router supplies an empty value', () => {
|
||||||
|
expect(normalizePolicyPackId(undefined)).toBe(DEFAULT_POLICY_PACK_ID);
|
||||||
|
expect(normalizePolicyPackId('')).toBe(DEFAULT_POLICY_PACK_ID);
|
||||||
|
expect(normalizePolicyPackId(' ')).toBe(DEFAULT_POLICY_PACK_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps valid policy pack ids untouched', () => {
|
||||||
|
expect(normalizePolicyPackId('policy-pack-prod-001')).toBe('policy-pack-prod-001');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses optional positive policy versions and rejects invalid values', () => {
|
||||||
|
expect(normalizeOptionalPolicyVersion('7')).toBe(7);
|
||||||
|
expect(normalizeOptionalPolicyVersion(3)).toBe(3);
|
||||||
|
expect(normalizeOptionalPolicyVersion(undefined)).toBeUndefined();
|
||||||
|
expect(normalizeOptionalPolicyVersion('0')).toBeUndefined();
|
||||||
|
expect(normalizeOptionalPolicyVersion('abc')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores default numeric values when routing supplies nothing usable', () => {
|
||||||
|
expect(normalizePolicyVersion(undefined, DEFAULT_POLICY_VERSION)).toBe(DEFAULT_POLICY_VERSION);
|
||||||
|
expect(normalizePolicyVersion('', DEFAULT_POLICY_VERSION)).toBe(DEFAULT_POLICY_VERSION);
|
||||||
|
expect(normalizePolicyVersion('5', DEFAULT_POLICY_VERSION)).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores the default target environment when the route has no environment segment', () => {
|
||||||
|
expect(normalizeTargetEnvironment(undefined)).toBe(DEFAULT_TARGET_ENVIRONMENT);
|
||||||
|
expect(normalizeTargetEnvironment('')).toBe(DEFAULT_TARGET_ENVIRONMENT);
|
||||||
|
expect(normalizeTargetEnvironment('stage')).toBe('stage');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export const DEFAULT_POLICY_PACK_ID = 'policy-pack-001';
|
||||||
|
export const DEFAULT_POLICY_VERSION = 2;
|
||||||
|
export const DEFAULT_TARGET_ENVIRONMENT = 'production';
|
||||||
|
|
||||||
|
export function normalizePolicyPackId(value: string | null | undefined): string {
|
||||||
|
const normalized = value?.trim();
|
||||||
|
return normalized ? normalized : DEFAULT_POLICY_PACK_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeOptionalPolicyVersion(value: number | string | null | undefined): number | undefined {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericValue = typeof value === 'number' ? value : Number(value);
|
||||||
|
if (!Number.isFinite(numericValue) || numericValue <= 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.trunc(numericValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizePolicyVersion(
|
||||||
|
value: number | string | null | undefined,
|
||||||
|
fallback: number,
|
||||||
|
): number {
|
||||||
|
return normalizeOptionalPolicyVersion(value) ?? fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeTargetEnvironment(value: string | null | undefined): string {
|
||||||
|
const normalized = value?.trim();
|
||||||
|
return normalized ? normalized : DEFAULT_TARGET_ENVIRONMENT;
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import { SimpleChange } from '@angular/core';
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
|
import { POLICY_SIMULATION_API, PolicySimulationApi } from '../../core/api/policy-simulation.client';
|
||||||
|
import {
|
||||||
|
CoverageResult,
|
||||||
|
PolicyDiffResult,
|
||||||
|
PolicyLintResult,
|
||||||
|
PromotionGateResult,
|
||||||
|
} from '../../core/api/policy-simulation.models';
|
||||||
|
import { CoverageFixtureComponent } from './coverage-fixture.component';
|
||||||
|
import { PolicyDiffViewerComponent } from './policy-diff-viewer.component';
|
||||||
|
import { PolicyLintComponent } from './policy-lint.component';
|
||||||
|
import { PromotionGateComponent } from './promotion-gate.component';
|
||||||
|
|
||||||
|
function createPolicySimulationApiMock(): jasmine.SpyObj<PolicySimulationApi> {
|
||||||
|
return jasmine.createSpyObj('PolicySimulationApi', [
|
||||||
|
'getCoverage',
|
||||||
|
'runCoverageTests',
|
||||||
|
'getDiff',
|
||||||
|
'lintPolicy',
|
||||||
|
'checkPromotionGate',
|
||||||
|
'overridePromotionGate',
|
||||||
|
]) as jasmine.SpyObj<PolicySimulationApi>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const coverageResult: CoverageResult = {
|
||||||
|
summary: {
|
||||||
|
policyPackId: 'policy-pack-001',
|
||||||
|
policyVersion: 1,
|
||||||
|
totalRules: 1,
|
||||||
|
coveredRules: 1,
|
||||||
|
partialRules: 0,
|
||||||
|
uncoveredRules: 0,
|
||||||
|
overallCoveragePercent: 100,
|
||||||
|
totalTestCases: 1,
|
||||||
|
passedTestCases: 1,
|
||||||
|
failedTestCases: 0,
|
||||||
|
computedAt: '2026-03-10T00:00:00Z',
|
||||||
|
},
|
||||||
|
rules: [],
|
||||||
|
testCases: [],
|
||||||
|
traceId: 'trace-coverage',
|
||||||
|
};
|
||||||
|
|
||||||
|
const diffResult: PolicyDiffResult = {
|
||||||
|
diffId: 'diff-001',
|
||||||
|
policyPackId: 'policy-pack-001',
|
||||||
|
fromVersion: 1,
|
||||||
|
toVersion: 2,
|
||||||
|
files: [],
|
||||||
|
stats: { additions: 0, deletions: 0, modifications: 0, filesChanged: 0 },
|
||||||
|
createdAt: '2026-03-10T00:00:00Z',
|
||||||
|
traceId: 'trace-diff',
|
||||||
|
};
|
||||||
|
|
||||||
|
const lintResult: PolicyLintResult = {
|
||||||
|
policyPackId: 'policy-pack-001',
|
||||||
|
policyVersion: 1,
|
||||||
|
compiled: true,
|
||||||
|
totalIssues: 0,
|
||||||
|
errorCount: 0,
|
||||||
|
warningCount: 0,
|
||||||
|
infoCount: 0,
|
||||||
|
issues: [],
|
||||||
|
lintedAt: '2026-03-10T00:00:00Z',
|
||||||
|
traceId: 'trace-lint',
|
||||||
|
};
|
||||||
|
|
||||||
|
const gateResult: PromotionGateResult = {
|
||||||
|
policyPackId: 'policy-pack-001',
|
||||||
|
policyVersion: 2,
|
||||||
|
targetEnvironment: 'production',
|
||||||
|
overallStatus: 'ready',
|
||||||
|
checks: [],
|
||||||
|
allRequiredPassed: true,
|
||||||
|
blockingIssues: 0,
|
||||||
|
warnings: 0,
|
||||||
|
canOverride: false,
|
||||||
|
computedAt: '2026-03-10T00:00:00Z',
|
||||||
|
traceId: 'trace-gate',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('policy simulation direct-route defaults', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hydrates coverage with defaulted inputs on first render', async () => {
|
||||||
|
const api = createPolicySimulationApiMock();
|
||||||
|
api.getCoverage.and.returnValue(of(coverageResult));
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CoverageFixtureComponent],
|
||||||
|
providers: [{ provide: POLICY_SIMULATION_API, useValue: api }],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(CoverageFixtureComponent);
|
||||||
|
const component = fixture.componentInstance;
|
||||||
|
|
||||||
|
(component as any).policyPackId = ' ';
|
||||||
|
(component as any).policyVersion = '0';
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
|
||||||
|
expect(component.policyPackId).toBe('policy-pack-001');
|
||||||
|
expect(component.policyVersion).toBeUndefined();
|
||||||
|
expect(api.getCoverage).toHaveBeenCalledWith({
|
||||||
|
tenantId: 'default',
|
||||||
|
policyPackId: 'policy-pack-001',
|
||||||
|
policyVersion: undefined,
|
||||||
|
});
|
||||||
|
expect(component.result()).toEqual(coverageResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores diff fallbacks before loading a direct route comparison', async () => {
|
||||||
|
const api = createPolicySimulationApiMock();
|
||||||
|
api.getDiff.and.returnValue(of(diffResult));
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PolicyDiffViewerComponent],
|
||||||
|
providers: [{ provide: POLICY_SIMULATION_API, useValue: api }],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(PolicyDiffViewerComponent);
|
||||||
|
const component = fixture.componentInstance;
|
||||||
|
|
||||||
|
(component as any).policyPackId = undefined;
|
||||||
|
(component as any).fromVersion = '';
|
||||||
|
(component as any).toVersion = '0';
|
||||||
|
|
||||||
|
component.ngOnChanges({
|
||||||
|
policyPackId: new SimpleChange('custom-pack', undefined, false),
|
||||||
|
fromVersion: new SimpleChange(5, '' as unknown as number, false),
|
||||||
|
toVersion: new SimpleChange(6, '0' as unknown as number, false),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(component.policyPackId).toBe('policy-pack-001');
|
||||||
|
expect(component.fromVersion).toBe(1);
|
||||||
|
expect(component.toVersion).toBe(2);
|
||||||
|
expect(api.getDiff).toHaveBeenCalledWith('policy-pack-001', 1, 2);
|
||||||
|
expect(component.result()).toEqual(diffResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes lint inputs before issuing the live lint request', async () => {
|
||||||
|
const api = createPolicySimulationApiMock();
|
||||||
|
api.lintPolicy.and.returnValue(of(lintResult));
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PolicyLintComponent],
|
||||||
|
providers: [{ provide: POLICY_SIMULATION_API, useValue: api }],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(PolicyLintComponent);
|
||||||
|
const component = fixture.componentInstance;
|
||||||
|
|
||||||
|
(component as any).policyPackId = undefined;
|
||||||
|
(component as any).policyVersion = '0';
|
||||||
|
|
||||||
|
component.runLint();
|
||||||
|
|
||||||
|
expect(component.policyPackId).toBe('policy-pack-001');
|
||||||
|
expect(component.policyVersion).toBeUndefined();
|
||||||
|
expect(api.lintPolicy).toHaveBeenCalledWith('policy-pack-001', undefined);
|
||||||
|
expect(component.result()).toEqual(lintResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes promotion-gate defaults before checking the gate', async () => {
|
||||||
|
const api = createPolicySimulationApiMock();
|
||||||
|
api.checkPromotionGate.and.returnValue(of(gateResult));
|
||||||
|
api.overridePromotionGate.and.returnValue(of(gateResult));
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PromotionGateComponent],
|
||||||
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
{ provide: POLICY_SIMULATION_API, useValue: api },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(PromotionGateComponent);
|
||||||
|
const component = fixture.componentInstance;
|
||||||
|
|
||||||
|
(component as any).policyPackId = undefined;
|
||||||
|
(component as any).policyVersion = '0';
|
||||||
|
(component as any).targetEnvironment = ' ';
|
||||||
|
|
||||||
|
component.checkGate();
|
||||||
|
|
||||||
|
expect(component.policyPackId).toBe('policy-pack-001');
|
||||||
|
expect(component.policyVersion).toBe(2);
|
||||||
|
expect(component.targetEnvironment).toBe('production');
|
||||||
|
expect(api.checkPromotionGate).toHaveBeenCalledWith({
|
||||||
|
tenantId: 'default',
|
||||||
|
policyPackId: 'policy-pack-001',
|
||||||
|
policyVersion: 2,
|
||||||
|
targetEnvironment: 'production',
|
||||||
|
});
|
||||||
|
expect(component.result()).toEqual(gateResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -47,7 +47,7 @@ describe('PromotionGateComponent', () => {
|
|||||||
docsUrl: 'https://docs.example.com/shadow-mode',
|
docsUrl: 'https://docs.example.com/shadow-mode',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
checkedAt: new Date().toISOString(),
|
computedAt: new Date().toISOString(),
|
||||||
traceId: 'trace-123',
|
traceId: 'trace-123',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,9 +79,9 @@ describe('PromotionGateComponent', () => {
|
|||||||
mockApi = jasmine.createSpyObj('PolicySimulationApi', [
|
mockApi = jasmine.createSpyObj('PolicySimulationApi', [
|
||||||
'checkPromotionGate',
|
'checkPromotionGate',
|
||||||
'overridePromotionGate',
|
'overridePromotionGate',
|
||||||
]);
|
]) as jasmine.SpyObj<PolicySimulationApi>;
|
||||||
mockApi.checkPromotionGate.and.returnValue(of(mockGateResult));
|
mockApi.checkPromotionGate.and.returnValue(of(mockGateResult));
|
||||||
mockApi.overridePromotionGate.and.returnValue(of({ ...mockBlockedResult, overallStatus: 'overridden' }));
|
mockApi.overridePromotionGate.and.returnValue(of({ ...mockBlockedResult, overallStatus: 'ready' }));
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [PromotionGateComponent, ReactiveFormsModule],
|
imports: [PromotionGateComponent, ReactiveFormsModule],
|
||||||
@@ -121,16 +121,31 @@ describe('PromotionGateComponent', () => {
|
|||||||
expect(component.policyPackId).toBe('custom-pack');
|
expect(component.policyPackId).toBe('custom-pack');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should restore the default policy pack id when routing supplies undefined', () => {
|
||||||
|
(component as any).policyPackId = undefined;
|
||||||
|
expect(component.policyPackId).toBe('policy-pack-001');
|
||||||
|
});
|
||||||
|
|
||||||
it('should accept policyVersion input', () => {
|
it('should accept policyVersion input', () => {
|
||||||
component.policyVersion = 5;
|
component.policyVersion = 5;
|
||||||
expect(component.policyVersion).toBe(5);
|
expect(component.policyVersion).toBe(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should restore the default policy version when routing supplies an unusable value', () => {
|
||||||
|
(component as any).policyVersion = '0';
|
||||||
|
expect(component.policyVersion).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
it('should accept targetEnvironment input', () => {
|
it('should accept targetEnvironment input', () => {
|
||||||
component.targetEnvironment = 'staging';
|
component.targetEnvironment = 'staging';
|
||||||
expect(component.targetEnvironment).toBe('staging');
|
expect(component.targetEnvironment).toBe('staging');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should restore the default target environment when routing supplies an empty value', () => {
|
||||||
|
(component as any).targetEnvironment = ' ';
|
||||||
|
expect(component.targetEnvironment).toBe('production');
|
||||||
|
});
|
||||||
|
|
||||||
it('should clear result on input change', fakeAsync(() => {
|
it('should clear result on input change', fakeAsync(() => {
|
||||||
component['result'].set(mockGateResult);
|
component['result'].set(mockGateResult);
|
||||||
component.ngOnChanges({
|
component.ngOnChanges({
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ import {
|
|||||||
PromotionGateCheck,
|
PromotionGateCheck,
|
||||||
GateCheckStatus,
|
GateCheckStatus,
|
||||||
} from '../../core/api/policy-simulation.models';
|
} from '../../core/api/policy-simulation.models';
|
||||||
|
import {
|
||||||
|
DEFAULT_POLICY_PACK_ID,
|
||||||
|
DEFAULT_POLICY_VERSION,
|
||||||
|
DEFAULT_TARGET_ENVIRONMENT,
|
||||||
|
normalizePolicyPackId,
|
||||||
|
normalizePolicyVersion,
|
||||||
|
normalizeTargetEnvironment,
|
||||||
|
} from './policy-simulation-defaults';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Promotion gate component for checklist enforcement before production apply.
|
* Promotion gate component for checklist enforcement before production apply.
|
||||||
@@ -629,10 +637,36 @@ export class PromotionGateComponent implements OnChanges {
|
|||||||
private readonly api = inject(POLICY_SIMULATION_API);
|
private readonly api = inject(POLICY_SIMULATION_API);
|
||||||
private readonly fb = inject(FormBuilder);
|
private readonly fb = inject(FormBuilder);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
|
private _policyPackId = DEFAULT_POLICY_PACK_ID;
|
||||||
|
private _policyVersion = DEFAULT_POLICY_VERSION;
|
||||||
|
private _targetEnvironment = DEFAULT_TARGET_ENVIRONMENT;
|
||||||
|
|
||||||
@Input() policyPackId = 'policy-pack-001';
|
@Input()
|
||||||
@Input() policyVersion = 2;
|
set policyPackId(value: string | null | undefined) {
|
||||||
@Input() targetEnvironment = 'production';
|
this._policyPackId = normalizePolicyPackId(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get policyPackId(): string {
|
||||||
|
return this._policyPackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set policyVersion(value: number | string | null | undefined) {
|
||||||
|
this._policyVersion = normalizePolicyVersion(value, DEFAULT_POLICY_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
get policyVersion(): number {
|
||||||
|
return this._policyVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set targetEnvironment(value: string | null | undefined) {
|
||||||
|
this._targetEnvironment = normalizeTargetEnvironment(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get targetEnvironment(): string {
|
||||||
|
return this._targetEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
readonly loading = signal(false);
|
readonly loading = signal(false);
|
||||||
readonly result = signal<PromotionGateResult | undefined>(undefined);
|
readonly result = signal<PromotionGateResult | undefined>(undefined);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
"src/app/features/deploy-diff/components/deploy-diff-panel/deploy-diff-panel.component.spec.ts",
|
"src/app/features/deploy-diff/components/deploy-diff-panel/deploy-diff-panel.component.spec.ts",
|
||||||
"src/app/features/deploy-diff/pages/deploy-diff.page.spec.ts",
|
"src/app/features/deploy-diff/pages/deploy-diff.page.spec.ts",
|
||||||
"src/app/features/deploy-diff/services/deploy-diff.service.spec.ts",
|
"src/app/features/deploy-diff/services/deploy-diff.service.spec.ts",
|
||||||
|
"src/app/features/policy-simulation/policy-simulation-direct-route-defaults.spec.ts",
|
||||||
|
"src/app/features/policy-simulation/policy-simulation-defaults.spec.ts",
|
||||||
"src/app/features/policy-simulation/simulation-dashboard.component.spec.ts",
|
"src/app/features/policy-simulation/simulation-dashboard.component.spec.ts",
|
||||||
"src/app/features/registry-admin/registry-admin.component.spec.ts",
|
"src/app/features/registry-admin/registry-admin.component.spec.ts",
|
||||||
"src/app/features/evidence-thread/__tests__/evidence-thread-view.component.spec.ts"
|
"src/app/features/evidence-thread/__tests__/evidence-thread-view.component.spec.ts"
|
||||||
|
|||||||
Reference in New Issue
Block a user