mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Drop Patrol state members no surface reads (#2695)
`usePatrolIntelligenceState()` returned 18 members that no file outside the hook reads. A TypeScript checker scan of src and tests found no readers and no spread, computed or `Object.*` escapes of the state object. Several were also dead inside the hook. ## Change - **Return object.** Drops activePatrolFindings, activityRefreshTrigger, acceptedManualRunId, assistantHandoffFindingId, circuitBreakerStatus, fullModeUnlocked, initialSurfaceReady, isRefreshing, loadAllData, openAssistantOperationsLoopForFinding, the four decision/outcome counts, both work-evidence counts, recordPatrolWorkflowStarterActivity and setFullModeUnlocked, plus handleAssistantFindingHandoff (see below). - **Dead locals.** Removes the write-only signals (initialSurfaceReady, isRefreshing, fullModeUnlocked, assistantHandoffFindingId), the 15 s background refresh timeout that only reset isRefreshing, the unread memos, and the never-called `openAssistantOperationsLoopForFinding`. That last one was a second copy of the handoff `FindingsPanel` already performs. Locals still used inside the hook (activePatrolFindings, loadAllData, activityRefreshTrigger, acceptedManualRunId) stay. - **Test-only exports.** `openPatrolAssistantWorkflowHandoff` and `recordPatrolWorkflowStarterActivity` were used only by that dead path and by their own tests, so they go. - **Workspace.** `handleAssistantFindingHandoff` only wrote the unread handoff signal, so `PatrolIntelligenceWorkspace` no longer passes `FindingsPanel` an `onAssistantHandoff` callback. The prop is optional. - **Tests.** Raw-source pins on removed code are dropped. The manual-sync timeout test now pins the Retry path's own generation guard; a mutation of its timeout fails it. The non-blocking starter test now exercises `recordPatrolControlStarterActivity`. - **Contracts.** - patrol-intelligence said a finding-to-Assistant handoff *must* record a `pulse_patrol` workflow starter. Only the dead path did that; the live `FindingsPanel` handoff never has, so `pulse_patrol` has no browser producer. The clause now says that and keeps the content-free rules as a condition. - The "local Patrol state must expose work evidence" sentence no longer requires counts nothing reads. - Three refresh clauses that described a header refresh action now name the stale-data Retry, the only operator refresh left. - api-contracts and frontend-primitives record that `FindingsPanel` owns the handoff. ## Not in this PR The store still fetches `GET /api/ai/circuit/status` for nothing. That removal is a follow-up after #2677, because it edits the same lines. ## Evidence - Production builds before and after compared with chunk hashes, SRI strings and short identifiers normalised. Only the removed hook code differs, the unused `pulse_patrol` constant drops out of `aiChat.js`, and `index.js` reorders the same preload list. `check-bundle-size.mjs` passes. - Private demo-mode stacks for both builds, driven by the same Playwright script at 1440x900 and 390x844: Patrol load, Activity > Finding options and history, Open in Assistant (drawer opens with the finding), tab hide/show, and the forced stale-data banner plus Retry. Each phase hit the same `/api` endpoints on both builds, and neither build had console errors. - `tsc --noEmit`, eslint, and vitest for `src/features/patrol`, `AIIntelligence.test.tsx`, `frontendResourceTypeBoundaries.test.ts` and `FindingsPanel.test.ts` pass, as does the full pre-commit battery. Stacked on #2687 (which carries #2682 and #2686). Until those merge, this PR shows their commits too. `git merge-tree` is clean against main, #2690, #2677 and #2691.
This commit is contained in:
@@ -3326,6 +3326,7 @@ a new API state machine, queue contract, or verification-accounting field.
|
||||
and any frontend presentation of the AI summary's `policy_posture` counts, which must render that canonical snapshot through one shared component instead of page-local posture loops; no frontend surface renders those counts today, and the resource drawer stays on per-resource policy lines
|
||||
and the dedicated `frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts` owner, so if Patrol recent-change, learned-correlation, or policy-coverage summary text returns, it is derived from the canonical AI payload in that one place instead of as hook-local count and pluralization logic; Patrol renders no such summary today and the model carries no such derivation
|
||||
and the Watch-only forward-path handoffs owned by `frontend-modern/src/features/patrol/patrolControlPresentation.ts` together with the Actions inbox empty-state read of `GET /api/ai/patrol/autonomy`, so finding-level and inbox-level mode guidance consumes the canonical Patrol autonomy read/save contract through the Patrol state hook (`handleAutonomyChange`) instead of introducing a second mode mutation path or a page-local autonomy dialect
|
||||
and the Patrol finding-to-Assistant handoff owned by `frontend-modern/src/components/AI/FindingsPanel.tsx`, so the pending-approval, investigation, and proposed-fix reads behind that handoff happen once in the panel instead of in a second, unwired copy inside the Patrol state hook
|
||||
and that same Patrol investigation-context owner, so visible Assistant
|
||||
drawer handoffs may include live pending-approval metadata only as safe
|
||||
operator context: approval ID, status, risk, requested/expiry timestamps,
|
||||
|
||||
@@ -3767,11 +3767,11 @@ production table, router and styles; it does not qualify full-app scrolling.
|
||||
assessment readout, but shared drawer/chrome primitives stay free of
|
||||
the `FindingsTrustSummary` shape so adding new trust signals goes
|
||||
through the contract first rather than per-shell branching.
|
||||
Patrol header refresh controls stay on that same feature-owned shell
|
||||
Patrol refresh controls stay on that same feature-owned shell
|
||||
boundary: `frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts`
|
||||
must make the refresh affordance generation-aware and timeout-bounded, so
|
||||
a slow supporting intelligence read cannot permanently disable the shared
|
||||
Patrol header control while Patrol findings and status remain visible.
|
||||
a slow supporting intelligence read cannot permanently disable the
|
||||
stale-data Retry control while Patrol findings and status remain visible.
|
||||
That feature-owned
|
||||
presentation helper is the single emitter for investigation-record
|
||||
`impact` and `rollback` fields: when an investigation record exists but those fields
|
||||
@@ -3915,6 +3915,10 @@ production table, router and styles; it does not qualify full-app scrolling.
|
||||
finding-presentation destinations such as the Patrol provider-settings
|
||||
route, while `patrol_autonomy` and legacy Pro activation URLs remain parser
|
||||
aliases only and verified review links use the plain Patrol history anchor.
|
||||
The Patrol finding-to-Assistant handoff has one owner,
|
||||
`frontend-modern/src/components/AI/FindingsPanel.tsx`; the Patrol state
|
||||
hook keeps no second copy of it, and the Patrol workspace passes no
|
||||
handoff callback because nothing on the page reacts to a handoff.
|
||||
UI surfaces must not duplicate the `patrolControlStarter` query string or
|
||||
write Patrol control or legacy entry-point starter telemetry from local
|
||||
click handlers.
|
||||
@@ -6856,7 +6860,7 @@ That same Patrol shell ownership includes refresh affordance state:
|
||||
`frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts` must keep
|
||||
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
|
||||
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
|
||||
|
||||
@@ -329,13 +329,14 @@ or historical proof/counting for resolved-only work.
|
||||
shared parent node, or explicit correlated finding IDs. Grouping must not
|
||||
infer causality from title or description prose, and the queue's affected
|
||||
resource count must continue to count canonical resources rather than
|
||||
visual groups. Because a
|
||||
contextual Assistant handoff from that workflow is still a first-party Patrol
|
||||
starter for the same governed journey, it must record content-free workflow
|
||||
prompt activity through the shared marker route with the `pulse_patrol`
|
||||
surface before the drawer opens; it must not include finding IDs, prompt
|
||||
text, resource context, model output, or direct-action payloads in that
|
||||
marker.
|
||||
visual groups. A contextual Assistant handoff from that workflow opens
|
||||
through `frontend-modern/src/components/AI/FindingsPanel.tsx` and records no
|
||||
workflow starter today, so the first-party `pulse_patrol` marker has no
|
||||
browser producer. If that handoff starts recording one, it must write
|
||||
content-free workflow prompt activity through the shared marker route with
|
||||
the `pulse_patrol` surface before the drawer opens, with no finding IDs,
|
||||
prompt text, resource context, model output, or direct-action payloads in
|
||||
that marker.
|
||||
A compact Patrol work-group row may appear above the list only when it adds
|
||||
cross-source current-work grouping that the row title alone cannot express:
|
||||
pending approvals, failed governed actions, a failed/latest Patrol check,
|
||||
@@ -471,8 +472,11 @@ or historical proof/counting for resolved-only work.
|
||||
compact progress label must describe current work state (`ready`, `needs
|
||||
attention`, `approval needed`, `outcome verified`, `no active work`) instead
|
||||
of repeating the selected mode; the Patrol mode selector/header
|
||||
owns current-mode copy, and local Patrol state must expose work evidence as
|
||||
owns current-mode copy, and any local Patrol work evidence must be
|
||||
Patrol-owned issue/work counts rather than legacy proof counts.
|
||||
No Patrol surface reads issue-evidence, governed-action decision, or
|
||||
verified-outcome counts today, so `usePatrolIntelligenceState.ts` computes
|
||||
none of them.
|
||||
When no active finding or pending approval remains, terminal verified or
|
||||
rejected outcomes must read as history behind a `no active work` current
|
||||
state; current-state copy must not ask operators to reconcile old approved
|
||||
@@ -771,12 +775,12 @@ attention`, `approval needed`, `outcome verified`, `no active work`) instead
|
||||
only block the selected run view while the run references finding ids that
|
||||
still need the direct Patrol findings payload.
|
||||
Patrol page refresh state must also separate background data loads from the
|
||||
operator-clicked Update status action: slow or stalled supporting
|
||||
intelligence reads may continue in the background, but they must not make the
|
||||
shared header action spin or stay disabled once Patrol findings and status
|
||||
remain visible. The header action is a status/history sync affordance; it
|
||||
must not read like another Patrol run or imply that it changes
|
||||
infrastructure.
|
||||
operator-clicked Retry on the stale-data banner: background polls hold no
|
||||
busy state, and slow or stalled supporting intelligence reads may continue
|
||||
in the background, but they must not make Retry spin or stay disabled once
|
||||
Patrol findings and status remain visible. Retry is a status/history sync
|
||||
affordance; it must not read like another Patrol run or imply that it
|
||||
changes infrastructure.
|
||||
Patrol trigger-status copy in the default header and activity strip must stay
|
||||
actionable. Runtime policy pauses for alert/anomaly-triggered background
|
||||
checks, such as the local development safety guard, may explain a blocked
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "1a48d27aa0ddfcbc4df0354a08c2312cc1849603",
|
||||
"verified_at": "2026-10-07T23:28:00Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts": "9bdafb9fa51b259ddada914607c80265f43cb4055e1c56128648c6ed0135c1a4"
|
||||
},
|
||||
"routes": [
|
||||
"/patrol",
|
||||
"/patrol (Activity > Finding options and history)",
|
||||
"/patrol (Pulse Assistant drawer)"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 844
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Follow-up receipt for the merge of origin/main (7f45148d15) into this branch: the hook's final content differs from both parents only by main's own removals of summaryStats (#2690) and the loadCorrelations supporting read (#2677).",
|
||||
"Embedded production builds (vite build into internal/api/frontend-modern/dist, go build) of origin/main 7f45148d15 and of this merged tree (1a48d27aa0), run on pulse-dev because local memory pressure refused heavy work; each on a private DEMO_MODE mock stack with a fresh data dir; the same Playwright script ran against both at both widths.",
|
||||
"Patrol load with one served active finding (page.route on /api/ai/patrol/findings and /api/ai/unified/findings), Activity > Finding options and history, Resolve or dismiss > Open in Assistant: the Pulse Assistant drawer opens with the finding attached on both builds; no console errors on either.",
|
||||
"Tab hidden then shown (visibilitychange) and the stale-data banner forced by failing GET /api/settings/ai on reload: 'Patrol could not refresh' shows on both builds and Retry clears it on both, at both widths (state-based waits; a first run with fixed 5 s sleeps missed the banner on the loaded host for timing reasons).",
|
||||
"Across the whole run both builds hit the same set of /api endpoints; per-endpoint counts differ by one to three only for timer-driven attention, pending-action and active-alert polls, which this change does not touch.",
|
||||
"Dist proof on pulse-dev: 187 files each side; after normalising chunk hashes, SRI strings and short identifiers, only the removed hook code differs in AIIntelligence.js, the unused pulse_patrol constant drops out of aiChat.js, and index.js reorders the same preload list."
|
||||
],
|
||||
"interactions": [
|
||||
"Demo auto sign-in; on /patrol open the Activity tab and 'Finding options and history', expand the served finding, open 'Resolve or dismiss', click 'Open in Assistant', close the drawer.",
|
||||
"Dispatch visibilitychange hidden then visible; reload with GET /api/settings/ai failing, wait for the banner, restore the route and click Retry, wait for the banner to clear.",
|
||||
"Desktop 1440x900 and phone 390x844 contexts; screenshots of the Patrol page, Finding options and history, the Assistant drawer and the stale-data banner inspected."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "2cf213a77d2a50dcd47c0b0ab07c49f97bdd0534",
|
||||
"verified_at": "2026-10-07T15:52:00Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx",
|
||||
"frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx": "04ef5ff520eb84b7350d203799a406c22252a35aa6947de6aa7c922e5f4050b4",
|
||||
"frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts": "c217fd2affd44a102ef39ba440519b5ca931db54b0f46555a23924d968bc7beb"
|
||||
},
|
||||
"routes": [
|
||||
"/patrol",
|
||||
"/patrol (Activity > Finding options and history)",
|
||||
"/patrol (Pulse Assistant drawer)"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 844
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Embedded production builds (vite build into internal/api/frontend-modern/dist, go build) of the parent tree (92abdcd9ad; rebased onto 2cf213a77d, which changes only patrol-intelligence.md) and this tree, each on a private DEMO_MODE mock stack with its own data dir and port; the same Playwright script ran against both at both widths.",
|
||||
"Patrol load with one served active finding (page.route on /api/ai/patrol/findings and /api/ai/unified/findings): Inbox renders, Activity > Finding options and history lists the finding, no console errors, same /api endpoint set as the parent build.",
|
||||
"Finding expanded, Resolve or dismiss menu open, Open in Assistant clicked: the Pulse Assistant drawer opens with the finding attached ('High CPU usage on web-server (host)'), same requests as the parent build and no workflow-starter POST on either build.",
|
||||
"Tab hidden then shown (visibilitychange): polling pauses and one reload runs; same endpoint set as the parent build.",
|
||||
"Stale-data banner forced by failing GET /api/settings/ai on reload: 'Patrol could not refresh' with Retry shows on both builds; after unrouting, Retry clears the banner on both builds.",
|
||||
"Dist proof: 187 files each side; after normalising chunk hashes, SRI strings and short identifiers, only the removed hook code differs in AIIntelligence.js, the now-unused pulse_patrol constant drops out of aiChat.js, and index.js reorders the same AIIntelligence preload list; check-bundle-size.mjs passes."
|
||||
],
|
||||
"interactions": [
|
||||
"Demo auto sign-in; on /patrol open the Activity tab and 'Finding options and history', expand the served finding, open 'Resolve or dismiss', click 'Open in Assistant', close the drawer.",
|
||||
"Dispatch visibilitychange hidden then visible; reload with GET /api/settings/ai failing, then click Retry with the route restored.",
|
||||
"Desktop 1440x900 and phone 390x844 contexts; screenshots of the Patrol page, the Assistant drawer and the stale-data banner inspected at both widths."
|
||||
]
|
||||
}
|
||||
@@ -282,7 +282,6 @@ export function PatrolIntelligenceWorkspace(props: {
|
||||
findingsSource="patrol"
|
||||
runSnapshot={state.selectedRun() ?? undefined}
|
||||
showControls={!state.selectedRun()}
|
||||
onAssistantHandoff={(finding) => state.handleAssistantFindingHandoff(finding.id)}
|
||||
patrolProHandoff={(finding) =>
|
||||
getPatrolProInvestigationHandoff({
|
||||
autoFixLocked: state.autoFixLocked(),
|
||||
|
||||
@@ -2,18 +2,14 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
PATROL_MANUAL_SYNC_TIMEOUT_MS,
|
||||
PATROL_REFRESH_TIMEOUT_MS,
|
||||
getPatrolSavedReadinessWarning,
|
||||
openPatrolAssistantWorkflowHandoff,
|
||||
patrolStartFailureMessage,
|
||||
recordPatrolControlStarterActivity,
|
||||
recordPatrolWorkflowStarterActivity,
|
||||
resolvePatrolAutonomyLevelForSave,
|
||||
resolvePatrolAutonomySettingsForSave,
|
||||
resolvePatrolBlockedActionCause,
|
||||
} from '../usePatrolIntelligenceState';
|
||||
import patrolIntelligenceStateSource from '../usePatrolIntelligenceState.ts?raw';
|
||||
import type { AIChatContext } from '@/stores/aiChat';
|
||||
import type { AISettings, PatrolReadiness } from '@/types/ai';
|
||||
|
||||
const recordWorkflowPromptActivityMock = vi.hoisted(() => vi.fn());
|
||||
@@ -57,19 +53,31 @@ describe('usePatrolIntelligenceState', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('bounds refresh UI state with a generation-aware timeout', () => {
|
||||
expect(PATROL_REFRESH_TIMEOUT_MS).toBe(15000);
|
||||
it('bounds manual sync UI state with a generation-aware timeout', () => {
|
||||
const manualRefreshStart = patrolIntelligenceStateSource.indexOf(
|
||||
'async function handleRefreshPatrol()',
|
||||
);
|
||||
const manualRefreshBody = patrolIntelligenceStateSource.slice(
|
||||
manualRefreshStart,
|
||||
patrolIntelligenceStateSource.indexOf(
|
||||
'async function consumeRoutePatrolControlStarterHandoff()',
|
||||
manualRefreshStart,
|
||||
),
|
||||
);
|
||||
|
||||
expect(PATROL_MANUAL_SYNC_TIMEOUT_MS).toBe(5000);
|
||||
expect(patrolIntelligenceStateSource).toContain('let refreshRequestId = 0;');
|
||||
expect(patrolIntelligenceStateSource).toContain('let manualRefreshRequestId = 0;');
|
||||
expect(manualRefreshBody).toContain('const requestId = ++manualRefreshRequestId;');
|
||||
expect(manualRefreshBody).toContain('}, PATROL_MANUAL_SYNC_TIMEOUT_MS);');
|
||||
expect(manualRefreshBody).toContain('if (requestId === manualRefreshRequestId) {');
|
||||
expect(manualRefreshBody).toContain('setIsManualRefreshRunning(false);');
|
||||
expect(manualRefreshBody).toContain('clearManualRefreshTimeout();');
|
||||
});
|
||||
|
||||
it('ignores load results that a newer background load superseded', () => {
|
||||
expect(patrolIntelligenceStateSource).toContain('let refreshRequestId = 0;');
|
||||
expect(patrolIntelligenceStateSource).toContain('const requestId = ++refreshRequestId;');
|
||||
expect(patrolIntelligenceStateSource).toContain('if (requestId === refreshRequestId) {');
|
||||
expect(patrolIntelligenceStateSource).toContain('setIsRefreshing(false);');
|
||||
expect(patrolIntelligenceStateSource).toContain('setIsManualRefreshRunning(false);');
|
||||
expect(patrolIntelligenceStateSource).toContain('handleRefreshPatrol');
|
||||
expect(patrolIntelligenceStateSource).toContain('clearRefreshTimeout();');
|
||||
expect(patrolIntelligenceStateSource).toContain('clearManualRefreshTimeout();');
|
||||
expect(patrolIntelligenceStateSource).toContain('PATROL_MANUAL_SYNC_TIMEOUT_MS');
|
||||
});
|
||||
|
||||
it('keeps browser/network start failures distinct from backend rejections', () => {
|
||||
@@ -90,7 +98,6 @@ describe('usePatrolIntelligenceState', () => {
|
||||
expect(patrolIntelligenceStateSource).toContain('const [patrolLoadError, setPatrolLoadError]');
|
||||
expect(patrolIntelligenceStateSource).toContain('rememberPatrolLoadError');
|
||||
expect(patrolIntelligenceStateSource).toContain('Promise.allSettled([');
|
||||
expect(patrolIntelligenceStateSource).toContain('setInitialSurfaceReady(true);');
|
||||
expect(patrolIntelligenceStateSource).toContain(
|
||||
"logger.debug('[Patrol] Failed to refresh Patrol data'",
|
||||
);
|
||||
@@ -128,30 +135,13 @@ describe('usePatrolIntelligenceState', () => {
|
||||
expect(patrolIntelligenceStateSource).toContain(
|
||||
'function loadSupportingPatrolDataInBackground()',
|
||||
);
|
||||
expect(loadAllDataBody).toContain('PATROL_REFRESH_TIMEOUT_MS');
|
||||
expect(loadAllDataBody).not.toContain('PATROL_MANUAL_SYNC_TIMEOUT_MS');
|
||||
expect(manualRefreshBody).toContain('await loadVisiblePatrolData();');
|
||||
expect(manualRefreshBody).toContain('loadSupportingPatrolDataInBackground();');
|
||||
expect(manualRefreshBody).toContain('PATROL_MANUAL_SYNC_TIMEOUT_MS');
|
||||
expect(manualRefreshBody).not.toContain('PATROL_REFRESH_TIMEOUT_MS');
|
||||
expect(manualRefreshBody).not.toContain('await loadAllData();');
|
||||
});
|
||||
|
||||
it('counts rejected Patrol fixes as governed action decisions', () => {
|
||||
expect(patrolIntelligenceStateSource).toContain('const PATROL_GOVERNED_ACTION_OUTCOMES');
|
||||
expect(patrolIntelligenceStateSource).toContain("'fix_rejected'");
|
||||
expect(patrolIntelligenceStateSource).toContain('patrolGovernedActionCount');
|
||||
});
|
||||
|
||||
it('separates generic Patrol runs from issue-backed Patrol work evidence', () => {
|
||||
expect(patrolIntelligenceStateSource).toContain('const patrolWorkIssueEvidenceCount');
|
||||
expect(patrolIntelligenceStateSource).toContain("finding.status === 'active'");
|
||||
expect(patrolIntelligenceStateSource).toContain("finding.status === 'resolved'");
|
||||
expect(patrolIntelligenceStateSource).toContain('status?.findings_count');
|
||||
expect(patrolIntelligenceStateSource).toContain('status?.trust?.fix_verified');
|
||||
expect(patrolIntelligenceStateSource).toContain('patrolWorkIssueEvidenceCount,');
|
||||
});
|
||||
|
||||
it('keeps MCP readiness out of first-party Patrol workflow state', () => {
|
||||
expect(patrolIntelligenceStateSource).not.toContain('agentCapabilitiesManifest');
|
||||
expect(patrolIntelligenceStateSource).not.toContain('fetchAgentCapabilitiesManifest');
|
||||
@@ -182,18 +172,6 @@ describe('usePatrolIntelligenceState', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('records direct Patrol workflow starters with the shared content-free activity route', () => {
|
||||
recordWorkflowPromptActivityMock.mockResolvedValueOnce(undefined);
|
||||
|
||||
recordPatrolWorkflowStarterActivity();
|
||||
|
||||
expect(recordWorkflowPromptActivityMock).toHaveBeenCalledWith({
|
||||
name: 'pulse_operations_loop',
|
||||
surface: 'pulse_patrol',
|
||||
});
|
||||
expect(patrolIntelligenceStateSource).toContain('recordPatrolWorkflowStarterActivity,');
|
||||
});
|
||||
|
||||
it('records route-backed Patrol mode starters with the shared content-free activity route', async () => {
|
||||
recordWorkflowPromptActivityMock.mockResolvedValueOnce(undefined);
|
||||
|
||||
@@ -216,56 +194,18 @@ describe('usePatrolIntelligenceState', () => {
|
||||
expect(patrolIntelligenceStateSource).not.toContain('await loadPatrolWorkStatus();');
|
||||
});
|
||||
|
||||
it('keeps direct Patrol starter recording non-blocking when the marker route fails', async () => {
|
||||
it('keeps Patrol mode starter recording non-blocking when the marker route fails', async () => {
|
||||
const error = new Error('offline');
|
||||
recordWorkflowPromptActivityMock.mockRejectedValueOnce(error);
|
||||
|
||||
recordPatrolWorkflowStarterActivity();
|
||||
await Promise.resolve();
|
||||
await expect(recordPatrolControlStarterActivity()).resolves.toBeUndefined();
|
||||
|
||||
expect(loggerDebugMock).toHaveBeenCalledWith(
|
||||
'[Patrol] Failed to record Patrol workflow starter',
|
||||
'[Patrol mode handoff] Failed to record Patrol workflow starter',
|
||||
error,
|
||||
);
|
||||
});
|
||||
|
||||
it('records a Patrol starter before opening the Assistant workflow handoff', () => {
|
||||
const callOrder: string[] = [];
|
||||
const recordStarterActivity = vi.fn(() => callOrder.push('record'));
|
||||
const openAssistant = vi.fn((context: AIChatContext) => {
|
||||
callOrder.push('open');
|
||||
expect(context).toMatchObject({
|
||||
targetType: 'storage',
|
||||
targetId: 'storage-1',
|
||||
findingId: 'finding-1',
|
||||
autonomousMode: false,
|
||||
preferredWorkflowPromptName: 'pulse_operations_loop',
|
||||
});
|
||||
expect(context.handoffContext).toBe('Scoped Patrol context');
|
||||
});
|
||||
|
||||
openPatrolAssistantWorkflowHandoff(
|
||||
{
|
||||
context: {
|
||||
targetType: 'storage',
|
||||
targetId: 'storage-1',
|
||||
findingId: 'finding-1',
|
||||
autonomousMode: false,
|
||||
handoffContext: 'Scoped Patrol context',
|
||||
preferredWorkflowPromptName: 'legacy_prompt',
|
||||
},
|
||||
},
|
||||
{
|
||||
recordStarterActivity,
|
||||
openAssistant,
|
||||
},
|
||||
);
|
||||
|
||||
expect(recordStarterActivity).toHaveBeenCalledWith();
|
||||
expect(openAssistant).toHaveBeenCalledTimes(1);
|
||||
expect(callOrder).toEqual(['record', 'open']);
|
||||
});
|
||||
|
||||
describe('resolvePatrolAutonomyLevelForSave', () => {
|
||||
it('clamps stale paid autonomy to monitor when governed fixes are locked', () => {
|
||||
expect(resolvePatrolAutonomyLevelForSave('full', true, true)).toBe('monitor');
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
type AssistantWorkflowPromptActivitySurface,
|
||||
PULSE_OPERATIONS_LOOP_WORKFLOW_PROMPT_NAME,
|
||||
PULSE_PATROL_CONTROL_WORKFLOW_PROMPT_SURFACE,
|
||||
PULSE_PATROL_WORKFLOW_PROMPT_SURFACE,
|
||||
} from '@/api/aiChat';
|
||||
import { aiIntelligenceStore } from '@/stores/aiIntelligence';
|
||||
import {
|
||||
@@ -28,7 +27,6 @@ import {
|
||||
loadAIRuntimeSettings,
|
||||
syncAIRuntimeSettings,
|
||||
} from '@/stores/aiRuntimeState';
|
||||
import { aiChatStore, type AIChatContext } from '@/stores/aiChat';
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
import { usePatrolStream } from '@/hooks/usePatrolStream';
|
||||
import { createNonSuspendingQuery } from '@/hooks/createNonSuspendingQuery';
|
||||
@@ -41,10 +39,7 @@ import {
|
||||
import { PATROL_AUTONOMY_FEATURE_KEY } from './patrolAutonomyAvailability';
|
||||
import type { AISettings } from '@/types/ai';
|
||||
import { apiErrorStatus } from '@/api/responseUtils';
|
||||
import {
|
||||
hasFindingInvestigationHandoffPointer,
|
||||
isPatrolRuntimeFinding,
|
||||
} from '@/utils/aiFindingPresentation';
|
||||
import { isPatrolRuntimeFinding } from '@/utils/aiFindingPresentation';
|
||||
import { getCanonicalScopeResourceIds } from '@/utils/patrolFormat';
|
||||
import { normalizePatrolRuntimeBlockedReason } from '@/utils/patrolRuntimePresentation';
|
||||
import { logger } from '@/utils/logger';
|
||||
@@ -54,30 +49,10 @@ import {
|
||||
PATROL_CONTROL_STARTER_QUERY_PARAM,
|
||||
PATROL_OPERATIONS_LOOP_STARTER_QUERY_PARAM,
|
||||
} from '@/routing/resourceLinks';
|
||||
import {
|
||||
buildPatrolAssistantApprovalBriefingInput,
|
||||
buildPatrolAssistantFindingHandoffFromUnifiedFinding,
|
||||
buildPatrolAssistantProposedFixBriefingInput,
|
||||
buildPatrolAssistantProposedFixBriefingInputFromApproval,
|
||||
patrolAssistantFindingHandoffRequiresApprovalMode,
|
||||
type PatrolAssistantApprovalBriefingInput,
|
||||
type PatrolAssistantFindingHandoff,
|
||||
} from './patrolInvestigationContextModel';
|
||||
import { schedulePatrolRunAcceptanceReconciliation } from './patrolRunAcceptance';
|
||||
|
||||
type PatrolTab = 'findings' | 'history';
|
||||
|
||||
const PATROL_GOVERNED_ACTION_OUTCOMES = new Set([
|
||||
'fix_executed',
|
||||
'fix_failed',
|
||||
'fix_rejected',
|
||||
'fix_verified',
|
||||
'fix_verification_failed',
|
||||
'fix_verification_unknown',
|
||||
]);
|
||||
const PATROL_VERIFIED_OUTCOMES = new Set(['fix_verified']);
|
||||
const PATROL_OPERATIONS_LOOP_WORKFLOW_PROMPT = PULSE_OPERATIONS_LOOP_WORKFLOW_PROMPT_NAME;
|
||||
|
||||
const recordPatrolWorkflowStarterActivityForSurface = async (
|
||||
surface: AssistantWorkflowPromptActivitySurface,
|
||||
logContext: string,
|
||||
@@ -92,18 +67,10 @@ const recordPatrolWorkflowStarterActivityForSurface = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const PATROL_REFRESH_TIMEOUT_MS = 15000;
|
||||
export const PATROL_MANUAL_SYNC_TIMEOUT_MS = 5000;
|
||||
export const PATROL_ACCEPTANCE_RECONCILE_MS = 15000;
|
||||
export const PATROL_ACCEPTANCE_REFRESH_TIMEOUT_MS = 5000;
|
||||
|
||||
export function recordPatrolWorkflowStarterActivity(): void {
|
||||
void recordPatrolWorkflowStarterActivityForSurface(
|
||||
PULSE_PATROL_WORKFLOW_PROMPT_SURFACE,
|
||||
'Patrol',
|
||||
);
|
||||
}
|
||||
|
||||
export function recordPatrolControlStarterActivity(): Promise<void> {
|
||||
return recordPatrolWorkflowStarterActivityForSurface(
|
||||
PULSE_PATROL_CONTROL_WORKFLOW_PROMPT_SURFACE,
|
||||
@@ -111,27 +78,6 @@ export function recordPatrolControlStarterActivity(): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
interface PatrolAssistantWorkflowHandoffOptions {
|
||||
recordStarterActivity?: () => void;
|
||||
openAssistant?: (context: AIChatContext) => void;
|
||||
}
|
||||
|
||||
export function openPatrolAssistantWorkflowHandoff(
|
||||
handoff: PatrolAssistantFindingHandoff,
|
||||
options: PatrolAssistantWorkflowHandoffOptions = {},
|
||||
): void {
|
||||
const {
|
||||
recordStarterActivity = recordPatrolWorkflowStarterActivity,
|
||||
openAssistant = (context) => aiChatStore.open(context),
|
||||
} = options;
|
||||
|
||||
recordStarterActivity();
|
||||
openAssistant({
|
||||
...handoff.context,
|
||||
preferredWorkflowPromptName: PATROL_OPERATIONS_LOOP_WORKFLOW_PROMPT,
|
||||
});
|
||||
}
|
||||
|
||||
const patrolErrorMessage = (error: unknown, fallback: string) =>
|
||||
error instanceof Error && error.message.trim() ? error.message : fallback;
|
||||
|
||||
@@ -199,12 +145,10 @@ export function resolvePatrolBlockedActionCause(
|
||||
}
|
||||
|
||||
export function usePatrolIntelligenceState() {
|
||||
const [initialSurfaceReady, setInitialSurfaceReady] = createSignal(false);
|
||||
const [activeTab, setActiveTab] = createSignal<PatrolTab>('findings');
|
||||
const [findingsFilterOverride, setFindingsFilterOverride] = createSignal<
|
||||
'all' | 'active' | 'resolved' | 'approvals' | 'attention' | undefined
|
||||
>(undefined);
|
||||
const [isRefreshing, setIsRefreshing] = createSignal(false);
|
||||
const [isManualRefreshRunning, setIsManualRefreshRunning] = createSignal(false);
|
||||
const [autonomyLevel, setAutonomyLevel] = createSignal<PatrolAutonomyLevel>('monitor');
|
||||
const [requestedAutonomyLevel, setRequestedAutonomyLevel] =
|
||||
@@ -219,16 +163,13 @@ export function usePatrolIntelligenceState() {
|
||||
const [liveRunStartedAt, setLiveRunStartedAt] = createSignal('');
|
||||
const [investigationBudget, setInvestigationBudget] = createSignal(15);
|
||||
const [investigationTimeout, setInvestigationTimeout] = createSignal(300);
|
||||
const [fullModeUnlocked, setFullModeUnlocked] = createSignal(false);
|
||||
const [isTogglingPatrol, setIsTogglingPatrol] = createSignal(false);
|
||||
const [isTriggeringPatrol, setIsTriggeringPatrol] = createSignal(false);
|
||||
const [selectedRun, setSelectedRun] = createSignal<PatrolRunRecord | null>(null);
|
||||
const [assistantHandoffFindingId, setAssistantHandoffFindingId] = createSignal('');
|
||||
const [patrolLoadError, setPatrolLoadError] = createSignal('');
|
||||
|
||||
let cancelAcceptanceReconciliation: (() => void) | undefined;
|
||||
let findingScrollTimerRef: ReturnType<typeof setTimeout> | undefined;
|
||||
let refreshTimeoutRef: ReturnType<typeof setTimeout> | undefined;
|
||||
let manualRefreshTimeoutRef: ReturnType<typeof setTimeout> | undefined;
|
||||
let refreshRequestId = 0;
|
||||
let manualRefreshRequestId = 0;
|
||||
@@ -249,13 +190,6 @@ export function usePatrolIntelligenceState() {
|
||||
setManualRunRequested(false);
|
||||
};
|
||||
|
||||
const clearRefreshTimeout = () => {
|
||||
if (refreshTimeoutRef !== undefined) {
|
||||
clearTimeout(refreshTimeoutRef);
|
||||
refreshTimeoutRef = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const clearManualRefreshTimeout = () => {
|
||||
if (manualRefreshTimeoutRef !== undefined) {
|
||||
clearTimeout(manualRefreshTimeoutRef);
|
||||
@@ -263,14 +197,6 @@ export function usePatrolIntelligenceState() {
|
||||
}
|
||||
};
|
||||
|
||||
const finishRefresh = (requestId: number) => {
|
||||
if (requestId !== refreshRequestId) {
|
||||
return;
|
||||
}
|
||||
clearRefreshTimeout();
|
||||
setIsRefreshing(false);
|
||||
};
|
||||
|
||||
const rememberPatrolLoadError = (error: unknown, fallback: string) => {
|
||||
const message = patrolErrorMessage(error, fallback);
|
||||
setPatrolLoadError(message);
|
||||
@@ -526,7 +452,6 @@ export function usePatrolIntelligenceState() {
|
||||
});
|
||||
|
||||
const intelligenceSummary = createMemo(() => aiIntelligenceStore.intelligenceSummary);
|
||||
const circuitBreakerStatus = createMemo(() => aiIntelligenceStore.circuitBreakerStatus);
|
||||
const liveRunRecord = createMemo<PatrolRunRecord | null>(() => {
|
||||
if (!shouldShowLiveRun()) return null;
|
||||
return {
|
||||
@@ -573,7 +498,6 @@ export function usePatrolIntelligenceState() {
|
||||
setRequestedAutonomyLevel(settings.requested_autonomy_level);
|
||||
setAutonomyLevel(settings.effective_autonomy_level);
|
||||
setAutopilotStatus(settings.autopilot_acknowledgement);
|
||||
setFullModeUnlocked(settings.autopilot_acknowledgement.active);
|
||||
setInvestigationBudget(settings.investigation_budget);
|
||||
setInvestigationTimeout(settings.investigation_timeout_sec);
|
||||
} catch (err) {
|
||||
@@ -663,72 +587,6 @@ export function usePatrolIntelligenceState() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleAssistantFindingHandoff(findingId: string) {
|
||||
setAssistantHandoffFindingId(findingId.trim());
|
||||
}
|
||||
|
||||
const loadLatestInvestigationProposedFixBriefing = async (
|
||||
finding: ReturnType<typeof allPatrolFindings>[number],
|
||||
pendingApprovalBriefing: PatrolAssistantApprovalBriefingInput | undefined,
|
||||
) => {
|
||||
if (finding.investigationRecord?.proposed_fix) {
|
||||
return undefined;
|
||||
}
|
||||
const hasInvestigationPointer =
|
||||
hasFindingInvestigationHandoffPointer(finding) || Boolean(pendingApprovalBriefing?.id);
|
||||
if (!hasInvestigationPointer) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
!patrolAssistantFindingHandoffRequiresApprovalMode({
|
||||
investigationOutcome: finding.investigationOutcome,
|
||||
remediationId: finding.remediationPlanId,
|
||||
pendingApproval: pendingApprovalBriefing,
|
||||
investigationRecord: finding.investigationRecord,
|
||||
})
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const investigation = await AIAPI.getInvestigation(finding.id);
|
||||
return buildPatrolAssistantProposedFixBriefingInput(investigation?.proposed_fix);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
async function openAssistantOperationsLoopForFinding(findingId: string): Promise<boolean> {
|
||||
const normalizedFindingId = findingId.trim();
|
||||
if (!normalizedFindingId) return false;
|
||||
|
||||
const finding = allPatrolFindings().find((candidate) => candidate.id === normalizedFindingId);
|
||||
if (!finding) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await aiIntelligenceStore.loadPendingApprovals();
|
||||
const pendingApproval = aiIntelligenceStore.patrolPendingApprovals.find(
|
||||
(approval) => approval.toolId === 'investigation_fix' && approval.targetId === finding.id,
|
||||
);
|
||||
const pendingApprovalBriefing = buildPatrolAssistantApprovalBriefingInput(pendingApproval);
|
||||
const latestInvestigationProposedFix = await loadLatestInvestigationProposedFixBriefing(
|
||||
finding,
|
||||
pendingApprovalBriefing,
|
||||
);
|
||||
const proposedFix =
|
||||
latestInvestigationProposedFix ||
|
||||
buildPatrolAssistantProposedFixBriefingInputFromApproval(pendingApproval);
|
||||
const handoff = buildPatrolAssistantFindingHandoffFromUnifiedFinding(finding, {
|
||||
pendingApproval: pendingApprovalBriefing,
|
||||
proposedFix,
|
||||
});
|
||||
|
||||
openPatrolAssistantWorkflowHandoff(handoff);
|
||||
setAssistantHandoffFindingId(finding.id);
|
||||
return true;
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
clearInterval(refreshInterval);
|
||||
clearInterval(approvalPollInterval);
|
||||
@@ -743,15 +601,6 @@ export function usePatrolIntelligenceState() {
|
||||
|
||||
async function loadAllData() {
|
||||
const requestId = ++refreshRequestId;
|
||||
clearRefreshTimeout();
|
||||
setIsRefreshing(true);
|
||||
refreshTimeoutRef = setTimeout(() => {
|
||||
if (requestId === refreshRequestId) {
|
||||
refreshTimeoutRef = undefined;
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}, PATROL_REFRESH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
// The Patrol surface needs dismissed findings too: the attention
|
||||
@@ -773,8 +622,6 @@ export function usePatrolIntelligenceState() {
|
||||
if (requestId === refreshRequestId) {
|
||||
rememberPatrolLoadError(error, 'Patrol could not refresh.');
|
||||
}
|
||||
} finally {
|
||||
finishRefresh(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -854,54 +701,6 @@ export function usePatrolIntelligenceState() {
|
||||
}
|
||||
return activeFindings.length > 0 && activeFindings.every(isPatrolRuntimeFinding);
|
||||
});
|
||||
const patrolWorkIssueEvidenceCount = createMemo(() => {
|
||||
const issueFindingCount = allPatrolFindings().filter(
|
||||
(finding) =>
|
||||
finding.status === 'active' ||
|
||||
finding.status === 'resolved' ||
|
||||
Boolean(finding.investigationSessionId) ||
|
||||
Boolean(finding.investigationStatus) ||
|
||||
Boolean(finding.investigationOutcome) ||
|
||||
Boolean(finding.remediationPlanId),
|
||||
).length;
|
||||
const status = patrolStatus();
|
||||
const statusFindingCount =
|
||||
(status?.findings_count ?? 0) +
|
||||
(status?.fixed_count ?? 0) +
|
||||
(status?.trust?.resolved ?? 0) +
|
||||
(status?.trust?.fix_verified ?? 0);
|
||||
return issueFindingCount + patrolPendingApprovalCount() + statusFindingCount;
|
||||
});
|
||||
const patrolWorkEvidenceCount = createMemo(
|
||||
() =>
|
||||
(patrolRunHistory.value()?.length ?? 0) +
|
||||
allPatrolFindings().length +
|
||||
(patrolStatus()?.last_patrol_at || patrolStatus()?.last_activity_at ? 1 : 0),
|
||||
);
|
||||
const patrolGovernedActionCount = createMemo(
|
||||
() =>
|
||||
allPatrolFindings().filter(
|
||||
(finding) =>
|
||||
finding.investigationOutcome &&
|
||||
PATROL_GOVERNED_ACTION_OUTCOMES.has(finding.investigationOutcome),
|
||||
).length,
|
||||
);
|
||||
const patrolRejectedDecisionCount = createMemo(
|
||||
() =>
|
||||
allPatrolFindings().filter((finding) => finding.investigationOutcome === 'fix_rejected')
|
||||
.length,
|
||||
);
|
||||
const patrolApprovedDecisionCount = createMemo(() =>
|
||||
Math.max(0, patrolGovernedActionCount() - patrolRejectedDecisionCount()),
|
||||
);
|
||||
const patrolVerifiedOutcomeCount = createMemo(
|
||||
() =>
|
||||
allPatrolFindings().filter(
|
||||
(finding) =>
|
||||
finding.investigationOutcome &&
|
||||
PATROL_VERIFIED_OUTCOMES.has(finding.investigationOutcome),
|
||||
).length,
|
||||
);
|
||||
const findingsTabBadgeFindings = createMemo(() => {
|
||||
const snapshotFindings = selectedRunPatrolFindings();
|
||||
if (snapshotFindings === null) {
|
||||
@@ -921,23 +720,19 @@ export function usePatrolIntelligenceState() {
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await consumeRoutePatrolControlStarterHandoff();
|
||||
const initialLoads = await Promise.allSettled([
|
||||
loadRuntimeCapabilities(),
|
||||
loadAllData(),
|
||||
loadAutonomySettings(),
|
||||
loadAIRuntimeModels(),
|
||||
loadAIRuntimeSettings(),
|
||||
]);
|
||||
const failedLoad = initialLoads.find(
|
||||
(result): result is PromiseRejectedResult => result.status === 'rejected',
|
||||
);
|
||||
if (failedLoad) {
|
||||
rememberPatrolLoadError(failedLoad.reason, 'Patrol could not refresh.');
|
||||
}
|
||||
} finally {
|
||||
setInitialSurfaceReady(true);
|
||||
await consumeRoutePatrolControlStarterHandoff();
|
||||
const initialLoads = await Promise.allSettled([
|
||||
loadRuntimeCapabilities(),
|
||||
loadAllData(),
|
||||
loadAutonomySettings(),
|
||||
loadAIRuntimeModels(),
|
||||
loadAIRuntimeSettings(),
|
||||
]);
|
||||
const failedLoad = initialLoads.find(
|
||||
(result): result is PromiseRejectedResult => result.status === 'rejected',
|
||||
);
|
||||
if (failedLoad) {
|
||||
rememberPatrolLoadError(failedLoad.reason, 'Patrol could not refresh.');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -956,7 +751,6 @@ export function usePatrolIntelligenceState() {
|
||||
document.addEventListener('visibilitychange', handleVisibility);
|
||||
onCleanup(() => {
|
||||
document.removeEventListener('visibilitychange', handleVisibility);
|
||||
clearRefreshTimeout();
|
||||
clearManualRefreshTimeout();
|
||||
});
|
||||
});
|
||||
@@ -974,10 +768,6 @@ export function usePatrolIntelligenceState() {
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
activePatrolFindings,
|
||||
activityRefreshTrigger,
|
||||
acceptedManualRunId,
|
||||
assistantHandoffFindingId,
|
||||
autonomyLevel,
|
||||
requestedAutonomyLevel,
|
||||
autopilotStatus,
|
||||
@@ -989,54 +779,39 @@ export function usePatrolIntelligenceState() {
|
||||
blockedCause,
|
||||
blockedReason,
|
||||
canTriggerPatrol,
|
||||
circuitBreakerStatus,
|
||||
displayRunHistory,
|
||||
findingsTabBadgeCount,
|
||||
findingsTabBadgeFindings,
|
||||
findingsFilterOverride,
|
||||
fullModeUnlocked,
|
||||
handleAutonomyChange,
|
||||
handleAssistantFindingHandoff,
|
||||
handleRefreshPatrol,
|
||||
handleRunPatrol,
|
||||
handleTogglePatrol,
|
||||
historicalRegressionCount,
|
||||
initialSurfaceReady,
|
||||
intelligenceSummary,
|
||||
isManualRefreshRunning,
|
||||
isRefreshing,
|
||||
isTogglingPatrol,
|
||||
isTriggeringPatrol,
|
||||
isUpdatingAutonomy,
|
||||
revokeAutopilot,
|
||||
licenseRequired,
|
||||
loadAllData,
|
||||
licenseRuntimeIdentity,
|
||||
manualRunRequested,
|
||||
openAssistantOperationsLoopForFinding,
|
||||
patrolEnabledLocal,
|
||||
patrolApprovedDecisionCount,
|
||||
patrolGovernedActionCount,
|
||||
patrolPreflight,
|
||||
patrolLoadError,
|
||||
patrolPendingApprovalCount,
|
||||
patrolReadiness,
|
||||
patrolRejectedDecisionCount,
|
||||
patrolRunHistory,
|
||||
patrolVerifiedOutcomeCount,
|
||||
patrolWorkEvidenceCount,
|
||||
patrolWorkIssueEvidenceCount,
|
||||
runtimeState,
|
||||
patrolStatus,
|
||||
patrolStream,
|
||||
recordPatrolWorkflowStarterActivity,
|
||||
selectedRun,
|
||||
selectedRunFindingIds,
|
||||
selectedRunHasFindingsSnapshot,
|
||||
selectedRunScopeResourceIds,
|
||||
setActiveTab,
|
||||
setFindingsFilterOverride,
|
||||
setFullModeUnlocked,
|
||||
setAutopilotDialogOpen,
|
||||
setSelectedRun,
|
||||
setFindingScrollTimer: (timer: ReturnType<typeof setTimeout> | undefined) => {
|
||||
|
||||
@@ -568,8 +568,8 @@ async function openPatrolActivityMode() {
|
||||
|
||||
describe('AIIntelligence entitlement gating', () => {
|
||||
it('keeps Patrol page data sync bounded without making it a primary action', () => {
|
||||
expect(patrolIntelligenceStateSource).toContain('PATROL_REFRESH_TIMEOUT_MS');
|
||||
expect(patrolIntelligenceStateSource).toContain('finishRefresh(requestId)');
|
||||
expect(patrolIntelligenceStateSource).toContain('PATROL_MANUAL_SYNC_TIMEOUT_MS');
|
||||
expect(patrolIntelligenceStateSource).toContain('requestId === manualRefreshRequestId');
|
||||
expect(patrolIntelligenceStateSource).toContain('requestId === refreshRequestId');
|
||||
expect(patrolIntelligenceStateSource).toContain('isManualRefreshRunning');
|
||||
expect(patrolIntelligenceStateSource).toContain('handleRefreshPatrol');
|
||||
|
||||
@@ -3603,11 +3603,10 @@ describe('frontend resource type boundaries', () => {
|
||||
expect(aiIntelligenceSummaryModelSource).toContain(
|
||||
'export function normalizeIntelligenceSummary',
|
||||
);
|
||||
// Verify the symbol is imported from the model without prescribing a
|
||||
// specific single- vs multi-line import shape; usePatrolIntelligenceState
|
||||
// groups several patrolInvestigationContextModel imports together.
|
||||
expect(patrolIntelligenceStateSource).toMatch(
|
||||
/import\s*\{[^}]*\bbuildPatrolAssistantFindingHandoffFromUnifiedFinding\b[^}]*\}\s*from '\.\/patrolInvestigationContextModel';/,
|
||||
// FindingsPanel owns the Patrol finding to Assistant handoff; the page
|
||||
// state hook must not carry a second, unwired copy of it.
|
||||
expect(patrolIntelligenceStateSource).not.toContain(
|
||||
'buildPatrolAssistantFindingHandoffFromUnifiedFinding',
|
||||
);
|
||||
expect(patrolIntelligenceStateSource).toContain('getPatrolSavedReadinessWarning');
|
||||
expect(patrolIntelligenceStateSource).not.toContain('recent_changes?.length');
|
||||
|
||||
Reference in New Issue
Block a user