Files
git.stella-ops.org/src/Web/StellaOps.Web/e2e/playbook-suggestions.e2e.spec.ts
2026-01-10 20:38:13 +02:00

277 lines
8.9 KiB
TypeScript

/**
* @file playbook-suggestions.e2e.spec.ts
* @sprint SPRINT_20260107_006_005_FE (OM-FE-006)
* @description E2E tests for OpsMemory playbook suggestions in decision drawer.
*/
import { test, expect } from '@playwright/test';
test.describe('Playbook Suggestions in Decision Drawer', () => {
test.beforeEach(async ({ page }) => {
// Mock the OpsMemory API
await page.route('**/api/v1/opsmemory/suggestions*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
suggestions: [
{
suggestedAction: 'accept_risk',
confidence: 0.85,
rationale: 'Similar situations resolved successfully with risk acceptance',
evidenceCount: 5,
matchingFactors: ['severity', 'reachability', 'componentType'],
evidence: [
{
memoryId: 'mem-abc123',
cveId: 'CVE-2023-44487',
action: 'accept_risk',
outcome: 'success',
resolutionTime: 'PT4H',
similarity: 0.92,
},
{
memoryId: 'mem-def456',
cveId: 'CVE-2023-12345',
action: 'accept_risk',
outcome: 'success',
resolutionTime: 'PT2H',
similarity: 0.87,
},
],
},
{
suggestedAction: 'target_fix',
confidence: 0.65,
rationale: 'Some similar situations required fixes',
evidenceCount: 2,
matchingFactors: ['severity'],
evidence: [
{
memoryId: 'mem-ghi789',
cveId: 'CVE-2023-99999',
action: 'target_fix',
outcome: 'success',
resolutionTime: 'P1DT4H',
similarity: 0.70,
},
],
},
],
situationHash: 'abc123def456',
}),
});
});
// Navigate to triage page (mock or real)
await page.goto('/triage/findings/test-finding-123');
});
test('playbook panel appears in decision drawer', async ({ page }) => {
// Open the decision drawer
await page.click('[data-testid="open-decision-drawer"]');
// Check playbook panel is visible
const playbookPanel = page.locator('stellaops-playbook-suggestion');
await expect(playbookPanel).toBeVisible();
// Check header text
await expect(playbookPanel.locator('.playbook-panel__title')).toContainText(
'Past Decisions'
);
});
test('shows suggestions with confidence badges', async ({ page }) => {
await page.click('[data-testid="open-decision-drawer"]');
// Wait for suggestions to load
await page.waitForResponse('**/api/v1/opsmemory/suggestions*');
// Check first suggestion
const firstSuggestion = page.locator('.playbook-suggestion').first();
await expect(firstSuggestion).toBeVisible();
// Check action badge
await expect(
firstSuggestion.locator('.playbook-suggestion__action')
).toContainText('Accept Risk');
// Check confidence
await expect(
firstSuggestion.locator('.playbook-suggestion__confidence')
).toContainText('85%');
});
test('clicking "Use This Approach" pre-fills decision form', async ({
page,
}) => {
await page.click('[data-testid="open-decision-drawer"]');
await page.waitForResponse('**/api/v1/opsmemory/suggestions*');
// Click "Use This Approach" on first suggestion
await page.click('.playbook-btn--use');
// Verify form was pre-filled
const statusRadio = page.locator('input[name="status"][value="not_affected"]');
await expect(statusRadio).toBeChecked();
// Check reason notes contain suggestion context
const reasonText = page.locator('.reason-text');
await expect(reasonText).toContainText('similar past decisions');
});
test('expanding evidence details shows past decisions', async ({ page }) => {
await page.click('[data-testid="open-decision-drawer"]');
await page.waitForResponse('**/api/v1/opsmemory/suggestions*');
// Click "Show Details" on first suggestion
await page.click('.playbook-btn--expand');
// Check evidence cards are visible
const evidenceCards = page.locator('stellaops-evidence-card');
await expect(evidenceCards).toHaveCount(2);
// Check evidence content
const firstCard = evidenceCards.first();
await expect(firstCard.locator('.evidence-card__cve')).toContainText(
'CVE-2023-44487'
);
await expect(firstCard.locator('.evidence-card__similarity')).toContainText(
'92%'
);
});
test('shows empty state when no suggestions', async ({ page }) => {
// Override route to return empty
await page.route('**/api/v1/opsmemory/suggestions*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
suggestions: [],
situationHash: 'empty123',
}),
});
});
await page.click('[data-testid="open-decision-drawer"]');
await page.waitForResponse('**/api/v1/opsmemory/suggestions*');
// Check empty state message
await expect(page.locator('.playbook-empty')).toContainText(
'No similar past decisions'
);
});
test('handles API errors gracefully', async ({ page }) => {
// Override route to return error
await page.route('**/api/v1/opsmemory/suggestions*', async (route) => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ message: 'Internal server error' }),
});
});
await page.click('[data-testid="open-decision-drawer"]');
// Wait for error state
await expect(page.locator('.playbook-error')).toBeVisible();
// Check retry button
const retryBtn = page.locator('.playbook-error__retry');
await expect(retryBtn).toBeVisible();
});
test('retry button refetches suggestions', async ({ page }) => {
let callCount = 0;
await page.route('**/api/v1/opsmemory/suggestions*', async (route) => {
callCount++;
if (callCount === 1) {
await route.fulfill({ status: 500 });
} else {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
suggestions: [
{
suggestedAction: 'accept_risk',
confidence: 0.85,
rationale: 'Test',
evidenceCount: 1,
matchingFactors: [],
evidence: [],
},
],
situationHash: 'retry123',
}),
});
}
});
await page.click('[data-testid="open-decision-drawer"]');
// Wait for error state
await expect(page.locator('.playbook-error')).toBeVisible();
// Click retry
await page.click('.playbook-error__retry');
// Should now show suggestions
await expect(page.locator('.playbook-suggestion')).toBeVisible();
});
test('keyboard navigation works', async ({ page }) => {
await page.click('[data-testid="open-decision-drawer"]');
await page.waitForResponse('**/api/v1/opsmemory/suggestions*');
// Focus on playbook panel header
await page.focus('.playbook-panel__header');
// Press Enter to toggle (if collapsed)
await page.keyboard.press('Enter');
// Tab to first suggestion's "Use This Approach" button
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
// Press Enter to use suggestion
await page.keyboard.press('Enter');
// Verify form was pre-filled
const statusRadio = page.locator('input[name="status"][value="not_affected"]');
await expect(statusRadio).toBeChecked();
});
test('panel can be collapsed and expanded', async ({ page }) => {
await page.click('[data-testid="open-decision-drawer"]');
await page.waitForResponse('**/api/v1/opsmemory/suggestions*');
// Panel content should be visible
await expect(page.locator('.playbook-panel__content')).toBeVisible();
// Click header to collapse
await page.click('.playbook-panel__header');
// Content should be hidden
await expect(page.locator('.playbook-panel__content')).not.toBeVisible();
// Click again to expand
await page.click('.playbook-panel__header');
// Content should be visible again
await expect(page.locator('.playbook-panel__content')).toBeVisible();
});
test('matching factors are displayed', async ({ page }) => {
await page.click('[data-testid="open-decision-drawer"]');
await page.waitForResponse('**/api/v1/opsmemory/suggestions*');
const factors = page.locator('.playbook-suggestion__factor');
await expect(factors).toHaveCount(3);
await expect(factors.first()).toContainText('severity');
});
});