Search/AdvisoryAI and DAL conversion to EF finishes up. Preparation for microservices consolidation.

This commit is contained in:
master
2026-02-25 18:19:22 +02:00
parent 4db038123b
commit 63c70a6d37
447 changed files with 52257 additions and 2636 deletions

View File

@@ -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,
});
});
}