mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
Drop the unused Patrol readiness-failure object behind the toggle toast (#2687)
Stacked on #2686 (which stacks on #2682 and #2674); this branch carries
their commits until they land. Only the top two commits are new.
## What
`buildPatrolSettingsReadinessFailure` built a full
`PatrolConfigurationFailureInput` for the Patrol page's inline "needs
attention" state. That state lost its render in 27ba85ddc6 (2026-06-23),
when the Configure Patrol popover was removed. Since then its only
production caller, the Patrol on/off toggle, has read just `.message`,
and that was always the fixed copy "Patrol setting was saved, but Patrol
is not ready to run."
- `getPatrolSavedReadinessWarning` returns that message when the saved
response reports `patrol_readiness` `not_ready`, and the toggle shows it
as a warning toast, as before.
- `buildReadinessDetails` and the `PatrolConfigurationFailureInput` type
are removed. Its last other consumer,
`getPatrolConfigurationFailureInlineDetails`, goes in #2686.
- The Assistant drawer still restores saved
`patrol_configuration_failure` sessions. Nothing on that path changed.
## Runtime delta
- Production builds of the parent and this branch differ in one chunk,
`AIIntelligence.js` (the Patrol page), which is 935 bytes smaller. After
normalising chunk hashes, SRI strings and minified identifiers, the only
change is the removed builder and details helper, replaced by one status
check. The toast string is byte-identical.
- The minifier had already folded `message || readiness.summary ||
default` to the caller's constant, so the readiness-summary fallback was
unreachable.
- The toggle no longer reads seven Patrol signals to build the object it
discarded.
- `check-bundle-size.mjs` passes.
## Verification
The same Playwright script ran against embedded builds of the parent and
this branch, at 1440 and 390 px, on a demo mock stack. Responses were
stubbed with `page.route`. All 31 observed fields were identical between
the two builds. In every case only a toast appears, with no inline
alert, no "needs attention" text and no Assistant action:
- Patrol on/off, switched off and back on, with a not-ready response:
the warning toast fires in both directions.
- A rejected Patrol mode save shows an error toast with the server
reason, and the mode returns to Watch only. An accepted save switches to
Ask first with no toast.
- Settings > Pulse Intelligence > Patrol and Provider & Models saves
with a not-ready response show a warning toast naming the summary,
provider and model.
- When Patrol status reports not ready, the page's own readiness banner
names the summary, provider and model, and the header shows Fix setup.
Tests: `src/features`, `src/utils` and `src/__tests__` pass, except the
known Mac-only `agentInstallCommand.test.ts`. `tsc --noEmit` passes.
contract, registry and status audits pass.
## Tests and pins
- The hook tests now cover the warning helper.
- The #1789 test pinned `blockedCause: patrolStatus()?.blocked_cause`.
That matched only the removed dead field, not the `blockedCause` memo
the test was written for, so it now pins the memo.
- `frontendResourceTypeBoundaries` pins the new helper name.
- A negative control (mutating both pinned lines) fails all four
affected tests.
## Contracts
`patrol-intelligence`, `frontend-primitives` and `api-contracts` still
described the retired Patrol schedule and model popover, its inline
dialog failure state, and a saved-configuration-issue surface with cause
metadata. They now say:
- where Patrol settings live (Settings > Pulse Intelligence > Patrol,
and Settings > Provider & Models);
- that every save outcome is a toast;
- that the Patrol page's readiness banner carries the not-ready
diagnosis.
The popover rules apply only if a popover returns. The popover-era
alert-trigger clauses are handled separately in
docs/patrol-settings-contract-location.
A second, docs-only commit fixes the patrol-intelligence clause that
still placed the Patrol mode selector in a "configuration dialog". The
selector now renders only in the header's `Mode and automation`
disclosure, and that clause sits directly under #2682's hunk, so it
lands here rather than in docs/patrol-settings-contract-location to
avoid a conflict.
This commit is contained in:
@@ -4039,12 +4039,16 @@ the authoritative analysis outcome.
|
||||
`/api/settings/ai/update` may save a selected Patrol provider/model even
|
||||
when that model is not ready for tool-backed Patrol execution, but it must
|
||||
echo `patrol_readiness` with stable `cause` metadata and execution routes
|
||||
must continue to fail closed before model calls. Frontend Patrol settings
|
||||
must continue to fail closed before model calls. Frontend settings
|
||||
consumers must surface that saved-but-not-ready response as a saved
|
||||
configuration issue with the echoed provider, model, cause, and summary
|
||||
instead of reporting the successful save as a failed save or hiding the
|
||||
readiness blocker behind a generic notification; and the structured
|
||||
investigation-record contract, so unified findings may
|
||||
configuration issue rather than a failed save, and never as inline failure
|
||||
state: every Pulse Intelligence settings page (Provider & Models, Patrol,
|
||||
Assistant, Service context) raises a warning notification naming the echoed
|
||||
summary, provider, and model, while the Patrol page's on/off save raises a
|
||||
fixed not-ready warning and leaves the diagnosis to the page's own readiness
|
||||
surfaces, such as the readiness banner that names the summary, provider,
|
||||
and model while the header offers `Fix setup` when Patrol is active; and
|
||||
the structured investigation-record contract, so unified findings may
|
||||
expose `investigation_record` only through the shared
|
||||
`aicontracts.InvestigationRecord` payload shape, with frontend API types
|
||||
and backend contract tests updated in the same slice as any field change.
|
||||
|
||||
@@ -3164,33 +3164,30 @@ Agent`), with the plain-language source phrase available through accessible
|
||||
operator action or selected Patrol context, uses distinct monitor labels,
|
||||
and does not become a nested card, generic dashboard strip, trust summary,
|
||||
or duplicate Patrol empty-work list. The
|
||||
Patrol schedule and model drawer is part of that shared
|
||||
feature-presentation boundary: it must stay viewport-bounded, expose an
|
||||
accessible dialog label, keep the four-level control policy on the default
|
||||
Patrol header, and keep provider model, schedule, trigger tuning, and
|
||||
readiness validation inside the secondary disclosure. Backend save rejection reasons must pass
|
||||
through as inline dialog state instead of being replaced with generic toast
|
||||
copy, and that advanced disclosure must open when the inline state exists.
|
||||
When the failure includes
|
||||
Patrol readiness context, the inline state must expose the provider, model,
|
||||
and readiness summary next to a direct provider-settings action instead of
|
||||
hiding that diagnosis behind Assistant alone. The provider-model selector in
|
||||
that popover must stay bound to the shared runtime settings/model catalog
|
||||
even when the popover mounts after async catalog loading, but the full
|
||||
catalog must stay behind an explicit change action so the default advanced
|
||||
drawer leads with the current effective model summary rather than a raw
|
||||
provider route list. A saved direct-provider Patrol model still renders as
|
||||
that model instead of visually falling back to the default selection.
|
||||
Successful provider-model saves that return a not-ready Patrol
|
||||
readiness snapshot must use that same inline surface with `needs attention`
|
||||
wording, and any Assistant handoff from that state describes a saved
|
||||
configuration issue rather than a failed save. When governed fixes are locked, the same Patrol state
|
||||
owner must clear stale full-mode unlock state before persisting the
|
||||
monitor-only autonomy payload, so disabled paid controls cannot leak stale
|
||||
permission into a save. If that inline state opens Assistant, the Patrol
|
||||
feature must hand off
|
||||
a source-named, model-only briefing and close the popover so the shared
|
||||
Assistant drawer is not visually hidden behind feature chrome. When a
|
||||
Patrol page keeps the four-level control policy on its header and has no
|
||||
schedule and model drawer: schedule, trigger tuning, and the Patrol model
|
||||
override live on Settings > Pulse Intelligence > Patrol, and providers and
|
||||
the shared default model on Settings > Provider & Models. Those saves
|
||||
report through the shared notification toasts, not inline state: a
|
||||
rejected save raises an error toast carrying the backend's concrete reason
|
||||
rather than generic fallback copy, and a saved response whose Patrol
|
||||
readiness is not ready or degraded raises a warning toast naming the
|
||||
readiness summary, provider, and model instead of a success toast. On the
|
||||
Patrol page, an on/off save whose response reports Patrol not ready raises
|
||||
a fixed warning toast, and a failed Patrol mode save raises an error toast
|
||||
and restores the previous mode; neither renders inline save-failure state
|
||||
or offers an Assistant handoff. If a Patrol settings popover or inline
|
||||
save-failure state returns, it must stay viewport-bounded, expose an
|
||||
accessible dialog label, bind any model selector to the shared runtime
|
||||
settings and model catalog, show readiness context (provider, model,
|
||||
summary) next to a direct provider-settings action rather than behind
|
||||
Assistant alone, and describe a saved but not ready response as a saved
|
||||
configuration issue rather than a failed save. An Assistant handoff from it
|
||||
must send a source-named, model-only briefing and close the popover so the
|
||||
shared Assistant drawer is not visually hidden behind feature chrome. When
|
||||
governed fixes are locked, the Patrol state owner must clear stale
|
||||
full-mode unlock state before persisting the monitor-only autonomy payload,
|
||||
so disabled paid controls cannot leak stale permission into a save. When a
|
||||
restored Patrol assessment session is attached, the shared Assistant drawer
|
||||
empty state must
|
||||
stay aligned with that source-named briefing and must not render generic
|
||||
|
||||
@@ -1581,17 +1581,18 @@ suppress stale healthy summary headlines such as `Health A · 100/100` even if
|
||||
the last summary payload still looks healthy. Legacy `/ai` entry points must
|
||||
redirect into that same Patrol-owned shell rather than preserving a second
|
||||
canonical route.
|
||||
That same browser proof now covers the Patrol control and advanced-settings
|
||||
split. The advanced Patrol settings drawer must stay within the desktop
|
||||
viewport, avoid duplicating the inline Patrol control policy, expose
|
||||
provider/model, schedule, trigger, and user-level model checks directly, and surface
|
||||
the backend's concrete license/validation reason when a settings change is
|
||||
rejected instead of replacing it with a generic `Failed to save advanced
|
||||
settings` toast. That inline failure no longer
|
||||
opens Assistant. If it does again, the handoff is model-only explanation
|
||||
context: raw command, script, credential, and provider-detail payloads stay
|
||||
redacted, Assistant opens with `autonomousMode:false`, and the Patrol control
|
||||
panel closes so the operator is not left behind an overlapping popover.
|
||||
That same browser proof now covers where Patrol settings are saved. The
|
||||
Patrol page has no settings drawer or popover: Patrol schedule, triggers, and
|
||||
the Patrol model override live on Settings > Pulse Intelligence > Patrol
|
||||
(`Save Patrol settings`), and providers and the shared default model on
|
||||
Settings > Provider & Models. A rejected save there raises an error
|
||||
notification carrying the backend's concrete license/validation reason
|
||||
instead of generic fallback copy, and it offers no Assistant handoff. If a
|
||||
Patrol settings popover or an Assistant handoff from a settings failure
|
||||
returns, the handoff is model-only explanation context: raw command, script,
|
||||
credential, and provider-detail payloads stay redacted, Assistant opens with
|
||||
`autonomousMode:false`, and the popover closes so the operator is not left
|
||||
behind it.
|
||||
Patrol-control save-failure sessions saved by earlier builds keep the
|
||||
compatible `handoff_metadata.kind=patrol_configuration_failure` plus only the
|
||||
runtime-failure boolean needed for drawer/session presentation, so they restore
|
||||
@@ -1637,12 +1638,13 @@ ready raises a fixed warning notification, a failed Patrol mode save raises an e
|
||||
notification, and neither offers an Assistant handoff. A provider or model save
|
||||
in Settings > Provider & Models reports the readiness summary, provider and
|
||||
model in its own save message.
|
||||
The Patrol mode selector in that header and configuration dialog must
|
||||
compose the shared `frontend-modern/src/components/shared/FilterButtonGroup.tsx`
|
||||
instead of rebuilding a local active-button group. The wide default Patrol
|
||||
header uses the segmented layout; the constrained configuration dialog may use
|
||||
the shared prominent layout so all four mode labels remain readable without
|
||||
inventing a Patrol-local selector. Patrol owns the default visible four-level
|
||||
The Patrol mode selector in that header must compose the shared
|
||||
`frontend-modern/src/components/shared/FilterButtonGroup.tsx` instead of
|
||||
rebuilding a local active-button group. When the header shows Patrol mode
|
||||
options, it renders the selector in the segmented layout inside its `Mode and
|
||||
automation` disclosure; a constrained surface that hosts it may use the shared
|
||||
prominent layout so all four mode labels remain readable without inventing a
|
||||
Patrol-local selector. Patrol owns the default visible four-level
|
||||
policy presentation (`Watch only`, `Ask first`, `Safe auto-fix`,
|
||||
`Autopilot`), entitlement locks, and the rule that choosing the highest
|
||||
Autopilot level sends `full_mode_unlocked:true` while choosing any lower level clears that
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "421d1e5423e5b83b7923ea8c3b133ad520cd5f60",
|
||||
"verified_at": "2026-10-07T15:20:38Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts",
|
||||
"frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts": "f1e249c36f0f0f9810330a37cc9a80cb43baa1cd123db1defd3f27720d39912a",
|
||||
"frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts": "cb0a6962ed929238a974a3bbe5f69f4c89961ff30c88c2c94398073b9ef99a1c"
|
||||
},
|
||||
"routes": [
|
||||
"/patrol",
|
||||
"/patrol (Mode and automation disclosure)",
|
||||
"/settings/pulse-intelligence/patrol",
|
||||
"/settings/pulse-intelligence"
|
||||
],
|
||||
"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 (333e51d2de; rebased onto 421d1e5423, which changes only contract docs) and this tree, each on a private DEMO_MODE mock stack with a fresh data dir; the same Playwright script ran against both and produced identical results on all 31 observed fields at both widths.",
|
||||
"Patrol on/off save whose PUT /api/settings/ai/update response (page.route) reports patrol_readiness not_ready: switching off and back on each raise only the warning toast 'Patrol setting was saved, but Patrol is not ready to run.' (role=alert inside the Notifications region, close button only); no role=alert outside the toast region, no 'needs attention' text, no readiness summary rendered inline, no Assistant action.",
|
||||
"Failed Patrol mode save (Pro mode selector unlocked by adding ai_autofix to runtime-capabilities via page.route; PUT /api/ai/patrol/autonomy answered 500 {error}): only the error toast with the server reason; mode badge returns to Watch only; no inline error or 'needs attention' state.",
|
||||
"Successful Patrol mode save (PUT 200, reconcile GET returns approval): Ask first becomes pressed and the badge reads Ask first; no new toast and no inline alert.",
|
||||
"Settings > Pulse Intelligence > Patrol save (Schedule -> Every hour) and Settings > Provider & Models save with a not_ready response: only the warning toast '<saved label>, but Patrol is not ready: The selected model cannot run Patrol tools. \u00b7 Provider: OpenRouter \u00b7 Model: openrouter:deepseek/deepseek-r1'; no inline alert.",
|
||||
"Patrol status with readiness not_ready (page.route on /api/ai/patrol/status): the page's own 'Patrol model issue' banner names summary, provider and model, and the header shows Fix setup, at both widths.",
|
||||
"Dist proof: 187 files each side; after normalising chunk hashes and SRI strings only assets/AIIntelligence.js differs (935 bytes smaller): the removed failure builder, details helper and surface callback replaced by one status check, toast string byte-identical; check-bundle-size.mjs passes."
|
||||
],
|
||||
"interactions": [
|
||||
"Demo sign-in; on /patrol click Toggle Patrol twice (off, then on) with a not-ready settings response; open 'Mode and automation' and click Ask first with a rejected save, then again with an accepted save; desktop mouse and phone touch (isMobile) contexts.",
|
||||
"On /settings/pulse-intelligence/patrol select Schedule 'Every hour' and click Save Patrol settings; on /settings/pulse-intelligence click Save provider settings; both with a not-ready response.",
|
||||
"Screenshots of each toast state captured and inspected at both widths."
|
||||
]
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
PATROL_MANUAL_SYNC_TIMEOUT_MS,
|
||||
PATROL_REFRESH_TIMEOUT_MS,
|
||||
buildPatrolSettingsReadinessFailure,
|
||||
getPatrolSavedReadinessWarning,
|
||||
openPatrolAssistantWorkflowHandoff,
|
||||
patrolStartFailureMessage,
|
||||
recordPatrolControlStarterActivity,
|
||||
@@ -338,24 +338,25 @@ describe('usePatrolIntelligenceState', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildPatrolSettingsReadinessFailure', () => {
|
||||
it('ignores settings snapshots that do not block Patrol readiness', () => {
|
||||
describe('getPatrolSavedReadinessWarning', () => {
|
||||
it('stays quiet when the saved settings do not block Patrol readiness', () => {
|
||||
expect(getPatrolSavedReadinessWarning(null)).toBeNull();
|
||||
expect(
|
||||
buildPatrolSettingsReadinessFailure({
|
||||
settings: settingsWithReadiness({
|
||||
getPatrolSavedReadinessWarning(
|
||||
settingsWithReadiness({
|
||||
status: 'warning',
|
||||
ready: true,
|
||||
summary: 'Patrol can run with reduced confidence.',
|
||||
checks: [],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('builds a saved configuration issue from a not-ready settings response', () => {
|
||||
it('warns that a not-ready Patrol setting was still saved', () => {
|
||||
expect(
|
||||
buildPatrolSettingsReadinessFailure({
|
||||
settings: settingsWithReadiness({
|
||||
getPatrolSavedReadinessWarning(
|
||||
settingsWithReadiness({
|
||||
status: 'not_ready',
|
||||
ready: false,
|
||||
cause: 'model_unsupported_tools',
|
||||
@@ -364,40 +365,8 @@ describe('usePatrolIntelligenceState', () => {
|
||||
model: 'ollama:deepseek-r1:7b',
|
||||
checks: [],
|
||||
}),
|
||||
autonomyLevel: 'monitor',
|
||||
fullModeUnlocked: false,
|
||||
investigationBudget: 15,
|
||||
investigationTimeoutSec: 300,
|
||||
runtimeState: 'blocked',
|
||||
blockedReason: 'Connect a tool-capable Patrol model.',
|
||||
}),
|
||||
).toEqual({
|
||||
message: 'The selected model cannot run Patrol tools.',
|
||||
code: 'patrol_readiness_not_ready',
|
||||
status: 409,
|
||||
saved: true,
|
||||
details: {
|
||||
status: 'not_ready',
|
||||
cause: 'model_unsupported_tools',
|
||||
summary: 'The selected model cannot run Patrol tools.',
|
||||
provider: 'ollama',
|
||||
model: 'ollama:deepseek-r1:7b',
|
||||
},
|
||||
autonomyLevel: 'monitor',
|
||||
fullModeUnlocked: false,
|
||||
investigationBudget: 15,
|
||||
investigationTimeoutSec: 300,
|
||||
readiness: {
|
||||
status: 'not_ready',
|
||||
cause: 'model_unsupported_tools',
|
||||
summary: 'The selected model cannot run Patrol tools.',
|
||||
provider: 'ollama',
|
||||
model: 'ollama:deepseek-r1:7b',
|
||||
},
|
||||
runtimeState: 'blocked',
|
||||
blockedReason: 'Connect a tool-capable Patrol model.',
|
||||
blockedCause: undefined,
|
||||
});
|
||||
),
|
||||
).toBe('Patrol setting was saved, but Patrol is not ready to run.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -410,6 +379,8 @@ describe('usePatrolIntelligenceState', () => {
|
||||
'provider_not_configured',
|
||||
);
|
||||
expect(resolvePatrolBlockedActionCause('', '')).toBeUndefined();
|
||||
expect(patrolIntelligenceStateSource).toContain('blockedCause: patrolStatus()?.blocked_cause');
|
||||
expect(patrolIntelligenceStateSource).toContain(
|
||||
'const blockedCause = createMemo(() => patrolStatus()?.blocked_cause);',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -182,27 +182,6 @@ export interface PatrolRunAssistantHandoff {
|
||||
context: AIChatContext;
|
||||
}
|
||||
|
||||
export interface PatrolConfigurationFailureInput {
|
||||
message: string;
|
||||
code?: string;
|
||||
status?: number;
|
||||
saved?: boolean;
|
||||
details?: Record<string, string>;
|
||||
autonomyLevel?: string;
|
||||
fullModeUnlocked?: boolean;
|
||||
investigationBudget?: number;
|
||||
investigationTimeoutSec?: number;
|
||||
readiness?: {
|
||||
status?: string;
|
||||
cause?: string;
|
||||
summary?: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
} | null;
|
||||
runtimeState?: string;
|
||||
blockedReason?: string;
|
||||
blockedCause?: string;
|
||||
}
|
||||
const MAX_PATROL_RUN_HANDOFF_RESOURCES = 8;
|
||||
|
||||
export function buildPatrolInvestigationRecordPresentation(
|
||||
|
||||
@@ -62,7 +62,6 @@ import {
|
||||
patrolAssistantFindingHandoffRequiresApprovalMode,
|
||||
type PatrolAssistantApprovalBriefingInput,
|
||||
type PatrolAssistantFindingHandoff,
|
||||
type PatrolConfigurationFailureInput,
|
||||
} from './patrolInvestigationContextModel';
|
||||
import { schedulePatrolRunAcceptanceReconciliation } from './patrolRunAcceptance';
|
||||
|
||||
@@ -141,19 +140,6 @@ export const patrolStartFailureMessage = (error: unknown): string =>
|
||||
? `Could not reach Pulse to start Patrol: ${patrolErrorMessage(error, 'network request failed')}`
|
||||
: patrolErrorMessage(error, 'Pulse rejected the Patrol run');
|
||||
|
||||
const buildReadinessDetails = (
|
||||
readiness: NonNullable<AISettings['patrol_readiness']>,
|
||||
): Record<string, string> => {
|
||||
const details: Record<string, string> = {
|
||||
status: readiness.status,
|
||||
};
|
||||
if (readiness.cause?.trim()) details.cause = readiness.cause.trim();
|
||||
if (readiness.summary?.trim()) details.summary = readiness.summary.trim();
|
||||
if (readiness.provider?.trim()) details.provider = readiness.provider.trim();
|
||||
if (readiness.model?.trim()) details.model = readiness.model.trim();
|
||||
return details;
|
||||
};
|
||||
|
||||
export function resolvePatrolAutonomyLevelForSave(
|
||||
level: PatrolAutonomyLevel,
|
||||
fullModeUnlocked: boolean,
|
||||
@@ -184,52 +170,16 @@ export function resolvePatrolAutonomySettingsForSave({
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPatrolSettingsReadinessFailure({
|
||||
settings,
|
||||
message,
|
||||
autonomyLevel,
|
||||
fullModeUnlocked,
|
||||
investigationBudget,
|
||||
investigationTimeoutSec,
|
||||
runtimeState,
|
||||
blockedReason,
|
||||
blockedCause,
|
||||
}: {
|
||||
settings: AISettings | null | undefined;
|
||||
message?: string;
|
||||
autonomyLevel?: string;
|
||||
fullModeUnlocked?: boolean;
|
||||
investigationBudget?: number;
|
||||
investigationTimeoutSec?: number;
|
||||
runtimeState?: string;
|
||||
blockedReason?: string;
|
||||
blockedCause?: string;
|
||||
}): PatrolConfigurationFailureInput | null {
|
||||
const readiness = settings?.patrol_readiness;
|
||||
if (!readiness || readiness.status !== 'not_ready') return null;
|
||||
|
||||
return {
|
||||
message:
|
||||
message || readiness.summary || 'Patrol settings were saved, but Patrol is not ready to run.',
|
||||
code: 'patrol_readiness_not_ready',
|
||||
status: 409,
|
||||
saved: true,
|
||||
details: buildReadinessDetails(readiness),
|
||||
autonomyLevel,
|
||||
fullModeUnlocked,
|
||||
investigationBudget,
|
||||
investigationTimeoutSec,
|
||||
readiness: {
|
||||
status: readiness.status,
|
||||
cause: readiness.cause,
|
||||
summary: readiness.summary,
|
||||
provider: readiness.provider,
|
||||
model: readiness.model,
|
||||
},
|
||||
runtimeState,
|
||||
blockedReason,
|
||||
blockedCause,
|
||||
};
|
||||
/**
|
||||
* A Patrol on/off save whose response reports Patrol not ready was still
|
||||
* saved, so it raises a warning rather than an error.
|
||||
*/
|
||||
export function getPatrolSavedReadinessWarning(
|
||||
settings: AISettings | null | undefined,
|
||||
): string | null {
|
||||
return settings?.patrol_readiness?.status === 'not_ready'
|
||||
? 'Patrol setting was saved, but Patrol is not ready to run.'
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -380,26 +330,6 @@ export function usePatrolIntelligenceState() {
|
||||
applyPatrolAISettings(aiRuntimeSettings());
|
||||
});
|
||||
|
||||
const surfaceSavedPatrolReadinessIssue = (
|
||||
settings: AISettings | null | undefined,
|
||||
message?: string,
|
||||
) => {
|
||||
const failure = buildPatrolSettingsReadinessFailure({
|
||||
settings,
|
||||
message,
|
||||
autonomyLevel: autonomyLevel(),
|
||||
fullModeUnlocked: fullModeUnlocked(),
|
||||
investigationBudget: investigationBudget(),
|
||||
investigationTimeoutSec: investigationTimeout(),
|
||||
runtimeState: runtimeState(),
|
||||
blockedReason: blockedReason(),
|
||||
blockedCause: patrolStatus()?.blocked_cause,
|
||||
});
|
||||
if (failure) {
|
||||
notificationStore.warning(failure.message);
|
||||
}
|
||||
};
|
||||
|
||||
async function handleTogglePatrol() {
|
||||
if (isTogglingPatrol()) return;
|
||||
setIsTogglingPatrol(true);
|
||||
@@ -419,10 +349,10 @@ export function usePatrolIntelligenceState() {
|
||||
} else {
|
||||
setPatrolEnabledLocal(newValue);
|
||||
}
|
||||
surfaceSavedPatrolReadinessIssue(
|
||||
data,
|
||||
'Patrol setting was saved, but Patrol is not ready to run.',
|
||||
);
|
||||
const readinessWarning = getPatrolSavedReadinessWarning(data);
|
||||
if (readinessWarning) {
|
||||
notificationStore.warning(readinessWarning);
|
||||
}
|
||||
if (refetchPatrolStatus) {
|
||||
refetchPatrolStatus();
|
||||
}
|
||||
|
||||
@@ -3609,7 +3609,7 @@ describe('frontend resource type boundaries', () => {
|
||||
expect(patrolIntelligenceStateSource).toMatch(
|
||||
/import\s*\{[^}]*\bbuildPatrolAssistantFindingHandoffFromUnifiedFinding\b[^}]*\}\s*from '\.\/patrolInvestigationContextModel';/,
|
||||
);
|
||||
expect(patrolIntelligenceStateSource).toContain('buildPatrolSettingsReadinessFailure');
|
||||
expect(patrolIntelligenceStateSource).toContain('getPatrolSavedReadinessWarning');
|
||||
expect(patrolIntelligenceStateSource).not.toContain('recent_changes?.length');
|
||||
expect(patrolIntelligenceStateSource).not.toContain('governed resource${');
|
||||
expect(patrolInvestigationContextModelSource).toContain(
|
||||
|
||||
Reference in New Issue
Block a user