feat(ui): adopt policy components on decisioning surfaces [SPRINT-019]
Wire PolicyEvaluatePanel, RemediationHint, and PolicyPackEditor into policy-decisioning gates page and policy-studio editor within the canonical /ops/policy shell. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,44 @@
|
|||||||
|
# Orphan Policy Component Adoption
|
||||||
|
|
||||||
|
Sprint: SPRINT_20260308_019_FE_orphan_policy_component_adoption
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Revived three dormant shared policy widgets by adopting them on mounted canonical surfaces inside the `/ops/policy` shell:
|
||||||
|
|
||||||
|
1. **PolicyEvaluatePanelComponent** + **RemediationHintComponent**: Adopted on the `PolicyDecisioningGatesPageComponent` (`/ops/policy/gates/*`). The gates page now renders a structured decision banner, gate results table, and contextual remediation hints derived from its existing gate results data. The `RemediationHintComponent` is imported transitively through the evaluate panel.
|
||||||
|
|
||||||
|
2. **PolicyPackEditorComponent**: Adopted as a visual gate configuration sidebar within the `PolicyEditorComponent` (`/ops/policy/packs/:packId/edit`). Operators can view and modify gates visually alongside the Monaco DSL editor. No second editor route tree was created.
|
||||||
|
|
||||||
|
## Mounted hosts
|
||||||
|
|
||||||
|
| Widget | Host component | Route | Adoption type |
|
||||||
|
|---|---|---|---|
|
||||||
|
| PolicyEvaluatePanelComponent | PolicyDecisioningGatesPageComponent | `/ops/policy/gates/*` | Inline evaluation panel |
|
||||||
|
| RemediationHintComponent | PolicyDecisioningGatesPageComponent (via evaluate panel) | `/ops/policy/gates/*` | Transitive via evaluate panel |
|
||||||
|
| PolicyPackEditorComponent | PolicyEditorComponent | `/ops/policy/packs/:packId/edit` | Sidebar visual config |
|
||||||
|
|
||||||
|
## Exclusions
|
||||||
|
|
||||||
|
| Surface | Reason |
|
||||||
|
|---|---|
|
||||||
|
| `features/releases/release-flow.component` | Uses bespoke `RemediationHintsComponent` with domain-specific data contract (`PolicyGateResult`/`RemediationStep` from `release.models`) including automated step triggers, copy-to-clipboard, and exception request flow. The shared `RemediationHintComponent` does not support these features. |
|
||||||
|
| Policy Studio as a separate navigation branch | Explicitly excluded per sprint scope - canonical policy shell only. |
|
||||||
|
|
||||||
|
## Data contracts
|
||||||
|
|
||||||
|
- `PolicyEvaluateResponse` (from `policy-interop.models.ts`): gates page derives this from its existing `GateResult[]` via a computed signal.
|
||||||
|
- `PolicyPackDocument` (from `policy-interop.models.ts`): editor derives this from the loaded `PolicyPack` metadata.
|
||||||
|
- `RemediationHint` (from `policy-interop.models.ts`): consumed transitively through the evaluate panel.
|
||||||
|
|
||||||
|
## Test coverage
|
||||||
|
|
||||||
|
- `policy-decisioning-gates-page.component.spec.ts`: Verifies policyEvaluateResult derivation, gate mapping, remediation hints, decision computation, and template rendering of the shared panel.
|
||||||
|
- `policy-editor-pack-editor.component.spec.ts`: Verifies packDocument derivation, template rendering of the shared pack editor, and change handler.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- `src/Web/StellaOps.Web/src/app/features/policy-decisioning/policy-decisioning-gates-page.component.ts`
|
||||||
|
- `src/Web/StellaOps.Web/src/app/features/policy-studio/editor/policy-editor.component.ts`
|
||||||
|
- `src/Web/StellaOps.Web/src/app/features/policy-decisioning/policy-decisioning-gates-page.component.spec.ts` (new)
|
||||||
|
- `src/Web/StellaOps.Web/src/app/features/policy-studio/editor/policy-editor-pack-editor.component.spec.ts` (new)
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Sprint 20260308-019 - FE Orphan Policy Component Adoption
|
||||||
|
|
||||||
|
## Topic & Scope
|
||||||
|
- Revive the dormant shared policy widgets that still fit the current Policy Decisioning Studio and release-context policy workflows.
|
||||||
|
- Adopt `PolicyEvaluatePanelComponent`, `PolicyPackEditorComponent`, and `RemediationHintComponent` inside the shipped `/ops/policy` shell and current release decisioning surfaces.
|
||||||
|
- Keep the sprint inside the canonical policy shell; do not restore Policy Studio as a separate navigation branch.
|
||||||
|
- Working directory: `src/Web/StellaOps.Web`.
|
||||||
|
- Allowed coordination edits: `docs/modules/ui/orphan-revival-batch/README.md`, `docs/modules/ui/TASKS.md`, `docs/modules/ui/implementation_plan.md`, `docs/features/checked/web/`, `src/Web/StellaOps.Web/src/app/shared/components/policy/`, `src/Web/StellaOps.Web/src/app/features/policy-decisioning/`, `src/Web/StellaOps.Web/src/app/features/policy-gates/`, and `src/Web/StellaOps.Web/src/app/features/release-orchestrator/`.
|
||||||
|
- Expected evidence: focused Angular tests, one checked-feature note, and sprint execution-log updates.
|
||||||
|
|
||||||
|
## Dependencies & Concurrency
|
||||||
|
- Hard dependency inside the orphan revival batch: none.
|
||||||
|
- External prerequisite already satisfied: the canonical Policy Decisioning Studio shell and release-context policy entry points are already shipped.
|
||||||
|
- Safe parallelism:
|
||||||
|
- Can run in parallel with every other queued sprint.
|
||||||
|
- This sprint exclusively owns adoption of the shared policy widget family while staffed.
|
||||||
|
|
||||||
|
## Documentation Prerequisites
|
||||||
|
- `docs/modules/ui/policy-decisioning-studio/README.md`
|
||||||
|
- `docs/modules/ui/orphan-revival-batch/README.md`
|
||||||
|
- `src/Web/StellaOps.Web/AGENTS.md`
|
||||||
|
- `src/Web/StellaOps.Web/src/app/shared/components/policy/policy-evaluate-panel.component.ts`
|
||||||
|
- `src/Web/StellaOps.Web/src/app/shared/components/policy/policy-pack-editor.component.ts`
|
||||||
|
- `src/Web/StellaOps.Web/src/app/shared/components/policy/remediation-hint.component.ts`
|
||||||
|
- `src/Web/StellaOps.Web/src/app/features/policy-decisioning/policy-decisioning.routes.ts`
|
||||||
|
|
||||||
|
## Delivery Tracker
|
||||||
|
|
||||||
|
### FE-OPC-001 - Freeze mounted policy host pages and data contracts
|
||||||
|
Status: DONE
|
||||||
|
Dependency: none
|
||||||
|
Owners: Developer (FE), Product Manager
|
||||||
|
Task description:
|
||||||
|
- Freeze the mounted host pages inside the current policy and release decisioning flows that will adopt the dormant shared policy widgets.
|
||||||
|
- Confirm that the current API contracts provide enough data for the widgets or explicitly scope any contract gaps.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Mounted policy hosts are recorded in the execution log.
|
||||||
|
- [x] Data-contract assumptions for each adopted widget are recorded.
|
||||||
|
- [x] Dead policy-studio pages are explicitly excluded.
|
||||||
|
|
||||||
|
### FE-OPC-002 - Adopt evaluation and remediation widgets
|
||||||
|
Status: DONE
|
||||||
|
Dependency: FE-OPC-001
|
||||||
|
Owners: Developer (FE)
|
||||||
|
Task description:
|
||||||
|
- Wire `PolicyEvaluatePanelComponent` and `RemediationHintComponent` into the frozen mounted policy and release-context hosts where they make gate outcomes and next actions more usable.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Mounted hosts render the shared evaluation panel or remediation hints where appropriate.
|
||||||
|
- [x] Current gate verdict semantics remain consistent with the shipped policy shell.
|
||||||
|
- [x] Remediation content stays contextual to the gate or release decision being viewed.
|
||||||
|
|
||||||
|
### FE-OPC-003 - Adopt pack editor on canonical policy authoring paths
|
||||||
|
Status: DONE
|
||||||
|
Dependency: FE-OPC-001
|
||||||
|
Owners: Developer (FE)
|
||||||
|
Task description:
|
||||||
|
- Wire `PolicyPackEditorComponent` into the frozen mounted policy authoring or editing flows if it improves the canonical policy shell without reviving a parallel editor branch.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] The shared pack editor is adopted only on canonical authoring paths inside `/ops/policy`.
|
||||||
|
- [x] There is no second editor route tree outside the canonical policy shell.
|
||||||
|
- [x] Any editor exclusions are recorded if the current shell already has a stronger purpose-built editor.
|
||||||
|
|
||||||
|
### FE-OPC-004 - Verify and document policy-widget revival
|
||||||
|
Status: DONE
|
||||||
|
Dependency: FE-OPC-002
|
||||||
|
Owners: Test Automation, Documentation author
|
||||||
|
Task description:
|
||||||
|
- Add focused Angular coverage for the revived policy widgets and document the shipped adoption slice.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Angular tests cover the adopted policy widgets in their mounted hosts.
|
||||||
|
- [x] Checked-feature note exists under `docs/features/checked/web/`.
|
||||||
|
- [x] UI plan/task docs reflect the shared policy-widget adoption.
|
||||||
|
|
||||||
|
## Execution Log
|
||||||
|
| Date (UTC) | Update | Owner |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 2026-03-08 | Sprint created from the orphan-revival batch to revive dormant shared policy widgets inside the canonical Policy Decisioning and release-context flows. | Project Manager |
|
||||||
|
| 2026-03-08 | FE-OPC-001: Frozen mounted host matrix. Hosts: (1) `PolicyDecisioningGatesPageComponent` at `/ops/policy/gates/*` - adopts PolicyEvaluatePanelComponent + RemediationHintComponent; (2) `PolicyEditorComponent` at `/ops/policy/packs/:packId/edit` - adopts PolicyPackEditorComponent as visual gate config sidebar. Excluded: `features/releases/release-flow.component` already has its own bespoke `RemediationHintsComponent` with domain-specific step/action/copy semantics. Data contracts: PolicyEvaluateResponse and PolicyPackDocument from policy-interop.models.ts are compatible. The gates page derives PolicyEvaluateResponse from its existing GateResult[] data. The pack editor derives PolicyPackDocument from PolicyPack metadata. | Developer (FE) |
|
||||||
|
| 2026-03-08 | FE-OPC-002: Wired PolicyEvaluatePanelComponent and RemediationHintComponent (via evaluate panel's imports) into PolicyDecisioningGatesPageComponent. Added policyEvaluateResult computed signal that maps GateResult[] to PolicyEvaluateResponse with decision banner, gate table, and remediation hints. | Developer (FE) |
|
||||||
|
| 2026-03-08 | FE-OPC-003: Wired PolicyPackEditorComponent into PolicyEditorComponent sidebar at `/ops/policy/packs/:packId/edit`. Visual gate config complements the Monaco DSL editor. No second editor route tree created. Exclusion recorded: the Monaco editor is the purpose-built authoring tool; the shared pack editor provides visual gate configuration alongside it. | Developer (FE) |
|
||||||
|
| 2026-03-08 | FE-OPC-004: Added focused Angular tests for both adoption hosts. Created checked-feature note. | Developer (FE) |
|
||||||
|
|
||||||
|
## Decisions & Risks
|
||||||
|
- Decision: this sprint strengthens the canonical policy shell instead of restoring Policy Studio as a sibling product.
|
||||||
|
- Risk: some shared policy widgets may lag behind the current policy shell's route model or data contract.
|
||||||
|
- Mitigation: freeze host pages first and record any widget that should remain dormant because it no longer fits the canonical experience.
|
||||||
|
- Decision: PolicyPackEditorComponent is adopted as a sidebar visual config tool within the existing `/ops/policy/packs/:packId/edit` route, not as a replacement for the Monaco DSL editor.
|
||||||
|
- Decision: The release-flow's bespoke RemediationHintsComponent (features/releases/) is excluded because it uses a different data contract (PolicyGateResult/RemediationStep from release.models) with domain-specific step actions, automated triggers, and copy-to-clipboard that the shared RemediationHintComponent does not support.
|
||||||
|
|
||||||
|
## Next Checkpoints
|
||||||
|
- 2026-03-09: mounted policy hosts frozen.
|
||||||
|
- 2026-03-11: widget-adoption criteria agreed.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// SPDX-License-Identifier: BUSL-1.1
|
||||||
|
// Copyright (c) 2025 StellaOps
|
||||||
|
// Sprint: SPRINT_20260308_019_FE_orphan_policy_component_adoption
|
||||||
|
// Task: FE-OPC-004 - Verify policy-widget revival
|
||||||
|
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { PolicyDecisioningGatesPageComponent } from './policy-decisioning-gates-page.component';
|
||||||
|
|
||||||
|
describe('PolicyDecisioningGatesPageComponent (policy widget adoption)', () => {
|
||||||
|
let component: PolicyDecisioningGatesPageComponent;
|
||||||
|
let fixture: ComponentFixture<PolicyDecisioningGatesPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PolicyDecisioningGatesPageComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
snapshot: {
|
||||||
|
root: { queryParams: {} },
|
||||||
|
paramMap: { get: () => null },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(PolicyDecisioningGatesPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compute policyEvaluateResult from gate results', () => {
|
||||||
|
const result = component.policyEvaluateResult();
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result!.decision).toBeDefined();
|
||||||
|
expect(['allow', 'warn', 'block']).toContain(result!.decision);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should derive gates array in policyEvaluateResult', () => {
|
||||||
|
const result = component.policyEvaluateResult();
|
||||||
|
expect(result!.gates).toBeDefined();
|
||||||
|
expect(result!.gates!.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
for (const gate of result!.gates!) {
|
||||||
|
expect(gate.gate_id).toBeTruthy();
|
||||||
|
expect(gate.gate_type).toBeTruthy();
|
||||||
|
expect(typeof gate.passed).toBe('boolean');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should derive remediation hints for non-passing gates', () => {
|
||||||
|
const result = component.policyEvaluateResult();
|
||||||
|
expect(result!.remediation).toBeDefined();
|
||||||
|
|
||||||
|
// There should be remediation for WARN/BLOCK gates only
|
||||||
|
const nonPassingGates = component.gateResults().filter(
|
||||||
|
(g) => g.state === 'BLOCK' || g.state === 'WARN'
|
||||||
|
);
|
||||||
|
expect(result!.remediation!.length).toBe(nonPassingGates.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set decision to warn when gates have WARN state but no BLOCK', () => {
|
||||||
|
// In global scope, reachability is WARN, not BLOCK
|
||||||
|
const result = component.policyEvaluateResult();
|
||||||
|
// Global scope has WARN gates but no BLOCK
|
||||||
|
expect(result!.decision).toBe('warn');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the shared evaluation panel in template', () => {
|
||||||
|
const el: HTMLElement = fixture.nativeElement;
|
||||||
|
const panelHost = el.querySelector('[data-testid="policy-evaluate-panel-host"]');
|
||||||
|
expect(panelHost).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render stella-policy-evaluate-panel element', () => {
|
||||||
|
const el: HTMLElement = fixture.nativeElement;
|
||||||
|
const panel = el.querySelector('stella-policy-evaluate-panel');
|
||||||
|
expect(panel).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -23,6 +23,10 @@ import {
|
|||||||
import {
|
import {
|
||||||
buildContextRouteParams,
|
buildContextRouteParams,
|
||||||
} from '../../shared/ui/context-route-state/context-route-state';
|
} from '../../shared/ui/context-route-state/context-route-state';
|
||||||
|
import {
|
||||||
|
PolicyEvaluatePanelComponent,
|
||||||
|
} from '../../shared/components/policy/policy-evaluate-panel.component';
|
||||||
|
import type { PolicyEvaluateResponse } from '../../core/api/policy-interop.models';
|
||||||
|
|
||||||
type DecisioningGateScope = 'global' | 'environment' | 'release' | 'approval';
|
type DecisioningGateScope = 'global' | 'environment' | 'release' | 'approval';
|
||||||
|
|
||||||
@@ -33,6 +37,7 @@ type DecisioningGateScope = 'global' | 'environment' | 'release' | 'approval';
|
|||||||
RouterLink,
|
RouterLink,
|
||||||
GateSummaryPanelComponent,
|
GateSummaryPanelComponent,
|
||||||
GateExplainDrawerComponent,
|
GateExplainDrawerComponent,
|
||||||
|
PolicyEvaluatePanelComponent,
|
||||||
],
|
],
|
||||||
template: `
|
template: `
|
||||||
<section class="policy-gates-page" data-testid="policy-gates-page">
|
<section class="policy-gates-page" data-testid="policy-gates-page">
|
||||||
@@ -132,6 +137,19 @@ type DecisioningGateScope = 'global' | 'environment' | 'release' | 'approval';
|
|||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Policy Evaluation Summary (shared widget) -->
|
||||||
|
@if (policyEvaluateResult()) {
|
||||||
|
<section class="evaluation-panel" data-testid="policy-evaluate-panel-host">
|
||||||
|
<article class="details-card">
|
||||||
|
<h3>Policy evaluation</h3>
|
||||||
|
<stella-policy-evaluate-panel
|
||||||
|
[result]="policyEvaluateResult()"
|
||||||
|
(retryEvaluate)="onRetryEvaluate()"
|
||||||
|
/>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
<section class="details-panel">
|
<section class="details-panel">
|
||||||
<article class="details-card">
|
<article class="details-card">
|
||||||
<h3>Blocking reasons</h3>
|
<h3>Blocking reasons</h3>
|
||||||
@@ -274,6 +292,11 @@ type DecisioningGateScope = 'global' | 'environment' | 'release' | 'approval';
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.evaluation-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.details-panel {
|
.details-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
@@ -528,6 +551,49 @@ export class PolicyDecisioningGatesPageComponent {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive a PolicyEvaluateResponse from the current gate results
|
||||||
|
* so the shared PolicyEvaluatePanelComponent can render a structured
|
||||||
|
* decision banner, gate table, and remediation hints.
|
||||||
|
*/
|
||||||
|
readonly policyEvaluateResult = computed<PolicyEvaluateResponse | null>(() => {
|
||||||
|
const gates = this.gateResults();
|
||||||
|
if (gates.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasBlock = gates.some((g) => g.state === 'BLOCK');
|
||||||
|
const hasWarn = gates.some((g) => g.state === 'WARN');
|
||||||
|
const decision: 'allow' | 'warn' | 'block' = hasBlock
|
||||||
|
? 'block'
|
||||||
|
: hasWarn
|
||||||
|
? 'warn'
|
||||||
|
: 'allow';
|
||||||
|
|
||||||
|
return {
|
||||||
|
decision,
|
||||||
|
gates: gates.map((g) => ({
|
||||||
|
gate_id: g.id,
|
||||||
|
gate_type: g.gateType ?? 'StandardGate',
|
||||||
|
passed: g.state === 'PASS',
|
||||||
|
reason: g.reason,
|
||||||
|
})),
|
||||||
|
remediation: gates
|
||||||
|
.filter((g) => g.state === 'BLOCK' || g.state === 'WARN')
|
||||||
|
.map((g) => ({
|
||||||
|
code: g.id,
|
||||||
|
title: g.reason ?? g.name,
|
||||||
|
severity: g.state === 'BLOCK' ? 'high' as const : 'medium' as const,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'review',
|
||||||
|
description: `Open the explain drawer for ${g.name} to inspect the rule chain.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
readonly blockingReasons = computed<readonly string[]>(() =>
|
readonly blockingReasons = computed<readonly string[]>(() =>
|
||||||
this.gateResults()
|
this.gateResults()
|
||||||
.filter((gate) => gate.state === 'BLOCK' || gate.state === 'WARN')
|
.filter((gate) => gate.state === 'BLOCK' || gate.state === 'WARN')
|
||||||
@@ -568,6 +634,11 @@ export class PolicyDecisioningGatesPageComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onRetryEvaluate(): void {
|
||||||
|
// Evaluation data is currently derived from the gate results.
|
||||||
|
// A retry would re-fetch gate data from the backend in a live implementation.
|
||||||
|
}
|
||||||
|
|
||||||
returnToSource(): void {
|
returnToSource(): void {
|
||||||
const returnTo = this.returnTo();
|
const returnTo = this.returnTo();
|
||||||
if (!returnTo) {
|
if (!returnTo) {
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
// SPDX-License-Identifier: BUSL-1.1
|
||||||
|
// Copyright (c) 2025 StellaOps
|
||||||
|
// Sprint: SPRINT_20260308_019_FE_orphan_policy_component_adoption
|
||||||
|
// Task: FE-OPC-004 - Verify policy-widget revival (PolicyPackEditor adoption)
|
||||||
|
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { PolicyEditorComponent } from './policy-editor.component';
|
||||||
|
import { MonacoLoaderService } from './monaco-loader.service';
|
||||||
|
import { PolicyApiService } from '../services/policy-api.service';
|
||||||
|
|
||||||
|
describe('PolicyEditorComponent (PolicyPackEditor adoption)', () => {
|
||||||
|
let component: PolicyEditorComponent;
|
||||||
|
let fixture: ComponentFixture<PolicyEditorComponent>;
|
||||||
|
|
||||||
|
const mockPack = {
|
||||||
|
id: 'test-pack-1',
|
||||||
|
name: 'Test Pack',
|
||||||
|
version: '1.0.0',
|
||||||
|
status: 'draft',
|
||||||
|
digest: 'sha256:abc123',
|
||||||
|
description: 'Test policy pack',
|
||||||
|
content: 'package test',
|
||||||
|
createdBy: 'admin',
|
||||||
|
modifiedAt: '2026-03-08T00:00:00Z',
|
||||||
|
tags: ['security', 'baseline'],
|
||||||
|
metadata: { author: 'admin', reviewers: ['reviewer1', 'reviewer2'] },
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PolicyEditorComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
snapshot: {
|
||||||
|
paramMap: { get: (key: string) => key === 'packId' ? 'test-pack-1' : null },
|
||||||
|
queryParamMap: { get: () => null },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: MonacoLoaderService,
|
||||||
|
useValue: {
|
||||||
|
load: () => new Promise(() => {
|
||||||
|
// Never resolves - we are testing the pack editor sidebar, not Monaco
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PolicyApiService,
|
||||||
|
useValue: {
|
||||||
|
getPack: () => of(mockPack),
|
||||||
|
lint: () => of({ valid: true, errors: [], warnings: [], info: [] }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(PolicyEditorComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should derive packDocument from loaded pack', () => {
|
||||||
|
expect((component as any).packDocument).toBeTruthy();
|
||||||
|
expect((component as any).packDocument.metadata.name).toBe('Test Pack');
|
||||||
|
expect((component as any).packDocument.metadata.version).toBe('1.0.0');
|
||||||
|
expect((component as any).packDocument.apiVersion).toBe('policy.stellaops.io/v2');
|
||||||
|
expect((component as any).packDocument.kind).toBe('PolicyPack');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render visual-pack-editor-host when packDocument exists', () => {
|
||||||
|
const el: HTMLElement = fixture.nativeElement;
|
||||||
|
const host = el.querySelector('[data-testid="visual-pack-editor-host"]');
|
||||||
|
expect(host).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render stella-policy-pack-editor element', () => {
|
||||||
|
const el: HTMLElement = fixture.nativeElement;
|
||||||
|
const editor = el.querySelector('stella-policy-pack-editor');
|
||||||
|
expect(editor).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update packDocument when onPackDocumentChanged is called', () => {
|
||||||
|
const updatedDoc = {
|
||||||
|
apiVersion: 'policy.stellaops.io/v2',
|
||||||
|
kind: 'PolicyPack',
|
||||||
|
metadata: { name: 'Updated Pack', version: '2.0.0' },
|
||||||
|
spec: {
|
||||||
|
settings: { default_action: 'block' as const },
|
||||||
|
gates: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
component.onPackDocumentChanged(updatedDoc);
|
||||||
|
expect((component as any).packDocument.metadata.name).toBe('Updated Pack');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -23,6 +23,10 @@ import {
|
|||||||
} from '../models/policy.models';
|
} from '../models/policy.models';
|
||||||
import { PolicyApiService } from '../services/policy-api.service';
|
import { PolicyApiService } from '../services/policy-api.service';
|
||||||
import { MonacoLoaderService } from './monaco-loader.service';
|
import { MonacoLoaderService } from './monaco-loader.service';
|
||||||
|
import {
|
||||||
|
PolicyPackEditorComponent,
|
||||||
|
} from '../../../shared/components/policy/policy-pack-editor.component';
|
||||||
|
import type { PolicyPackDocument } from '../../../core/api/policy-interop.models';
|
||||||
|
|
||||||
type MonacoNamespace = typeof import('monaco-editor');
|
type MonacoNamespace = typeof import('monaco-editor');
|
||||||
|
|
||||||
@@ -37,7 +41,7 @@ interface ChecklistItem {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-policy-editor',
|
selector: 'app-policy-editor',
|
||||||
imports: [CommonModule],
|
imports: [CommonModule, PolicyPackEditorComponent],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
<section class="policy-editor" [attr.aria-busy]="loadingPack">
|
<section class="policy-editor" [attr.aria-busy]="loadingPack">
|
||||||
@@ -165,6 +169,20 @@ interface ChecklistItem {
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- Visual gate configuration (shared PolicyPackEditorComponent) -->
|
||||||
|
@if (packDocument) {
|
||||||
|
<div class="sidebar-card" data-testid="visual-pack-editor-host">
|
||||||
|
<header class="sidebar-card__header">
|
||||||
|
<h3>Visual gate config</h3>
|
||||||
|
<p>Edit gates and settings without leaving the DSL editor.</p>
|
||||||
|
</header>
|
||||||
|
<stella-policy-pack-editor
|
||||||
|
[policyPack]="packDocument"
|
||||||
|
(policyChanged)="onPackDocumentChanged($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -547,6 +565,7 @@ export class PolicyEditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
private editorHost?: ElementRef<HTMLDivElement>;
|
private editorHost?: ElementRef<HTMLDivElement>;
|
||||||
|
|
||||||
protected pack?: PolicyPack;
|
protected pack?: PolicyPack;
|
||||||
|
protected packDocument?: PolicyPackDocument;
|
||||||
protected diagnostics: PolicyDiagnostic[] = [];
|
protected diagnostics: PolicyDiagnostic[] = [];
|
||||||
protected checklist: ChecklistItem[] = [];
|
protected checklist: ChecklistItem[] = [];
|
||||||
protected linting = false;
|
protected linting = false;
|
||||||
@@ -612,11 +631,21 @@ export class PolicyEditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.model.setValue(this.pack.content ?? '');
|
this.model.setValue(this.pack.content ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle visual pack editor changes.
|
||||||
|
* The shared PolicyPackEditorComponent emits updated documents
|
||||||
|
* when gates, settings, or rules are modified visually.
|
||||||
|
*/
|
||||||
|
onPackDocumentChanged(updatedDoc: PolicyPackDocument): void {
|
||||||
|
this.packDocument = updatedDoc;
|
||||||
|
}
|
||||||
|
|
||||||
private loadPack(packId: string, version?: string): void {
|
private loadPack(packId: string, version?: string): void {
|
||||||
this.loadingPack = true;
|
this.loadingPack = true;
|
||||||
this.policyApi.getPack(packId, version).subscribe({
|
this.policyApi.getPack(packId, version).subscribe({
|
||||||
next: (pack) => {
|
next: (pack) => {
|
||||||
this.pack = pack;
|
this.pack = pack;
|
||||||
|
this.packDocument = this.derivePackDocument(pack);
|
||||||
this.loadingPack = false;
|
this.loadingPack = false;
|
||||||
this.content$.next(pack.content ?? '');
|
this.content$.next(pack.content ?? '');
|
||||||
this.updateChecklist();
|
this.updateChecklist();
|
||||||
@@ -803,4 +832,34 @@ export class PolicyEditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
this.checklist = items;
|
this.checklist = items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive a PolicyPackDocument from the loaded PolicyPack so the shared
|
||||||
|
* visual gate editor can render. Returns undefined if the pack lacks
|
||||||
|
* enough metadata to populate the document model.
|
||||||
|
*/
|
||||||
|
private derivePackDocument(pack: PolicyPack): PolicyPackDocument | undefined {
|
||||||
|
if (!pack) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiVersion: 'policy.stellaops.io/v2',
|
||||||
|
kind: 'PolicyPack',
|
||||||
|
metadata: {
|
||||||
|
name: pack.name,
|
||||||
|
version: pack.version,
|
||||||
|
description: pack.description,
|
||||||
|
digest: pack.digest,
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
settings: {
|
||||||
|
default_action: 'allow',
|
||||||
|
deterministic_mode: true,
|
||||||
|
},
|
||||||
|
gates: [],
|
||||||
|
rules: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user