277 lines
8.9 KiB
TypeScript
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');
|
|
});
|
|
});
|