Render clarify search prompts as guidance only
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# Sprint 20260308-011 - FE Search Clarify Guidance Not Query
|
||||
|
||||
## Topic & Scope
|
||||
- Stop rendering clarify prompts as executable search suggestions when they are only asking the operator to narrow the target.
|
||||
- Fix the release route wording so `/releases/**` no longer suggests dead natural-language queries like "Which environment or release should I narrow this to?".
|
||||
- Cover the behavior with component and browser tests so dead clarify prompts do not regress back into the shipped search experience.
|
||||
- Working directory: `src/Web/StellaOps.Web`.
|
||||
- Expected evidence: component tests, Playwright regression, and updated UI contract docs.
|
||||
|
||||
## Dependencies & Concurrency
|
||||
- Depends on the archived search rollout/correction sprints and the archived live search setup lane.
|
||||
- Safe parallelism: do not edit unrelated Router, release cutover, or audit-bundle files while executing this search-only fix.
|
||||
- Cross-module allowance: documentation updates may touch `docs/modules/ui/**` when clarifying the search self-serve contract.
|
||||
|
||||
## Documentation Prerequisites
|
||||
- `src/Web/StellaOps.Web/AGENTS.md`
|
||||
- `docs/modules/ui/search-self-serve-contract.md`
|
||||
- `docs/modules/ui/search-zero-learning-primary-entry.md`
|
||||
|
||||
## Delivery Tracker
|
||||
|
||||
### FE-CLARIFY-001 - Treat clarify prompts as guidance, not runnable search chips
|
||||
Status: DONE
|
||||
Dependency: none
|
||||
Owners: Developer (FE), QA
|
||||
Task description:
|
||||
- Update the global search answer panel so clarify prompts are rendered as guidance hints instead of clickable search chips.
|
||||
- Keep grounded follow-up questions executable; only the clarify branch should stop behaving like a query launcher.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Clarify prompts no longer execute searches when rendered from the answer panel.
|
||||
- [x] Grounded follow-up questions remain executable.
|
||||
- [x] Search UX still offers actionable next steps through viable starters and related searches.
|
||||
|
||||
### FE-CLARIFY-002 - Fix release-route clarify wording and regression coverage
|
||||
Status: DONE
|
||||
Dependency: FE-CLARIFY-001
|
||||
Owners: Developer (FE), Test Automation
|
||||
Task description:
|
||||
- Replace the release clarify wording with guidance phrasing instead of a literal natural-language question.
|
||||
- Add unit and Playwright coverage proving the clarify UI is guidance-only and release users do not see the old dead-query text as an executable suggestion.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Release clarify copy is guidance-oriented.
|
||||
- [x] Unit coverage proves clarify prompts are guidance-only.
|
||||
- [x] Playwright regression covers the clarify-answer rendering path.
|
||||
|
||||
### FE-CLARIFY-003 - Document and close
|
||||
Status: DONE
|
||||
Dependency: FE-CLARIFY-002
|
||||
Owners: Documentation, Project Manager
|
||||
Task description:
|
||||
- Update the UI search contract docs to state that clarify prompts are explanatory narrowing guidance, not executable searches.
|
||||
- Archive the sprint after implementation and evidence are recorded.
|
||||
|
||||
Completion criteria:
|
||||
- [x] UI docs reflect the clarify-guidance behavior.
|
||||
- [x] Execution log captures code/test evidence.
|
||||
- [x] Sprint is archived only after all tasks are DONE.
|
||||
|
||||
## Execution Log
|
||||
| Date (UTC) | Update | Owner |
|
||||
| --- | --- | --- |
|
||||
| 2026-03-08 | Sprint created to stop dead clarify prompts from being surfaced like runnable searches. | Developer |
|
||||
| 2026-03-08 | Updated global search clarify rendering to guidance-only rows, changed route clarifier copy to narrowing guidance, and kept grounded follow-up questions executable. | Developer |
|
||||
| 2026-03-08 | Verified focused FE coverage: `ambient-context.service.spec.ts` + `global-search.component.spec.ts` passed `37/37`; Playwright clarify regression pack passed `18/18`. | QA |
|
||||
|
||||
## Decisions & Risks
|
||||
- Decision: fix this at the search contract level rather than patching only `/releases/versions`.
|
||||
- Risk: clarify prompts currently share the same rendering path as grounded follow-up questions.
|
||||
- Mitigation: split the clarify-answer rendering path while keeping grounded follow-ups and related searches untouched.
|
||||
- Decision: clarify prompts are now guidance copy everywhere they are generated, so the UI never presents a dead natural-language query as an executable chip.
|
||||
- Docs: `docs/modules/ui/search-self-serve-contract.md`, `docs/modules/ui/search-zero-learning-primary-entry.md`.
|
||||
|
||||
## Next Checkpoints
|
||||
- 2026-03-08: clarify answer panel updated and covered by tests.
|
||||
- 2026-03-08: sprint archived after green regression coverage.
|
||||
@@ -42,7 +42,7 @@
|
||||
- `clarify`
|
||||
- `insufficient`
|
||||
- `grounded` states must expose visible evidence summary plus citations or top-result references.
|
||||
- `clarify` states must offer narrowing questions instead of a blank panel.
|
||||
- `clarify` states must offer narrowing guidance instead of a blank panel.
|
||||
- `insufficient` states must explain the lack of grounding and still provide credible next questions or next searches.
|
||||
|
||||
## Runtime behavior
|
||||
@@ -53,7 +53,7 @@
|
||||
- fallback -> related follow-up question
|
||||
- Result-state answer panels use:
|
||||
- `commonQuestions[]` as follow-up questions when grounded evidence exists
|
||||
- `clarifyingQuestions[]` when no grounded answer exists
|
||||
- `clarifyingQuestions[]` as non-executable narrowing guidance when no grounded answer exists
|
||||
- "Related searches" remains driven by contextual chip logic so pages do not need to define a second parallel action system.
|
||||
|
||||
## Optional telemetry hooks
|
||||
@@ -85,7 +85,7 @@
|
||||
2. Add unit coverage for answer-state rendering in `global-search.component.spec.ts`.
|
||||
3. Add Playwright coverage for:
|
||||
- grounded answer
|
||||
- clarify recovery
|
||||
- clarify guidance rendering
|
||||
- answer-to-AdvisoryAI handoff
|
||||
- priority-route rollout journeys (`findings`, `policy`, `doctor`, `timeline`, `releases`)
|
||||
4. Keep route and API behavior mocked/deterministic; no live network dependencies.
|
||||
|
||||
@@ -18,12 +18,14 @@
|
||||
- The current history contract cannot reliably remove old failed searches after reload because the local store still accepts legacy bare-string entries with no result outcome attached.
|
||||
- `Did you mean` is still visually tied to the results surface rather than the input correction moment. It needs to live immediately below the search field.
|
||||
- Suggestions are still too easy to surface without enough corpus proof. Search must treat corpus readiness and suggestion executability as a product requirement, not a test-only concern.
|
||||
- Clarify prompts can still be mistaken for executable searches when they are phrased like literal user queries. Those prompts must behave as narrowing guidance, not dead-end starter searches.
|
||||
|
||||
## What still fails after direct operator usage
|
||||
- Search and assistant still feel like sibling products in some flows. The operator should always start from search; assistant is a secondary detail view opened from that same surface.
|
||||
- Any visible "scope" mechanic is wrong for the default path. Current route, visible entities, and recent actions should weight results automatically. If the best answer is outside the current page, show it as overflow only after trying the current page first.
|
||||
- The product still risks teaching internal mechanics through labels, panels, and helper copy. The operator should not need to learn Stella structure, search science, or result modes just to get a useful answer.
|
||||
- Suggestions are not acceptable unless they execute. A surfaced starter chip that leads to zero useful results is a product defect even when the service is healthy.
|
||||
- Clarify text must not look like an executable starter search unless the backend has explicitly grounded it. Guidance such as "add an environment or release" belongs in the answer panel as guidance, not as a clickable query.
|
||||
- When multiple high-confidence results are close, search should summarize them automatically. The assistant exists to expand and deepen the answer, not to compensate for a weak primary result.
|
||||
|
||||
## Product rules
|
||||
|
||||
@@ -94,11 +94,11 @@ export const DEFAULT_COMMON_QUESTIONS: readonly SearchQuestionChip[] = withQuest
|
||||
export const DEFAULT_CLARIFYING_QUESTIONS: readonly SearchQuestionChip[] = withQuestionKind([
|
||||
{
|
||||
key: 'ui.search.question.default.clarify_target',
|
||||
fallback: 'Which workload, release, or policy should I narrow this to?',
|
||||
fallback: 'Add the workload, release, or policy you want to inspect.',
|
||||
},
|
||||
{
|
||||
key: 'ui.search.question.default.clarify_scope',
|
||||
fallback: 'What exact blocker or symptom should I narrow this to?',
|
||||
fallback: 'Add the exact blocker or symptom you want to inspect.',
|
||||
},
|
||||
], 'clarify');
|
||||
|
||||
@@ -153,8 +153,8 @@ const FINDINGS_SELF_SERVE: SearchSelfServeDefinition = {
|
||||
{ key: 'ui.search.question.findings.remediation', fallback: 'What is the safest remediation path?' },
|
||||
], 'page'),
|
||||
clarifyingQuestions: withQuestionKind([
|
||||
{ key: 'ui.search.question.findings.clarify_target', fallback: 'Which CVE, workload, or package should I narrow this to?' },
|
||||
{ key: 'ui.search.question.findings.clarify_scope', fallback: 'What part of this finding matters most right now?' },
|
||||
{ key: 'ui.search.question.findings.clarify_target', fallback: 'Add the CVE, workload, or package you want to inspect.' },
|
||||
{ key: 'ui.search.question.findings.clarify_scope', fallback: 'Add the part of the finding that matters most right now.' },
|
||||
], 'clarify'),
|
||||
};
|
||||
|
||||
@@ -165,8 +165,8 @@ const VEX_SELF_SERVE: SearchSelfServeDefinition = {
|
||||
{ key: 'ui.search.question.vex.conflicting_evidence', fallback: 'What evidence conflicts with this VEX?' },
|
||||
], 'page'),
|
||||
clarifyingQuestions: withQuestionKind([
|
||||
{ key: 'ui.search.question.vex.clarify_statement', fallback: 'Which statement, component, or product range should I narrow this to?' },
|
||||
{ key: 'ui.search.question.vex.clarify_need', fallback: 'Do you want exploitability meaning, coverage, or conflict evidence?' },
|
||||
{ key: 'ui.search.question.vex.clarify_statement', fallback: 'Add the statement, component, or product range you want to inspect.' },
|
||||
{ key: 'ui.search.question.vex.clarify_need', fallback: 'Add whether you need exploitability meaning, coverage, or conflict evidence.' },
|
||||
], 'clarify'),
|
||||
};
|
||||
|
||||
@@ -177,8 +177,8 @@ const POLICY_SELF_SERVE: SearchSelfServeDefinition = {
|
||||
{ key: 'ui.search.question.policy.exception', fallback: 'What is the safest exception path?' },
|
||||
], 'page'),
|
||||
clarifyingQuestions: withQuestionKind([
|
||||
{ key: 'ui.search.question.policy.clarify_target', fallback: 'Which rule, environment, or control should I narrow this to?' },
|
||||
{ key: 'ui.search.question.policy.clarify_need', fallback: 'Do you want recent failures, exceptions, or promotion impact?' },
|
||||
{ key: 'ui.search.question.policy.clarify_target', fallback: 'Add the rule, environment, or control you want to inspect.' },
|
||||
{ key: 'ui.search.question.policy.clarify_need', fallback: 'Add whether you need recent failures, exceptions, or promotion impact.' },
|
||||
], 'clarify'),
|
||||
};
|
||||
|
||||
@@ -189,8 +189,8 @@ const DOCTOR_SELF_SERVE: SearchSelfServeDefinition = {
|
||||
{ key: 'ui.search.question.doctor.changed', fallback: 'What changed before this health issue?' },
|
||||
], 'page'),
|
||||
clarifyingQuestions: withQuestionKind([
|
||||
{ key: 'ui.search.question.doctor.clarify_check', fallback: 'Which check or symptom should I narrow this to?' },
|
||||
{ key: 'ui.search.question.doctor.clarify_need', fallback: 'Do you want diagnosis, remediation, or verification steps?' },
|
||||
{ key: 'ui.search.question.doctor.clarify_check', fallback: 'Add the check or symptom you want to inspect.' },
|
||||
{ key: 'ui.search.question.doctor.clarify_need', fallback: 'Add whether you need diagnosis, remediation, or verification steps.' },
|
||||
], 'clarify'),
|
||||
};
|
||||
|
||||
@@ -201,8 +201,8 @@ const GRAPH_SELF_SERVE: SearchSelfServeDefinition = {
|
||||
{ key: 'ui.search.question.graph.next_hop', fallback: 'What should I inspect next on this path?' },
|
||||
], 'page'),
|
||||
clarifyingQuestions: withQuestionKind([
|
||||
{ key: 'ui.search.question.graph.clarify_node', fallback: 'Which node, package, or edge should I narrow this to?' },
|
||||
{ key: 'ui.search.question.graph.clarify_need', fallback: 'Do you want reachability, impact, or next-step guidance?' },
|
||||
{ key: 'ui.search.question.graph.clarify_node', fallback: 'Add the node, package, or edge you want to inspect.' },
|
||||
{ key: 'ui.search.question.graph.clarify_need', fallback: 'Add whether you need reachability, impact, or next-step guidance.' },
|
||||
], 'clarify'),
|
||||
};
|
||||
|
||||
@@ -213,8 +213,8 @@ const OPS_MEMORY_SELF_SERVE: SearchSelfServeDefinition = {
|
||||
{ key: 'ui.search.question.ops_memory.repeat', fallback: 'What repeated failures are related to this?' },
|
||||
], 'page'),
|
||||
clarifyingQuestions: withQuestionKind([
|
||||
{ key: 'ui.search.question.ops_memory.clarify_job', fallback: 'Which job, incident, or recurring failure should I narrow this to?' },
|
||||
{ key: 'ui.search.question.ops_memory.clarify_need', fallback: 'Do you want precedent, likely cause, or recommended recovery?' },
|
||||
{ key: 'ui.search.question.ops_memory.clarify_job', fallback: 'Add the job, incident, or recurring failure you want to inspect.' },
|
||||
{ key: 'ui.search.question.ops_memory.clarify_need', fallback: 'Add whether you need precedent, likely cause, or recommended recovery.' },
|
||||
], 'clarify'),
|
||||
};
|
||||
|
||||
@@ -225,8 +225,8 @@ const TIMELINE_SELF_SERVE: SearchSelfServeDefinition = {
|
||||
{ key: 'ui.search.question.timeline.next_event', fallback: 'What else happened around this event?' },
|
||||
], 'page'),
|
||||
clarifyingQuestions: withQuestionKind([
|
||||
{ key: 'ui.search.question.timeline.clarify_window', fallback: 'Which deployment, incident, or time window should I narrow this to?' },
|
||||
{ key: 'ui.search.question.timeline.clarify_need', fallback: 'Do you want causes, impacts, or follow-up events?' },
|
||||
{ key: 'ui.search.question.timeline.clarify_window', fallback: 'Add the deployment, incident, or time window you want to inspect.' },
|
||||
{ key: 'ui.search.question.timeline.clarify_need', fallback: 'Add whether you need causes, impacts, or follow-up events.' },
|
||||
], 'clarify'),
|
||||
};
|
||||
|
||||
@@ -237,8 +237,8 @@ const RELEASES_SELF_SERVE: SearchSelfServeDefinition = {
|
||||
{ key: 'ui.search.question.releases.next_step', fallback: 'What is the safest next step to ship?' },
|
||||
], 'page'),
|
||||
clarifyingQuestions: withQuestionKind([
|
||||
{ key: 'ui.search.question.releases.clarify_target', fallback: 'Which environment or release should I narrow this to?' },
|
||||
{ key: 'ui.search.question.releases.clarify_need', fallback: 'Do you want blockers, approvals, or policy impact?' },
|
||||
{ key: 'ui.search.question.releases.clarify_target', fallback: 'Add the environment or release you want to inspect.' },
|
||||
{ key: 'ui.search.question.releases.clarify_need', fallback: 'Add whether you need blockers, approvals, or policy impact.' },
|
||||
], 'clarify'),
|
||||
};
|
||||
|
||||
|
||||
@@ -78,6 +78,8 @@ type SearchAnswerView = {
|
||||
}>;
|
||||
questionLabel: string;
|
||||
questions: SearchQuestionView[];
|
||||
guidanceLabel: string | null;
|
||||
guidance: SearchQuestionView[];
|
||||
nextSearches: SearchSuggestionView[];
|
||||
};
|
||||
type SuccessfulSearchHistoryEntry = {
|
||||
@@ -202,6 +204,22 @@ type SuccessfulSearchHistoryEntry = {
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (answer.guidance.length > 0) {
|
||||
<div class="search__answer-guidance">
|
||||
<div class="search__group-label">{{ answer.guidanceLabel }}</div>
|
||||
<div class="search__guidance-list">
|
||||
@for (question of answer.guidance; track question.query) {
|
||||
<div
|
||||
class="search__guidance-item"
|
||||
data-answer-guidance="clarify"
|
||||
>
|
||||
{{ question.query }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (answer.nextSearches.length > 0) {
|
||||
<div class="search__answer-next">
|
||||
<div class="search__group-label">{{ t('ui.search.answer.next_searches', 'Related searches') }}</div>
|
||||
@@ -681,6 +699,26 @@ type SuccessfulSearchHistoryEntry = {
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.search__answer-guidance {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.search__guidance-list {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0.75rem 0;
|
||||
}
|
||||
|
||||
.search__guidance-item {
|
||||
padding: 0.5rem 0.625rem;
|
||||
border: 1px dashed var(--color-border-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface-primary);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.search__answer {
|
||||
margin: 0.625rem 0.75rem 0.5rem;
|
||||
padding: 0.75rem;
|
||||
@@ -1174,7 +1212,15 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
questionLabel: backendAnswer.status === 'clarify'
|
||||
? this.t('ui.search.answer.questions.clarify', 'Try one of these')
|
||||
: this.t('ui.search.answer.questions.follow_up', 'Related questions'),
|
||||
questions: this.buildBackendAnswerQuestions(backendAnswer),
|
||||
questions: backendAnswer.status === 'clarify'
|
||||
? []
|
||||
: this.buildBackendAnswerQuestions(backendAnswer),
|
||||
guidanceLabel: backendAnswer.status === 'clarify'
|
||||
? this.t('ui.search.answer.guidance.clarify', 'Narrow the search by adding')
|
||||
: null,
|
||||
guidance: backendAnswer.status === 'clarify'
|
||||
? this.buildClarifyGuidance(backendAnswer)
|
||||
: [],
|
||||
nextSearches,
|
||||
};
|
||||
}
|
||||
@@ -1191,6 +1237,8 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
citations: this.buildAnswerCitations(response),
|
||||
questionLabel: this.t('ui.search.answer.questions.follow_up', 'Related questions'),
|
||||
questions: this.commonQuestions().slice(0, 3),
|
||||
guidanceLabel: null,
|
||||
guidance: [],
|
||||
nextSearches,
|
||||
};
|
||||
}
|
||||
@@ -1211,8 +1259,10 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
'No direct grounded answer was found from the current page context.',
|
||||
),
|
||||
citations: [],
|
||||
questionLabel: this.t('ui.search.answer.questions.clarify', 'Try one of these'),
|
||||
questions: clarifyingQuestions,
|
||||
questionLabel: '',
|
||||
questions: [],
|
||||
guidanceLabel: this.t('ui.search.answer.guidance.clarify', 'Narrow the search by adding'),
|
||||
guidance: clarifyingQuestions,
|
||||
nextSearches,
|
||||
};
|
||||
}
|
||||
@@ -1233,6 +1283,8 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
citations: [],
|
||||
questionLabel: this.t('ui.search.answer.questions.retry', 'Try one of these'),
|
||||
questions: this.commonQuestions().slice(0, 2),
|
||||
guidanceLabel: null,
|
||||
guidance: [],
|
||||
nextSearches,
|
||||
};
|
||||
});
|
||||
@@ -1974,6 +2026,21 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
||||
: this.commonQuestions().slice(0, 3);
|
||||
}
|
||||
|
||||
private buildClarifyGuidance(answer: NonNullable<UnifiedSearchResponse['contextAnswer']>): SearchQuestionView[] {
|
||||
const backendGuidance = (answer.questions ?? [])
|
||||
.filter((question) => question.query.trim().length > 0)
|
||||
.map((question) => ({
|
||||
query: question.query,
|
||||
kind: 'clarify' as const,
|
||||
}));
|
||||
|
||||
if (backendGuidance.length > 0) {
|
||||
return backendGuidance.slice(0, 3);
|
||||
}
|
||||
|
||||
return this.clarifyingQuestions().slice(0, 3);
|
||||
}
|
||||
|
||||
private answerTitleForStatus(status: SearchAnswerView['status']): string {
|
||||
switch (status) {
|
||||
case 'clarify':
|
||||
|
||||
@@ -199,8 +199,8 @@ describe('AmbientContextService', () => {
|
||||
events.next(new NavigationEnd(1, '/ops/policy', '/ops/policy'));
|
||||
|
||||
const questions = service.getClarifyingQuestions().map((item) => item.fallback);
|
||||
expect(questions).toContain('Which rule, environment, or control should I narrow this to?');
|
||||
expect(questions).toContain('Do you want recent failures, exceptions, or promotion impact?');
|
||||
expect(questions).toContain('Add the rule, environment, or control you want to inspect.');
|
||||
expect(questions).toContain('Add whether you need recent failures, exceptions, or promotion impact.');
|
||||
});
|
||||
|
||||
it('returns timeline self-serve questions after navigating to the timeline route', () => {
|
||||
@@ -215,8 +215,8 @@ describe('AmbientContextService', () => {
|
||||
|
||||
expect(commonQuestions).toContain('What changed before this incident?');
|
||||
expect(commonQuestions).toContain('Which release introduced this risk?');
|
||||
expect(clarifyingQuestions).toContain('Which deployment, incident, or time window should I narrow this to?');
|
||||
expect(clarifyingQuestions).toContain('Do you want causes, impacts, or follow-up events?');
|
||||
expect(clarifyingQuestions).toContain('Add the deployment, incident, or time window you want to inspect.');
|
||||
expect(clarifyingQuestions).toContain('Add whether you need causes, impacts, or follow-up events.');
|
||||
});
|
||||
|
||||
it('returns release-control self-serve questions for release routes without forcing a search domain', () => {
|
||||
@@ -233,7 +233,7 @@ describe('AmbientContextService', () => {
|
||||
expect(panel?.titleFallback).toBe('Release control');
|
||||
expect(commonQuestions).toContain('What blocked this promotion?');
|
||||
expect(commonQuestions).toContain('Which approvals are missing?');
|
||||
expect(clarifyingQuestions).toContain('Which environment or release should I narrow this to?');
|
||||
expect(clarifyingQuestions).toContain('Do you want blockers, approvals, or policy impact?');
|
||||
expect(clarifyingQuestions).toContain('Add the environment or release you want to inspect.');
|
||||
expect(clarifyingQuestions).toContain('Add whether you need blockers, approvals, or policy impact.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -158,12 +158,12 @@ describe('GlobalSearchComponent', () => {
|
||||
ambientContext.getClarifyingQuestions.and.returnValue([
|
||||
{
|
||||
key: 'ui.search.question.findings.clarify_target',
|
||||
fallback: 'Which CVE, workload, or package should I narrow this to?',
|
||||
fallback: 'Add the CVE, workload, or package you want to inspect.',
|
||||
kind: 'clarify',
|
||||
},
|
||||
{
|
||||
key: 'ui.search.question.findings.clarify_scope',
|
||||
fallback: 'What part of this finding matters most right now?',
|
||||
fallback: 'Add the part of the finding that matters most right now.',
|
||||
kind: 'clarify',
|
||||
},
|
||||
]);
|
||||
@@ -807,11 +807,15 @@ describe('GlobalSearchComponent', () => {
|
||||
const answerQuestions = Array.from(
|
||||
fixture.nativeElement.querySelectorAll('[data-answer-question]') as NodeListOf<HTMLButtonElement>,
|
||||
).map((node) => node.textContent?.trim());
|
||||
const guidanceItems = Array.from(
|
||||
fixture.nativeElement.querySelectorAll('[data-answer-guidance="clarify"]') as NodeListOf<HTMLElement>,
|
||||
).map((node) => node.textContent?.trim());
|
||||
|
||||
expect(answerPanel).not.toBeNull();
|
||||
expect(answerPanel?.textContent).toContain('Tighten the question');
|
||||
expect(answerQuestions).toContain('Which CVE, workload, or package should I narrow this to?');
|
||||
expect(answerQuestions).toContain('What part of this finding matters most right now?');
|
||||
expect(answerQuestions).toEqual([]);
|
||||
expect(guidanceItems).toContain('Add the CVE, workload, or package you want to inspect.');
|
||||
expect(guidanceItems).toContain('Add the part of the finding that matters most right now.');
|
||||
});
|
||||
|
||||
it('does not hard-filter search requests to the current route scope', async () => {
|
||||
|
||||
@@ -342,6 +342,26 @@ test.describe('Unified Search - Experience Quality UX', () => {
|
||||
await expect(page.locator('.search__group').filter({ hasText: 'Recent' })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('renders release clarify guidance as non-executable hints on /releases/versions', async ({ page }) => {
|
||||
await mockSearchResponses(page, () => emptyResponse('mystery release blocker'));
|
||||
|
||||
await page.goto('/releases/versions');
|
||||
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
await typeInSearch(page, 'mystery release blocker');
|
||||
await waitForResults(page);
|
||||
|
||||
await expect(page.locator('[data-answer-status="clarify"]')).toBeVisible();
|
||||
await expect(page.locator('[data-answer-guidance="clarify"]').filter({
|
||||
hasText: /add the environment or release you want to inspect\./i,
|
||||
})).toHaveCount(1);
|
||||
await expect(page.locator('[data-answer-guidance="clarify"]').filter({
|
||||
hasText: /add whether you need blockers, approvals, or policy impact\./i,
|
||||
})).toHaveCount(1);
|
||||
await expect(page.getByRole('button', { name: 'Add the environment or release you want to inspect.' })).toHaveCount(0);
|
||||
await expect(page.locator('app-global-search input[type="text"]')).toHaveValue('mystery release blocker');
|
||||
});
|
||||
|
||||
test('uses backend answer framing and shows overflow as secondary results without manual filters', async ({ page }) => {
|
||||
await mockSearchResponses(page, (query) =>
|
||||
query.includes('critical findings')
|
||||
|
||||
@@ -230,48 +230,6 @@ const doctorGroundedResponse = buildResponse(
|
||||
},
|
||||
);
|
||||
|
||||
const timelineGroundedResponse = buildResponse(
|
||||
'Which deployment, incident, or time window should I narrow this to?',
|
||||
[
|
||||
timelineCard({
|
||||
eventId: 'incident-db-spike',
|
||||
title: 'Database latency spike before promotion',
|
||||
snippet: 'A deployment preceded the incident and introduced the latency spike that now blocks the rollout.',
|
||||
}),
|
||||
],
|
||||
undefined,
|
||||
{
|
||||
contextAnswer: {
|
||||
status: 'grounded',
|
||||
code: 'retrieved_scope_weighted_evidence',
|
||||
summary: 'The timeline shows a deployment immediately before the incident that introduced the blocking latency spike.',
|
||||
reason: 'Timeline evidence became grounded once the time window was narrowed.',
|
||||
evidence: 'Grounded in 1 source across Timeline.',
|
||||
citations: [
|
||||
{
|
||||
entityKey: 'timeline:incident-db-spike',
|
||||
title: 'Database latency spike before promotion',
|
||||
domain: 'timeline',
|
||||
},
|
||||
],
|
||||
},
|
||||
coverage: {
|
||||
currentScopeDomain: 'timeline',
|
||||
currentScopeWeighted: true,
|
||||
domains: [
|
||||
{
|
||||
domain: 'timeline',
|
||||
candidateCount: 1,
|
||||
visibleCardCount: 1,
|
||||
topScore: 0.9,
|
||||
isCurrentScope: true,
|
||||
hasVisibleResults: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const releasesGroundedResponse = buildResponse(
|
||||
'What blocked this promotion?',
|
||||
[
|
||||
@@ -437,7 +395,7 @@ test.describe('Unified Search - Priority Route Self-Serve Journeys', () => {
|
||||
}).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('recovers a timeline search from clarify to grounded with the route-owned narrowing question', async ({ page }) => {
|
||||
test('shows timeline narrowing guidance instead of a runnable clarify prompt', async ({ page }) => {
|
||||
await mockPriorityRouteSearch(page);
|
||||
|
||||
await openRoute(page, '/ops/timeline');
|
||||
@@ -445,14 +403,12 @@ test.describe('Unified Search - Priority Route Self-Serve Journeys', () => {
|
||||
await waitForResults(page);
|
||||
|
||||
await expect(page.locator('[data-answer-status="clarify"]')).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Which deployment, incident, or time window should I narrow this to?' }).click();
|
||||
await waitForResults(page);
|
||||
await waitForEntityCards(page, 1);
|
||||
|
||||
await expect(page.locator('app-global-search input[type="text"]')).toHaveValue(
|
||||
'Which deployment, incident, or time window should I narrow this to?',
|
||||
);
|
||||
await expect(page.locator('[data-answer-status="grounded"]')).toContainText(/deployment immediately before the incident/i);
|
||||
await expect(page.locator('[data-answer-guidance="clarify"]')).toContainText([
|
||||
'Add the deployment, incident, or time window you want to inspect.',
|
||||
'Add whether you need causes, impacts, or follow-up events.',
|
||||
]);
|
||||
await expect(page.getByRole('button', { name: 'Add the deployment, incident, or time window you want to inspect.' })).toHaveCount(0);
|
||||
await expect(page.locator('app-global-search input[type="text"]')).toHaveValue('spike');
|
||||
});
|
||||
|
||||
test('reruns a policy next-search inside the current policy route context', async ({ page }) => {
|
||||
@@ -557,10 +513,6 @@ function resolvePriorityRouteResponse(currentRoute: string, query: string): unkn
|
||||
}
|
||||
|
||||
if (currentRoute.includes('/ops/timeline')) {
|
||||
if (query.includes('which deployment, incident, or time window should i narrow this to')) {
|
||||
return timelineGroundedResponse;
|
||||
}
|
||||
|
||||
if (query.includes('spike')) {
|
||||
return emptyResponse('spike');
|
||||
}
|
||||
|
||||
@@ -46,25 +46,6 @@ const groundedFindingResponse = {
|
||||
},
|
||||
};
|
||||
|
||||
const narrowedFindingResponse = buildResponse(
|
||||
'Which CVE, workload, or package should I narrow this to?',
|
||||
[
|
||||
findingCard({
|
||||
cveId: 'CVE-2023-38545',
|
||||
title: 'CVE-2023-38545 in edge-router',
|
||||
snippet: 'A narrowed finding result is now grounded and ready for review.',
|
||||
severity: 'high',
|
||||
}),
|
||||
],
|
||||
{
|
||||
summary: 'Narrowing the question exposed a grounded finding answer.',
|
||||
template: 'finding_overview',
|
||||
confidence: 'high',
|
||||
sourceCount: 1,
|
||||
domainsCovered: ['Findings'],
|
||||
},
|
||||
);
|
||||
|
||||
test.describe('Unified Search - Self-Serve Answer Panel', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupBasicMocks(page);
|
||||
@@ -103,12 +84,8 @@ test.describe('Unified Search - Self-Serve Answer Panel', () => {
|
||||
await expect(answerPanel.locator('[data-answer-citation]')).toContainText(['CVE-2024-21626 in api-gateway']);
|
||||
});
|
||||
|
||||
test('uses clarify questions to rerun search and recover a grounded answer', async ({ page }) => {
|
||||
test('shows clarify guidance without rendering dead clarify prompts as buttons', async ({ page }) => {
|
||||
await mockSearchResponses(page, (query) => {
|
||||
if (query.includes('which cve, workload, or package should i narrow this to?')) {
|
||||
return narrowedFindingResponse;
|
||||
}
|
||||
|
||||
if (query.includes('mystery issue')) {
|
||||
return emptyResponse('mystery issue');
|
||||
}
|
||||
@@ -122,17 +99,18 @@ test.describe('Unified Search - Self-Serve Answer Panel', () => {
|
||||
await typeInSearch(page, 'mystery issue');
|
||||
await waitForResults(page);
|
||||
await expect(page.locator('[data-answer-status="clarify"]')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Which CVE, workload, or package should I narrow this to?' }).click();
|
||||
await waitForResults(page);
|
||||
await waitForEntityCards(page, 1);
|
||||
|
||||
await expect(page.locator('app-global-search input[type="text"]')).toHaveValue(
|
||||
'Which CVE, workload, or package should I narrow this to?',
|
||||
);
|
||||
await expect(page.locator('[data-answer-status="grounded"]')).toContainText(
|
||||
'Narrowing the question exposed a grounded finding answer.',
|
||||
);
|
||||
await expect(
|
||||
page.locator('[data-answer-guidance="clarify"]').filter({
|
||||
hasText: /Add the CVE, workload, or package you want to inspect\./i,
|
||||
}),
|
||||
).toHaveCount(1);
|
||||
await expect(
|
||||
page.locator('[data-answer-guidance="clarify"]').filter({
|
||||
hasText: /Add the part of the finding that matters most right now\./i,
|
||||
}),
|
||||
).toHaveCount(1);
|
||||
await expect(page.getByRole('button', { name: 'Add the CVE, workload, or package you want to inspect.' })).toHaveCount(0);
|
||||
await expect(page.locator('app-global-search input[type="text"]')).toHaveValue('mystery issue');
|
||||
});
|
||||
|
||||
test('opens AdvisoryAI from the answer panel with grounded answer context', async ({ page }) => {
|
||||
|
||||
Reference in New Issue
Block a user