context detemrinistic + randomized searches and fix for setup from stella-ops.local rather 127.1.0.*
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import {
|
||||
buildResponse,
|
||||
setupAuthenticatedSession,
|
||||
setupBasicMocks,
|
||||
typeInSearch,
|
||||
waitForEntityCards,
|
||||
waitForResults,
|
||||
} from './unified-search-fixtures';
|
||||
|
||||
const criticalFindingCard = {
|
||||
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: '/security/triage?q=CVE-2024-21626',
|
||||
isPrimary: true,
|
||||
},
|
||||
],
|
||||
sources: ['findings'],
|
||||
metadata: {},
|
||||
};
|
||||
|
||||
const criticalFindingsResponse = buildResponse(
|
||||
'critical findings',
|
||||
[criticalFindingCard],
|
||||
{
|
||||
summary: 'One critical finding matched. Ask AdvisoryAI for triage guidance.',
|
||||
template: 'finding_overview',
|
||||
confidence: 'high',
|
||||
sourceCount: 1,
|
||||
domainsCovered: ['findings'],
|
||||
},
|
||||
);
|
||||
|
||||
const cveFollowupResponse = buildResponse(
|
||||
'CVE-2024-21626',
|
||||
[criticalFindingCard],
|
||||
{
|
||||
summary: 'Follow-up query for CVE-2024-21626 returned one finding.',
|
||||
template: 'finding_overview',
|
||||
confidence: 'high',
|
||||
sourceCount: 1,
|
||||
domainsCovered: ['findings'],
|
||||
},
|
||||
);
|
||||
|
||||
test.describe('Unified Search - Contextual Suggestions', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupBasicMocks(page);
|
||||
await setupAuthenticatedSession(page);
|
||||
});
|
||||
|
||||
test('updates empty-state chips automatically when route changes', async ({ page }) => {
|
||||
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({ timeout: 10_000 });
|
||||
await expect(page.locator('.search__suggestions .search__chip', {
|
||||
hasText: /critical findings/i,
|
||||
}).first()).toBeVisible();
|
||||
|
||||
await page.goto('/ops/policy');
|
||||
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
|
||||
await searchInput.focus();
|
||||
await expect(page.locator('.search__results')).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.locator('.search__suggestions .search__chip', {
|
||||
hasText: /failing policy gates/i,
|
||||
}).first()).toBeVisible();
|
||||
|
||||
await page.goto('/ops/timeline');
|
||||
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
|
||||
await searchInput.focus();
|
||||
await expect(page.locator('.search__results')).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.locator('.search__suggestions .search__chip', {
|
||||
hasText: /failed deployments/i,
|
||||
}).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('promotes follow-up chips from recent search actions on the same page scope', async ({ page }) => {
|
||||
await page.route('**/search/query**', (route) => {
|
||||
const payload = route.request().postDataJSON() as { q?: string };
|
||||
const query = String(payload?.q ?? '');
|
||||
const response = query.toLowerCase().includes('cve-2024-21626')
|
||||
? cveFollowupResponse
|
||||
: criticalFindingsResponse;
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(response),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/security/triage');
|
||||
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
await typeInSearch(page, 'CVE-2024-21626');
|
||||
await waitForResults(page);
|
||||
await waitForEntityCards(page, 1);
|
||||
|
||||
await page.locator('app-entity-card').first().click();
|
||||
await expect(page).toHaveURL(/\/security\/triage\?q=CVE-2024-21626/i);
|
||||
|
||||
const searchInput = page.locator('app-global-search input[type="text"]');
|
||||
await searchInput.focus();
|
||||
await expect(page.locator('.search__results')).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
await expect(page.locator('.search__suggestions .search__chip', {
|
||||
hasText: /follow up:\s*CVE-2024-21626/i,
|
||||
}).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('chat search-for-more emits ambient lastAction and route context in follow-up search requests', async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockChatEndpoints(page);
|
||||
|
||||
const capturedRequests: Array<Record<string, unknown>> = [];
|
||||
await page.route('**/search/query**', (route) => {
|
||||
const body = route.request().postDataJSON() as Record<string, unknown>;
|
||||
capturedRequests.push(body);
|
||||
const query = String(body['q'] ?? '').toLowerCase();
|
||||
const response = query.includes('critical findings')
|
||||
? criticalFindingsResponse
|
||||
: cveFollowupResponse;
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(response),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/security/triage');
|
||||
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
await typeInSearch(page, 'critical findings');
|
||||
await waitForResults(page);
|
||||
await waitForEntityCards(page, 1);
|
||||
|
||||
await page.locator('.entity-card__action--ask-ai').first().click();
|
||||
await expect(page.locator('.assistant-drawer')).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
await page.locator('.search-more-link').click();
|
||||
|
||||
await expect.poll(() =>
|
||||
capturedRequests.some((request) =>
|
||||
String(request['q'] ?? '').toLowerCase().includes('cve-2024-21626')),
|
||||
).toBe(true);
|
||||
|
||||
const handoffRequest = capturedRequests.find((request) =>
|
||||
String(request['q'] ?? '').toLowerCase().includes('cve-2024-21626'));
|
||||
const ambient = handoffRequest?.['ambient'] as Record<string, unknown> | undefined;
|
||||
const lastAction = ambient?.['lastAction'] as Record<string, unknown> | undefined;
|
||||
|
||||
expect(ambient).toBeDefined();
|
||||
expect(String(ambient?.['currentRoute'] ?? '')).toContain('/security/triage');
|
||||
expect(lastAction?.['action']).toBe('chat_search_for_more');
|
||||
expect(lastAction?.['source']).toBe('advisory_ai_chat');
|
||||
expect(lastAction?.['domain']).toBe('findings');
|
||||
});
|
||||
});
|
||||
|
||||
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-context-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-context-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