Consume weighted search answers and suppress dead chips

This commit is contained in:
master
2026-03-07 18:38:02 +02:00
parent 86a4928109
commit e295768662
9 changed files with 812 additions and 121 deletions

View File

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

View File

@@ -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) => {

View File

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