diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index f68dc022aa..9ab54a9d35 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -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 diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index f415badc7d..f0d848dbfd 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -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 @@ -7295,11 +7295,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 78a6cd98de..2d7ffe53ea 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -6785,6 +6785,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 79374829db..bc31aa2854 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -2154,8 +2154,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 f88f35afd0..69b3cf77e3 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -4614,26 +4614,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..6ae0100ff0 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(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(() => ); 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",