From 6d8e2c9e2845b41a597dba96425fcee20f752291 Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:17:11 +0100 Subject: [PATCH 1/2] Stop Patrol fetching the correlation list nobody reads The Patrol page fetched GET /api/ai/intelligence/correlations on every mount, every 60 s poll, every return to the tab and on Retry, and the store's dashboard bundle fetched it again for FindingsPanel's refresh. Nothing has read that list since the Patrol supporting-context block was removed in 27ba85ddc6; the resource drawer reads per-resource correlations from the resource-intelligence payload instead. Remove the store's correlation signal, getter and loader, drop the read from the Patrol background refresh and the dashboard bundle, and remove AIAPI.getCorrelations, which had no other caller. The backend route stays as an API surface. Dead route stubs go from six E2E specs, and contract prose that said the Patrol page loads or renders correlations or recent changes is corrected. Measured on a demo-mode stack: correlation requests fall from one to zero per Patrol load, reload, 60 s poll and tab return at 1440x900 and 390x844, with every other AI read unchanged. Production bundle JS gzip drops 0.18 kB with no chunk renamed. --- .../v6/internal/subsystems/ai-runtime.md | 5 + .../v6/internal/subsystems/api-contracts.md | 14 +-- .../subsystems/frontend-primitives.md | 3 + .../subsystems/patrol-intelligence.md | 6 +- .../internal/subsystems/unified-resources.md | 21 ++-- .../082fd3b62b7d61ce.json | 40 +++++++ frontend-modern/src/api/__tests__/ai.test.ts | 6 -- frontend-modern/src/api/ai.ts | 8 -- .../patrol/usePatrolIntelligenceState.ts | 1 - .../pages/__tests__/AIIntelligence.test.tsx | 102 ++++-------------- .../aiIntelligence.branchcov0725am.test.ts | 1 - .../stores/__tests__/aiIntelligence.test.ts | 53 +++------ frontend-modern/src/stores/aiIntelligence.ts | 28 +---- .../tests/18-patrol-runtime-state.spec.ts | 16 --- .../tests/51-quickstart-cross-surface.spec.ts | 8 -- ...patrol-assistant-operator-briefing.spec.ts | 8 -- .../78-monitor-first-patrol-workbench.spec.ts | 8 -- ...rational-trust-attention-workbench.spec.ts | 7 -- ...erational-trust-availability-facet.spec.ts | 3 - 19 files changed, 108 insertions(+), 230 deletions(-) create mode 100644 frontend-modern/browser-verification/082fd3b62b7d61ce.json diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 48b93c4c1a..bb8c44a205 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -6729,6 +6729,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 diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 6b204396df..541009f3ae 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -3303,7 +3303,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 @@ -7316,11 +7316,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 diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 7b7e4abd89..39c3e11b73 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -6823,6 +6823,9 @@ operator refresh controls generation-aware, timeout-bounded, and separate from background polling state, so a slow supporting intelligence read cannot make the shared Patrol header Refresh Patrol 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 diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 4756dc70fd..69e22c8b59 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -2189,8 +2189,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` diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 3b9f065d08..b6628c8ccb 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -4553,26 +4553,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 diff --git a/frontend-modern/browser-verification/082fd3b62b7d61ce.json b/frontend-modern/browser-verification/082fd3b62b7d61ce.json new file mode 100644 index 0000000000..2153acd1ef --- /dev/null +++ b/frontend-modern/browser-verification/082fd3b62b7d61ce.json @@ -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." + ] +} diff --git a/frontend-modern/src/api/__tests__/ai.test.ts b/frontend-modern/src/api/__tests__/ai.test.ts index 5cbb9ac53d..1285539bd8 100644 --- a/frontend-modern/src/api/__tests__/ai.test.ts +++ b/frontend-modern/src/api/__tests__/ai.test.ts @@ -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 () => { diff --git a/frontend-modern/src/api/ai.ts b/frontend-modern/src/api/ai.ts index 5a494d6a68..b78e0b1d31 100644 --- a/frontend-modern/src/api/ai.ts +++ b/frontend-modern/src/api/ai.ts @@ -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 { - const params = resourceId ? `?resource_id=${encodeURIComponent(resourceId)}` : ''; - return apiFetchJSON( - `${this.baseUrl}/ai/intelligence/correlations${params}`, - ) as Promise; - } - // Analyze a Kubernetes cluster with AI static async analyzeKubernetesCluster(clusterId: string): Promise { return apiFetchJSON(`${this.baseUrl}/ai/kubernetes/analyze`, { diff --git a/frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts b/frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts index d2eb5d4dd6..3f98892832 100644 --- a/frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts +++ b/frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts @@ -870,7 +870,6 @@ export function usePatrolIntelligenceState() { aiIntelligenceStore.loadIntelligenceSummary(), aiIntelligenceStore.loadFindings(), aiIntelligenceStore.loadCircuitBreakerStatus(), - aiIntelligenceStore.loadCorrelations(), ]); } diff --git a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx index e98f75d56a..5474998bdb 100644 --- a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx +++ b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx @@ -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(undefined); + } }); 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(() => ); 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' })); diff --git a/frontend-modern/src/stores/__tests__/aiIntelligence.branchcov0725am.test.ts b/frontend-modern/src/stores/__tests__/aiIntelligence.branchcov0725am.test.ts index 62ef0bc4b6..d4be6ae9dd 100644 --- a/frontend-modern/src/stores/__tests__/aiIntelligence.branchcov0725am.test.ts +++ b/frontend-modern/src/stores/__tests__/aiIntelligence.branchcov0725am.test.ts @@ -11,7 +11,6 @@ vi.mock('@/api/ai', () => ({ approvePendingApproval: vi.fn(), denyPendingApproval: vi.fn(), getIntelligenceSummary: vi.fn(), - getCorrelations: vi.fn(), }, })); diff --git a/frontend-modern/src/stores/__tests__/aiIntelligence.test.ts b/frontend-modern/src/stores/__tests__/aiIntelligence.test.ts index eb7b91f5f3..1d22c8781f 100644 --- a/frontend-modern/src/stores/__tests__/aiIntelligence.test.ts +++ b/frontend-modern/src/stores/__tests__/aiIntelligence.test.ts @@ -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 () => { diff --git a/frontend-modern/src/stores/aiIntelligence.ts b/frontend-modern/src/stores/aiIntelligence.ts index 18abd3a3a0..ee56b0cfe2 100644 --- a/frontend-modern/src/stores/aiIntelligence.ts +++ b/frontend-modern/src/stores/aiIntelligence.ts @@ -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(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(), ]); }, diff --git a/tests/integration/tests/18-patrol-runtime-state.spec.ts b/tests/integration/tests/18-patrol-runtime-state.spec.ts index d50ded864c..da3a1deccb 100644 --- a/tests/integration/tests/18-patrol-runtime-state.spec.ts +++ b/tests/integration/tests/18-patrol-runtime-state.spec.ts @@ -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, diff --git a/tests/integration/tests/51-quickstart-cross-surface.spec.ts b/tests/integration/tests/51-quickstart-cross-surface.spec.ts index 600882a9f0..3efb255114 100644 --- a/tests/integration/tests/51-quickstart-cross-surface.spec.ts +++ b/tests/integration/tests/51-quickstart-cross-surface.spec.ts @@ -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, diff --git a/tests/integration/tests/73-patrol-assistant-operator-briefing.spec.ts b/tests/integration/tests/73-patrol-assistant-operator-briefing.spec.ts index 7662238f9f..e122ddf30e 100644 --- a/tests/integration/tests/73-patrol-assistant-operator-briefing.spec.ts +++ b/tests/integration/tests/73-patrol-assistant-operator-briefing.spec.ts @@ -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, diff --git a/tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts b/tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts index 9474470e16..976cdc1cf2 100644 --- a/tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts +++ b/tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts @@ -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, diff --git a/tests/integration/tests/91-operational-trust-attention-workbench.spec.ts b/tests/integration/tests/91-operational-trust-attention-workbench.spec.ts index 695e9be5a1..94d85f0e78 100644 --- a/tests/integration/tests/91-operational-trust-attention-workbench.spec.ts +++ b/tests/integration/tests/91-operational-trust-attention-workbench.spec.ts @@ -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, diff --git a/tests/integration/tests/92-operational-trust-availability-facet.spec.ts b/tests/integration/tests/92-operational-trust-availability-facet.spec.ts index 4b48f33375..b5d96d83ed 100644 --- a/tests/integration/tests/92-operational-trust-availability-facet.spec.ts +++ b/tests/integration/tests/92-operational-trust-availability-facet.spec.ts @@ -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", From a543907130db279fdda7238cadee489a5b8e0904 Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:13:21 +0100 Subject: [PATCH 2/2] Type the Patrol run test's summary mock restore The manual-run test restored the stalled loadIntelligenceSummary mock with mockResolvedValue(undefined), which the frontend type-check rejects because the store method resolves IntelligenceSummary | null. Restore it with null instead; the test behaviour is unchanged. --- frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx index 5474998bdb..6ae0100ff0 100644 --- a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx +++ b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx @@ -935,7 +935,7 @@ describe('AIIntelligence entitlement gating', () => { }); expect(screen.queryByRole('button', { name: /Starting/i })).not.toBeInTheDocument(); } finally { - loadIntelligenceSummary.mockResolvedValue(undefined); + loadIntelligenceSummary.mockResolvedValue(null); } });