Search/AdvisoryAI and DAL conversion to EF finishes up. Preparation for microservices consolidation.
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import {
|
||||
buildResponse,
|
||||
emptyResponse,
|
||||
mockSearchApiDynamic,
|
||||
setupAuthenticatedSession,
|
||||
setupBasicMocks,
|
||||
waitForEntityCards,
|
||||
waitForResults,
|
||||
} from './unified-search-fixtures';
|
||||
|
||||
const newcomerCard = {
|
||||
entityKey: 'cve:CVE-2024-21626',
|
||||
entityType: 'finding',
|
||||
domain: 'findings',
|
||||
title: 'CVE-2024-21626 in api-gateway',
|
||||
snippet: 'Reachable critical vulnerability detected in production workload.',
|
||||
score: 0.96,
|
||||
severity: 'critical',
|
||||
actions: [
|
||||
{
|
||||
label: 'Open finding',
|
||||
actionType: 'navigate',
|
||||
route: '/triage/findings/fnd-9001',
|
||||
isPrimary: true,
|
||||
},
|
||||
],
|
||||
sources: ['findings'],
|
||||
metadata: {},
|
||||
};
|
||||
|
||||
const healthySearchResponse = buildResponse(
|
||||
'critical findings',
|
||||
[newcomerCard],
|
||||
{
|
||||
summary: 'One critical finding matched. Ask AdvisoryAI for triage guidance.',
|
||||
template: 'finding_overview',
|
||||
confidence: 'high',
|
||||
sourceCount: 1,
|
||||
domainsCovered: ['findings'],
|
||||
},
|
||||
);
|
||||
|
||||
const degradedSearchResponse = {
|
||||
...buildResponse('critical findings', [newcomerCard]),
|
||||
diagnostics: {
|
||||
ftsMatches: 1,
|
||||
vectorMatches: 0,
|
||||
entityCardCount: 1,
|
||||
durationMs: 21,
|
||||
usedVector: false,
|
||||
mode: 'legacy-fallback',
|
||||
},
|
||||
};
|
||||
|
||||
const recoveredSearchResponse = {
|
||||
...buildResponse('CVE-2024-21626', [newcomerCard]),
|
||||
diagnostics: {
|
||||
ftsMatches: 4,
|
||||
vectorMatches: 2,
|
||||
entityCardCount: 1,
|
||||
durationMs: 37,
|
||||
usedVector: true,
|
||||
mode: 'hybrid',
|
||||
},
|
||||
};
|
||||
|
||||
test.describe('Assistant entry and search reliability', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupBasicMocks(page);
|
||||
await setupAuthenticatedSession(page);
|
||||
await mockChatEndpoints(page);
|
||||
});
|
||||
|
||||
test('healthy newcomer flow: search -> Ask AI -> search more -> action route', async ({ page }) => {
|
||||
await mockSearchApiDynamic(page, {
|
||||
'critical findings': healthySearchResponse,
|
||||
'cve-2024-21626': recoveredSearchResponse,
|
||||
}, recoveredSearchResponse);
|
||||
|
||||
await page.goto('/security/triage');
|
||||
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
const searchInput = page.locator('app-global-search input[type="text"]');
|
||||
await searchInput.focus();
|
||||
await expect(page.locator('.search__results')).toBeVisible();
|
||||
|
||||
const criticalSuggestion = page.locator('.search__suggestions .search__chip', {
|
||||
hasText: /critical findings/i,
|
||||
}).first();
|
||||
await expect(criticalSuggestion).toBeVisible();
|
||||
const suggestedQuery = (await criticalSuggestion.textContent())?.trim() || 'critical findings';
|
||||
await searchInput.fill(suggestedQuery);
|
||||
await expect(searchInput).toHaveValue(/critical findings/i);
|
||||
await waitForResults(page);
|
||||
await waitForEntityCards(page, 1);
|
||||
|
||||
await page.locator('.entity-card__action--ask-ai').first().click();
|
||||
const assistantDrawer = page.locator('.assistant-drawer');
|
||||
await expect(assistantDrawer).toBeVisible();
|
||||
await expect(assistantDrawer).toBeFocused();
|
||||
|
||||
const searchMoreButton = page.locator('.search-more-link');
|
||||
await expect(searchMoreButton).toBeVisible({ timeout: 10_000 });
|
||||
await searchMoreButton.click();
|
||||
|
||||
await expect(assistantDrawer).toBeHidden();
|
||||
await page.waitForResponse((response) => {
|
||||
if (!response.url().includes('/api/v1/search/query')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const body = response.request().postData() ?? '';
|
||||
return body.toLowerCase().includes('cve-2024-21626');
|
||||
});
|
||||
await expect(searchInput).toHaveValue(/CVE-2024-21626/i);
|
||||
await searchInput.focus();
|
||||
await expect(searchInput).toBeFocused();
|
||||
// Re-trigger the current query without Enter to avoid auto-select side effects.
|
||||
await searchInput.fill('CVE-2024-21626 ');
|
||||
await searchInput.press('Backspace');
|
||||
await waitForResults(page);
|
||||
await expect(page.locator('app-entity-card').first()).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
await page.locator('.entity-card__action--primary').first().click();
|
||||
await expect(page).toHaveURL(/\/security\/findings\/fnd-9001/i);
|
||||
});
|
||||
|
||||
test('degraded mode is visible and clears after recovery; focus order remains deterministic', async ({ page }) => {
|
||||
await mockSearchApiDynamic(page, {
|
||||
'critical findings': degradedSearchResponse,
|
||||
'cve-2024-21626': recoveredSearchResponse,
|
||||
}, recoveredSearchResponse);
|
||||
|
||||
await page.goto('/security/triage');
|
||||
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
const searchInput = page.locator('app-global-search input[type="text"]');
|
||||
await searchInput.focus();
|
||||
const criticalSuggestion = page.locator('.search__suggestions .search__chip', {
|
||||
hasText: /critical findings/i,
|
||||
}).first();
|
||||
await expect(criticalSuggestion).toBeVisible();
|
||||
const suggestedQuery = (await criticalSuggestion.textContent())?.trim() || 'critical findings';
|
||||
await searchInput.fill(suggestedQuery);
|
||||
await expect(searchInput).toHaveValue(/critical findings/i);
|
||||
await waitForResults(page);
|
||||
await waitForEntityCards(page, 1);
|
||||
|
||||
const degradedBanner = page.locator('.search__degraded-banner');
|
||||
await expect(degradedBanner).toBeVisible({ timeout: 10_000 });
|
||||
await expect(degradedBanner).toContainText(/fallback mode/i);
|
||||
|
||||
await searchInput.fill('CVE-2024-21626');
|
||||
await waitForResults(page);
|
||||
await waitForEntityCards(page, 1);
|
||||
await expect(degradedBanner).toBeHidden();
|
||||
|
||||
await page.locator('.entity-card__action--ask-ai').first().click();
|
||||
const assistantDrawer = page.locator('.assistant-drawer');
|
||||
await expect(assistantDrawer).toBeVisible();
|
||||
await expect(assistantDrawer).toBeFocused();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(assistantDrawer).toBeHidden();
|
||||
await expect(page.locator('.assistant-fab')).toBeFocused();
|
||||
});
|
||||
});
|
||||
|
||||
async function mockChatEndpoints(page: Page): Promise<void> {
|
||||
await page.route('**/api/v1/advisory-ai/conversations', async (route) => {
|
||||
if (route.request().method() !== 'POST') {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([]),
|
||||
});
|
||||
}
|
||||
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
conversationId: 'conv-newcomer-1',
|
||||
tenantId: 'test-tenant',
|
||||
userId: 'tester',
|
||||
context: {},
|
||||
turns: [],
|
||||
createdAt: '2026-02-25T00:00:00.000Z',
|
||||
updatedAt: '2026-02-25T00:00:00.000Z',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/v1/advisory-ai/conversations/*/turns', async (route) => {
|
||||
if (route.request().method() !== 'POST') {
|
||||
return route.continue();
|
||||
}
|
||||
|
||||
const ssePayload = [
|
||||
'event: progress',
|
||||
'data: {"stage":"searching"}',
|
||||
'',
|
||||
'event: token',
|
||||
'data: {"content":"CVE-2024-21626 remains relevant for this finding. "}',
|
||||
'',
|
||||
'event: citation',
|
||||
'data: {"type":"finding","path":"CVE-2024-21626","verified":true}',
|
||||
'',
|
||||
'event: done',
|
||||
'data: {"turnId":"turn-newcomer-1","groundingScore":0.92}',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'text/event-stream; charset=utf-8',
|
||||
'cache-control': 'no-cache',
|
||||
},
|
||||
body: ssePayload,
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user