/** * @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'); }); });