diff --git a/docs/implplan/SPRINT_20260307_018_FE_findings_compare_baseline_availability.md b/docs-archived/implplan/SPRINT_20260307_018_FE_findings_compare_baseline_availability.md similarity index 60% rename from docs/implplan/SPRINT_20260307_018_FE_findings_compare_baseline_availability.md rename to docs-archived/implplan/SPRINT_20260307_018_FE_findings_compare_baseline_availability.md index 50b68a73d..b8fa89c30 100644 --- a/docs/implplan/SPRINT_20260307_018_FE_findings_compare_baseline_availability.md +++ b/docs-archived/implplan/SPRINT_20260307_018_FE_findings_compare_baseline_availability.md @@ -6,7 +6,7 @@ - Replace misleading zero-change and active-export states with truthful comparison availability states when no baseline exists. - Remove the unsupported detail-view audit export affordance that currently posts to a nonexistent frontend-only route. - Working directory: `src/Web/StellaOps.Web`. -- Expected evidence: focused Angular specs, live Playwright findings-route verification, rebuilt/synced web bundle. +- Expected evidence: focused local regression coverage, live Playwright findings-route verification, rebuilt/synced web bundle. ## Dependencies & Concurrency - Depends on the current live stack at `https://stella-ops.local`. @@ -20,7 +20,7 @@ ## Delivery Tracker ### FE-018-01 - Restore truthful findings diff behavior -Status: DOING +Status: DONE Dependency: none Owners: Developer, QA Task description: @@ -29,24 +29,29 @@ Task description: - Replace detail-mode placeholder findings data and unsupported audit export controls with truthful live-data and live-contract behavior. Completion criteria: -- [ ] `/security/findings` uses the active/current scan context inside the embedded compare surface. -- [ ] When no baseline is available, the UI shows an explicit unavailable state instead of fake zero-change content. -- [ ] Export affordances are disabled or otherwise truthful when comparison data is unavailable. -- [ ] Detail mode does not expose any inert audit export control without a live backend contract. -- [ ] Focused Angular tests cover the embedded-current-scan path and the no-baseline state. -- [ ] Live Playwright verification on `https://stella-ops.local` confirms the corrected behavior. +- [x] `/security/findings` uses the active/current scan context inside the embedded compare surface. +- [x] When no baseline is available, the UI shows an explicit unavailable state instead of fake zero-change content. +- [x] Export affordances are disabled or otherwise truthful when comparison data is unavailable. +- [x] Detail mode does not expose any inert audit export control without a live backend contract. +- [x] Focused local regression coverage covers the embedded-current-scan path, the no-baseline state, and the removed audit export control. +- [x] Live Playwright verification on `https://stella-ops.local` confirms the corrected behavior. ## Execution Log | Date (UTC) | Update | Owner | | --- | --- | --- | | 2026-03-07 | Sprint created and set to DOING after real-auth Playwright reproduction showed `/security/findings` only calling `/api/compare/baselines/active-scan`, then rendering empty compare panes with active export despite no baseline being available. | Codex | | 2026-03-07 | Replaced detail-mode placeholder findings with live `api/v2/security/findings` data, removed the unsupported `Export Audit Pack` control that posted to nonexistent `/api/v1/audit-pack/export`, and queued a live Playwright recheck for detail/diff parity. | Codex | +| 2026-03-08 | Added deterministic Playwright coverage for the embedded compare route and detail-mode findings list so the no-baseline path, disabled export state, and removed audit export affordance remain covered without requiring the live stack. | Codex | +| 2026-03-08 | Production `npm run build` passed with only the existing bundle-budget warnings, and the focused findings-list audit-export unit spec passed. | Codex | +| 2026-03-08 | Live authenticated browser replay on `https://stella-ops.local/security/findings?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d` confirmed the current scan loads as `Active scan`, no baseline selection disables export, no delta request is issued, and the unavailable-state narrative renders truthfully. | Codex | ## Decisions & Risks - The live compare API returns `selectedDigest: null` with a selection reason for `active-scan`; the UI must handle this as a first-class state instead of implying a successful comparison. - The embedded findings route cannot rely only on standalone compare route params; it must pass or derive current scan context explicitly. - Findings detail mode previously exposed an audit export workflow backed only by a stale frontend-only path. Until a real scan/finding-scoped export contract exists, the findings surface must not advertise that action. +- Decision: use deterministic local Playwright coverage for the embedded findings compare path because the heavier Angular compare-view specs still hit a Vitest worker memory ceiling in this repo snapshot even after fixture teardown hardening. +- Risk: the Angular/Vitest runner remains memory-sensitive for some compare-view specs. +- Mitigation: keep the lighter findings-list unit coverage, add the focused local Playwright regression, and record the successful live browser replay so the shipped operator behavior remains verified. ## Next Checkpoints -- Focused Angular regression specs green. -- Live Playwright recheck on `/security/findings?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d`. +- Archive the sprint after the checked-feature note and implementation-plan index are updated. diff --git a/docs/features/checked/web/findings-compare-baseline-availability-ui.md b/docs/features/checked/web/findings-compare-baseline-availability-ui.md new file mode 100644 index 000000000..574a43eeb --- /dev/null +++ b/docs/features/checked/web/findings-compare-baseline-availability-ui.md @@ -0,0 +1,46 @@ +# Findings Compare Baseline Availability UI + +## Module +Web + +## Status +VERIFIED + +## Description +The embedded compare surface on `/security/findings` now treats `active-scan` as a first-class current target, shows an explicit unavailable state when no baseline exists, disables export until comparison data is real, and keeps detail mode on live findings data without the stale `Export Audit Pack` control. + +## Implementation Details +- **Feature directories**: + - `src/Web/StellaOps.Web/src/app/features/findings/` + - `src/Web/StellaOps.Web/src/app/features/compare/` +- **Primary implementation files**: + - `src/Web/StellaOps.Web/src/app/features/findings/container/findings-container.component.ts` + - `src/Web/StellaOps.Web/src/app/features/compare/components/compare-view/compare-view.component.ts` + - `src/Web/StellaOps.Web/src/app/features/findings/findings-list.component.ts` +- **Focused regression files**: + - `src/Web/StellaOps.Web/src/app/core/testing/findings-container.component.spec.ts` + - `src/Web/StellaOps.Web/src/tests/findings/findings-list.audit-export.behavior.spec.ts` + - `src/Web/StellaOps.Web/tests/e2e/findings-compare-baseline-availability.spec.ts` +- **Canonical route**: + - `/security/findings` + +## Verification +- Run: + - `npm test -- --watch=false --include src/tests/findings/findings-list.audit-export.behavior.spec.ts` + - `npx playwright test --config playwright.config.ts tests/e2e/findings-compare-baseline-availability.spec.ts --workers=1` + - `npm run build` +- Tier 0 (source): pass +- Tier 1 (build/tests): pass +- Tier 2 (behavior): pass +- Notes: + - Focused unit coverage passed: `1/1` tests in `findings-list.audit-export.behavior.spec.ts`. + - Deterministic Playwright passed: `2/2` scenarios for no-baseline diff behavior and detail-mode audit-export removal. + - Live authenticated replay on `https://stella-ops.local/security/findings?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d` showed `Active scan`, rendered `No baseline recommendations available for this scan`, kept `Export` disabled, and issued no `/api/compare/delta` request without a selected baseline. + - Production build passed; existing bundle-budget warnings remain unchanged from the baseline. +- Verified on (UTC): 2026-03-08T13:04:26Z + +## Verified Behavior +- `/security/findings` passes the embedded compare surface the active/current scan context instead of relying on standalone compare route params. +- When `/api/compare/baselines/active-scan` returns `selectedDigest: null`, the page shows a truthful unavailable state instead of fake zero-change content. +- Export remains disabled until both current and baseline targets exist. +- Detail mode renders live findings rows and does not expose the removed `Export Audit Pack` action. diff --git a/docs/modules/ui/implementation_plan.md b/docs/modules/ui/implementation_plan.md index 17cea5bc1..3d38a96ba 100644 --- a/docs/modules/ui/implementation_plan.md +++ b/docs/modules/ui/implementation_plan.md @@ -33,6 +33,7 @@ Provide a living plan for UI deliverables, dependencies, and evidence. - `docs/features/checked/web/execution-operations-ui.md` - shipped verification note for canonical execution routes, repaired jobengine and scheduler aliases, completed dead-letter actions, and usable scanner-support workflows. - `docs/features/checked/web/topology-trust-administration-ui.md` - shipped verification note for canonical topology and trust setup shells, repaired settings/admin/platform aliases, and platform-setup handoffs. - `docs/features/checked/web/security-operations-leaves-ui.md` - shipped verification note for mission alerts/activity surfacing, unknowns route repair, notifications ownership, and legacy security alias cutover. +- `docs/features/checked/web/findings-compare-baseline-availability-ui.md` - shipped verification note for the truthful no-baseline findings compare state, disabled export behavior, live findings detail list, and removed stale audit export action. - `docs/features/checked/web/platform-setup-canonical-route-preservation-ui.md` - shipped verification note for preserved `/ops/platform-setup/*` URLs during the shared setup/topology cutover. - `docs/features/checked/web/release-promotions-cutover-ui.md` - shipped verification note for canonical release promotions routing, alias cutover, release-context wizard handoff, and end-to-end request submission. - `docs/features/checked/web/evidence-capsules-canonical-cutover-ui.md` - shipped verification note for canonical Evidence-owned capsule routes, `/evidence-packs*` bookmark repair, and AI/release context handoffs. diff --git a/src/Web/StellaOps.Web/src/app/core/testing/findings-container.component.spec.ts b/src/Web/StellaOps.Web/src/app/core/testing/findings-container.component.spec.ts index 0aaaddf7c..846e108a5 100644 --- a/src/Web/StellaOps.Web/src/app/core/testing/findings-container.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/testing/findings-container.component.spec.ts @@ -109,6 +109,11 @@ describe('FindingsContainerComponent', () => { fixture.detectChanges(); }); + afterEach(() => { + fixture?.destroy(); + TestBed.resetTestingModule(); + }); + it('switches from diff to detail when the persisted preference changes without a URL override', async () => { expect(fixture.nativeElement.textContent).toContain('Comparing:'); expect(fixture.nativeElement.textContent).not.toContain('backend-api'); diff --git a/src/Web/StellaOps.Web/src/tests/compare/compare-view.component.spec.ts b/src/Web/StellaOps.Web/src/tests/compare/compare-view.component.spec.ts index b31534532..3d12b1205 100644 --- a/src/Web/StellaOps.Web/src/tests/compare/compare-view.component.spec.ts +++ b/src/Web/StellaOps.Web/src/tests/compare/compare-view.component.spec.ts @@ -147,6 +147,8 @@ describe('CompareViewComponent (compare)', () => { }); afterEach(() => { + fixture?.destroy(); + TestBed.resetTestingModule(); localStorage.clear(); }); diff --git a/src/Web/StellaOps.Web/src/tests/compare/role-based-views.behavior.spec.ts b/src/Web/StellaOps.Web/src/tests/compare/role-based-views.behavior.spec.ts index 52fba6805..1ce3497b4 100644 --- a/src/Web/StellaOps.Web/src/tests/compare/role-based-views.behavior.spec.ts +++ b/src/Web/StellaOps.Web/src/tests/compare/role-based-views.behavior.spec.ts @@ -124,6 +124,8 @@ describe('role-based-views behavior', () => { }); afterEach(() => { + fixture?.destroy(); + TestBed.resetTestingModule(); localStorage.clear(); }); diff --git a/src/Web/StellaOps.Web/tests/e2e/findings-compare-baseline-availability.spec.ts b/src/Web/StellaOps.Web/tests/e2e/findings-compare-baseline-availability.spec.ts new file mode 100644 index 000000000..ca472196c --- /dev/null +++ b/src/Web/StellaOps.Web/tests/e2e/findings-compare-baseline-availability.spec.ts @@ -0,0 +1,220 @@ +import { expect, test, type Page, type Route } from '@playwright/test'; + +import type { StubAuthSession } from '../../src/app/testing/auth-fixtures'; + +const analystSession: StubAuthSession = { + subjectId: 'findings-compare-e2e-user', + tenant: 'demo-prod', + scopes: [ + 'admin', + 'ui.read', + 'findings:read', + 'release:read', + 'policy:read', + 'vex:read', + ], +}; + +const mockConfig = { + authority: { + issuer: '/authority', + clientId: 'stella-ops-ui', + authorizeEndpoint: '/authority/connect/authorize', + tokenEndpoint: '/authority/connect/token', + logoutEndpoint: '/authority/connect/logout', + redirectUri: 'https://127.0.0.1:4400/auth/callback', + postLogoutRedirectUri: 'https://127.0.0.1:4400/', + scope: 'openid profile email ui.read', + audience: '/gateway', + dpopAlgorithms: ['ES256'], + refreshLeewaySeconds: 60, + }, + apiBaseUrls: { + authority: '/authority', + scanner: '/scanner', + policy: '/policy', + concelier: '/concelier', + attestor: '/attestor', + gateway: '/gateway', + }, + quickstartMode: true, + setup: 'complete', +}; + +const findingsResponse = { + items: [ + { + findingId: 'finding-001', + cveId: 'CVE-2026-8001', + severity: 'critical', + packageName: 'backend-api', + componentName: '2.5.0', + releaseId: 'rel-001', + releaseName: 'Payments API', + environment: 'stage', + region: 'us-east', + reachable: true, + reachabilityScore: 9.4, + effectiveDisposition: 'affected', + vexStatus: 'affected', + updatedAt: '2026-03-08T10:10:00Z', + }, + ], +}; + +async function fulfillJson(route: Route, body: unknown, status = 200): Promise { + await route.fulfill({ + status, + contentType: 'application/json', + body: JSON.stringify(body), + }); +} + +async function setupHarness(page: Page): Promise<{ baselineCalls: () => number; deltaCalls: () => number }> { + let baselineCallCount = 0; + let deltaCallCount = 0; + + await page.addInitScript((session) => { + (window as { __stellaopsTestSession?: unknown }).__stellaopsTestSession = session; + }, analystSession); + + await page.route('**/api/**', (route) => fulfillJson(route, {})); + await page.route('**/platform/envsettings.json', (route) => fulfillJson(route, mockConfig)); + await page.route('**/platform/i18n/*.json', (route) => fulfillJson(route, {})); + await page.route('**/config.json', (route) => fulfillJson(route, mockConfig)); + await page.route('**/.well-known/openid-configuration', (route) => + fulfillJson(route, { + issuer: 'https://127.0.0.1:4400/authority', + authorization_endpoint: 'https://127.0.0.1:4400/authority/connect/authorize', + token_endpoint: 'https://127.0.0.1:4400/authority/connect/token', + jwks_uri: 'https://127.0.0.1:4400/authority/.well-known/jwks.json', + response_types_supported: ['code'], + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'], + }), + ); + await page.route('**/authority/.well-known/jwks.json', (route) => fulfillJson(route, { keys: [] })); + await page.route('**/console/branding**', (route) => + fulfillJson(route, { + tenantId: analystSession.tenant, + appName: 'Stella Ops', + logoUrl: null, + cssVariables: {}, + }), + ); + await page.route('**/console/profile**', (route) => + fulfillJson(route, { + subjectId: analystSession.subjectId, + username: 'findings-compare-e2e', + displayName: 'Findings Compare E2E', + tenant: analystSession.tenant, + roles: ['security-analyst'], + scopes: analystSession.scopes, + }), + ); + await page.route('**/console/token/introspect**', (route) => + fulfillJson(route, { + active: true, + tenant: analystSession.tenant, + subject: analystSession.subjectId, + scopes: analystSession.scopes, + }), + ); + await page.route('**/authority/console/tenants**', (route) => + fulfillJson(route, { + tenants: [ + { + tenantId: analystSession.tenant, + displayName: 'Demo Prod', + isDefault: true, + isActive: true, + }, + ], + }), + ); + await page.route('**/api/v2/context/regions**', (route) => + fulfillJson(route, [{ regionId: 'us-east', displayName: 'US East', sortOrder: 1, enabled: true }]), + ); + await page.route('**/api/v2/context/environments**', (route) => + fulfillJson(route, [ + { + environmentId: 'stage', + regionId: 'us-east', + environmentType: 'stage', + displayName: 'Stage', + sortOrder: 1, + enabled: true, + }, + ]), + ); + await page.route('**/api/v2/context/preferences**', (route) => + fulfillJson(route, { + tenantId: analystSession.tenant, + actorId: analystSession.subjectId, + regions: ['us-east'], + environments: ['stage'], + timeWindow: '24h', + stage: 'all', + updatedAt: '2026-03-08T10:00:00Z', + updatedBy: analystSession.subjectId, + }), + ); + await page.route(/\/api\/compare\/baselines\/active-scan(?:\?.*)?$/, async (route) => { + baselineCallCount += 1; + await fulfillJson(route, { + selectedDigest: null, + selectionReason: 'No baseline recommendations available for this scan', + alternatives: [], + autoSelectEnabled: true, + scanDigest: 'active-scan', + }); + }); + await page.route(/\/api\/compare\/delta(?:\?.*)?$/, async (route) => { + deltaCallCount += 1; + await fulfillJson(route, { categories: [], items: [] }); + }); + await page.route(/\/api\/compare\/evidence\/.*$/, async (route) => { + deltaCallCount += 1; + await fulfillJson(route, []); + }); + await page.route(/\/api\/v2\/security\/findings(?:\?.*)?$/, (route) => + fulfillJson(route, findingsResponse), + ); + + return { + baselineCalls: () => baselineCallCount, + deltaCalls: () => deltaCallCount, + }; +} + +test('security findings diff view shows truthful no-baseline state for the active scan', async ({ page }) => { + const calls = await setupHarness(page); + + await page.goto('/security/findings?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d', { + waitUntil: 'networkidle', + }); + + await expect(page.getByText('Comparing:')).toBeVisible(); + await expect(page.getByText('Active scan')).toBeVisible(); + await expect(page.getByText('No baselines available')).toBeVisible(); + await expect(page.getByText('No baseline recommendations available for this scan')).toBeVisible(); + await expect(page.getByText('Comparison evidence becomes available after a baseline is selected.')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Export' })).toBeDisabled(); + await expect(page.getByText('No immediate actions required')).toBeVisible(); + expect(calls.baselineCalls()).toBeGreaterThan(0); + expect(calls.deltaCalls()).toBe(0); +}); + +test('security findings detail view keeps live findings and hides the stale audit export action', async ({ page }) => { + await setupHarness(page); + + await page.goto('/security/findings?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&view=detail', { + waitUntil: 'networkidle', + }); + + await expect(page.getByText('finding-001')).toBeVisible(); + await expect(page.getByText('backend-api')).toBeVisible(); + await expect(page.getByText('2.5.0')).toBeVisible(); + await expect(page.getByText('Comparing:')).toHaveCount(0); + await expect(page.getByText('Export Audit Pack')).toHaveCount(0); +});