From f40043ed50f824b5a6aa3bc68d7500fb4692ad46 Mon Sep 17 00:00:00 2001 From: master <> Date: Sun, 8 Mar 2026 20:23:37 +0200 Subject: [PATCH] fix(web): remediate orphan revival regressions --- ...E_orphan_revival_regression_remediation.md | 111 ++++++ ...idence-thread-persona-workspaces-routes.md | 2 + .../checked/web/filter-bar-unification.md | 15 +- .../web/orphan-finding-list-consolidation.md | 4 + ...rphan-revival-regression-remediation-ui.md | 34 ++ docs/modules/ui/TASKS.md | 4 +- docs/modules/ui/implementation_plan.md | 8 +- .../modules/ui/orphan-revival-batch/README.md | 1 + .../audit-log-table.component.spec.ts | 112 +++--- .../audit-log/audit-log-table.component.ts | 177 ++++----- .../evidence-thread-list.component.ts | 2 +- .../evidence-thread-view.component.ts | 2 +- ...er-finding-list-adoption.component.spec.ts | 107 +----- .../container/findings-container.component.ts | 70 +--- ...il-finding-list-adoption.component.spec.ts | 211 ++--------- .../release-detail.component.ts | 75 +--- .../trust-admin/trust-audit-log.component.ts | 169 +++++---- .../policy/policy-evaluate-panel.component.ts | 2 +- .../policy/policy-pack-editor.component.ts | 4 +- .../policy/remediation-hint.component.ts | 2 +- .../directives/glossary-tooltip.directive.ts | 13 +- .../evidence-thread-browser.component.spec.ts | 43 +-- ...han-revival-regression-remediation.spec.ts | 338 ++++++++++++++++++ 23 files changed, 823 insertions(+), 683 deletions(-) create mode 100644 docs-archived/implplan/SPRINT_20260308_024_FE_orphan_revival_regression_remediation.md create mode 100644 docs/features/checked/web/orphan-revival-regression-remediation-ui.md create mode 100644 src/Web/StellaOps.Web/src/tests/orphan_revival/orphan-revival-regression-remediation.spec.ts diff --git a/docs-archived/implplan/SPRINT_20260308_024_FE_orphan_revival_regression_remediation.md b/docs-archived/implplan/SPRINT_20260308_024_FE_orphan_revival_regression_remediation.md new file mode 100644 index 000000000..11b2cc8f1 --- /dev/null +++ b/docs-archived/implplan/SPRINT_20260308_024_FE_orphan_revival_regression_remediation.md @@ -0,0 +1,111 @@ +# Sprint 20260308-024 - FE Orphan Revival Regression Remediation + +## Topic & Scope +- Repair the concrete regressions found in review across the orphan-revival implementation batch before any more revival work proceeds. +- Fix the frontend build break, restore canonical evidence-thread navigation, restore lost audit and trust filtering capabilities, and remove fabricated finding evidence from mounted shells. +- Keep the remediation bounded to the shipped frontend and its verification/docs; do not reopen unrelated orphan candidates in this sprint. +- 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/shared/directives/`, `src/Web/StellaOps.Web/src/app/shared/ui/filter-bar/`, `src/Web/StellaOps.Web/src/app/routes/evidence.routes.ts`, `src/Web/StellaOps.Web/src/app/features/evidence-thread/`, `src/Web/StellaOps.Web/src/app/features/audit-log/`, `src/Web/StellaOps.Web/src/app/features/trust-admin/`, `src/Web/StellaOps.Web/src/app/features/findings/`, and `src/Web/StellaOps.Web/src/app/features/release-orchestrator/`. +- Expected evidence: green Angular build, focused frontend tests for each repaired area, one checked-feature note, and sprint execution-log updates. + +## Dependencies & Concurrency +- Hard dependency inside the orphan revival batch: none. +- External prerequisite already satisfied: the affected orphan-revival commits are already landed on `main` and are the baseline for this remediation. +- Safe parallelism: + - Do not staff this sprint in parallel with new orphan-revival implementation work on the same files. + - This sprint owns remediation of the reviewed regressions across sprints `015`, `019`, `020`, and `021`. + +## Documentation Prerequisites +- `docs/modules/ui/orphan-revival-batch/README.md` +- `docs/modules/ui/implementation_plan.md` +- `src/Web/StellaOps.Web/AGENTS.md` +- `src/Web/StellaOps.Web/src/app/shared/ui/filter-bar/filter-bar.component.ts` +- `src/Web/StellaOps.Web/src/app/features/evidence-thread/evidence-thread.routes.ts` +- `src/Web/StellaOps.Web/src/app/features/findings/container/findings-container.component.ts` +- `src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/release-detail/release-detail.component.ts` + +## Delivery Tracker + +### FE-ORM-001 - Clear build blockers in revived shared policy and glossary code +Status: DONE +Dependency: none +Owners: Developer (FE) +Task description: +- Repair the broken shared policy-widget imports and strict TypeScript issues introduced by the orphan revival batch so the Angular application builds again. +- Keep the fix minimal and bounded to the revived components and directive; do not redesign the widgets in this task. + +Completion criteria: +- [x] `npm run build` succeeds in `src/Web/StellaOps.Web`. +- [x] Shared policy widget imports resolve correctly. +- [x] The glossary directive no longer violates strict-null or typing rules. + +### FE-ORM-002 - Restore canonical evidence-thread navigation behavior +Status: DONE +Dependency: FE-ORM-001 +Owners: Developer (FE) +Task description: +- Repair the evidence-thread list/detail navigation so the reconnected `/evidence/threads/*` URLs are used consistently by row-click, back navigation, and any route-focused regression coverage. + +Completion criteria: +- [x] Evidence-thread list rows navigate to `/evidence/threads/:artifactDigest`. +- [x] Evidence-thread detail back navigation returns to `/evidence/threads`. +- [x] Focused route tests cover the repaired canonical URLs. + +### FE-ORM-003 - Restore full mounted audit and trust filter semantics +Status: DONE +Dependency: FE-ORM-001 +Owners: Developer (FE), UX +Task description: +- Restore the filter capabilities lost during the shared filter-bar migration on mounted audit pages, including actor filtering, custom date support, and any multi-select semantics that the pages previously exposed. +- The end result may extend the shared filter bar or use bounded page-local controls, but the mounted pages must regain their previously available filtering behavior. + +Completion criteria: +- [x] Audit log page supports actor filtering, custom date ranges, and multi-value module/action/severity filters again. +- [x] Trust audit log page supports start and end date filtering again. +- [x] Focused tests assert the restored filter-state behavior and request-shape wiring. + +### FE-ORM-004 - Remove fabricated finding evidence from revived mounted consumers +Status: DONE +Dependency: FE-ORM-001 +Owners: Developer (FE), Product Manager +Task description: +- Eliminate fabricated timestamps, synthetic scores, and invented PURLs from the revived findings and release-security consumers. +- If the shared finding-list contract cannot be satisfied truthfully for a mounted host, back that host out to its prior truthful presentation instead of synthesizing data. + +Completion criteria: +- [x] Mounted findings and release-security surfaces no longer fabricate `last_seen`, `risk_score`, or package identity fields. +- [x] Any remaining revived shared finding-list adoption is backed by truthful source data. +- [x] Focused tests assert truthful rendering or the bounded rollback decision for the affected hosts. + +### FE-ORM-005 - Verify, document, and sync the remediation +Status: DONE +Dependency: FE-ORM-002 +Owners: Test Automation, Documentation author +Task description: +- Run the focused build and test matrix for the repaired areas, record the outcomes, and sync the UI plan/docs with the remediation result. + +Completion criteria: +- [x] Focused frontend tests cover policy build blockers, evidence-thread routing, audit/trust filtering, and findings/release-security behavior. +- [x] Checked-feature note exists under `docs/features/checked/web/`. +- [x] UI plan/task docs reflect the remediation status and any bounded rollback decisions. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-03-08 | Sprint created to remediate the concrete regressions found during review of orphan-revival sprints 015, 019, 020, and 021. | Project Manager | +| 2026-03-08 | Repaired build blockers in revived policy widgets and glossary tooltip directive so the Angular production build compiles again. | Developer (FE) | +| 2026-03-08 | Repaired evidence-thread row and back navigation to use canonical `/evidence/threads/*` URLs. | Developer (FE) | +| 2026-03-08 | Restored full mounted audit and trust filter semantics by rolling the audit-log and trust-audit pages back to truthful page-local controls where the shared filter bar could not preserve behavior. | Developer (FE) | +| 2026-03-08 | Removed fabricated finding evidence from mounted findings and release-security hosts by rolling those consumers back to their truthful bespoke list/table rendering. | Developer (FE) | +| 2026-03-08 | Verified remediation with `npm run build` and focused Angular tests covering policy hosts, evidence-thread routing, audit/trust filtering, and truthful findings/release rendering. | Test Automation | + +## Decisions & Risks +- Decision: this sprint prioritizes truthful mounted behavior over preserving every individual orphan adoption. If a revived component forces fabricated data, the mounted host may be rolled back to its prior truthful UI. +- Decision: `audit-log-table` and `trust-audit-log` keep page-local filter controls for now; the shared `FilterBarComponent` remains adopted only where it does not remove operator capabilities. +- Decision: the shared `FindingListComponent` is no longer used on mounted findings and release-security hosts until a truthful data contract exists for those surfaces. +- Risk: older checked-feature notes for orphan revival sprints can drift from current shipped behavior after bounded rollbacks. +- Mitigation: record the remediation in a dedicated checked-feature note and sync the UI plan plus orphan-revival batch docs with the rollback decisions. + +## Next Checkpoints +- 2026-03-08: build blockers fixed and verified. +- 2026-03-09: routing, filter, and findings regressions resolved with focused test evidence. diff --git a/docs/features/checked/web/evidence-thread-persona-workspaces-routes.md b/docs/features/checked/web/evidence-thread-persona-workspaces-routes.md index 919f41f91..cf0f74ca4 100644 --- a/docs/features/checked/web/evidence-thread-persona-workspaces-routes.md +++ b/docs/features/checked/web/evidence-thread-persona-workspaces-routes.md @@ -12,6 +12,8 @@ SPRINT_20260308_021_FE_unreachable_evidence_thread_and_persona_workspaces_routes ## Description Reconnected the disconnected evidence-thread and persona-workspace route families under the canonical `/evidence` route shell. Evidence threads and auditor/developer workspaces are now reachable through Evidence-owned URLs, acting as evidence lenses rather than a parallel product shell. +Sprint `SPRINT_20260308_024_FE_orphan_revival_regression_remediation.md` completed the cutover by fixing evidence-thread list-row and back-button navigation to use the same canonical `/evidence/threads/*` URLs instead of the dead legacy `/evidence-thread` frontdoor. + ## Canonical URL Contract - `/evidence/threads` - Evidence thread list - `/evidence/threads/:artifactDigest` - Evidence thread detail diff --git a/docs/features/checked/web/filter-bar-unification.md b/docs/features/checked/web/filter-bar-unification.md index 5116b33a6..a953ba29d 100644 --- a/docs/features/checked/web/filter-bar-unification.md +++ b/docs/features/checked/web/filter-bar-unification.md @@ -4,9 +4,13 @@ Sprint: `SPRINT_20260308_015_FE_orphan_filter_bar_unification.md` Feature IDs: FE-OFB-001 through FE-OFB-004 Status: Shipped +## Post-remediation note + +Sprint `SPRINT_20260308_024_FE_orphan_revival_regression_remediation.md` partially rolled back this adoption on mounted audit hosts. `audit-log-table` and `trust-audit-log` no longer use the shared filter bar because that migration removed actor, multi-select, and date-filter behavior. The shared bar remains mounted only on the adopters where it preserves real page semantics. + ## What shipped -The dormant shared `FilterBarComponent` (`src/Web/StellaOps.Web/src/app/shared/ui/filter-bar/filter-bar.component.ts`) has been adopted across all seven eligible mounted list pages. Each page's bespoke filter toolbar markup has been replaced by a single `` element that renders search input, single-select filter dropdowns, active-filter chips, and a clear-all button through the shared component's existing API. +The dormant shared `FilterBarComponent` (`src/Web/StellaOps.Web/src/app/shared/ui/filter-bar/filter-bar.component.ts`) was adopted across seven mounted list pages in the original sprint. After remediation, five mounted pages still use the shared bar; the two audit-family pages listed below reverted to page-local controls to preserve truthful operator workflows. ## Shared contract (unchanged) @@ -26,7 +30,7 @@ The `FilterBarComponent` API was sufficient without extension: | Page | File | Filter options exposed | | --- | --- | --- | -| Audit Log Table | `features/audit-log/audit-log-table.component.ts` | module, action, severity, dateRange | +| Audit Log Table | `features/audit-log/audit-log-table.component.ts` | rolled back in sprint `024`; page-local filters restored | | Secret Findings List | `features/secret-detection/secret-findings-list.component.ts` | severity, status, category | | Console Admin Audit Log | `features/console-admin/audit/audit-log.component.ts` | eventType (16 event types) | @@ -36,7 +40,7 @@ The `FilterBarComponent` API was sufficient without extension: | --- | --- | --- | | Release List | `features/release-orchestrator/releases/release-list/release-list.component.ts` | type, stage, gate, risk, blocked, needsApproval, hotfixLane, replayMismatch | | Evidence Pack List | `features/evidence-pack/evidence-pack-list.component.ts` | (search only, no dropdowns) | -| Trust Audit Log | `features/trust-admin/trust-audit-log.component.ts` | resourceType, severity | +| Trust Audit Log | `features/trust-admin/trust-audit-log.component.ts` | rolled back in sprint `024`; start/end date filters restored via page-local controls | | Certificate Inventory | `features/trust-admin/certificate-inventory.component.ts` | status, type | ## Exclusions @@ -46,10 +50,9 @@ The `FilterBarComponent` API was sufficient without extension: ## Design decisions -- Multi-select dropdowns on the audit-log-table were simplified to single-select to match the shared bar's existing contract. The underlying filter arrays still work but now hold at most one value. -- Date-range inputs on the audit-log-table were mapped to preset dropdown options (24h, 7d, 30d, 90d). -- Date inputs on trust-admin pages were removed from the filter bar; dates remain in component state for API queries but are not exposed through the shared bar. +- Audit-log and trust-audit pages were reverted to bespoke controls because the shared bar could not preserve their real semantics. - Query-state persistence on the release-list via `buildQueryParams()` and route subscription remains intact. +- Future shared filter-bar expansion must prove parity before replacing mounted audit-family pages again. ## Test coverage diff --git a/docs/features/checked/web/orphan-finding-list-consolidation.md b/docs/features/checked/web/orphan-finding-list-consolidation.md index 113d68836..b7fe32b29 100644 --- a/docs/features/checked/web/orphan-finding-list-consolidation.md +++ b/docs/features/checked/web/orphan-finding-list-consolidation.md @@ -2,6 +2,10 @@ Sprint: SPRINT_20260308_020_FE_orphan_finding_list_consolidation +## Post-remediation note + +Sprint `SPRINT_20260308_024_FE_orphan_revival_regression_remediation.md` rolled back both mounted adopters described below. The shared `FindingListComponent` is no longer mounted on `FindingsContainerComponent` or the `ReleaseDetailComponent` security tab because those hosts required fabricated timestamps, severity-derived risk scores, and invented package identities to satisfy the shared contract. + ## Summary Revived the dormant shared `FindingListComponent` and `FindingRowComponent` by adopting them on two mounted surfaces that previously used bespoke finding list rendering: diff --git a/docs/features/checked/web/orphan-revival-regression-remediation-ui.md b/docs/features/checked/web/orphan-revival-regression-remediation-ui.md new file mode 100644 index 000000000..b75613c81 --- /dev/null +++ b/docs/features/checked/web/orphan-revival-regression-remediation-ui.md @@ -0,0 +1,34 @@ +# Orphan Revival Regression Remediation + +Sprint: `SPRINT_20260308_024_FE_orphan_revival_regression_remediation.md` +Status: Shipped + +## What changed + +This remediation sprint closed the concrete regressions introduced by the orphan-revival batch and kept mounted behavior truthful where the revived shared components did not fit the live hosts. + +- Revived shared policy widgets build again: broken `policy-interop.models` imports were corrected in the shared policy components, and the glossary tooltip directive was fixed for strict-null and typing compliance. +- Evidence-thread navigation now stays inside the canonical Evidence shell. Row click and back navigation both use `/evidence/threads` instead of dead `/evidence-thread` paths. +- Audit and trust filtering capabilities were restored on mounted pages. `audit-log-table` and `trust-audit-log` were intentionally rolled back to page-local controls so actor filters, multi-select filters, and start/end date filters remain usable. +- Fabricated finding evidence was removed from mounted consumers. `FindingsContainerComponent` and the `ReleaseDetailComponent` security tab no longer synthesize `last_seen`, `risk_score`, or fake package identities just to satisfy the shared `FindingListComponent` contract. + +## Bounded rollback decisions + +- `FilterBarComponent` remains valid, but not for every list page. Audit-log and trust-audit hosts keep their bespoke filter controls until the shared bar can support their real semantics without loss. +- `FindingListComponent` remains available for future truthful adopters, but it is no longer mounted on findings and release-security hosts that would require invented data. + +## Verification + +- `npm run build` + - Result: passed + - Notes: only the existing Angular bundle-budget warnings remained +- `npm test -- --watch=false --include src/tests/orphan_revival/orphan-revival-regression-remediation.spec.ts --include src/tests/evidence/evidence-thread-browser.component.spec.ts --include src/tests/releases/release-detail.live-refresh.spec.ts --include src/app/routes/evidence.routes.spec.ts` + - Result: passed + - Coverage: policy host rendering, evidence-thread canonical routes, audit/trust filter semantics, truthful findings rendering, release-security rendering, and evidence route declarations + +## Current shipped state + +- Canonical evidence-thread routes are usable and internally consistent. +- Shared policy widgets stay mounted on the active policy surfaces without breaking the app build. +- Audit and trust pages retain their full filtering workflows. +- Findings and release-security views favor truthful bespoke rendering over forced shared-component reuse. diff --git a/docs/modules/ui/TASKS.md b/docs/modules/ui/TASKS.md index 449b8075d..d3a0ff71d 100644 --- a/docs/modules/ui/TASKS.md +++ b/docs/modules/ui/TASKS.md @@ -1,9 +1,9 @@ # UI Task Board ## Active Sprint Links -- None currently. Completed UI delivery and verification sprints are archived under `docs-archived/implplan/`. - [DONE] `docs/implplan/SPRINT_20260308_014_FE_orphan_copy_inline_truncate_adoption.md` - CopyToClipboard, InlineCode, TruncatePipe adoption on console-admin, offline-kit, and triage replay-command surfaces. -- [DONE] `docs/implplan/SPRINT_20260308_015_FE_orphan_filter_bar_unification.md` - FilterBarComponent adoption on 7 mounted list pages (audit-log-table, secret-findings-list, console-admin audit-log, release-list, evidence-pack-list, trust-audit-log, certificate-inventory). +- [DONE] `docs/implplan/SPRINT_20260308_015_FE_orphan_filter_bar_unification.md` - Initial FilterBarComponent adoption batch; audit-log-table and trust-audit-log were later rolled back in sprint `024` to restore lost semantics. +- [DONE] `docs-archived/implplan/SPRINT_20260308_024_FE_orphan_revival_regression_remediation.md` - Fixed reviewed orphan-revival regressions: build blockers cleared, canonical evidence-thread navigation restored, audit/trust filter capabilities restored, and fabricated finding evidence removed from mounted hosts. ## Queued Sprint Links - `docs/modules/ui/orphan-revival-batch/README.md` - review index for the orphan shared-component and disconnected-route revival batch. diff --git a/docs/modules/ui/implementation_plan.md b/docs/modules/ui/implementation_plan.md index 954f3809e..2aceb8e65 100644 --- a/docs/modules/ui/implementation_plan.md +++ b/docs/modules/ui/implementation_plan.md @@ -6,13 +6,16 @@ Provide a living plan for UI deliverables, dependencies, and evidence. ## Active work - Track current sprints under `docs/implplan/SPRINT_*.md` for this module. - Update this file when new scoped work is approved. +- No active UI remediation sprint is open right now. ## Near-term deliverables - No active UI deliverables are currently staged in `docs/implplan`. - The next queued batch is `docs/modules/ui/orphan-revival-batch/README.md`, which stages independent review-ready sprints for orphan shared-component adoption and disconnected-route integration. - The queued orphan batch currently spans `SPRINT_20260308_013` through `SPRINT_20260308_023` and is intentionally not marked active until product review approves staffing. - Sprint `014` (CopyToClipboard, InlineCode, TruncatePipe adoption) is DONE. See `docs/features/checked/web/orphan-copy-inline-truncate-adoption.md`. -- Sprint `015` (FilterBarComponent adoption on 7 mounted list pages) is DONE. See `docs/features/checked/web/filter-bar-unification.md`. +- Sprint `015` (FilterBarComponent adoption) shipped, then was partially rolled back on audit-family pages to restore lost filter semantics. See `docs/features/checked/web/filter-bar-unification.md` and `docs/features/checked/web/orphan-revival-regression-remediation-ui.md`. +- Sprint `020` (FindingListComponent consolidation) shipped, then was rolled back on mounted findings and release-security hosts because the shared contract required fabricated data. See `docs/features/checked/web/orphan-finding-list-consolidation.md` and `docs/features/checked/web/orphan-revival-regression-remediation-ui.md`. +- Sprint `021` (Evidence thread and persona workspace reconnection) shipped, and its internal thread navigation was completed in the remediation sprint. See `docs/features/checked/web/evidence-thread-persona-workspaces-routes.md` and `docs/features/checked/web/orphan-revival-regression-remediation-ui.md`. ## Latest evidence - `docs/modules/ui/component-preservation-map/README.md` - root index for the first-pass preservation map. @@ -54,7 +57,8 @@ Provide a living plan for UI deliverables, dependencies, and evidence. - `docs/modules/ui/contextual-actions-patterns/README.md` - shared placement contract for stray actions, pages, drawers, and tabs. - `docs/modules/ui/unified-audit-surfaces/README.md` - shipped canonical audit owner, alias contract, and secondary entry-point rules for cross-module audit browsing. - `docs/modules/ui/orphan-revival-batch/README.md` - queued execution batch for reviving selected orphan shared components and reconnecting disconnected route families without reopening duplicate top-level products. -- `docs/features/checked/web/filter-bar-unification.md` - shipped verification note for the shared FilterBarComponent adoption on 7 mounted list pages (audit, security, release, evidence, trust families). +- `docs/features/checked/web/filter-bar-unification.md` - shipped verification note for the shared FilterBarComponent adoption, now superseded on audit-family pages by the remediation rollback that restored missing filter semantics. +- `docs/features/checked/web/orphan-revival-regression-remediation-ui.md` - shipped verification note for the orphan-revival regression remediation, including the audit/trust filter rollback, truthful findings/release rollback, policy build fixes, and canonical evidence-thread navigation repair. ## Dependencies - `docs/modules/ui/architecture.md` diff --git a/docs/modules/ui/orphan-revival-batch/README.md b/docs/modules/ui/orphan-revival-batch/README.md index 82b499e47..cf1085c32 100644 --- a/docs/modules/ui/orphan-revival-batch/README.md +++ b/docs/modules/ui/orphan-revival-batch/README.md @@ -9,6 +9,7 @@ - The preservation-map and restoration-topic work already resolved the major product-shape questions for Policy Decisioning Studio, Reachability Witnessing, Workflow Visualization, Watchlist, Triage Explainability, and the consolidated Operations and Setup shells. - This batch covers the remaining lower-level orphaned shared components and disconnected route files that still look worth reviving after those larger product merges landed. - These sprints are intentionally queued for review. They are not started by default. +- Follow-up remediation sprint `024` already proved that some orphan adoptions need bounded rollback when the shared contract removes mounted behavior or forces fabricated data. Treat sprint `015` and sprint `020` as cautionary examples, not as proof that every orphan shared component should stay mounted wherever it first lands. ## Corrections To The External Scan - `EvidenceDrawerComponent` is already mounted in `features/vulnerabilities/vulnerability-detail.component.html`; it is not a valid "finish the wiring" target in the current repo snapshot. diff --git a/src/Web/StellaOps.Web/src/app/features/audit-log/audit-log-table.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/audit-log/audit-log-table.component.spec.ts index bb7946659..d573b2656 100644 --- a/src/Web/StellaOps.Web/src/app/features/audit-log/audit-log-table.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/features/audit-log/audit-log-table.component.spec.ts @@ -1,7 +1,6 @@ /** * @file audit-log-table.component.spec.ts - * @sprint SPRINT_20260308_015_FE (FE-OFB-004) - * @description Focused tests verifying FilterBarComponent adoption on the audit-log-table page. + * @description Unit tests for AuditLogTableComponent filter behavior. */ import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -11,7 +10,7 @@ import { AuditLogTableComponent } from './audit-log-table.component'; import { AuditLogClient } from '../../core/api/audit-log.client'; import { AuditEventsPagedResponse } from '../../core/api/audit-log.models'; -describe('AuditLogTableComponent (filter-bar adoption)', () => { +describe('AuditLogTableComponent', () => { let component: AuditLogTableComponent; let fixture: ComponentFixture; @@ -44,72 +43,79 @@ describe('AuditLogTableComponent (filter-bar adoption)', () => { expect(component).toBeTruthy(); }); - it('should render the shared filter bar', () => { - const filterBar = fixture.nativeElement.querySelector('app-filter-bar'); - expect(filterBar).toBeTruthy(); + it('renders the audit-specific filter toolbar with actor search', () => { + const element: HTMLElement = fixture.nativeElement; + expect(element.querySelector('.filters-bar')).toBeTruthy(); + expect(element.textContent).toContain('Actor'); + expect(element.querySelector('app-filter-bar')).toBeFalsy(); }); - it('should expose four filter option groups (module, action, severity, dateRange)', () => { - expect(component.filterOptions.length).toBe(4); - expect(component.filterOptions.map(f => f.key)).toEqual(['module', 'action', 'severity', 'dateRange']); + it('builds filters with multi-select arrays and actor name', () => { + component.selectedModules = ['policy', 'scanner']; + component.selectedActions = ['approve', 'replay']; + component.selectedSeverities = ['warning', 'critical']; + component.actorFilter = 'ops@stella.local'; + + expect(component.buildFilters()).toEqual( + jasmine.objectContaining({ + modules: ['policy', 'scanner'], + actions: ['approve', 'replay'], + severities: ['warning', 'critical'], + actorName: 'ops@stella.local', + }), + ); }); - it('should populate module options from allModules array', () => { - const moduleFilter = component.filterOptions.find(f => f.key === 'module'); - expect(moduleFilter).toBeTruthy(); - expect(moduleFilter!.options.length).toBe(component.allModules.length); - expect(moduleFilter!.options[0].value).toBe('authority'); + it('builds custom start and end dates when custom range is selected', () => { + component.dateRange = 'custom'; + component.customStartDate = '2026-03-01'; + component.customEndDate = '2026-03-07'; + + expect(component.buildFilters()).toEqual( + jasmine.objectContaining({ + startDate: '2026-03-01', + endDate: '2026-03-07', + }), + ); }); - it('should update selectedModules on onFilterChanged with module key', () => { - component.onFilterChanged({ key: 'module', value: 'policy', label: 'Policy' }); - expect(component.selectedModules).toEqual(['policy']); + it('resets paging state when applyFilters is called', () => { + component.selectedModules = ['scanner']; + component.cursor.set('cursor-1'); + (component as { cursorStack: string[] }).cursorStack = ['cursor-0']; + component.hasPrev.set(true); + + component.applyFilters(); + + expect(component.cursor()).toBeNull(); + expect(component.hasPrev()).toBeFalse(); + expect(mockClient.getEvents).toHaveBeenCalled(); }); - it('should clear selectedModules on onFilterRemoved with module key', () => { - component.selectedModules = ['policy']; - component.onFilterRemoved({ key: 'module', value: 'policy', label: 'Module: Policy' }); - expect(component.selectedModules).toEqual([]); - }); + it('clears all restored filters', () => { + component.selectedModules = ['scanner']; + component.selectedActions = ['replay']; + component.selectedSeverities = ['error']; + component.dateRange = 'custom'; + component.customStartDate = '2026-03-01'; + component.customEndDate = '2026-03-07'; + component.searchQuery = 'digest'; + component.actorFilter = 'operator'; - it('should update dateRange on onFilterChanged with dateRange key', () => { - component.onFilterChanged({ key: 'dateRange', value: '30d', label: 'Last 30 days' }); - expect(component.dateRange).toBe('30d'); - }); - - it('should reset dateRange to 7d on onFilterRemoved with dateRange key', () => { - component.dateRange = '30d'; - component.onFilterRemoved({ key: 'dateRange', value: '30d', label: 'Date: Last 30 days' }); - expect(component.dateRange).toBe('7d'); - }); - - it('should update searchQuery on onSearchChange', () => { - component.onSearchChange('test keyword'); - expect(component.searchQuery).toBe('test keyword'); - }); - - it('should rebuild active filter list when a filter is applied', () => { - component.onFilterChanged({ key: 'severity', value: 'critical', label: 'Critical' }); - const active = component.activeFilterList(); - expect(active.length).toBeGreaterThanOrEqual(1); - expect(active.find(f => f.key === 'severity')).toBeTruthy(); - }); - - it('should clear all filters and reset active filter list', () => { - component.onFilterChanged({ key: 'module', value: 'scanner', label: 'Scanner' }); - component.onFilterChanged({ key: 'severity', value: 'error', label: 'Error' }); component.clearFilters(); + expect(component.selectedModules.length).toBe(0); expect(component.selectedActions.length).toBe(0); expect(component.selectedSeverities.length).toBe(0); expect(component.dateRange).toBe('7d'); expect(component.searchQuery).toBe(''); - expect(component.activeFilterList().length).toBe(0); + expect(component.actorFilter).toBe(''); + expect(component.customStartDate).toBe(''); + expect(component.customEndDate).toBe(''); }); - it('should not include dateRange chip when dateRange is 7d (default)', () => { - component.onFilterChanged({ key: 'module', value: 'vex', label: 'VEX' }); - const active = component.activeFilterList(); - expect(active.find(f => f.key === 'dateRange')).toBeFalsy(); + it('formats modules for the multi-select options', () => { + expect(component.formatModule('authority')).toBe('Authority'); + expect(component.formatModule('jobengine')).toBe('JobEngine'); }); }); diff --git a/src/Web/StellaOps.Web/src/app/features/audit-log/audit-log-table.component.ts b/src/Web/StellaOps.Web/src/app/features/audit-log/audit-log-table.component.ts index ab9d11f91..b8a67d397 100644 --- a/src/Web/StellaOps.Web/src/app/features/audit-log/audit-log-table.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/audit-log/audit-log-table.component.ts @@ -1,16 +1,14 @@ // Sprint: SPRINT_20251229_028_FE - Unified Audit Log Viewer -// Filter bar adoption: SPRINT_20260308_015_FE (FE-OFB-002) import { Component, OnInit, inject, signal, ChangeDetectionStrategy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { FormsModule } from '@angular/forms'; import { AuditLogClient } from '../../core/api/audit-log.client'; import { AuditEvent, AuditLogFilters, AuditModule, AuditAction, AuditSeverity } from '../../core/api/audit-log.models'; -import { FilterBarComponent, FilterOption, ActiveFilter } from '../../shared/ui/filter-bar/filter-bar.component'; @Component({ selector: 'app-audit-log-table', - imports: [CommonModule, RouterModule, FormsModule, FilterBarComponent], + imports: [CommonModule, RouterModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
@@ -21,15 +19,66 @@ import { FilterBarComponent, FilterOption, ActiveFilter } from '../../shared/ui/

Audit Events

- +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ @if (dateRange === 'custom') { +
+ + +
+
+ + +
+ } +
+
+
+ + + +
+
+ + +
+ +
+
@if (loading()) {
Loading events...
@@ -194,7 +243,18 @@ import { FilterBarComponent, FilterOption, ActiveFilter } from '../../shared/ui/ .breadcrumb { font-size: 0.85rem; color: var(--color-text-secondary); margin-bottom: 0.5rem; } .breadcrumb a { color: var(--color-brand-primary); text-decoration: none; } .page-header h1 { margin: 0; } - /* Filter bar styles handled by shared FilterBarComponent */ + .filters-bar { background: var(--color-surface-primary); border: 1px solid var(--color-border-primary); border-radius: var(--radius-lg); padding: 1rem; margin-bottom: 1.5rem; } + .filter-row { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.75rem; } + .filter-row:last-child { margin-bottom: 0; } + .filter-group { display: flex; flex-direction: column; gap: 0.25rem; } + .filter-group label { font-size: 0.75rem; color: var(--color-text-secondary); } + .filter-group select, .filter-group input { padding: 0.5rem; border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); font-size: 0.9rem; } + .filter-group select[multiple] { height: 80px; } + .search-group { flex: 1; min-width: 200px; flex-direction: row; align-items: flex-end; } + .search-group label { display: none; } + .search-group input { flex: 1; } + .btn-sm { padding: 0.5rem 0.75rem; cursor: pointer; background: var(--color-brand-primary); color: var(--color-text-heading); border: none; border-radius: var(--radius-sm); } + .btn-secondary { padding: 0.5rem 1rem; cursor: pointer; background: var(--color-surface-elevated); border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); align-self: flex-end; } .loading { text-align: center; padding: 3rem; color: var(--color-text-secondary); } .events-table { width: 100%; border-collapse: collapse; background: var(--color-surface-primary); border: 1px solid var(--color-border-primary); border-radius: var(--radius-lg); overflow: hidden; } .events-table th, .events-table td { padding: 0.75rem 0.5rem; text-align: left; border-bottom: 1px solid var(--color-border-primary); font-size: 0.85rem; } @@ -286,37 +346,6 @@ export class AuditLogTableComponent implements OnInit { readonly allActions: AuditAction[] = ['create', 'update', 'delete', 'promote', 'demote', 'revoke', 'issue', 'refresh', 'test', 'fail', 'complete', 'start', 'submit', 'approve', 'reject', 'sign', 'verify', 'rotate', 'enable', 'disable', 'deadletter', 'replay']; readonly allSeverities: AuditSeverity[] = ['info', 'warning', 'error', 'critical']; - // Shared filter bar options - readonly filterOptions: FilterOption[] = [ - { - key: 'module', - label: 'Module', - options: this.allModules.map(m => ({ value: m, label: this.formatModule(m) })), - }, - { - key: 'action', - label: 'Action', - options: this.allActions.map(a => ({ value: a, label: a })), - }, - { - key: 'severity', - label: 'Severity', - options: this.allSeverities.map(s => ({ value: s, label: s.charAt(0).toUpperCase() + s.slice(1) })), - }, - { - key: 'dateRange', - label: 'Date Range', - options: [ - { value: '24h', label: 'Last 24 hours' }, - { value: '7d', label: 'Last 7 days' }, - { value: '30d', label: 'Last 30 days' }, - { value: '90d', label: 'Last 90 days' }, - ], - }, - ]; - - readonly activeFilterList = signal([]); - ngOnInit(): void { this.loadEvents(); } @@ -365,7 +394,6 @@ export class AuditLogTableComponent implements OnInit { this.cursor.set(null); this.cursorStack = []; this.hasPrev.set(false); - this.rebuildActiveFilters(); this.loadEvents(); } @@ -381,65 +409,6 @@ export class AuditLogTableComponent implements OnInit { this.applyFilters(); } - onSearchChange(value: string): void { - this.searchQuery = value; - this.applyFilters(); - } - - onFilterChanged(filter: ActiveFilter): void { - switch (filter.key) { - case 'module': - this.selectedModules = [filter.value as AuditModule]; - break; - case 'action': - this.selectedActions = [filter.value as AuditAction]; - break; - case 'severity': - this.selectedSeverities = [filter.value as AuditSeverity]; - break; - case 'dateRange': - this.dateRange = filter.value; - break; - } - this.applyFilters(); - } - - onFilterRemoved(filter: ActiveFilter): void { - switch (filter.key) { - case 'module': - this.selectedModules = []; - break; - case 'action': - this.selectedActions = []; - break; - case 'severity': - this.selectedSeverities = []; - break; - case 'dateRange': - this.dateRange = '7d'; - break; - } - this.applyFilters(); - } - - private rebuildActiveFilters(): void { - const filters: ActiveFilter[] = []; - if (this.selectedModules.length > 0) { - filters.push({ key: 'module', value: this.selectedModules[0], label: 'Module: ' + this.formatModule(this.selectedModules[0]) }); - } - if (this.selectedActions.length > 0) { - filters.push({ key: 'action', value: this.selectedActions[0], label: 'Action: ' + this.selectedActions[0] }); - } - if (this.selectedSeverities.length > 0) { - filters.push({ key: 'severity', value: this.selectedSeverities[0], label: 'Severity: ' + this.selectedSeverities[0] }); - } - if (this.dateRange !== '7d') { - const labels: Record = { '24h': 'Last 24 hours', '30d': 'Last 30 days', '90d': 'Last 90 days' }; - filters.push({ key: 'dateRange', value: this.dateRange, label: 'Date: ' + (labels[this.dateRange] || this.dateRange) }); - } - this.activeFilterList.set(filters); - } - nextPage(): void { if (this.cursor()) { this.cursorStack.push(this.cursor()!); diff --git a/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-list/evidence-thread-list.component.ts b/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-list/evidence-thread-list.component.ts index 798288e27..397ea0438 100644 --- a/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-list/evidence-thread-list.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-list/evidence-thread-list.component.ts @@ -156,7 +156,7 @@ export class EvidenceThreadListComponent implements OnInit { onRowClick(thread: EvidenceThread): void { const encodedDigest = encodeURIComponent(thread.artifactDigest); - this.router.navigate(['/evidence-thread', encodedDigest]); + this.router.navigate(['/evidence/threads', encodedDigest]); } onRefresh(): void { diff --git a/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-view/evidence-thread-view.component.ts b/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-view/evidence-thread-view.component.ts index 8406416d8..10a894fc1 100644 --- a/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-view/evidence-thread-view.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/evidence-thread/components/evidence-thread-view/evidence-thread-view.component.ts @@ -158,7 +158,7 @@ export class EvidenceThreadViewComponent implements OnInit, OnDestroy { } onBack(): void { - this.router.navigate(['/evidence-thread']); + this.router.navigate(['/evidence/threads']); } getVerdictLabel(verdict?: EvidenceVerdict): string { diff --git a/src/Web/StellaOps.Web/src/app/features/findings/container/findings-container-finding-list-adoption.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/findings/container/findings-container-finding-list-adoption.component.spec.ts index 2e733e71b..87b43b694 100644 --- a/src/Web/StellaOps.Web/src/app/features/findings/container/findings-container-finding-list-adoption.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/features/findings/container/findings-container-finding-list-adoption.component.spec.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 // Copyright (c) 2025 StellaOps -// Sprint: SPRINT_20260308_020_FE_orphan_finding_list_consolidation -// Task: FE-OFL-004 - Verify shared FindingListComponent adoption on FindingsContainerComponent +// Sprint: SPRINT_20260308_024_FE_orphan_revival_regression_remediation +// Task: FE-ORM-004 - Verify truthful findings-container rollback on mounted surface import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -15,7 +15,7 @@ import { ViewPreferenceService, FindingsViewMode } from '../../../core/services/ import { CompareService } from '../../compare/services/compare.service'; import { SECURITY_FINDINGS_API } from '../../../core/api/security-findings.client'; -describe('FindingsContainerComponent (shared FindingList adoption)', () => { +describe('FindingsContainerComponent (truthful mounted findings view)', () => { let component: FindingsContainerComponent; let fixture: ComponentFixture; let queryParamMap$: BehaviorSubject; @@ -110,99 +110,30 @@ describe('FindingsContainerComponent (shared FindingList adoption)', () => { expect(component).toBeTruthy(); }); - it('should compute findingEvidenceItems from raw findings', async () => { + it('loads live findings into the mounted findings list contract', async () => { await fixture.whenStable(); fixture.detectChanges(); - const items = component.findingEvidenceItems(); - expect(items.length).toBe(3); + expect(component.findings().length).toBe(3); + expect(component.findings()[0]).toEqual( + jasmine.objectContaining({ + id: 'finding-1', + advisoryId: 'CVE-2026-8001', + packageName: 'backend-api', + packageVersion: '2.5.0', + severity: 'critical', + status: 'open', + publishedAt: '2026-02-10T09:30:00Z', + }), + ); }); - it('should map finding_id from Finding.id', async () => { - await fixture.whenStable(); - fixture.detectChanges(); - - const items = component.findingEvidenceItems(); - expect(items[0].finding_id).toBe('finding-1'); - expect(items[1].finding_id).toBe('finding-2'); - expect(items[2].finding_id).toBe('finding-3'); - }); - - it('should map cve from Finding.advisoryId', async () => { - await fixture.whenStable(); - fixture.detectChanges(); - - const items = component.findingEvidenceItems(); - expect(items[0].cve).toBe('CVE-2026-8001'); - expect(items[1].cve).toBe('CVE-2026-8002'); - }); - - it('should map component name and version', async () => { - await fixture.whenStable(); - fixture.detectChanges(); - - const items = component.findingEvidenceItems(); - expect(items[0].component?.name).toBe('backend-api'); - expect(items[0].component?.version).toBe('2.5.0'); - expect(items[1].component?.name).toBe('frontend-lib'); - expect(items[1].component?.version).toBe('1.0.3'); - }); - - it('should map severity to risk_score', async () => { - await fixture.whenStable(); - fixture.detectChanges(); - - const items = component.findingEvidenceItems(); - // critical -> 90 - expect(items[0].score_explain?.risk_score).toBe(90); - // high -> 70 - expect(items[1].score_explain?.risk_score).toBe(70); - // low -> 20 - expect(items[2].score_explain?.risk_score).toBe(20); - }); - - it('should map fixed status to VEX fixed', async () => { - await fixture.whenStable(); - fixture.detectChanges(); - - const items = component.findingEvidenceItems(); - // finding-2 has vexStatus 'fixed', delta 'resolved' -> status = 'fixed' - expect(items[1].vex?.status).toBe('fixed'); - }); - - it('should map excepted status to VEX not_affected', async () => { - await fixture.whenStable(); - fixture.detectChanges(); - - const items = component.findingEvidenceItems(); - // finding-3 has vexStatus 'not_affected' -> status = 'excepted' - expect(items[2].vex?.status).toBe('not_affected'); - }); - - it('should render shared finding list host element in detail view', async () => { + it('renders the truthful bespoke findings list instead of the shared evidence row list', async () => { await fixture.whenStable(); fixture.detectChanges(); const el: HTMLElement = fixture.nativeElement; - const host = el.querySelector('[data-testid="shared-finding-list-host"]'); - expect(host).toBeTruthy(); - }); - - it('should render stella-finding-list element in detail view', async () => { - await fixture.whenStable(); - fixture.detectChanges(); - - const el: HTMLElement = fixture.nativeElement; - const list = el.querySelector('stella-finding-list'); - expect(list).toBeTruthy(); - }); - - it('should not render bespoke app-findings-list element', async () => { - await fixture.whenStable(); - fixture.detectChanges(); - - const el: HTMLElement = fixture.nativeElement; - const bespoke = el.querySelector('app-findings-list'); - expect(bespoke).toBeFalsy(); + expect(el.querySelector('app-findings-list')).toBeTruthy(); + expect(el.querySelector('stella-finding-list')).toBeFalsy(); }); }); diff --git a/src/Web/StellaOps.Web/src/app/features/findings/container/findings-container.component.ts b/src/Web/StellaOps.Web/src/app/features/findings/container/findings-container.component.ts index 717a2a816..c668c2a9a 100644 --- a/src/Web/StellaOps.Web/src/app/features/findings/container/findings-container.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/findings/container/findings-container.component.ts @@ -2,8 +2,6 @@ // findings-container.component.ts // Sprint: SPRINT_1227_0005_0001_FE_diff_first_default // Task: T3 — Container component for findings with view switching -// Sprint: SPRINT_20260308_020_FE_orphan_finding_list_consolidation -// Task: FE-OFL-002 — Adopt shared FindingListComponent on canonical findings surface // ----------------------------------------------------------------------------- import { Component, ChangeDetectionStrategy, inject, signal, computed, OnInit } from '@angular/core'; @@ -20,9 +18,7 @@ import { forkJoin, of } from 'rxjs'; import { ViewPreferenceService, FindingsViewMode } from '../../../core/services/view-preference.service'; import { FindingsViewToggleComponent } from '../../../shared/components/findings-view-toggle/findings-view-toggle.component'; import { CompareViewComponent } from '../../compare/components/compare-view/compare-view.component'; -import { FindingListComponent } from '../../../shared/components/finding-list.component'; -import type { FindingEvidenceResponse } from '../../../core/api/triage-evidence.models'; -import { type Finding } from '../findings-list.component'; +import { FindingsListComponent, type Finding } from '../findings-list.component'; import { CompareService } from '../../compare/services/compare.service'; import { SECURITY_FINDINGS_API, @@ -102,7 +98,7 @@ function mapFinding(source: DetailFindingSource): Finding { MatProgressSpinnerModule, FindingsViewToggleComponent, CompareViewComponent, - FindingListComponent, + FindingsListComponent ], template: `
@@ -153,11 +149,9 @@ function mapFinding(source: DetailFindingSource): Finding {

{{ message }}

} @else { - + } } } @@ -263,15 +257,9 @@ export class FindingsContainerComponent implements OnInit { // Loading state readonly loading = signal(false); - // Findings for detail view (raw Finding model) + // Findings for detail view readonly findings = signal([]); - // Adapter: map Finding[] to FindingEvidenceResponse[] for the shared FindingListComponent - // Sprint: SPRINT_20260308_020_FE_orphan_finding_list_consolidation (FE-OFL-002) - readonly findingEvidenceItems = computed(() => { - return this.findings().map((f) => this.mapFindingToEvidence(f)); - }); - // Detail view data failure state readonly detailError = signal(null); @@ -352,52 +340,6 @@ export class FindingsContainerComponent implements OnInit { console.log('Selected finding:', finding.id); } - /** Handler for the shared FindingListComponent's findingSelected output (emits finding_id string). */ - onFindingSelected(findingId: string): void { - const finding = this.findings().find((f) => f.id === findingId); - if (finding) { - this.onFindingSelect(finding); - } - } - - /** - * Adapter: map the bespoke Finding interface to FindingEvidenceResponse - * so the shared FindingListComponent can render it. - * Sprint: SPRINT_20260308_020_FE_orphan_finding_list_consolidation (FE-OFL-002) - */ - private mapFindingToEvidence(finding: Finding): FindingEvidenceResponse { - const severityScore: Record = { - critical: 90, - high: 70, - medium: 45, - low: 20, - unknown: 0, - }; - - return { - finding_id: finding.id, - cve: finding.advisoryId, - component: { - purl: `pkg:generic/${finding.packageName}@${finding.packageVersion}`, - name: finding.packageName, - version: finding.packageVersion, - type: 'generic', - }, - score_explain: { - kind: 'ews', - risk_score: severityScore[finding.severity] ?? 0, - last_seen: finding.publishedAt ?? new Date().toISOString(), - summary: `Severity: ${finding.severity}, Status: ${finding.status}`, - }, - vex: finding.status === 'fixed' - ? { status: 'fixed' } - : finding.status === 'excepted' - ? { status: 'not_affected', justification: 'Excepted' } - : undefined, - last_seen: finding.publishedAt ?? new Date().toISOString(), - }; - } - private readViewMode(raw: string | null): FindingsViewMode | null { return raw === 'diff' || raw === 'detail' ? raw : null; } diff --git a/src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/release-detail/release-detail-finding-list-adoption.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/release-detail/release-detail-finding-list-adoption.component.spec.ts index 4c48fbea6..f9f897ce5 100644 --- a/src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/release-detail/release-detail-finding-list-adoption.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/release-detail/release-detail-finding-list-adoption.component.spec.ts @@ -1,19 +1,19 @@ // SPDX-License-Identifier: BUSL-1.1 // Copyright (c) 2025 StellaOps -// Sprint: SPRINT_20260308_020_FE_orphan_finding_list_consolidation -// Task: FE-OFL-004 - Verify shared FindingListComponent adoption on ReleaseDetailComponent +// Sprint: SPRINT_20260308_024_FE_orphan_revival_regression_remediation +// Task: FE-ORM-004 - Verify truthful release security rendering on mounted surface import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, convertToParamMap, provideRouter } from '@angular/router'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { of, BehaviorSubject, EMPTY } from 'rxjs'; +import { BehaviorSubject, EMPTY } from 'rxjs'; import { signal } from '@angular/core'; import { ReleaseDetailComponent } from './release-detail.component'; import { PlatformContextStore } from '../../../../core/context/platform-context.store'; import { ReleaseManagementStore } from '../release.store'; -describe('ReleaseDetailComponent (shared FindingList adoption)', () => { +describe('ReleaseDetailComponent (truthful release security rendering)', () => { let component: ReleaseDetailComponent; let fixture: ComponentFixture; @@ -36,14 +36,14 @@ describe('ReleaseDetailComponent (shared FindingList adoption)', () => { const queryParamMap$ = new BehaviorSubject(convertToParamMap({})); const mockContext = { - environment: signal('production'), - region: signal('us-east-1'), - tenant: signal('demo-prod'), + selectedRegions: signal([]), + selectedEnvironments: signal([]), }; const mockStore = { selectedRelease: signal(mockRelease), releases: signal([mockRelease]), + selectRelease: jasmine.createSpy('selectRelease'), loadRelease: () => EMPTY, }; @@ -69,6 +69,8 @@ describe('ReleaseDetailComponent (shared FindingList adoption)', () => { fixture = TestBed.createComponent(ReleaseDetailComponent); component = fixture.componentInstance; + component.mode.set('version'); + component.activeTab.set('security-inputs'); fixture.detectChanges(); }); @@ -76,46 +78,8 @@ describe('ReleaseDetailComponent (shared FindingList adoption)', () => { expect(component).toBeTruthy(); }); - it('should compute securityFindingEvidenceItems from empty findings', () => { - const items = component.securityFindingEvidenceItems(); - expect(items).toEqual([]); - }); - - it('should compute securityFindingEvidenceItems from security findings', () => { - // Set findings directly via the signal - (component as any).findings.set([ - { - findingId: 'f-001', - cveId: 'CVE-2026-9001', - severity: 'critical', - componentName: 'libcrypto', - releaseId: 'rel-001', - reachable: true, - reachabilityScore: 85, - effectiveDisposition: 'action_required', - vexStatus: 'under_investigation', - exceptionStatus: 'none', - }, - { - findingId: 'f-002', - cveId: 'CVE-2026-9002', - severity: 'high', - componentName: 'auth-lib', - releaseId: 'rel-001', - reachable: false, - reachabilityScore: 0, - effectiveDisposition: 'review_required', - vexStatus: 'not_affected', - exceptionStatus: 'approved', - }, - ]); - - const items = component.securityFindingEvidenceItems(); - expect(items.length).toBe(2); - }); - - it('should map finding_id from SecurityFindingProjection.findingId', () => { - (component as any).findings.set([ + it('renders the mounted release security table with truthful fields', () => { + component.findings.set([ { findingId: 'f-001', cveId: 'CVE-2026-9001', @@ -130,148 +94,21 @@ describe('ReleaseDetailComponent (shared FindingList adoption)', () => { }, ]); - const items = component.securityFindingEvidenceItems(); - expect(items[0].finding_id).toBe('f-001'); + fixture.detectChanges(); + + const element: HTMLElement = fixture.nativeElement; + expect(element.querySelector('stella-finding-list')).toBeFalsy(); + expect(element.textContent).toContain('CVE-2026-9001'); + expect(element.textContent).toContain('libcrypto'); + expect(element.textContent).toContain('action_required'); + expect(element.textContent).toContain('yes (85)'); }); - it('should map cve from SecurityFindingProjection.cveId', () => { - (component as any).findings.set([ - { - findingId: 'f-001', - cveId: 'CVE-2026-9001', - severity: 'critical', - componentName: 'libcrypto', - releaseId: 'rel-001', - reachable: true, - reachabilityScore: 85, - effectiveDisposition: 'action_required', - vexStatus: 'under_investigation', - exceptionStatus: 'none', - }, - ]); + it('shows the legacy empty-state row when the release has no findings', () => { + component.findings.set([]); + fixture.detectChanges(); - const items = component.securityFindingEvidenceItems(); - expect(items[0].cve).toBe('CVE-2026-9001'); - }); - - it('should map component name from SecurityFindingProjection.componentName', () => { - (component as any).findings.set([ - { - findingId: 'f-001', - cveId: 'CVE-2026-9001', - severity: 'critical', - componentName: 'libcrypto', - releaseId: 'rel-001', - reachable: true, - reachabilityScore: 85, - effectiveDisposition: 'action_required', - vexStatus: 'under_investigation', - exceptionStatus: 'none', - }, - ]); - - const items = component.securityFindingEvidenceItems(); - expect(items[0].component?.name).toBe('libcrypto'); - }); - - it('should map severity to risk_score (critical=90)', () => { - (component as any).findings.set([ - { - findingId: 'f-001', - cveId: 'CVE-2026-9001', - severity: 'critical', - componentName: 'libcrypto', - releaseId: 'rel-001', - reachable: true, - reachabilityScore: 85, - effectiveDisposition: 'action_required', - vexStatus: 'under_investigation', - exceptionStatus: 'none', - }, - ]); - - const items = component.securityFindingEvidenceItems(); - expect(items[0].score_explain?.risk_score).toBe(90); - }); - - it('should map reachable=true to non-empty reachable_path', () => { - (component as any).findings.set([ - { - findingId: 'f-001', - cveId: 'CVE-2026-9001', - severity: 'critical', - componentName: 'libcrypto', - releaseId: 'rel-001', - reachable: true, - reachabilityScore: 85, - effectiveDisposition: 'action_required', - vexStatus: 'under_investigation', - exceptionStatus: 'none', - }, - ]); - - const items = component.securityFindingEvidenceItems(); - expect(items[0].reachable_path).toBeDefined(); - expect(items[0].reachable_path!.length).toBeGreaterThan(0); - }); - - it('should map reachable=false to undefined reachable_path', () => { - (component as any).findings.set([ - { - findingId: 'f-002', - cveId: 'CVE-2026-9002', - severity: 'high', - componentName: 'auth-lib', - releaseId: 'rel-001', - reachable: false, - reachabilityScore: 0, - effectiveDisposition: 'review_required', - vexStatus: 'not_affected', - exceptionStatus: 'approved', - }, - ]); - - const items = component.securityFindingEvidenceItems(); - expect(items[0].reachable_path).toBeUndefined(); - }); - - it('should map vexStatus not_affected to VEX evidence', () => { - (component as any).findings.set([ - { - findingId: 'f-002', - cveId: 'CVE-2026-9002', - severity: 'high', - componentName: 'auth-lib', - releaseId: 'rel-001', - reachable: false, - reachabilityScore: 0, - effectiveDisposition: 'review_required', - vexStatus: 'not_affected', - exceptionStatus: 'approved', - }, - ]); - - const items = component.securityFindingEvidenceItems(); - expect(items[0].vex?.status).toBe('not_affected'); - }); - - it('should not produce VEX evidence for under_investigation status', () => { - (component as any).findings.set([ - { - findingId: 'f-001', - cveId: 'CVE-2026-9001', - severity: 'critical', - componentName: 'libcrypto', - releaseId: 'rel-001', - reachable: true, - reachabilityScore: 85, - effectiveDisposition: 'action_required', - vexStatus: 'under_investigation', - exceptionStatus: 'none', - }, - ]); - - const items = component.securityFindingEvidenceItems(); - expect(items[0].vex).toBeUndefined(); + const element: HTMLElement = fixture.nativeElement; + expect(element.textContent).toContain('No findings.'); }); }); diff --git a/src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/release-detail/release-detail.component.ts b/src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/release-detail/release-detail.component.ts index 650cb2cc3..51ee3000b 100644 --- a/src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/release-detail/release-detail.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/release-orchestrator/releases/release-detail/release-detail.component.ts @@ -10,8 +10,6 @@ import { ReleaseManagementStore } from '../release.store'; import { getEvidencePostureLabel, getGateStatusLabel, getRiskTierLabel } from '../../../../core/api/release-management.models'; import type { ManagedRelease } from '../../../../core/api/release-management.models'; import { DegradedStateBannerComponent } from '../../../../shared/components/degraded-state-banner/degraded-state-banner.component'; -import { FindingListComponent } from '../../../../shared/components/finding-list.component'; -import type { FindingEvidenceResponse } from '../../../../core/api/triage-evidence.models'; import { buildContextReturnTo } from '../../../../shared/ui/context-route-state/context-route-state'; import { AuditorOnlyDirective } from '../../../../shared/directives/auditor-only.directive'; import { OperatorOnlyDirective } from '../../../../shared/directives/operator-only.directive'; @@ -140,7 +138,7 @@ interface ReloadOptions { @Component({ selector: 'app-release-detail', standalone: true, - imports: [RouterLink, FormsModule, DegradedStateBannerComponent, FindingListComponent, AuditorOnlyDirective, OperatorOnlyDirective, ViewModeToggleComponent], + imports: [RouterLink, FormsModule, DegradedStateBannerComponent, AuditorOnlyDirective, OperatorOnlyDirective, ViewModeToggleComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
@@ -269,13 +267,17 @@ interface ReloadOptions { @case ('security-inputs') {

Release-Scoped Security

- + + + + @for (item of findings(); track item.findingId) { + + + + + } @empty { } + +
CVEComponentSeverityReachableVEXExceptionBlocks Promotion
{{ item.cveId }}{{ item.componentName }}{{ item.severity }}{{ item.reachable ? 'yes' : 'no' }} ({{ item.reachabilityScore }}){{ item.vexStatus }}{{ item.exceptionStatus }}{{ item.effectiveDisposition === 'action_required' ? 'yes' : 'no' }}
No findings.

} @@ -420,12 +422,6 @@ export class ReleaseDetailComponent { readonly diffRows = signal([]); readonly diffMode = signal<'sbom'|'findings'|'policy'|'topology'>('sbom'); - // Adapter: map SecurityFindingProjection[] to FindingEvidenceResponse[] for the shared FindingListComponent - // Sprint: SPRINT_20260308_020_FE_orphan_finding_list_consolidation (FE-OFL-003) - readonly securityFindingEvidenceItems = computed(() => { - return this.findings().map((f) => this.mapSecurityFindingToEvidence(f)); - }); - readonly selectedTimelineId = signal(null); readonly selectedTargets = signal>(new Set()); @@ -1278,52 +1274,5 @@ export class ReleaseDetailComponent { return false; } - - /** Handler for the shared FindingListComponent's findingSelected output. */ - onSecurityFindingSelected(findingId: string): void { - const finding = this.findings().find((f) => f.findingId === findingId); - if (finding) { - void this.router.navigate(['/security/triage'], { - queryParams: { releaseId: this.releaseContextId(), findingId: finding.findingId, cve: finding.cveId }, - }); - } - } - - /** - * Adapter: map SecurityFindingProjection to FindingEvidenceResponse - * for the shared FindingListComponent. - * Sprint: SPRINT_20260308_020_FE_orphan_finding_list_consolidation (FE-OFL-003) - */ - private mapSecurityFindingToEvidence(f: SecurityFindingProjection): FindingEvidenceResponse { - const severityScore: Record = { - critical: 90, - high: 70, - medium: 45, - low: 20, - }; - - return { - finding_id: f.findingId, - cve: f.cveId, - component: { - purl: `pkg:generic/${f.componentName}`, - name: f.componentName, - version: '', - type: 'generic', - }, - reachable_path: f.reachable ? ['reachable'] : undefined, - score_explain: { - kind: 'ews', - risk_score: severityScore[f.severity.toLowerCase()] ?? 0, - last_seen: new Date().toISOString(), - summary: `Severity: ${f.severity}, Reachability: ${f.reachabilityScore}, Disposition: ${f.effectiveDisposition}`, - }, - vex: f.vexStatus && f.vexStatus !== 'under_investigation' - ? { status: f.vexStatus as 'not_affected' | 'affected' | 'fixed' | 'under_investigation' } - : undefined, - last_seen: new Date().toISOString(), - }; - } } - diff --git a/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-audit-log.component.ts b/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-audit-log.component.ts index 790eabdee..5f9e9917e 100644 --- a/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-audit-log.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/trust-admin/trust-audit-log.component.ts @@ -2,7 +2,6 @@ * @file trust-audit-log.component.ts * @sprint SPRINT_20251229_018c_FE * @description Trust audit log viewer - * Filter bar adoption: SPRINT_20260308_015_FE (FE-OFB-003) */ import { Component, ChangeDetectionStrategy, signal, computed, inject, OnInit } from '@angular/core'; @@ -17,16 +16,76 @@ import { ListAuditEventsParams, TrustAuditFilter, } from '../../core/api/trust.models'; -import { FilterBarComponent, FilterOption, ActiveFilter } from '../../shared/ui/filter-bar/filter-bar.component'; @Component({ selector: 'app-trust-audit-log', - imports: [CommonModule, FormsModule, FilterBarComponent], + imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
- -
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ -
- + @if (hasFilters()) { + + } +
@@ -174,12 +229,39 @@ import { FilterBarComponent, FilterOption, ActiveFilter } from '../../shared/ui/ padding: 1.5rem; } - /* Filter bar styles handled by shared FilterBarComponent */ - - .audit-log__actions { + .audit-log__filters { display: flex; - justify-content: flex-end; - margin-bottom: 0.5rem; + flex-wrap: wrap; + gap: 1rem; + align-items: flex-end; + margin-bottom: 1.5rem; + } + + .filter-group { + display: flex; + flex-direction: column; + gap: 0.25rem; + } + + .filter-group label { + font-size: 0.8rem; + color: var(--color-text-muted); + } + + .filter-group input, + .filter-group select { + background: var(--color-surface-primary); + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-md); + color: var(--color-text-primary); + padding: 0.5rem 0.75rem; + min-width: 140px; + } + + .filter-group input:focus, + .filter-group select:focus { + outline: none; + border-color: var(--color-status-info); } .btn-export { @@ -476,14 +558,6 @@ export class TrustAuditLogComponent implements OnInit { this.endDate() !== '' ); - // Shared filter bar integration - readonly trustFilterOptions: FilterOption[] = [ - { key: 'resourceType', label: 'Resource Type', options: [{ value: 'key', label: 'Keys' }, { value: 'issuer', label: 'Issuers' }, { value: 'certificate', label: 'Certificates' }, { value: 'config', label: 'Config' }] }, - { key: 'severity', label: 'Severity', options: [{ value: 'info', label: 'Info' }, { value: 'warning', label: 'Warning' }, { value: 'error', label: 'Error' }, { value: 'critical', label: 'Critical' }] }, - ]; - - readonly activeTrustFilters = signal([]); - ngOnInit(): void { this.loadEvents(); } @@ -526,52 +600,14 @@ export class TrustAuditLogComponent implements OnInit { onSearch(): void { this.pageNumber.set(1); - this.rebuildActiveTrustFilters(); this.loadEvents(); } onFilterChange(): void { this.pageNumber.set(1); - this.rebuildActiveTrustFilters(); this.loadEvents(); } - onTrustSearch(value: string): void { - this.searchQuery.set(value); - this.onSearch(); - } - - onTrustFilterChanged(filter: ActiveFilter): void { - if (filter.key === 'resourceType') { - this.selectedResourceType.set(filter.value as any); - } else if (filter.key === 'severity') { - this.selectedSeverity.set(filter.value as any); - } - this.onFilterChange(); - } - - onTrustFilterRemoved(filter: ActiveFilter): void { - if (filter.key === 'resourceType') { - this.selectedResourceType.set('all'); - } else if (filter.key === 'severity') { - this.selectedSeverity.set('all'); - } - this.onFilterChange(); - } - - private rebuildActiveTrustFilters(): void { - const filters: ActiveFilter[] = []; - if (this.selectedResourceType() !== 'all') { - const opt = this.trustFilterOptions[0].options.find(o => o.value === this.selectedResourceType()); - filters.push({ key: 'resourceType', value: this.selectedResourceType(), label: 'Resource: ' + (opt?.label || this.selectedResourceType()) }); - } - if (this.selectedSeverity() !== 'all') { - const opt = this.trustFilterOptions[1].options.find(o => o.value === this.selectedSeverity()); - filters.push({ key: 'severity', value: this.selectedSeverity() as string, label: 'Severity: ' + (opt?.label || this.selectedSeverity()) }); - } - this.activeTrustFilters.set(filters); - } - onPageChange(page: number): void { this.pageNumber.set(page); this.loadEvents(); @@ -583,7 +619,6 @@ export class TrustAuditLogComponent implements OnInit { this.selectedSeverity.set('all'); this.startDate.set(''); this.endDate.set(''); - this.activeTrustFilters.set([]); this.pageNumber.set(1); this.loadEvents(); } diff --git a/src/Web/StellaOps.Web/src/app/shared/components/policy/policy-evaluate-panel.component.ts b/src/Web/StellaOps.Web/src/app/shared/components/policy/policy-evaluate-panel.component.ts index c07ca5499..d9b2e4454 100644 --- a/src/Web/StellaOps.Web/src/app/shared/components/policy/policy-evaluate-panel.component.ts +++ b/src/Web/StellaOps.Web/src/app/shared/components/policy/policy-evaluate-panel.component.ts @@ -6,7 +6,7 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RemediationHintComponent } from './remediation-hint.component'; -import { PolicyEvaluateResponse, GateEvaluation } from '../../core/api/policy-interop.models'; +import { PolicyEvaluateResponse } from '../../../core/api/policy-interop.models'; @Component({ selector: 'stella-policy-evaluate-panel', diff --git a/src/Web/StellaOps.Web/src/app/shared/components/policy/policy-pack-editor.component.ts b/src/Web/StellaOps.Web/src/app/shared/components/policy/policy-pack-editor.component.ts index 76a4864e3..3b8387790 100644 --- a/src/Web/StellaOps.Web/src/app/shared/components/policy/policy-pack-editor.component.ts +++ b/src/Web/StellaOps.Web/src/app/shared/components/policy/policy-pack-editor.component.ts @@ -11,7 +11,7 @@ import { PolicyGateDefinition, PolicyRuleDefinition, PolicyGateTypes, -} from '../../core/api/policy-interop.models'; +} from '../../../core/api/policy-interop.models'; interface GateEditState { gate: PolicyGateDefinition; @@ -269,7 +269,7 @@ export class PolicyPackEditorComponent { if (value) { this._document.set(value); this._gateStates.set( - (value.spec.gates || []).map((g) => ({ gate: { ...g }, expanded: false })) + (value.spec.gates || []).map((gate: PolicyGateDefinition) => ({ gate: { ...gate }, expanded: false })) ); } } diff --git a/src/Web/StellaOps.Web/src/app/shared/components/policy/remediation-hint.component.ts b/src/Web/StellaOps.Web/src/app/shared/components/policy/remediation-hint.component.ts index 9496adafc..40e4544a2 100644 --- a/src/Web/StellaOps.Web/src/app/shared/components/policy/remediation-hint.component.ts +++ b/src/Web/StellaOps.Web/src/app/shared/components/policy/remediation-hint.component.ts @@ -5,7 +5,7 @@ import { Component, Input } from '@angular/core'; -import { RemediationHint } from '../../core/api/policy-interop.models'; +import { RemediationHint } from '../../../core/api/policy-interop.models'; @Component({ selector: 'stella-remediation-hint', diff --git a/src/Web/StellaOps.Web/src/app/shared/directives/glossary-tooltip.directive.ts b/src/Web/StellaOps.Web/src/app/shared/directives/glossary-tooltip.directive.ts index eb93bb8be..364835cd1 100644 --- a/src/Web/StellaOps.Web/src/app/shared/directives/glossary-tooltip.directive.ts +++ b/src/Web/StellaOps.Web/src/app/shared/directives/glossary-tooltip.directive.ts @@ -147,10 +147,11 @@ export class GlossaryTooltipDirective implements OnInit, OnDestroy { private showTooltip(entry: GlossaryEntry, anchor: HTMLElement): void { this.hideTooltip(); - this.tooltipElement = this.renderer.createElement('div'); - this.renderer.addClass(this.tooltipElement, 'glossary-tooltip'); - this.renderer.setAttribute(this.tooltipElement, 'role', 'tooltip'); - this.renderer.setAttribute(this.tooltipElement, 'id', `glossary-tooltip-${entry.term.toLowerCase()}`); + const tooltip = this.renderer.createElement('div') as HTMLElement; + this.tooltipElement = tooltip; + this.renderer.addClass(tooltip, 'glossary-tooltip'); + this.renderer.setAttribute(tooltip, 'role', 'tooltip'); + this.renderer.setAttribute(tooltip, 'id', `glossary-tooltip-${entry.term.toLowerCase()}`); const content = `
@@ -164,8 +165,8 @@ export class GlossaryTooltipDirective implements OnInit, OnDestroy {
`; - this.tooltipElement.innerHTML = content; - this.renderer.appendChild(document.body, this.tooltipElement); + this.renderer.setProperty(tooltip, 'innerHTML', content); + this.renderer.appendChild(document.body, tooltip); // Position tooltip this.positionTooltip(anchor); diff --git a/src/Web/StellaOps.Web/src/tests/evidence/evidence-thread-browser.component.spec.ts b/src/Web/StellaOps.Web/src/tests/evidence/evidence-thread-browser.component.spec.ts index 9e25500ba..55bc72669 100644 --- a/src/Web/StellaOps.Web/src/tests/evidence/evidence-thread-browser.component.spec.ts +++ b/src/Web/StellaOps.Web/src/tests/evidence/evidence-thread-browser.component.spec.ts @@ -1,7 +1,6 @@ import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; -import { MatSnackBar } from '@angular/material/snack-bar'; import { ActivatedRoute, Router, provideRouter } from '@angular/router'; import { BehaviorSubject, of } from 'rxjs'; @@ -67,7 +66,7 @@ describe('Evidence thread browser', () => { it('navigates to encoded thread digest when a row is opened', () => { component.onRowClick(thread); expect(router.navigate).toHaveBeenCalledWith([ - '/evidence-thread', + '/evidence/threads', encodeURIComponent('sha256:artifact-1'), ]); }); @@ -76,6 +75,7 @@ describe('Evidence thread browser', () => { describe('EvidenceThreadViewComponent', () => { let fixture: ComponentFixture; let component: EvidenceThreadViewComponent; + let router: Router; let routeParams$: BehaviorSubject>; const graph: EvidenceThreadGraph = { @@ -154,6 +154,9 @@ describe('Evidence thread browser', () => { ], }).compileComponents(); + router = TestBed.inject(Router); + spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + fixture = TestBed.createComponent(EvidenceThreadViewComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -164,40 +167,10 @@ describe('Evidence thread browser', () => { expect(component.thread()?.thread.id).toBe('thread-1'); }); - it('shows snackbar when clipboard API is unavailable', () => { - const openSpy = spyOn((component as unknown as { snackBar: MatSnackBar }).snackBar, 'open'); - routeParams$.next({ artifactDigest: '' }); - fixture.detectChanges(); + it('navigates back to the canonical evidence threads list', () => { + component.onBack(); - component.copyDigest(); - - expect(openSpy).toHaveBeenCalledWith('Clipboard not available', 'Dismiss', { - duration: 3000, - }); - }); - - it('shows snackbar when clipboard write rejects', async () => { - const openSpy = spyOn((component as unknown as { snackBar: MatSnackBar }).snackBar, 'open'); - if (!navigator.clipboard) { - Object.defineProperty(navigator, 'clipboard', { - configurable: true, - value: { writeText: (_value: string) => Promise.resolve() }, - }); - } - - const writeText = spyOn(navigator.clipboard, 'writeText').and.returnValue( - Promise.reject(new Error('clipboard denied')) - ); - - component.copyDigest(); - await Promise.resolve(); - await Promise.resolve(); - - expect(writeText).toHaveBeenCalled(); - - expect(openSpy).toHaveBeenCalledWith('Failed to copy digest', 'Dismiss', { - duration: 3000, - }); + expect(router.navigate).toHaveBeenCalledWith(['/evidence/threads']); }); }); }); diff --git a/src/Web/StellaOps.Web/src/tests/orphan_revival/orphan-revival-regression-remediation.spec.ts b/src/Web/StellaOps.Web/src/tests/orphan_revival/orphan-revival-regression-remediation.spec.ts new file mode 100644 index 000000000..461922fe8 --- /dev/null +++ b/src/Web/StellaOps.Web/src/tests/orphan_revival/orphan-revival-regression-remediation.spec.ts @@ -0,0 +1,338 @@ +import { signal } from '@angular/core'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { ActivatedRoute, convertToParamMap, provideRouter } from '@angular/router'; +import { BehaviorSubject, EMPTY, of } from 'rxjs'; + +import { AuditLogTableComponent } from '../../app/features/audit-log/audit-log-table.component'; +import { FindingsContainerComponent } from '../../app/features/findings/container/findings-container.component'; +import { PolicyDecisioningGatesPageComponent } from '../../app/features/policy-decisioning/policy-decisioning-gates-page.component'; +import { ReleaseDetailComponent } from '../../app/features/release-orchestrator/releases/release-detail/release-detail.component'; +import { TrustAuditLogComponent } from '../../app/features/trust-admin/trust-audit-log.component'; +import { AuditLogClient } from '../../app/core/api/audit-log.client'; +import { SECURITY_FINDINGS_API } from '../../app/core/api/security-findings.client'; +import { PlatformContextStore } from '../../app/core/context/platform-context.store'; +import { ReleaseManagementStore } from '../../app/features/release-orchestrator/releases/release.store'; +import { CompareService } from '../../app/features/compare/services/compare.service'; +import { MockScoringApi, SCORING_API } from '../../app/core/services/scoring.service'; +import { ViewPreferenceService, FindingsViewMode } from '../../app/core/services/view-preference.service'; +import { TRUST_API } from '../../app/core/api/trust.client'; +import { PolicyPackEditorComponent } from '../../app/shared/components/policy/policy-pack-editor.component'; + +describe('Orphan revival regression remediation', () => { + describe('policy hosts', () => { + it('renders the policy evaluation panel on the mounted gates page', async () => { + await TestBed.configureTestingModule({ + imports: [PolicyDecisioningGatesPageComponent], + providers: [ + provideRouter([]), + { + provide: ActivatedRoute, + useValue: { + queryParams: of({}), + queryParamMap: of(convertToParamMap({})), + snapshot: { + root: { + queryParams: {}, + queryParamMap: convertToParamMap({}), + }, + queryParams: {}, + queryParamMap: convertToParamMap({}), + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(PolicyDecisioningGatesPageComponent); + fixture.detectChanges(); + + const element: HTMLElement = fixture.nativeElement; + expect(element.querySelector('stella-policy-evaluate-panel')).toBeTruthy(); + expect(element.textContent).toContain('Decision:'); + expect(element.textContent).toContain('WARN'); + expect(element.textContent).toContain('Gate Results'); + }); + + it('renders the shared policy pack editor with a valid pack document', async () => { + await TestBed.configureTestingModule({ + imports: [PolicyPackEditorComponent], + }).compileComponents(); + + const fixture = TestBed.createComponent(PolicyPackEditorComponent); + fixture.componentInstance.policyPack = { + apiVersion: 'policy.stella/v2', + kind: 'PolicyPack', + metadata: { + name: 'Release Safety', + version: '3.1.0', + digest: 'sha256:pack', + }, + spec: { + settings: { + default_action: 'block', + deterministic_mode: true, + }, + gates: [ + { + id: 'cvss-threshold', + type: 'CvssThresholdGate', + enabled: true, + config: { threshold: 8 }, + }, + ], + }, + }; + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const element: HTMLElement = fixture.nativeElement; + expect(element.textContent).toContain('Release Safety'); + expect(element.textContent).toContain('Gates'); + }); + }); + + describe('audit filtering', () => { + let fixture: ComponentFixture; + let component: AuditLogTableComponent; + const auditClient = { + getEvents: jasmine.createSpy('getEvents').and.returnValue( + of({ items: [], cursor: null, hasMore: false }), + ), + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AuditLogTableComponent], + providers: [provideRouter([]), { provide: AuditLogClient, useValue: auditClient }], + }).compileComponents(); + + fixture = TestBed.createComponent(AuditLogTableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + auditClient.getEvents.calls.reset(); + }); + + it('restores actor filtering and custom date ranges', () => { + component.actorFilter = 'auditor@stella.local'; + component.dateRange = 'custom'; + component.customStartDate = '2026-03-01'; + component.customEndDate = '2026-03-08'; + + expect(component.buildFilters()).toEqual( + jasmine.objectContaining({ + actorName: 'auditor@stella.local', + startDate: '2026-03-01', + endDate: '2026-03-08', + }), + ); + }); + + it('preserves multi-select module, action, and severity filters', () => { + component.selectedModules = ['policy', 'scanner']; + component.selectedActions = ['approve', 'replay']; + component.selectedSeverities = ['warning', 'critical']; + + expect(component.buildFilters()).toEqual( + jasmine.objectContaining({ + modules: ['policy', 'scanner'], + actions: ['approve', 'replay'], + severities: ['warning', 'critical'], + }), + ); + }); + }); + + describe('trust audit filtering', () => { + let fixture: ComponentFixture; + let component: TrustAuditLogComponent; + const trustApi = { + listAuditEvents: jasmine.createSpy('listAuditEvents').and.returnValue( + of({ items: [], totalCount: 0, pageNumber: 1, pageSize: 20 }), + ), + exportAuditLog: jasmine.createSpy('exportAuditLog').and.returnValue( + of(new Blob(['[]'], { type: 'application/json' })), + ), + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TrustAuditLogComponent], + providers: [{ provide: TRUST_API, useValue: trustApi }], + }).compileComponents(); + + fixture = TestBed.createComponent(TrustAuditLogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + trustApi.listAuditEvents.calls.reset(); + }); + + it('keeps start and end date filters in the request model', () => { + component.startDate.set('2026-03-01'); + component.endDate.set('2026-03-08'); + + component.onFilterChange(); + + expect(trustApi.listAuditEvents).toHaveBeenCalledWith( + jasmine.objectContaining({ + filter: jasmine.objectContaining({ + startDate: '2026-03-01', + endDate: '2026-03-08', + }), + }), + ); + }); + }); + + describe('truthful findings and release security rendering', () => { + it('renders the mounted findings container with the bespoke findings list', async () => { + const queryParamMap$ = new BehaviorSubject(convertToParamMap({ view: 'detail' })); + const paramMap$ = new BehaviorSubject(convertToParamMap({ scanId: 'scan-1' })); + const viewPreference = jasmine.createSpyObj( + 'ViewPreferenceService', + ['getViewMode', 'setViewMode'], + { viewMode: signal('detail').asReadonly() }, + ); + viewPreference.getViewMode.and.returnValue('detail'); + + const compareService = jasmine.createSpyObj('CompareService', [ + 'getBaselineRecommendations', + 'computeDelta', + ]); + compareService.getBaselineRecommendations.and.returnValue( + of({ selectedDigest: null, selectionReason: 'none', alternatives: [], autoSelectEnabled: true }), + ); + compareService.computeDelta.and.returnValue(of({ categories: [], items: [] })); + + const findingsApi = jasmine.createSpyObj('SecurityFindingsApi', ['listFindings']); + findingsApi.listFindings.and.returnValue( + of([ + { + id: 'finding-1', + advisoryId: 'CVE-2026-8001', + package: 'backend-api', + version: '2.5.0', + severity: 'CRITICAL', + vexStatus: 'affected', + delta: 'new', + firstSeen: '2026-02-10T09:30:00Z', + }, + ]), + ); + + await TestBed.configureTestingModule({ + imports: [FindingsContainerComponent, NoopAnimationsModule, HttpClientTestingModule], + providers: [ + provideRouter([]), + { provide: ViewPreferenceService, useValue: viewPreference }, + { provide: CompareService, useValue: compareService }, + { provide: SCORING_API, useClass: MockScoringApi }, + { provide: SECURITY_FINDINGS_API, useValue: findingsApi }, + { + provide: ActivatedRoute, + useValue: { + paramMap: paramMap$, + queryParamMap: queryParamMap$, + snapshot: { + paramMap: convertToParamMap({ scanId: 'scan-1' }), + queryParamMap: convertToParamMap({ view: 'detail' }), + }, + }, + }, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(FindingsContainerComponent); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const element: HTMLElement = fixture.nativeElement; + expect(element.querySelector('app-findings-list')).toBeTruthy(); + expect(element.querySelector('stella-finding-list')).toBeFalsy(); + expect(fixture.componentInstance.findings()[0].publishedAt).toBe('2026-02-10T09:30:00Z'); + }); + + it('renders the release security table instead of a synthetic shared finding list', async () => { + const mockRelease = { + releaseId: 'rel-001', + name: 'Test Release', + version: 'v1.0.0', + digest: 'sha256:abc', + releaseType: 'standard', + targetRegion: 'us-east-1', + gateStatus: 'passed', + evidencePosture: 'complete', + riskTier: 'low', + gateBlockingCount: 0, + replayMismatch: false, + }; + + await TestBed.configureTestingModule({ + imports: [ReleaseDetailComponent, HttpClientTestingModule], + providers: [ + provideRouter([]), + { + provide: PlatformContextStore, + useValue: { + initialize: jasmine.createSpy('initialize'), + contextVersion: signal(0).asReadonly(), + selectedRegions: signal([]), + selectedEnvironments: signal([]), + }, + }, + { + provide: ReleaseManagementStore, + useValue: { + selectedRelease: signal(mockRelease), + releases: signal([mockRelease]), + selectRelease: jasmine.createSpy('selectRelease'), + loadRelease: () => EMPTY, + }, + }, + { + provide: ActivatedRoute, + useValue: { + data: of({ semanticObject: 'version' }), + paramMap: of(convertToParamMap({ releaseId: 'rel-001' })), + queryParamMap: of(convertToParamMap({})), + snapshot: { + paramMap: convertToParamMap({ releaseId: 'rel-001' }), + queryParamMap: convertToParamMap({}), + }, + }, + }, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(ReleaseDetailComponent); + const component = fixture.componentInstance; + component.mode.set('version'); + component.activeTab.set('security-inputs'); + component.findings.set([ + { + findingId: 'f-001', + cveId: 'CVE-2026-9001', + severity: 'critical', + componentName: 'libcrypto', + releaseId: 'rel-001', + reachable: true, + reachabilityScore: 85, + effectiveDisposition: 'action_required', + vexStatus: 'under_investigation', + exceptionStatus: 'none', + }, + ]); + fixture.detectChanges(); + + const element: HTMLElement = fixture.nativeElement; + expect(element.querySelector('stella-finding-list')).toBeFalsy(); + expect(element.textContent).toContain('CVE-2026-9001'); + expect(element.textContent).toContain('libcrypto'); + expect(element.textContent).toContain('yes (85)'); + }); + }); +});