Consume weighted search answers and suppress dead chips
This commit is contained in:
@@ -162,6 +162,58 @@ test.describe('Unified Search - Contextual Suggestions', () => {
|
||||
await expect(page.locator('app-entity-card').first()).toContainText(/cve-2024-21626/i);
|
||||
});
|
||||
|
||||
test('suppresses non-viable contextual chips before they are shown', async ({ page }) => {
|
||||
await page.route('**/search/suggestions/evaluate**', async (route) => {
|
||||
const body = route.request().postDataJSON() as { queries?: string[] };
|
||||
const queries = body.queries ?? [];
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
suggestions: queries.map((query) => ({
|
||||
query,
|
||||
viable: !/how do i deploy/i.test(query),
|
||||
status: /how do i deploy/i.test(query) ? 'insufficient' : 'grounded',
|
||||
code: /how do i deploy/i.test(query) ? 'no_grounded_evidence' : 'retrieved_scope_weighted_evidence',
|
||||
cardCount: /how do i deploy/i.test(query) ? 0 : 1,
|
||||
leadingDomain: /critical findings/i.test(query) ? 'findings' : 'vex',
|
||||
reason: /how do i deploy/i.test(query)
|
||||
? 'No grounded evidence matched the suggestion in the active corpus.'
|
||||
: 'Evidence is available for this suggestion.',
|
||||
})),
|
||||
coverage: {
|
||||
currentScopeDomain: 'findings',
|
||||
currentScopeWeighted: true,
|
||||
domains: [
|
||||
{
|
||||
domain: 'findings',
|
||||
candidateCount: 4,
|
||||
visibleCardCount: 1,
|
||||
topScore: 0.96,
|
||||
isCurrentScope: true,
|
||||
hasVisibleResults: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
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 waitForResults(page);
|
||||
|
||||
await expect(page.locator('.search__suggestions .search__chip', {
|
||||
hasText: /^How do I deploy\?$/i,
|
||||
})).toHaveCount(0);
|
||||
await expect(page.locator('.search__suggestions .search__chip', {
|
||||
hasText: /critical findings/i,
|
||||
}).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('chat search-for-more emits ambient lastAction and route context in follow-up search requests', async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -54,6 +54,74 @@ const policyBlockerResponse = buildResponse(
|
||||
},
|
||||
);
|
||||
|
||||
const weightedOverflowResponse = buildResponse(
|
||||
'critical findings',
|
||||
[
|
||||
findingCard({
|
||||
cveId: 'CVE-2024-21626',
|
||||
title: 'CVE-2024-21626 in api-gateway',
|
||||
snippet: 'Reachable critical vulnerability remains the strongest match for this page.',
|
||||
severity: 'critical',
|
||||
}),
|
||||
],
|
||||
undefined,
|
||||
{
|
||||
contextAnswer: {
|
||||
status: 'grounded',
|
||||
code: 'retrieved_scope_weighted_evidence',
|
||||
summary: 'Current-page findings matched first, with one policy blocker held as related overflow.',
|
||||
reason: 'The search weighted the active findings page first.',
|
||||
evidence: 'Grounded in 2 sources across Findings and Policy.',
|
||||
citations: [
|
||||
{
|
||||
entityKey: 'cve:CVE-2024-21626',
|
||||
title: 'CVE-2024-21626 in api-gateway',
|
||||
domain: 'findings',
|
||||
},
|
||||
],
|
||||
questions: [
|
||||
{
|
||||
query: 'What evidence blocks this release?',
|
||||
kind: 'follow_up',
|
||||
},
|
||||
],
|
||||
},
|
||||
overflow: {
|
||||
currentScopeDomain: 'findings',
|
||||
reason: 'Policy results remain relevant but are weaker than the current findings context.',
|
||||
cards: [
|
||||
policyCard({
|
||||
ruleId: 'POL-118',
|
||||
title: 'POL-118 release blocker',
|
||||
snippet: 'Production rollout is blocked while this CVE remains unresolved.',
|
||||
}),
|
||||
],
|
||||
},
|
||||
coverage: {
|
||||
currentScopeDomain: 'findings',
|
||||
currentScopeWeighted: true,
|
||||
domains: [
|
||||
{
|
||||
domain: 'findings',
|
||||
candidateCount: 3,
|
||||
visibleCardCount: 1,
|
||||
topScore: 0.96,
|
||||
isCurrentScope: true,
|
||||
hasVisibleResults: true,
|
||||
},
|
||||
{
|
||||
domain: 'policy',
|
||||
candidateCount: 1,
|
||||
visibleCardCount: 1,
|
||||
topScore: 0.74,
|
||||
isCurrentScope: false,
|
||||
hasVisibleResults: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
test.describe('Unified Search - Experience Quality UX', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupBasicMocks(page);
|
||||
@@ -192,6 +260,29 @@ test.describe('Unified Search - Experience Quality UX', () => {
|
||||
await expect(page.locator('.search__group').filter({ hasText: 'Recent' })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('uses backend answer framing and shows overflow as secondary results without manual filters', async ({ page }) => {
|
||||
await mockSearchResponses(page, (query) =>
|
||||
query.includes('critical findings')
|
||||
? weightedOverflowResponse
|
||||
: emptyResponse(query));
|
||||
|
||||
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 expect(page.locator('[data-answer-status="grounded"]')).toContainText(
|
||||
/current-page findings matched first/i,
|
||||
);
|
||||
await expect(page.locator('.search__scope-hint')).toContainText(/weighted toward findings/i);
|
||||
await expect(page.locator('[data-overflow-results]')).toContainText(/also relevant outside findings/i);
|
||||
await expect(page.locator('[data-overflow-results]')).toContainText(/policy results remain relevant/i);
|
||||
await expect(page.locator('[data-role="domain-filter"]')).toHaveCount(0);
|
||||
await expect(page.locator('app-synthesis-panel')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('returns structured next-step policy searches back into global search with metadata', async ({ page }) => {
|
||||
const capturedRequests: Array<Record<string, unknown>> = [];
|
||||
await page.route('**/search/query**', async (route) => {
|
||||
|
||||
@@ -287,6 +287,35 @@ export function buildResponse(
|
||||
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,
|
||||
@@ -307,6 +336,10 @@ export function buildResponse(
|
||||
usedVector: true,
|
||||
mode: 'hybrid',
|
||||
},
|
||||
suggestions: options?.suggestions,
|
||||
contextAnswer: options?.contextAnswer,
|
||||
overflow: options?.overflow,
|
||||
coverage: options?.coverage,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user