Merge commit 'a543907130' into fix/drop-unread-circuit-breaker-read

This commit is contained in:
rcourtman
2026-10-07 17:20:04 +01:00
19 changed files with 108 additions and 230 deletions
@@ -6723,6 +6723,11 @@ seed prompt correlations now flow through the shared AI intelligence facade
first, so the detector remains an implementation detail behind one canonical
correlation access path instead of being routed directly by handlers or prompt
builders.
The browser does not read the global learned-correlation list:
`frontend-modern/src/api/ai.ts` carries no client for
`/api/ai/intelligence/correlations`, and neither the shared AI intelligence
store nor the Patrol page loads it, because no page renders it. The resource
drawer reads per-resource correlations from the resource-intelligence payload.
AI-facing policy metadata must also be cloned through the shared unified-
resource policy helper so chat and tools consumers do not maintain their own
policy copy logic. Chat mention prefetch now calls that shared helper directly
@@ -3304,7 +3304,7 @@ a new API state machine, queue contract, or verification-accounting field.
and the `/api/ai/intelligence/changes` route plus `internal/api/contract_test.go`, so the canonical recent-changes endpoint stays on the same intelligence facade and contract snapshot instead of bypassing the shared timeline source
and the canonical policy-posture snapshot derived from unified resources, so sensitivity, routing, and redaction counts stay owned by the same AI summary contract instead of being reconstructed as a page-local governance rollup
and the resource-intelligence payload carried by the drawer AI card, so the resource-detail surface stays on one canonical intelligence contract instead of introducing a separate detail endpoint
and the learned-correlation payload loaded into the shared AI intelligence store, so the Patrol intelligence page and the AI summary page consume the same governed correlation slice instead of each page fetching its own copy
and the global learned-correlation route `/api/ai/intelligence/correlations`, which has no frontend client today: no page renders that list, so the shared AI intelligence store neither loads nor holds it and the resource drawer reads per-resource correlations from the resource-intelligence payload; a surface that later renders the global list must add one store-owned load instead of a page-local fetch
and the shared dashboard-load bundle inside `frontend-modern/src/stores/aiIntelligence.ts`, so the page orchestration stays on the store-owned bundle instead of enumerating the AI fetches inline
and the Patrol page refresh lifecycle in `frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts`, so slow or stalled secondary reads from that shared dashboard-load bundle may continue resolving in the background while the operator-facing Patrol refresh control remains generation-aware, timeout-bounded, and reusable once Patrol findings and status are already visible
and the Patrol header support drawer in `frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx`, so API-owned Patrol status and trigger facts can feed a secondary Schedule & model surface without turning provider model, schedule, trigger tuning, or background-only runtime-policy pauses into the primary Patrol control decision
@@ -7298,11 +7298,13 @@ The same AI resource-intelligence payload now also carries dependency and
dependent correlation arrays plus correlation evidence, so the drawer can render
canonical correlation context from the shared AI contract instead of inferring it
from the relationship facet payload alone.
The same AI frontend client now also loads `/api/ai/intelligence/correlations`
through the shared `frontend-modern/src/stores/aiIntelligence.ts` store for
the Patrol intelligence page and the AI summary page, so the
learned-correlation list is governed by the same API contract that backs the
resource drawer's correlation evidence instead of being fetched as page-local state.
The frontend does not load `/api/ai/intelligence/correlations`. No page renders
the global learned-correlation list, so `frontend-modern/src/api/ai.ts` carries
no client for it, and neither the dashboard load bundle in
`frontend-modern/src/stores/aiIntelligence.ts` nor the Patrol refresh path
fetches it. The resource drawer's correlation evidence comes from the
resource-intelligence payload above. The route stays an API surface behind the
`ai:execute` scope.
That correlations route now reads through the canonical AI intelligence
facade first, so the handler and its payload keep the detector behind one
shared access layer instead of routing directly to Patrol-local correlation
@@ -6820,6 +6820,9 @@ operator refresh controls generation-aware, timeout-bounded, and separate from
background polling state, so a slow supporting intelligence read cannot make the
Patrol stale-data Retry action spin indefinitely or stay disabled
while Patrol findings and status remain visible.
Neither the Patrol load and poll path (the store's `loadDashboardData` bundle)
nor the Retry path's background supporting reads fetch the global
learned-correlation list, which no Patrol surface shows.
That same Patrol shell should make scoped trigger policy legible without
another navigation step. `frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx`
should keep actionable scoped-trigger state legible without promoting
@@ -2181,8 +2181,10 @@ requester identity as safe metadata, without copying the approval command
payload into Assistant.
That same store now owns the Patrol dashboard load bundle as well, so the
page refresh path stays aligned on a single orchestrated AI bundle instead of
repeating the individual summary, findings, approval, and correlation fetches
inline.
repeating the individual summary, findings, circuit-breaker, and approval
fetches inline. Neither that bundle nor the Patrol refresh path fetches the
global learned-correlation list: the Patrol page renders no correlations, so
the store holds no copy of that list.
The shared
`frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx` and
`frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx`
@@ -4552,26 +4552,29 @@ reconstructing a separate type-token summary in the emitter.
The same AI resource-intelligence payload now also carries canonical
correlation evidence from the shared detector, so the drawer can show learned
edge patterns alongside the dependency relationships without rebuilding correlation
reasoning from raw events. The Patrol intelligence page now also renders that
correlation evidence through the shared
reasoning from raw events. The drawer renders that correlation evidence
through the shared
`frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx`
card, so the same learned-edge list stays governed by one frontend surface
instead of separate page-local implementations. That shared card also owns
card, and is that card's only caller today; the Patrol page renders no learned
correlations and the frontend does not load the global correlation list. Any later surface
that shows learned edges must reuse that card instead of a page-local
implementation. That shared card also owns
the first-class relationship-map surface for canonical `resource.relationships`,
the correlation ordering, and the truncation rule, so callers pass raw
relationships and correlation lists instead of encoding their own sort or
top-N behavior.
Canonical parent edges now also originate in this subsystem: `ParentID` is
folded into the facet relationship set through
`ResourceRelationshipsWithCanonicalParent` before any drawer or Patrol
consumer renders a relationship map, so pages do not rederive parent topology
`ResourceRelationshipsWithCanonicalParent` before any consumer renders a
relationship map, so pages do not rederive parent topology
from raw resource fields or invent relationship-map fallbacks locally.
The same surfaces now also render recent changes through the shared
The drawer renders recent changes through the shared
`frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx`
card, so canonical timeline wording and ordering stay governed by one
frontend feed instead of separate page-local loops. Callers may suppress
resource-change metadata badges only for compact operator-context surfaces such
as Patrol's supporting context; the shared card still owns headline/reason
resource-change metadata badges only for compact operator-context surfaces;
no caller does today, and the Patrol page renders no recent changes. The
shared card still owns headline/reason
dedupe so prefixed backend reasons do not render as duplicated visible copy.
Assistant finding handoffs are part of that same timeline contract: when the AI
runtime needs recent changes for product-originated handoff resources, it should
@@ -0,0 +1,40 @@
{
"version": 1,
"base_sha": "bdf0f76b7bf61d550b1cb8574497eef75b354acb",
"verified_at": "2026-10-07T14:17:03Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/api/ai.ts",
"frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts",
"frontend-modern/src/stores/aiIntelligence.ts"
],
"content_sha256": {
"frontend-modern/src/api/ai.ts": "03a3c67b18121618f60a3bde3718a27d0be94b9cbfd96328340d0f8d5b8909a0",
"frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts": "087cb28ff8c235f9c4b03ad3115021a40b6952b3a3c5844dbfc3eaa57d2753c0",
"frontend-modern/src/stores/aiIntelligence.ts": "bd947cc05d31c9ad126f6c992555bf8cffa8ee23eae88598485fa9fb9d812f29"
},
"routes": [
"/patrol"
],
"viewports": [
{
"width": 1440,
"height": 900
},
{
"width": 390,
"height": 844
}
],
"states": [
"Private DEMO_MODE mock stack (demo/demo, PULSE_TELEMETRY=false) serving an embedded production build of this tree on base bdf0f76b7b, plus a before build of unmodified main on a second port for comparison; headless Chromium at 1440x900 and 390x844.",
"Patrol Inbox renders unchanged at both widths: Watch only mode row, Check now and Settings, Inbox/Protection/Activity tabs, the 7-decision inbox with Start review, Review 1 approval and Refresh. No console errors after the change.",
"Before: one GET /api/ai/intelligence/correlations per initial Patrol load, browser reload, 60 s poll tick and tab hide/show at both widths. After: zero in every phase. Every other /api/ai/* read in each phase is identical before and after.",
"The production bundle no longer contains the /ai/intelligence/correlations string or the 'Failed to load correlations' log (control string /ai/intelligence still present); check-bundle-size passes with no chunk renamed.",
"Not covered in the browser: the Patrol could not refresh banner's Retry path, which every Patrol read swallows so it cannot be forced from the network; it calls the same background reader edited here and is covered by unit tests."
],
"interactions": [
"Signed in through the demo auto sign-in, opened /patrol, reloaded the page, waited 68 s for the 60 s poll, and simulated a tab hide then show (visibilitychange) to trigger the visibility reload, counting /api/ai/* requests per phase with Playwright.",
"Repeated the load, reload and tab hide/show phases on the final rebased build at both widths and inspected the screenshots."
]
}
@@ -266,12 +266,6 @@ describe('AIAPI', () => {
expect(apiFetchJSONMock).toHaveBeenCalledWith(
'/api/ai/intelligence?resource_id=vm%2F100%3Ffilter%3Dall',
);
apiFetchJSONMock.mockResolvedValueOnce({} as any);
await AIAPI.getCorrelations('storage/1?filter=all');
expect(apiFetchJSONMock).toHaveBeenCalledWith(
'/api/ai/intelligence/correlations?resource_id=storage%2F1%3Ffilter%3Dall',
);
});
it('treats 402 responses from optional AI paywalled collections as empty state', async () => {
-8
View File
@@ -20,7 +20,6 @@ import type {
} from '@/types/ai';
import type {
AnomaliesResponse,
CorrelationsResponse,
IntelligenceSummary,
LearningStatusResponse,
ResourceIntelligence,
@@ -154,13 +153,6 @@ export class AIAPI {
return (await this.fetchIntelligence(resourceId)) as ResourceIntelligence;
}
static async getCorrelations(resourceId?: string): Promise<CorrelationsResponse> {
const params = resourceId ? `?resource_id=${encodeURIComponent(resourceId)}` : '';
return apiFetchJSON(
`${this.baseUrl}/ai/intelligence/correlations${params}`,
) as Promise<CorrelationsResponse>;
}
// Analyze a Kubernetes cluster with AI
static async analyzeKubernetesCluster(clusterId: string): Promise<AIExecuteResponse> {
return apiFetchJSON(`${this.baseUrl}/ai/kubernetes/analyze`, {
@@ -647,7 +647,6 @@ export function usePatrolIntelligenceState() {
aiIntelligenceStore.loadIntelligenceSummary(),
aiIntelligenceStore.loadFindings(),
aiIntelligenceStore.loadCircuitBreakerStatus(),
aiIntelligenceStore.loadCorrelations(),
]);
}
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, fireEvent, render, screen, waitFor, within } from '@solidjs/testing-library';
import { Suspense, createSignal, type JSX } from 'solid-js';
import { Suspense, type JSX } from 'solid-js';
import { resetCreateNonSuspendingQueryCacheForTest } from '@/hooks/createNonSuspendingQuery';
import {
PATROL_CONTROL_ANCHOR,
@@ -17,6 +17,7 @@ import patrolIntelligenceHeaderSource from '@/features/patrol/PatrolIntelligence
import patrolIntelligenceSurfaceSource from '@/features/patrol/PatrolIntelligenceSurface.tsx?raw';
import patrolIntelligenceStateSource from '@/features/patrol/usePatrolIntelligenceState.ts?raw';
import { AGENT_PATROL_CONTROL_STATUS_PATH } from '@/api/agentCapabilities';
import { aiIntelligenceStore } from '@/stores/aiIntelligence';
import { AIIntelligence } from '../AIIntelligence';
@@ -85,29 +86,9 @@ const { findingsPanelState, runHistoryState, intelligenceState } = vi.hoisted(()
incidents_tracked: number;
};
} | null,
correlations: null as {
correlations: Array<{
source_id: string;
source_name: string;
source_type: string;
target_id: string;
target_name: string;
target_type: string;
event_pattern: string;
occurrences: number;
avg_delay: number | string;
confidence: number;
last_seen: string;
description?: string;
}>;
count: number;
} | null,
},
}));
const getCorrelationsMock = vi.fn();
const [correlationsState, setCorrelationsState] =
createSignal<(typeof intelligenceState)['correlations']>(null);
const getPatrolStatusMock = vi.fn();
const getPatrolAutonomySettingsMock = vi.fn();
const updatePatrolAutonomySettingsMock = vi.fn();
@@ -215,7 +196,6 @@ vi.mock('@/api/patrol', () => ({
vi.mock('@/api/ai', () => ({
AIAPI: {
getCorrelations: (...args: unknown[]) => getCorrelationsMock(...args),
getModels: (...args: unknown[]) => apiFetchJSONMock('/api/ai/models', ...args),
getSettings: (...args: unknown[]) => apiFetchJSONMock('/api/settings/ai', ...args),
updateSettings: (...args: unknown[]) => apiFetchJSONMock('/api/settings/ai/update', ...args),
@@ -276,19 +256,12 @@ vi.mock('@/stores/aiIntelligence', () => {
loadIntelligenceSummary: vi.fn().mockResolvedValue(undefined),
loadCircuitBreakerStatus: vi.fn().mockResolvedValue(undefined),
loadPendingApprovals: vi.fn().mockResolvedValue(undefined),
loadCorrelations: vi.fn().mockImplementation(async () => {
const response = await getCorrelationsMock();
intelligenceState.correlations = response;
setCorrelationsState(response);
return response;
}),
loadDashboardData: vi.fn().mockImplementation(async () => {
await Promise.all([
store.loadFindings(),
store.loadIntelligenceSummary(),
store.loadCircuitBreakerStatus(),
store.loadPendingApprovals(),
store.loadCorrelations(),
]);
}),
get findings() {
@@ -306,9 +279,6 @@ vi.mock('@/stores/aiIntelligence', () => {
get patrolPendingApprovals() {
return [];
},
get correlations() {
return correlationsState();
},
};
return {
@@ -663,9 +633,6 @@ describe('AIIntelligence entitlement gating', () => {
intelligenceState.findings = [];
intelligenceState.circuitBreakerStatus = null;
intelligenceState.summary = null;
intelligenceState.correlations = null;
setCorrelationsState(null);
getCorrelationsMock.mockReset();
getPatrolStatusMock.mockResolvedValue(defaultPatrolStatus());
getPatrolAutonomySettingsMock.mockResolvedValue(defaultPatrolAutonomySettings());
@@ -728,10 +695,6 @@ describe('AIIntelligence entitlement gating', () => {
presentationPolicyHidesCommercialSurfacesMock.mockReturnValue(false);
recordWorkflowPromptActivityMock.mockResolvedValue(undefined);
window.history.replaceState({}, '', '/patrol');
getCorrelationsMock.mockResolvedValue({
correlations: [],
count: 0,
});
});
it('keeps the Patrol page heading accessible name singular when the logo is present', async () => {
@@ -957,15 +920,23 @@ describe('AIIntelligence entitlement gating', () => {
expect(screen.getAllByRole('button', { name: /Check now/i }).length).toBeGreaterThan(0);
});
getCorrelationsMock.mockImplementation(() => new Promise(() => {}));
// Stall the Patrol refresh that follows an accepted start: the run button
// must leave Starting without waiting on it.
const loadIntelligenceSummary = vi.mocked(aiIntelligenceStore.loadIntelligenceSummary);
loadIntelligenceSummary.mockClear();
loadIntelligenceSummary.mockImplementation(() => new Promise(() => {}));
try {
fireEvent.click(screen.getAllByRole('button', { name: /Check now/i })[0]);
fireEvent.click(screen.getAllByRole('button', { name: /Check now/i })[0]);
await waitFor(() => {
expect(triggerPatrolRunMock).toHaveBeenCalled();
expect(screen.getAllByRole('button', { name: /Running/i }).length).toBeGreaterThan(0);
});
expect(screen.queryByRole('button', { name: /Starting/i })).not.toBeInTheDocument();
await waitFor(() => {
expect(triggerPatrolRunMock).toHaveBeenCalled();
expect(screen.getAllByRole('button', { name: /Running/i }).length).toBeGreaterThan(0);
expect(loadIntelligenceSummary).toHaveBeenCalled();
});
expect(screen.queryByRole('button', { name: /Starting/i })).not.toBeInTheDocument();
} finally {
loadIntelligenceSummary.mockResolvedValue(null);
}
});
it('surfaces backend readiness rejection when a stale manual run request reaches the server', async () => {
@@ -1066,40 +1037,6 @@ describe('AIIntelligence entitlement gating', () => {
incidents_tracked: 0,
},
};
getCorrelationsMock.mockResolvedValue({
correlations: [
{
source_id: 'storage-2',
source_name: 'Storage 2',
source_type: 'storage',
target_id: 'vm-200',
target_name: 'VM 200',
target_type: 'vm',
event_pattern: 'disk_full -> restart',
occurrences: 2,
avg_delay: '1m30s',
confidence: 0.95,
last_seen: '2026-03-01T00:05:00Z',
description: 'Disk pressure often precedes restarts',
},
{
source_id: 'storage-1',
source_name: 'Storage 1',
source_type: 'storage',
target_id: 'vm-100',
target_name: 'VM 100',
target_type: 'vm',
event_pattern: 'cpu_high -> restart',
occurrences: 1,
avg_delay: '3m',
confidence: 0.5,
last_seen: '2026-03-01T00:03:00Z',
description: 'Lower-confidence backup pattern',
},
],
count: 2,
});
render(() => <AIIntelligence />);
await waitFor(() => {
@@ -1126,9 +1063,6 @@ describe('AIIntelligence entitlement gating', () => {
expect(
screen.queryByText('What Patrol was allowed to inspect or act on.'),
).not.toBeInTheDocument();
expect(screen.queryByText('Storage 2')).not.toBeInTheDocument();
expect(screen.queryByText('VM 200')).not.toBeInTheDocument();
expect(screen.queryByText('Disk Full → Restart')).not.toBeInTheDocument();
await openPatrolActivityMode();
fireEvent.click(screen.getByRole('button', { name: 'History' }));
@@ -11,7 +11,6 @@ vi.mock('@/api/ai', () => ({
approvePendingApproval: vi.fn(),
denyPendingApproval: vi.fn(),
getIntelligenceSummary: vi.fn(),
getCorrelations: vi.fn(),
},
}));
@@ -6,7 +6,7 @@ vi.mock('@/api/ai', () => ({
getRemediationPlans: vi.fn(),
getPendingApprovals: vi.fn(),
getIntelligenceSummary: vi.fn(),
getCorrelations: vi.fn(),
getCircuitBreakerStatus: vi.fn(),
},
}));
@@ -593,35 +593,6 @@ describe('aiIntelligenceStore', () => {
});
});
it('loads the canonical correlations response', async () => {
vi.mocked(AIAPI.getCorrelations).mockResolvedValueOnce({
correlations: [
{
source_id: 'storage-1',
source_name: 'Storage 1',
source_type: 'storage',
target_id: 'vm-42',
target_name: 'VM 42',
target_type: 'vm',
event_pattern: 'disk_full -> restart',
occurrences: 2,
avg_delay: '1m30s',
confidence: 0.875,
last_seen: '2026-03-01T00:00:00Z',
description: 'Disk pressure often precedes restarts',
},
],
count: 1,
});
await aiIntelligenceStore.loadCorrelations();
expect(aiIntelligenceStore.correlations).toMatchObject({
count: 1,
});
expect(aiIntelligenceStore.correlations?.correlations).toHaveLength(1);
});
it('loads the canonical AI dashboard bundle', async () => {
vi.mocked(AIAPI.getIntelligenceSummary).mockResolvedValueOnce({
timestamp: '2026-03-01T00:00:00Z',
@@ -651,17 +622,27 @@ describe('aiIntelligenceStore', () => {
incidents_tracked: 0,
},
});
vi.mocked(AIAPI.getCorrelations).mockResolvedValueOnce({
correlations: [],
count: 0,
});
vi.mocked(AIAPI.getCircuitBreakerStatus).mockResolvedValueOnce(null as never);
await aiIntelligenceStore.loadDashboardData();
expect(AIAPI.getIntelligenceSummary).toHaveBeenCalledTimes(1);
expect(AIAPI.getCorrelations).toHaveBeenCalledTimes(1);
expect(aiIntelligenceStore.intelligenceSummary?.findings_count.total).toBe(0);
expect(aiIntelligenceStore.correlations?.count).toBe(0);
// These are every AIAPI read the bundle makes (Patrol findings come from
// the Patrol API module). It does not load the global learned-correlation
// list: no page renders it, and the resource drawer reads per-resource
// correlations from resource intelligence instead.
const aiReads = Object.entries(AIAPI)
.filter(([, read]) => vi.isMockFunction(read) && read.mock.calls.length > 0)
.map(([name]) => name)
.sort();
expect(aiReads).toEqual([
'getCircuitBreakerStatus',
'getIntelligenceSummary',
'getPendingApprovals',
'getUnifiedFindings',
]);
expect(getPatrolFindings).toHaveBeenCalledTimes(1);
});
it('treats queued fixes without a live approval as findings needing attention', async () => {
+1 -27
View File
@@ -41,7 +41,7 @@ import {
import { getApprovalExpiryTime, isLivePendingApproval } from '@/utils/approvalState';
import { sortPendingApprovalsByUrgency } from '@/utils/approvalRiskPresentation';
import { logger } from '@/utils/logger';
import type { CorrelationsResponse, IntelligenceSummary } from '@/types/aiIntelligence';
import type { IntelligenceSummary } from '@/types/aiIntelligence';
import { normalizeIntelligenceSummary } from './aiIntelligenceSummaryModel';
import { presentationPolicyIsDemoMode } from './sessionPresentationPolicy';
import { eventBus } from './events';
@@ -465,12 +465,6 @@ const [intelligenceSummary, setIntelligenceSummary] = createSignal<IntelligenceS
null,
);
// ============================================
// Learned Correlations
// ============================================
const [correlations, setCorrelations] = createSignal<CorrelationsResponse | null>(null);
// ============================================
// Org Switch Reset
// ============================================
@@ -490,7 +484,6 @@ eventBus.on('org_switched', () => {
setApprovalsError(null);
setCircuitBreakerStatus(null);
setIntelligenceSummary(null);
setCorrelations(null);
});
// ============================================
@@ -809,30 +802,12 @@ export const aiIntelligenceStore = {
},
circuitBreakerStatusSignal: circuitBreakerStatus,
// Learned Correlations
get correlations() {
return correlations();
},
correlationsSignal: correlations,
// Canonical Intelligence Summary
get intelligenceSummary() {
return intelligenceSummary();
},
intelligenceSummarySignal: intelligenceSummary,
async loadCorrelations(resourceId?: string) {
try {
const response = await AIAPI.getCorrelations(resourceId);
setCorrelations(response);
return response;
} catch (e) {
logger.error('Failed to load correlations:', e);
setCorrelations(null);
return null;
}
},
async loadDashboardData() {
await Promise.all([
this.loadIntelligenceSummary(),
@@ -840,7 +815,6 @@ export const aiIntelligenceStore = {
this.loadPatrolFindings(),
this.loadCircuitBreakerStatus(),
this.loadPendingApprovals(),
this.loadCorrelations(),
]);
},
@@ -341,14 +341,6 @@ async function mockBlockedPatrolRuntimeState(
});
});
await page.route("**/api/ai/intelligence/correlations*", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ correlations: [], count: 0 }),
});
});
await page.route("**/api/ai/intelligence", async (route) => {
await route.fulfill({
status: 200,
@@ -505,14 +497,6 @@ async function mockScopedTriggerPatrolRuntimeState(
});
});
await page.route("**/api/ai/intelligence/correlations*", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ correlations: [], count: 0 }),
});
});
await page.route("**/api/ai/intelligence", async (route) => {
await route.fulfill({
status: 200,
@@ -249,14 +249,6 @@ async function mockRetiredQuickstartSurface(
});
});
await page.route("**/api/ai/intelligence/correlations*", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ correlations: [], count: 0 }),
});
});
await page.route("**/api/ai/intelligence", async (route) => {
await route.fulfill({
status: 200,
@@ -506,14 +506,6 @@ test.describe("Patrol Assistant operator briefing", () => {
});
});
await page.route("**/api/ai/intelligence/correlations*", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ correlations: [], count: 0 }),
});
});
await page.route("**/api/ai/intelligence", async (route) => {
await route.fulfill({
status: 200,
@@ -476,14 +476,6 @@ async function mockMonitorFirstPatrolWorkbench(
});
});
await page.route("**/api/ai/intelligence/correlations*", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ correlations: [], count: 0 }),
});
});
await page.route("**/api/ai/circuit/status", async (route) => {
await route.fulfill({
status: 200,
@@ -493,13 +493,6 @@ async function mockAttention(
}),
});
});
await page.route("**/api/ai/intelligence/correlations*", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ correlations: [], count: 0 }),
});
});
await page.route("**/api/ai/circuit/status", async (route) => {
await route.fulfill({
status: 200,
@@ -730,9 +730,6 @@ async function routePatrolSupport(
},
});
});
await page.route("**/api/ai/intelligence/correlations*", async (route) => {
await fulfillJSON(route, { correlations: [], count: 0 });
});
await page.route("**/api/ai/circuit/status", async (route) => {
await fulfillJSON(route, {
state: "closed",