audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories

This commit is contained in:
master
2026-01-07 18:49:59 +02:00
parent 04ec098046
commit 608a7f85c0
866 changed files with 56323 additions and 6231 deletions

View File

@@ -0,0 +1,360 @@
// -----------------------------------------------------------------------------
// quiet-triage-workflow.e2e.spec.ts
// Sprint: SPRINT_20260106_004_001_FE_quiet_triage_ux_integration
// Task: T030 - E2E tests for complete workflow
// Description: End-to-end tests for the quiet-by-default triage workflow
// -----------------------------------------------------------------------------
import { test, expect, Page } from '@playwright/test';
const BASE_URL = process.env.E2E_BASE_URL || 'http://localhost:4200';
test.describe('Quiet Triage Workflow', () => {
let page: Page;
test.beforeEach(async ({ browser }) => {
page = await browser.newPage();
// Navigate to findings page
await page.goto(`${BASE_URL}/triage/findings`);
await page.waitForLoadState('networkidle');
});
test.afterEach(async () => {
await page.close();
});
test.describe('Lane Toggle', () => {
test('should default to Quiet lane showing only actionable findings', async () => {
// Verify Quiet lane is active by default
const quietButton = page.locator('app-triage-lane-toggle button:has-text("Actionable")');
await expect(quietButton).toHaveClass(/lane-toggle__btn--active/);
// Verify no gated findings are visible
const gatedBadges = page.locator('.gated-badge');
await expect(gatedBadges).toHaveCount(0);
});
test('should toggle to Review lane with single click', async () => {
// Click Review button
const reviewButton = page.locator('app-triage-lane-toggle button:has-text("Review")');
await reviewButton.click();
// Verify Review lane is now active
await expect(reviewButton).toHaveClass(/lane-toggle__btn--active/);
// Verify gated findings are now visible (if any exist)
const findingCards = page.locator('.finding-card');
const count = await findingCards.count();
if (count > 0) {
// All visible findings should be gated
const gatedCards = page.locator('.finding-card--gated');
await expect(gatedCards).toHaveCount(count);
}
});
test('should support Q keyboard shortcut for Quiet lane', async () => {
// First switch to Review
await page.locator('app-triage-lane-toggle button:has-text("Review")').click();
// Press Q key
await page.keyboard.press('q');
// Verify Quiet lane is active
const quietButton = page.locator('app-triage-lane-toggle button:has-text("Actionable")');
await expect(quietButton).toHaveClass(/lane-toggle__btn--active/);
});
test('should support R keyboard shortcut for Review lane', async () => {
// Press R key
await page.keyboard.press('r');
// Verify Review lane is active
const reviewButton = page.locator('app-triage-lane-toggle button:has-text("Review")');
await expect(reviewButton).toHaveClass(/lane-toggle__btn--active/);
});
});
test.describe('Gated Bucket Chips', () => {
test('should display bucket counts when on Review lane', async () => {
// Switch to Review lane
await page.locator('app-triage-lane-toggle button:has-text("Review")').click();
// Verify bucket chips are visible
const bucketChips = page.locator('app-gated-bucket-chips');
await expect(bucketChips).toBeVisible();
});
test('should filter by gating reason when chip is clicked', async () => {
// Switch to Review lane
await page.locator('app-triage-lane-toggle button:has-text("Review")').click();
// Click on a bucket chip (if available)
const unreachableChip = page.locator('.chip:has-text("Not Reachable")');
if (await unreachableChip.isVisible()) {
await unreachableChip.click();
// Verify filter is applied
const reasonFilter = page.locator('app-gating-reason-filter select');
await expect(reasonFilter).toHaveValue('Unreachable');
}
});
});
test.describe('Finding Selection and Breadcrumb', () => {
test('should display detail panel when finding is selected', async () => {
// Click on first finding
const firstFinding = page.locator('.finding-card').first();
await firstFinding.click();
// Verify detail panel is visible
const detailPanel = page.locator('.detail-panel');
await expect(detailPanel).toBeVisible();
});
test('should display provenance breadcrumb in detail panel', async () => {
// Select a finding
await page.locator('.finding-card').first().click();
// Verify breadcrumb is visible
const breadcrumb = page.locator('app-provenance-breadcrumb');
await expect(breadcrumb).toBeVisible();
});
test('should navigate breadcrumb levels on click', async () => {
// Select a finding
await page.locator('.finding-card').first().click();
// Click on layer level in breadcrumb
const layerLink = page.locator('.breadcrumb-item:has-text("layer")');
if (await layerLink.isVisible()) {
await layerLink.click();
// Verify navigation event (could trigger a modal or navigation)
// This depends on implementation
}
});
});
test.describe('Decision Drawer', () => {
test('should open decision drawer when Record Decision is clicked', async () => {
// Select a finding
await page.locator('.finding-card').first().click();
// Click Record Decision button
const recordButton = page.locator('button:has-text("Record Decision")');
await recordButton.click();
// Verify drawer is open
const drawer = page.locator('app-decision-drawer-enhanced.open, .decision-drawer.open');
await expect(drawer).toBeVisible();
});
test('should support A/N/U keyboard shortcuts in drawer', async () => {
// Select a finding and open drawer
await page.locator('.finding-card').first().click();
await page.locator('button:has-text("Record Decision")').click();
// Wait for drawer to be visible
await page.waitForSelector('.decision-drawer.open');
// Press 'N' for Not Affected
await page.keyboard.press('n');
// Verify Not Affected is selected
const notAffectedOption = page.locator('.radio-option:has-text("Not Affected")');
await expect(notAffectedOption).toHaveClass(/selected/);
});
test('should close drawer on Escape key', async () => {
// Open drawer
await page.locator('.finding-card').first().click();
await page.locator('button:has-text("Record Decision")').click();
await page.waitForSelector('.decision-drawer.open');
// Press Escape
await page.keyboard.press('Escape');
// Verify drawer is closed
const drawer = page.locator('.decision-drawer.open');
await expect(drawer).not.toBeVisible();
});
test('should show undo toast after submitting decision', async () => {
// This test requires mocking the API
// Skipping for now as it needs backend integration
test.skip();
});
});
test.describe('Evidence Export', () => {
test('should display export button in detail panel', async () => {
// Select a finding
await page.locator('.finding-card').first().click();
// Verify export button is visible
const exportButton = page.locator('app-export-evidence-button');
await expect(exportButton).toBeVisible();
});
test('should show progress indicator when export is triggered', async () => {
// Select a finding
await page.locator('.finding-card').first().click();
// Click export button
const exportButton = page.locator('app-export-evidence-button button');
if (await exportButton.isEnabled()) {
await exportButton.click();
// Verify progress indicator appears (may need API mock)
const progress = page.locator('.export-progress');
// Progress might appear briefly before completion
}
});
});
test.describe('Accessibility', () => {
test('should have proper ARIA labels on lane toggle', async () => {
const laneToggle = page.locator('app-triage-lane-toggle [role="tablist"]');
await expect(laneToggle).toHaveAttribute('aria-label', 'Triage lane selection');
const tabs = page.locator('app-triage-lane-toggle [role="tab"]');
const count = await tabs.count();
expect(count).toBe(2);
});
test('should support keyboard navigation in findings list', async () => {
// Focus on first finding
const firstFinding = page.locator('.finding-card').first();
await firstFinding.focus();
// Press Enter to select
await page.keyboard.press('Enter');
// Verify detail panel opens
const detailPanel = page.locator('.detail-panel');
await expect(detailPanel).toBeVisible();
});
test('should work in high contrast mode', async () => {
// Emulate high contrast mode
await page.emulateMedia({ colorScheme: 'dark' });
// Verify page still renders correctly
const pageContent = page.locator('.findings-page');
await expect(pageContent).toBeVisible();
// Verify critical elements have sufficient contrast
const severityBadge = page.locator('.severity-badge').first();
if (await severityBadge.isVisible()) {
// Badge should have border in high contrast mode
const styles = await severityBadge.evaluate(el =>
window.getComputedStyle(el).borderWidth
);
// This assertion depends on CSS implementation
}
});
});
test.describe('Performance', () => {
test('should render findings list within 2 seconds', async () => {
const startTime = Date.now();
await page.goto(`${BASE_URL}/triage/findings`);
await page.waitForSelector('.finding-card', { timeout: 2000 });
const renderTime = Date.now() - startTime;
expect(renderTime).toBeLessThan(2000);
});
test('should display skeleton UI while loading', async () => {
// This test requires slow network simulation
await page.route('**/api/v1/triage/**', async route => {
await new Promise(resolve => setTimeout(resolve, 500));
await route.continue();
});
await page.goto(`${BASE_URL}/triage/findings`);
// Skeleton should be visible during loading
const skeleton = page.locator('.skeleton, [class*="loading"]');
// Assertion depends on skeleton implementation
});
});
});
test.describe('Complete Triage Workflow', () => {
test('full workflow: view -> toggle -> select -> breadcrumb -> export', async ({ page }) => {
// 1. Navigate to findings
await page.goto(`${BASE_URL}/triage/findings`);
await page.waitForLoadState('networkidle');
// 2. Verify default is Quiet lane
const quietButton = page.locator('app-triage-lane-toggle button:has-text("Actionable")');
await expect(quietButton).toHaveClass(/lane-toggle__btn--active/);
// 3. Toggle to Review lane
const reviewButton = page.locator('app-triage-lane-toggle button:has-text("Review")');
await reviewButton.click();
await expect(reviewButton).toHaveClass(/lane-toggle__btn--active/);
// 4. Toggle back to Quiet lane
await quietButton.click();
await expect(quietButton).toHaveClass(/lane-toggle__btn--active/);
// 5. Select a finding
const firstFinding = page.locator('.finding-card').first();
if (await firstFinding.isVisible()) {
await firstFinding.click();
// 6. Verify breadcrumb is shown
const breadcrumb = page.locator('app-provenance-breadcrumb');
await expect(breadcrumb).toBeVisible();
// 7. Verify export button is available
const exportButton = page.locator('app-export-evidence-button');
await expect(exportButton).toBeVisible();
}
});
test('approval workflow: select -> open drawer -> submit -> verify toast', async ({ page }) => {
// Mock the approval API
await page.route('**/api/v1/scans/*/approvals', route => {
route.fulfill({
status: 201,
contentType: 'application/json',
body: JSON.stringify({
id: 'approval-123',
createdAt: new Date().toISOString(),
}),
});
});
await page.goto(`${BASE_URL}/triage/findings`);
await page.waitForLoadState('networkidle');
// Select a finding
const firstFinding = page.locator('.finding-card').first();
if (await firstFinding.isVisible()) {
await firstFinding.click();
// Open decision drawer
await page.locator('button:has-text("Record Decision")').click();
await page.waitForSelector('.decision-drawer.open');
// Select status
await page.keyboard.press('n'); // Not Affected
// Select reason
const reasonSelect = page.locator('.reason-select');
await reasonSelect.selectOption('vulnerable_code_not_present');
// Submit decision
const submitButton = page.locator('button:has-text("Sign & Apply")');
await submitButton.click();
// Verify undo toast appears
const undoToast = page.locator('.undo-toast');
await expect(undoToast).toBeVisible({ timeout: 5000 });
}
});
});

View File

@@ -0,0 +1,249 @@
/**
* @license
* Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
*/
import { test, expect } from '@playwright/test';
test.describe('Timeline Feature', () => {
const mockTimelineResponse = {
correlationId: 'test-corr-001',
events: [
{
eventId: 'evt-001',
tHlc: '1704067200000:0:node1',
tsWall: '2024-01-01T12:00:00Z',
correlationId: 'test-corr-001',
service: 'Scheduler',
kind: 'EXECUTE',
payload: '{"jobId": "job-001"}',
payloadDigest: 'abc123',
engineVersion: { name: 'Test', version: '1.0.0', digest: 'def456' },
schemaVersion: 1,
},
{
eventId: 'evt-002',
tHlc: '1704067201000:0:node1',
tsWall: '2024-01-01T12:00:01Z',
correlationId: 'test-corr-001',
service: 'AirGap',
kind: 'IMPORT',
payload: '{}',
payloadDigest: 'ghi789',
engineVersion: { name: 'Test', version: '1.0.0', digest: 'jkl012' },
schemaVersion: 1,
},
],
totalCount: 2,
hasMore: false,
};
const mockCriticalPathResponse = {
correlationId: 'test-corr-001',
totalDurationMs: 5000,
stages: [
{
stage: 'ENQUEUE->EXECUTE',
service: 'Scheduler',
durationMs: 1000,
percentage: 20,
fromHlc: '1704067200000:0:node1',
toHlc: '1704067201000:0:node1',
},
],
};
test.beforeEach(async ({ page }) => {
// Mock API responses
await page.route('**/api/v1/timeline/test-corr-001', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockTimelineResponse),
});
});
await page.route('**/api/v1/timeline/test-corr-001/critical-path', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockCriticalPathResponse),
});
});
});
test('should display timeline page', async ({ page }) => {
await page.goto('/timeline/test-corr-001');
// Check page title
await expect(page.locator('h1')).toContainText('Timeline');
// Check correlation ID is displayed
await expect(page.locator('.correlation-id')).toContainText('test-corr-001');
});
test('should display causal lanes with events', async ({ page }) => {
await page.goto('/timeline/test-corr-001');
// Wait for events to load
await expect(page.locator('.lane')).toHaveCount(2);
// Check lane names
await expect(page.locator('.lane-name').first()).toBeVisible();
});
test('should display critical path', async ({ page }) => {
await page.goto('/timeline/test-corr-001');
// Check critical path is rendered
await expect(page.locator('.critical-path-container')).toBeVisible();
await expect(page.locator('.stage-bar')).toHaveCount(1);
});
test('should select event and show details', async ({ page }) => {
await page.goto('/timeline/test-corr-001');
// Wait for events
await expect(page.locator('.event-marker').first()).toBeVisible();
// Click on an event
await page.locator('.event-marker').first().click();
// Check detail panel shows event info
await expect(page.locator('.event-detail-panel')).toBeVisible();
await expect(page.locator('.event-id code')).toContainText('evt-001');
});
test('should filter by service', async ({ page }) => {
await page.goto('/timeline/test-corr-001');
// Open service filter
await page.locator('mat-select').first().click();
// Select Scheduler
await page.locator('mat-option').filter({ hasText: 'Scheduler' }).click();
// Press escape to close
await page.keyboard.press('Escape');
// Check URL params updated
await expect(page).toHaveURL(/services=Scheduler/);
});
test('should support keyboard navigation', async ({ page }) => {
await page.goto('/timeline/test-corr-001');
// Wait for events
await expect(page.locator('.event-marker').first()).toBeVisible();
// Tab to first event
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
// Focus should be on event marker
const focused = page.locator('.event-marker:focus');
await expect(focused).toBeVisible();
// Press enter to select
await page.keyboard.press('Enter');
// Detail panel should show
await expect(page.locator('.event-detail-panel .event-id')).toBeVisible();
});
test('should export timeline', async ({ page }) => {
// Mock export endpoints
await page.route('**/api/v1/timeline/test-corr-001/export', async (route) => {
await route.fulfill({
status: 202,
contentType: 'application/json',
body: JSON.stringify({
exportId: 'exp-001',
correlationId: 'test-corr-001',
format: 'ndjson',
signBundle: false,
status: 'INITIATED',
estimatedEventCount: 2,
}),
});
});
await page.route('**/api/v1/timeline/export/exp-001', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
exportId: 'exp-001',
status: 'COMPLETED',
format: 'ndjson',
eventCount: 2,
fileSizeBytes: 1024,
}),
});
});
await page.route('**/api/v1/timeline/export/exp-001/download', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/x-ndjson',
body: '{"event": "test"}\n',
});
});
await page.goto('/timeline/test-corr-001');
// Click export button
await page.locator('button:has-text("Export")').click();
// Select NDJSON option
await page.locator('button:has-text("NDJSON")').first().click();
// Wait for download (or snackbar)
await expect(page.locator('text=Export downloaded successfully')).toBeVisible({
timeout: 10000,
});
});
test('should handle empty state', async ({ page }) => {
await page.goto('/timeline');
// Should show empty state
await expect(page.locator('.empty-state')).toBeVisible();
await expect(page.locator('.empty-state p')).toContainText(
'Enter a correlation ID'
);
});
test('should handle error state', async ({ page }) => {
// Mock error response
await page.route('**/api/v1/timeline/error-test', async (route) => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal server error' }),
});
});
await page.goto('/timeline/error-test');
// Should show error state
await expect(page.locator('.error-state')).toBeVisible();
});
test('should be accessible', async ({ page }) => {
await page.goto('/timeline/test-corr-001');
// Check ARIA labels
await expect(page.locator('[role="main"]')).toHaveAttribute(
'aria-label',
'Event Timeline'
);
await expect(page.locator('[role="region"]').first()).toBeVisible();
// Check focus indicators
await page.keyboard.press('Tab');
const focusedElement = page.locator(':focus');
await expect(focusedElement).toBeVisible();
});
});