702 lines
21 KiB
TypeScript
702 lines
21 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test';
|
|
|
|
import {
|
|
buildResponse,
|
|
emptyResponse,
|
|
findingCard,
|
|
policyCard,
|
|
releaseCard,
|
|
setupAuthenticatedSession,
|
|
setupBasicMocks,
|
|
timelineCard,
|
|
typeInSearch,
|
|
waitForEntityCards,
|
|
waitForResults,
|
|
} from './unified-search-fixtures';
|
|
|
|
const doctorConnectivityCard = {
|
|
entityKey: 'doctor:check.core.db.connectivity',
|
|
entityType: 'doctor',
|
|
domain: 'knowledge',
|
|
title: 'PostgreSQL connectivity',
|
|
snippet: 'Database connectivity is degraded and blocks the current release readiness check.',
|
|
score: 0.95,
|
|
actions: [
|
|
{
|
|
label: 'Open check',
|
|
actionType: 'navigate',
|
|
route: '/ops/operations/doctor?check=check.core.db.connectivity',
|
|
isPrimary: true,
|
|
},
|
|
],
|
|
sources: ['knowledge'],
|
|
metadata: {
|
|
checkId: 'check.core.db.connectivity',
|
|
},
|
|
};
|
|
|
|
const findingsGroundedResponse = buildResponse(
|
|
'What evidence blocks this release?',
|
|
[
|
|
findingCard({
|
|
cveId: 'CVE-2024-21626',
|
|
title: 'CVE-2024-21626 in api-gateway',
|
|
snippet: 'A reachable critical vulnerability remains unresolved in the active production workload.',
|
|
severity: 'critical',
|
|
}),
|
|
],
|
|
undefined,
|
|
{
|
|
contextAnswer: {
|
|
status: 'grounded',
|
|
code: 'retrieved_scope_weighted_evidence',
|
|
summary: 'A reachable critical finding in api-gateway is the strongest release blocker on this page.',
|
|
reason: 'Findings evidence ranked ahead of related policy matches.',
|
|
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 is the safest remediation path?',
|
|
kind: 'follow_up',
|
|
},
|
|
],
|
|
},
|
|
overflow: {
|
|
currentScopeDomain: 'findings',
|
|
reason: 'Policy impact stays relevant but secondary to the current finding.',
|
|
cards: [
|
|
policyCard({
|
|
ruleId: 'POL-118',
|
|
title: 'POL-118 release blocker',
|
|
snippet: 'Production rollout remains blocked while this finding is unresolved.',
|
|
}),
|
|
],
|
|
},
|
|
coverage: {
|
|
currentScopeDomain: 'findings',
|
|
currentScopeWeighted: true,
|
|
domains: [
|
|
{
|
|
domain: 'findings',
|
|
candidateCount: 2,
|
|
visibleCardCount: 1,
|
|
topScore: 0.95,
|
|
isCurrentScope: true,
|
|
hasVisibleResults: true,
|
|
},
|
|
{
|
|
domain: 'policy',
|
|
candidateCount: 1,
|
|
visibleCardCount: 1,
|
|
topScore: 0.73,
|
|
isCurrentScope: false,
|
|
hasVisibleResults: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
);
|
|
|
|
const policyGroundedResponse = buildResponse(
|
|
'Why is this gate failing?',
|
|
[
|
|
policyCard({
|
|
ruleId: 'POL-118',
|
|
title: 'POL-118 release blocker',
|
|
snippet: 'The gate is failing because reachable critical findings still block production promotion.',
|
|
}),
|
|
],
|
|
undefined,
|
|
{
|
|
contextAnswer: {
|
|
status: 'grounded',
|
|
code: 'retrieved_scope_weighted_evidence',
|
|
summary: 'The release gate is failing because production still has reachable critical findings with no approved exception.',
|
|
reason: 'Policy evidence matched directly in the current route.',
|
|
evidence: 'Grounded in 2 sources across Policy and Findings.',
|
|
citations: [
|
|
{
|
|
entityKey: 'policy:POL-118',
|
|
title: 'POL-118 release blocker',
|
|
domain: 'policy',
|
|
},
|
|
],
|
|
questions: [
|
|
{
|
|
query: 'What findings are impacted by this rule?',
|
|
kind: 'follow_up',
|
|
},
|
|
],
|
|
},
|
|
coverage: {
|
|
currentScopeDomain: 'policy',
|
|
currentScopeWeighted: true,
|
|
domains: [
|
|
{
|
|
domain: 'policy',
|
|
candidateCount: 2,
|
|
visibleCardCount: 1,
|
|
topScore: 0.94,
|
|
isCurrentScope: true,
|
|
hasVisibleResults: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
);
|
|
|
|
const policyStarterResponse = buildResponse(
|
|
'failing policy gates',
|
|
[
|
|
policyCard({
|
|
ruleId: 'POL-118',
|
|
title: 'POL-118 release blocker',
|
|
snippet: 'Current policy blockers remain concentrated around reachable production findings.',
|
|
}),
|
|
],
|
|
undefined,
|
|
{
|
|
contextAnswer: {
|
|
status: 'grounded',
|
|
code: 'retrieved_scope_weighted_evidence',
|
|
summary: 'Failing policy gates remain concentrated in the current policy workspace.',
|
|
reason: 'Starter search stayed inside policy scope first.',
|
|
evidence: 'Grounded in 1 source across Policy.',
|
|
citations: [
|
|
{
|
|
entityKey: 'policy:POL-118',
|
|
title: 'POL-118 release blocker',
|
|
domain: 'policy',
|
|
},
|
|
],
|
|
},
|
|
coverage: {
|
|
currentScopeDomain: 'policy',
|
|
currentScopeWeighted: true,
|
|
domains: [
|
|
{
|
|
domain: 'policy',
|
|
candidateCount: 1,
|
|
visibleCardCount: 1,
|
|
topScore: 0.9,
|
|
isCurrentScope: true,
|
|
hasVisibleResults: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
);
|
|
|
|
const doctorGroundedResponse = buildResponse(
|
|
'database connectivity',
|
|
[doctorConnectivityCard],
|
|
undefined,
|
|
{
|
|
contextAnswer: {
|
|
status: 'grounded',
|
|
code: 'retrieved_scope_weighted_evidence',
|
|
summary: 'Database connectivity is the highest-signal release readiness blocker in the current Doctor view.',
|
|
reason: 'Doctor evidence ranked first on the current page.',
|
|
evidence: 'Grounded in 1 source across Knowledge.',
|
|
citations: [
|
|
{
|
|
entityKey: 'doctor:check.core.db.connectivity',
|
|
title: 'PostgreSQL connectivity',
|
|
domain: 'knowledge',
|
|
route: '/ops/operations/doctor?check=check.core.db.connectivity',
|
|
},
|
|
],
|
|
},
|
|
coverage: {
|
|
currentScopeDomain: 'knowledge',
|
|
currentScopeWeighted: true,
|
|
domains: [
|
|
{
|
|
domain: 'knowledge',
|
|
candidateCount: 2,
|
|
visibleCardCount: 1,
|
|
topScore: 0.95,
|
|
isCurrentScope: true,
|
|
hasVisibleResults: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
);
|
|
|
|
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?',
|
|
[
|
|
releaseCard({
|
|
releaseId: 'rel-2026-03-08',
|
|
title: 'Release 2026.03.08',
|
|
snippet: 'Promotion is blocked by missing production approvals and a policy gate still tied to reachable findings.',
|
|
}),
|
|
],
|
|
undefined,
|
|
{
|
|
contextAnswer: {
|
|
status: 'grounded',
|
|
code: 'retrieved_scope_weighted_evidence',
|
|
summary: 'The promotion is blocked by missing approvals first, with a related policy gate also still open.',
|
|
reason: 'Release-control evidence outranked broader platform matches.',
|
|
evidence: 'Grounded in 2 sources across Platform and Policy.',
|
|
citations: [
|
|
{
|
|
entityKey: 'release:rel-2026-03-08',
|
|
title: 'Release 2026.03.08',
|
|
domain: 'platform',
|
|
route: '/releases/rel-2026-03-08',
|
|
},
|
|
],
|
|
},
|
|
overflow: {
|
|
currentScopeDomain: 'platform',
|
|
reason: 'A related policy blocker is still relevant but secondary to the current release page.',
|
|
cards: [
|
|
policyCard({
|
|
ruleId: 'POL-118',
|
|
title: 'POL-118 release blocker',
|
|
snippet: 'The same release remains tied to a critical finding with no approved exception.',
|
|
}),
|
|
],
|
|
},
|
|
coverage: {
|
|
currentScopeDomain: 'platform',
|
|
currentScopeWeighted: true,
|
|
domains: [
|
|
{
|
|
domain: 'platform',
|
|
candidateCount: 2,
|
|
visibleCardCount: 1,
|
|
topScore: 0.91,
|
|
isCurrentScope: true,
|
|
hasVisibleResults: true,
|
|
},
|
|
{
|
|
domain: 'policy',
|
|
candidateCount: 1,
|
|
visibleCardCount: 1,
|
|
topScore: 0.72,
|
|
isCurrentScope: false,
|
|
hasVisibleResults: true,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
);
|
|
|
|
type PriorityRouteCheck = {
|
|
path: string;
|
|
contextTitle: RegExp;
|
|
starterText: RegExp;
|
|
};
|
|
|
|
const priorityRoutes: readonly PriorityRouteCheck[] = [
|
|
{
|
|
path: '/security/triage',
|
|
contextTitle: /findings triage/i,
|
|
starterText: /why is this exploitable in my environment\?/i,
|
|
},
|
|
{
|
|
path: '/ops/policy',
|
|
contextTitle: /policy workspace/i,
|
|
starterText: /why is this gate failing\?/i,
|
|
},
|
|
{
|
|
path: '/ops/operations/doctor',
|
|
contextTitle: /doctor diagnostics/i,
|
|
starterText: /which failing check is blocking release\?/i,
|
|
},
|
|
{
|
|
path: '/ops/timeline',
|
|
contextTitle: /timeline analysis/i,
|
|
starterText: /what changed before this incident\?/i,
|
|
},
|
|
{
|
|
path: '/releases/deployments',
|
|
contextTitle: /release control/i,
|
|
starterText: /what blocked this promotion\?/i,
|
|
},
|
|
] as const;
|
|
|
|
test.describe('Unified Search - Priority Route Self-Serve Journeys', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await setupBasicMocks(page);
|
|
await setupAuthenticatedSession(page);
|
|
await setupDoctorPageMocks(page);
|
|
});
|
|
|
|
test('surfaces route-owned starter queries across all priority routes', async ({ page }) => {
|
|
await mockPriorityRouteSearch(page);
|
|
|
|
for (const route of priorityRoutes) {
|
|
await openRoute(page, route.path);
|
|
const searchInput = page.locator('app-global-search input[type="text"]');
|
|
await searchInput.focus();
|
|
await waitForResults(page);
|
|
|
|
await expect(page.locator('.search__context-title')).toContainText(route.contextTitle);
|
|
await expect(page.locator('[data-starter-kind]', {
|
|
hasText: route.starterText,
|
|
}).first()).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('opens deeper help from a grounded findings answer without leaving the page route', async ({ page }) => {
|
|
const capturedTurnBodies: Array<Record<string, unknown>> = [];
|
|
await mockPriorityRouteSearch(page);
|
|
await mockChatConversation(page, capturedTurnBodies);
|
|
|
|
await openRoute(page, '/security/triage');
|
|
await page.locator('app-global-search input[type="text"]').focus();
|
|
await waitForResults(page);
|
|
|
|
const blockerQuestion = page.getByRole('button', { name: 'What evidence blocks this release?' });
|
|
await blockerQuestion.evaluate((element: HTMLElement) => element.click());
|
|
await waitForResults(page);
|
|
await waitForEntityCards(page, 1);
|
|
|
|
await expect(page.locator('[data-answer-status="grounded"]')).toContainText(/strongest release blocker/i);
|
|
await page.locator('[data-answer-action="ask-ai"]').click();
|
|
|
|
await expect(page.locator('.assistant-drawer')).toBeVisible({ timeout: 10_000 });
|
|
await expect.poll(() => capturedTurnBodies.length).toBeGreaterThan(0);
|
|
expect(String(capturedTurnBodies.at(-1)?.['content'] ?? '')).toMatch(/what evidence blocks this release\?/i);
|
|
expect(String(capturedTurnBodies.at(-1)?.['content'] ?? '')).toMatch(/findings triage/i);
|
|
await expect(page).toHaveURL(/\/security\/triage(\?.*)?$/i);
|
|
});
|
|
|
|
test('promotes a follow-up chip after opening a grounded doctor result', async ({ page }) => {
|
|
await mockPriorityRouteSearch(page);
|
|
|
|
await openRoute(page, '/ops/operations/doctor');
|
|
const searchInput = page.locator('app-global-search input[type="text"]');
|
|
await searchInput.focus();
|
|
await waitForResults(page);
|
|
|
|
await page.getByRole('button', { name: 'database connectivity' }).click();
|
|
await waitForResults(page);
|
|
await waitForEntityCards(page, 1);
|
|
|
|
await page.locator('app-entity-card').first().click();
|
|
await expect(page).toHaveURL(/\/ops\/operations\/doctor\?check=check\.core\.db\.connectivity/i);
|
|
|
|
await page.locator('app-global-search input[type="text"]').focus();
|
|
await waitForResults(page);
|
|
await expect(page.locator('.search__suggestions .search__chip', {
|
|
hasText: /follow up:\s*database connectivity/i,
|
|
}).first()).toBeVisible();
|
|
});
|
|
|
|
test('recovers a timeline search from clarify to grounded with the route-owned narrowing question', async ({ page }) => {
|
|
await mockPriorityRouteSearch(page);
|
|
|
|
await openRoute(page, '/ops/timeline');
|
|
await typeInSearch(page, 'spike');
|
|
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);
|
|
});
|
|
|
|
test('reruns a policy next-search inside the current policy route context', async ({ page }) => {
|
|
const capturedRequests: Array<Record<string, unknown>> = [];
|
|
await mockPriorityRouteSearch(page, capturedRequests);
|
|
|
|
await openRoute(page, '/ops/policy');
|
|
await page.locator('app-global-search input[type="text"]').focus();
|
|
await waitForResults(page);
|
|
|
|
await page.getByRole('button', { name: 'Why is this gate failing?' }).click();
|
|
await waitForResults(page);
|
|
await waitForEntityCards(page, 1);
|
|
|
|
const nextSearchButton = page.locator('[data-answer-next-search]').filter({
|
|
hasNotText: /follow up:/i,
|
|
}).first();
|
|
await expect(nextSearchButton).toBeVisible();
|
|
await nextSearchButton.click();
|
|
await waitForResults(page);
|
|
await waitForEntityCards(page, 1);
|
|
|
|
const rerunQuery = await page.locator('app-global-search input[type="text"]').inputValue();
|
|
expect(rerunQuery.trim().length).toBeGreaterThan(0);
|
|
await expect.poll(() =>
|
|
capturedRequests.some((request) => String(request['q'] ?? '') === rerunQuery),
|
|
).toBe(true);
|
|
await expect(page).toHaveURL(/\/ops\/policy/i);
|
|
});
|
|
|
|
test('shows overflow as a secondary section for grounded release-control answers', async ({ page }) => {
|
|
await mockPriorityRouteSearch(page);
|
|
|
|
await openRoute(page, '/releases/deployments');
|
|
await page.locator('app-global-search input[type="text"]').focus();
|
|
await waitForResults(page);
|
|
|
|
await page.getByRole('button', { name: 'What blocked this promotion?' }).click();
|
|
await waitForResults(page);
|
|
await waitForEntityCards(page, 1);
|
|
|
|
await expect(page.locator('[data-answer-status="grounded"]')).toContainText(/missing approvals first/i);
|
|
await expect(page.locator('[data-overflow-results]')).toContainText(/also relevant elsewhere/i);
|
|
await expect(page.locator('[data-overflow-results]')).toContainText(/related policy blocker is still relevant/i);
|
|
});
|
|
});
|
|
|
|
async function openRoute(page: Page, path: string): Promise<void> {
|
|
await page.goto(path);
|
|
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
|
|
}
|
|
|
|
async function mockPriorityRouteSearch(
|
|
page: Page,
|
|
capturedRequests: Array<Record<string, unknown>> = [],
|
|
): Promise<void> {
|
|
await page.route('**/search/query**', async (route) => {
|
|
const body = (route.request().postDataJSON() as Record<string, unknown> | null) ?? {};
|
|
capturedRequests.push(body);
|
|
|
|
const query = String(body['q'] ?? '').trim().toLowerCase();
|
|
const ambient = body['ambient'] as Record<string, unknown> | undefined;
|
|
const currentRoute = String(ambient?.['currentRoute'] ?? '').toLowerCase();
|
|
|
|
return route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(resolvePriorityRouteResponse(currentRoute, query)),
|
|
});
|
|
});
|
|
}
|
|
|
|
function resolvePriorityRouteResponse(currentRoute: string, query: string): unknown {
|
|
if (currentRoute.includes('/security/triage')) {
|
|
if (query.includes('what evidence blocks this release')) {
|
|
return findingsGroundedResponse;
|
|
}
|
|
|
|
if (query.includes('what is the safest remediation path')) {
|
|
return findingsGroundedResponse;
|
|
}
|
|
}
|
|
|
|
if (currentRoute.includes('/ops/policy')) {
|
|
if (query.includes('why is this gate failing')) {
|
|
return policyGroundedResponse;
|
|
}
|
|
|
|
if (query.includes('failing policy gates')) {
|
|
return policyStarterResponse;
|
|
}
|
|
|
|
if (query.includes('policy exceptions') || query.includes('production deny rules')) {
|
|
return policyStarterResponse;
|
|
}
|
|
}
|
|
|
|
if (currentRoute.includes('/ops/operations/doctor')) {
|
|
if (query.includes('database connectivity')) {
|
|
return doctorGroundedResponse;
|
|
}
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|
|
|
|
if (currentRoute.includes('/releases')) {
|
|
if (query.includes('what blocked this promotion')) {
|
|
return releasesGroundedResponse;
|
|
}
|
|
}
|
|
|
|
return emptyResponse(query);
|
|
}
|
|
|
|
async function mockChatConversation(
|
|
page: Page,
|
|
capturedTurnBodies: Array<Record<string, unknown>>,
|
|
): 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-priority-routes-1',
|
|
tenantId: 'test-tenant',
|
|
userId: 'tester',
|
|
context: {},
|
|
turns: [],
|
|
createdAt: '2026-03-08T00:00:00.000Z',
|
|
updatedAt: '2026-03-08T00:00:00.000Z',
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route('**/api/v1/advisory-ai/conversations/*/turns', async (route) => {
|
|
if (route.request().method() !== 'POST') {
|
|
return route.continue();
|
|
}
|
|
|
|
capturedTurnBodies.push((route.request().postDataJSON() as Record<string, unknown> | null) ?? {});
|
|
const events = [
|
|
'event: progress',
|
|
'data: {"stage":"searching"}',
|
|
'',
|
|
'event: token',
|
|
'data: {"content":"I can expand the current grounded answer and tell you the safest next step."}',
|
|
'',
|
|
'event: done',
|
|
'data: {"turnId":"turn-priority-routes-1","groundingScore":0.94}',
|
|
'',
|
|
].join('\n');
|
|
|
|
return route.fulfill({
|
|
status: 200,
|
|
headers: {
|
|
'content-type': 'text/event-stream; charset=utf-8',
|
|
'cache-control': 'no-cache',
|
|
},
|
|
body: events,
|
|
});
|
|
});
|
|
}
|
|
|
|
async function setupDoctorPageMocks(page: Page): Promise<void> {
|
|
await page.route('**/doctor/api/v1/doctor/plugins**', (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
plugins: [
|
|
{
|
|
pluginId: 'integration.registry',
|
|
displayName: 'Registry Integration',
|
|
category: 'integration',
|
|
version: '1.0.0',
|
|
checkCount: 3,
|
|
},
|
|
],
|
|
total: 1,
|
|
}),
|
|
}),
|
|
);
|
|
|
|
await page.route('**/doctor/api/v1/doctor/checks**', (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
checks: [
|
|
{
|
|
checkId: 'check.core.db.connectivity',
|
|
name: 'Database connectivity',
|
|
description: 'Verify PostgreSQL connectivity for release readiness.',
|
|
pluginId: 'integration.registry',
|
|
category: 'integration',
|
|
defaultSeverity: 'fail',
|
|
tags: ['database', 'postgresql'],
|
|
estimatedDurationMs: 5000,
|
|
},
|
|
],
|
|
total: 1,
|
|
}),
|
|
}),
|
|
);
|
|
|
|
await page.route('**/doctor/api/v1/doctor/run', (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ runId: 'dr-priority-route-001' }),
|
|
}),
|
|
);
|
|
|
|
await page.route('**/doctor/api/v1/doctor/run/**', (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
runId: 'dr-priority-route-001',
|
|
status: 'completed',
|
|
startedAt: '2026-03-08T08:00:00Z',
|
|
completedAt: '2026-03-08T08:00:06Z',
|
|
durationMs: 6000,
|
|
summary: { passed: 0, info: 0, warnings: 0, failed: 1, skipped: 0, total: 1 },
|
|
overallSeverity: 'fail',
|
|
results: [],
|
|
}),
|
|
}),
|
|
);
|
|
}
|