Files
git.stella-ops.org/src/Web/StellaOps.Web/tests/e2e/unified-search-fixtures.ts
2026-03-08 08:50:38 +02:00

683 lines
19 KiB
TypeScript

// -----------------------------------------------------------------------------
// unified-search-fixtures.ts
// Shared mock data & helpers for all unified search e2e test suites.
// -----------------------------------------------------------------------------
import type { Page, Route } from '@playwright/test';
import { policyAuthorSession } from '../../src/app/testing';
// ---------------------------------------------------------------------------
// Auth / config fixtures
// ---------------------------------------------------------------------------
export const mockConfig = {
authority: {
issuer: 'https://authority.local',
clientId: 'stella-ops-ui',
authorizeEndpoint: 'https://authority.local/connect/authorize',
tokenEndpoint: 'https://authority.local/connect/token',
logoutEndpoint: 'https://authority.local/connect/logout',
redirectUri: 'http://127.0.0.1:4400/auth/callback',
postLogoutRedirectUri: 'http://127.0.0.1:4400/',
scope:
'openid profile email ui.read advisory:read advisory-ai:view advisory-ai:operate findings:read vex:read policy:read health:read',
audience: 'https://scanner.local',
dpopAlgorithms: ['ES256'],
refreshLeewaySeconds: 60,
},
apiBaseUrls: {
authority: 'https://authority.local',
scanner: 'https://scanner.local',
policy: 'https://policy.local',
concelier: 'https://concelier.local',
attestor: 'https://attestor.local',
gateway: 'https://gateway.local',
},
quickstartMode: true,
setup: 'complete',
};
export const oidcConfig = {
issuer: mockConfig.authority.issuer,
authorization_endpoint: mockConfig.authority.authorizeEndpoint,
token_endpoint: mockConfig.authority.tokenEndpoint,
jwks_uri: 'https://authority.local/.well-known/jwks.json',
response_types_supported: ['code'],
subject_types_supported: ['public'],
id_token_signing_alg_values_supported: ['RS256'],
};
export const shellSession = {
...policyAuthorSession,
scopes: [
...new Set([
...policyAuthorSession.scopes,
'ui.read',
'admin',
'advisory:read',
'advisory-ai:view',
'advisory-ai:operate',
'findings:read',
'vex:read',
'policy:read',
'health:read',
]),
],
};
// ---------------------------------------------------------------------------
// Page setup helpers
// ---------------------------------------------------------------------------
export async function setupBasicMocks(page: Page) {
const jsonStubUnlessDocument = (defaultGetBody: unknown = []): ((route: Route) => Promise<void>) => {
return async (route) => {
if (route.request().resourceType() === 'document') {
await route.fallback();
return;
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(route.request().method() === 'GET' ? defaultGetBody : {}),
});
};
};
page.on('console', (message) => {
if (message.type() !== 'error') return;
const text = message.text();
if (text.includes('status of 404') || text.includes('HttpErrorResponse')) return;
console.log('[browser:error]', text);
});
await page.route('**/config.json', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockConfig),
}),
);
await page.route('**/platform/envsettings.json', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockConfig),
}),
);
await page.route('https://authority.local/**', (route) => {
const url = route.request().url();
if (url.includes('/.well-known/openid-configuration')) {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(oidcConfig),
});
}
if (url.includes('/.well-known/jwks.json')) {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ keys: [] }),
});
}
if (url.includes('authorize')) {
return route.abort();
}
return route.fulfill({ status: 400, body: 'blocked' });
});
await page.route('**/api/**', jsonStubUnlessDocument());
await page.route('**/gateway/**', jsonStubUnlessDocument());
await page.route('**/policy/**', jsonStubUnlessDocument());
await page.route('**/scanner/**', jsonStubUnlessDocument());
await page.route('**/concelier/**', jsonStubUnlessDocument());
await page.route('**/attestor/**', jsonStubUnlessDocument());
await page.route('**/api/v1/search/suggestions/evaluate', async (route) => {
const body = (route.request().postDataJSON() as {
queries?: string[];
ambient?: { currentRoute?: string };
} | null) ?? {};
const queries = body.queries ?? [];
const currentScopeDomain = resolveMockScopeDomain(body.ambient?.currentRoute);
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
suggestions: queries.map((query) => ({
query,
viable: true,
status: 'grounded',
code: 'retrieved_scope_weighted_evidence',
cardCount: 1,
leadingDomain: currentScopeDomain,
reason: 'Evidence is available for this suggestion.',
})),
coverage: {
currentScopeDomain,
currentScopeWeighted: true,
domains: [
{
domain: currentScopeDomain,
candidateCount: Math.max(1, queries.length),
visibleCardCount: Math.max(1, queries.length),
topScore: 0.9,
isCurrentScope: true,
hasVisibleResults: true,
},
],
},
}),
});
});
}
export async function setupAuthenticatedSession(page: Page) {
await page.addInitScript((stubSession) => {
(window as any).__stellaopsTestSession = stubSession;
}, shellSession);
}
export async function waitForShell(page: Page) {
await page.goto('/');
await page.locator('aside.sidebar').waitFor({ state: 'visible', timeout: 15_000 });
}
// ---------------------------------------------------------------------------
// Search interaction helpers
// ---------------------------------------------------------------------------
/** Intercepts unified search and replies with the given fixture. */
export async function mockSearchApi(page: Page, responseBody: unknown) {
await page.route('**/search/query**', (route) => {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(responseBody),
});
});
}
/**
* Intercepts unified search and replies with a fixture chosen at
* runtime based on `q`/`query` from JSON body or URL params. This allows a single test
* to issue multiple different queries and receive different mock responses.
*/
export async function mockSearchApiDynamic(
page: Page,
responseMap: Record<string, unknown>,
fallback?: unknown,
) {
await page.route('**/search/query**', async (route) => {
try {
const request = route.request();
let q = '';
const rawPostData = request.postData();
if (rawPostData) {
try {
const body = JSON.parse(rawPostData);
q = String(body.q ?? body.query ?? '');
} catch {
q = '';
}
}
if (!q) {
const url = new URL(request.url());
q = String(url.searchParams.get('q') ?? url.searchParams.get('query') ?? '');
}
const key = Object.keys(responseMap).find((k) => q.toLowerCase().includes(k.toLowerCase()));
const payload = key ? responseMap[key] : (fallback ?? emptyResponse(q));
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(payload),
});
} catch {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(fallback ?? emptyResponse('')),
});
}
});
}
export async function typeInSearch(page: Page, query: string) {
const input = page.locator('app-global-search input[type="text"]');
await input.focus();
await input.fill(query);
return input;
}
export async function waitForResults(page: Page) {
await page.locator('.search__results').waitFor({ state: 'visible', timeout: 10_000 });
const loading = page.locator('.search__loading');
if ((await loading.count()) > 0) {
await loading.first().waitFor({ state: 'hidden', timeout: 8_000 }).catch(() => {
// Ignore timeout here; some result states intentionally do not render loading.
});
}
}
export async function waitForEntityCards(page: Page, count?: number) {
const cards = page.locator(
'.search__cards app-entity-card:visible, [role="list"] [role="listitem"]:visible, [role="listbox"] [role="option"]:visible',
);
const waitTimeoutMs = 8_000;
const waitForCards = async () => {
if (count !== undefined) {
await cards.nth(count - 1).waitFor({ state: 'visible', timeout: waitTimeoutMs });
} else {
await cards.first().waitFor({ state: 'visible', timeout: waitTimeoutMs });
}
};
try {
await waitForCards();
} catch (firstError) {
// Retry once by re-triggering the current query input in case the
// debounced search call was dropped under long stress runs.
try {
const input = page.locator('app-global-search input[type="text"]');
if ((await input.count()) > 0) {
const searchInput = input.first();
const current = await searchInput.inputValue();
if (current.trim().length >= 2) {
await searchInput.focus();
await searchInput.fill(current);
await searchInput.press(' ');
await searchInput.press('Backspace');
await searchInput.press('Enter').catch(() => {
// Some environments do not bind Enter on the input.
});
await waitForResults(page);
}
}
} catch {
// Ignore retry interaction errors and still attempt one more card wait.
}
try {
await waitForCards();
} catch {
throw firstError;
}
}
return cards;
}
// ---------------------------------------------------------------------------
// Mock response builders
// ---------------------------------------------------------------------------
export interface CardFixture {
entityKey: string;
entityType: string;
domain: string;
title: string;
snippet: string;
score: number;
severity?: string;
actions: Array<{
label: string;
actionType: string;
route?: string;
command?: string;
isPrimary: boolean;
}>;
sources: string[];
metadata?: Record<string, string>;
}
export function buildResponse(
query: string,
cards: CardFixture[],
synthesis?: {
summary: string;
template: string;
confidence: string;
sourceCount: number;
domainsCovered: string[];
},
options?: {
suggestions?: Array<{ text: string; reason: string; domain?: string; candidateCount?: number }>;
contextAnswer?: {
status: 'grounded' | 'clarify' | 'insufficient';
code: string;
summary: string;
reason: string;
evidence: string;
citations?: Array<{ entityKey: string; title: string; domain: string; route?: string }>;
questions?: Array<{ query: string; kind?: string }>;
};
overflow?: {
currentScopeDomain: string;
reason: string;
cards: CardFixture[];
};
coverage?: {
currentScopeDomain?: string;
currentScopeWeighted: boolean;
domains: Array<{
domain: string;
candidateCount: number;
visibleCardCount: number;
topScore: number;
isCurrentScope: boolean;
hasVisibleResults: boolean;
}>;
};
},
) {
return {
query,
topK: 10,
cards,
synthesis: synthesis ?? {
summary: `Found ${cards.length} result(s) for "${query}".`,
template: cards.length > 0 ? 'mixed_overview' : 'empty',
confidence: cards.length >= 3 ? 'high' : cards.length >= 1 ? 'medium' : 'low',
sourceCount: cards.length,
domainsCovered: [...new Set(cards.map((c) => c.domain))],
},
diagnostics: {
ftsMatches: cards.length + 2,
vectorMatches: cards.length,
entityCardCount: cards.length,
durationMs: 38,
usedVector: true,
mode: 'hybrid',
},
suggestions: options?.suggestions,
contextAnswer: options?.contextAnswer,
overflow: options?.overflow,
coverage: options?.coverage,
};
}
export function emptyResponse(query: string) {
return buildResponse(query, [], {
summary: 'No results found for the given query.',
template: 'empty',
confidence: 'high',
sourceCount: 0,
domainsCovered: [],
});
}
// ---------------------------------------------------------------------------
// Domain-specific card factories
// ---------------------------------------------------------------------------
export function doctorCard(opts: {
checkId: string;
title: string;
snippet: string;
score?: number;
}): CardFixture {
return {
entityKey: `doctor:${opts.checkId}`,
entityType: 'doctor',
domain: 'knowledge',
title: opts.title,
snippet: opts.snippet,
score: opts.score ?? 0.85,
actions: [
{ label: 'Run Check', actionType: 'run', command: `stella doctor run ${opts.checkId}`, isPrimary: true },
{ label: 'View Docs', actionType: 'navigate', route: '/ops/operations/data-integrity', isPrimary: false },
],
sources: ['knowledge'],
};
}
export function findingCard(opts: {
cveId: string;
title: string;
snippet: string;
severity: string;
score?: number;
}): CardFixture {
return {
entityKey: `cve:${opts.cveId}`,
entityType: 'finding',
domain: 'findings',
title: opts.title,
snippet: opts.snippet,
score: opts.score ?? 0.92,
severity: opts.severity,
actions: [
{ label: 'View Finding', actionType: 'navigate', route: `/security/triage?q=${opts.cveId}`, isPrimary: true },
{ label: 'Copy CVE', actionType: 'copy', command: opts.cveId, isPrimary: false },
],
sources: ['findings'],
};
}
export function secretCard(opts: {
title: string;
snippet: string;
severity: string;
score?: number;
}): CardFixture {
return {
entityKey: `secret:${opts.title.toLowerCase().replace(/\s+/g, '-')}`,
entityType: 'finding',
domain: 'findings',
title: opts.title,
snippet: opts.snippet,
score: opts.score ?? 0.88,
severity: opts.severity,
actions: [
{ label: 'View Secret', actionType: 'navigate', route: '/security/triage?type=secret', isPrimary: true },
],
sources: ['findings'],
};
}
export function vexCard(opts: {
cveId: string;
status: string;
title: string;
snippet: string;
score?: number;
}): CardFixture {
return {
entityKey: `vex:${opts.cveId}`,
entityType: 'vex_statement',
domain: 'vex',
title: opts.title,
snippet: opts.snippet,
score: opts.score ?? 0.80,
actions: [
{
label: 'View VEX',
actionType: 'navigate',
route: `/security/advisories-vex?q=${opts.cveId}`,
isPrimary: true,
},
],
sources: ['vex'],
metadata: { status: opts.status },
};
}
export function policyCard(opts: {
ruleId: string;
title: string;
snippet: string;
score?: number;
}): CardFixture {
return {
entityKey: `policy:${opts.ruleId}`,
entityType: 'policy_rule',
domain: 'policy',
title: opts.title,
snippet: opts.snippet,
score: opts.score ?? 0.78,
actions: [
{
label: 'View Rule',
actionType: 'navigate',
route: `/ops/policy?rule=${opts.ruleId}`,
isPrimary: true,
},
{ label: 'Simulate', actionType: 'navigate', route: `/ops/policy/simulate`, isPrimary: false },
],
sources: ['policy'],
};
}
export function timelineCard(opts: {
eventId: string;
title: string;
snippet: string;
score?: number;
}): CardFixture {
return {
entityKey: `timeline:${opts.eventId}`,
entityType: 'ops_event',
domain: 'timeline',
title: opts.title,
snippet: opts.snippet,
score: opts.score ?? 0.81,
actions: [
{
label: 'Open event',
actionType: 'navigate',
route: `/ops/timeline/${opts.eventId}`,
isPrimary: true,
},
{
label: 'Open audit trail',
actionType: 'navigate',
route: `/evidence/audit-log/events/${opts.eventId}`,
isPrimary: false,
},
],
sources: ['timeline'],
};
}
export function releaseCard(opts: {
releaseId: string;
title: string;
snippet: string;
score?: number;
}): CardFixture {
return {
entityKey: `release:${opts.releaseId}`,
entityType: 'platform_entity',
domain: 'platform',
title: opts.title,
snippet: opts.snippet,
score: opts.score ?? 0.84,
actions: [
{
label: 'Open release',
actionType: 'navigate',
route: `/releases/${opts.releaseId}`,
isPrimary: true,
},
{
label: 'Open approvals',
actionType: 'navigate',
route: '/releases/approvals',
isPrimary: false,
},
],
sources: ['platform'],
};
}
export function docsCard(opts: {
docPath: string;
title: string;
snippet: string;
score?: number;
}): CardFixture {
return {
entityKey: `docs:${opts.docPath}`,
entityType: 'docs',
domain: 'knowledge',
title: opts.title,
snippet: opts.snippet,
score: opts.score ?? 0.70,
actions: [
{ label: 'Open', actionType: 'navigate', route: `/docs/${opts.docPath}`, isPrimary: true },
],
sources: ['knowledge'],
};
}
export function apiCard(opts: {
endpoint: string;
method: string;
title: string;
snippet: string;
score?: number;
}): CardFixture {
return {
entityKey: `api:${opts.method}:${opts.endpoint}`,
entityType: 'api',
domain: 'knowledge',
title: opts.title,
snippet: opts.snippet,
score: opts.score ?? 0.75,
actions: [
{ label: 'Try API', actionType: 'curl', command: `curl -X ${opts.method} ${opts.endpoint}`, isPrimary: true },
{ label: 'Open Docs', actionType: 'navigate', route: `/docs/api`, isPrimary: false },
],
sources: ['knowledge'],
};
}
function resolveMockScopeDomain(currentRoute?: string): string {
const normalizedRoute = (currentRoute ?? '').trim().toLowerCase();
if (normalizedRoute.includes('/ops/policy/vex')
|| normalizedRoute.includes('/security/advisories-vex')
|| normalizedRoute.includes('/vex-hub')) {
return 'vex';
}
if (normalizedRoute.includes('/ops/policy')) {
return 'policy';
}
if (normalizedRoute.includes('/ops/operations/doctor')
|| normalizedRoute.includes('/ops/operations/system-health')) {
return 'knowledge';
}
if (normalizedRoute.includes('/ops/timeline')
|| normalizedRoute.includes('/evidence/audit-log')
|| normalizedRoute.includes('/audit')) {
return 'timeline';
}
if (normalizedRoute.includes('/releases')
|| normalizedRoute.includes('/mission-control')) {
return 'platform';
}
if (normalizedRoute.includes('/ops/graph')
|| normalizedRoute.includes('/security/reach')) {
return 'graph';
}
if (normalizedRoute.includes('/ops/operations/jobs')
|| normalizedRoute.includes('/ops/operations/scheduler')) {
return 'ops_memory';
}
return 'findings';
}