mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Point Patrol trigger contracts at the Patrol settings page (#2691)
## What
27ba85ddc6 removed the Patrol page's "Configure Patrol" popover.
Schedule, trigger and model settings moved to **Settings > Pulse
Intelligence > Patrol** (`PatrolSettingsContent` in `AISettings.tsx`,
saved through `useAISettingsState.ts`). Several contract clauses still
described the popover. This PR rewrites them to say where the controls
live and how they save.
| Contract | Old claim | Source today |
|---|---|---|
| patrol-intelligence (alert-trigger policy) | The selector sits in the
Patrol control panel and persists through its own `AIAPI.updateSettings`
call with optimistic state and revert-on-error | It is a field in the
settings form. Nothing is written until `Save Patrol settings`. The
severity goes in the page's single payload only when it changed. A
rejected save keeps the edit, and a successful save reloads the form
from the response |
| frontend-primitives (severity selector) | Lives under
`features/patrol/` in an "advanced Patrol settings disclosure" |
`FormSelect` in the settings page's Triggers section, rendered only
while alert triggers are on |
| frontend-primitives (toggles) | Toggles are named through
`ariaLabelledBy` | They carry explicit `ariaLabel` strings that contain
the visible label (`Enable alert-triggered Patrols`) |
| patrol-intelligence, api-contracts, frontend-primitives #61 | A header
"Schedule & model" drawer, a header settings surface with trigger
toggles, and a Patrol mode dialog | The header has a `Settings` link
(hidden while setup blocks), a conditional trigger-status line, and the
mode selector inside its `Mode and automation` disclosure |
Clauses owned by sibling PRs are left alone: the frontend-primitives
"Patrol schedule and model drawer" block, the patrol-intelligence
"advanced Patrol settings drawer" paragraph, and the patrol-intelligence
"header and configuration dialog" sentence (being folded into #2687).
Merge-tree against #2682, #2686, #2677 and the unpushed sibling heads is
clean.
## Tests
Three new cases in `AISettings.test.tsx` pin the rewritten claims:
- The severity selector is hidden while alert triggers are off, keeps
its value across off/on, and writes nothing before Save.
- A rejected save leaves the edit in the form for a retry. A successful
save reloads the form from the response and does not resend an unchanged
severity.
- Each Patrol trigger toggle's accessible name contains its visible
label.
Each assertion fails against a deliberately broken build: selector
always rendered, autosave on change, form reset on error, no reload
after success, severity always sent, and a divergent toggle `ariaLabel`.
`contract_audit.py`, `registry_audit.py` and `status_audit.py` report no
errors. The test-only frontend change needs no browser receipt; the
guard skips it.
This commit is contained in:
@@ -3183,10 +3183,11 @@ a new API state machine, queue contract, or verification-accounting field.
|
||||
`Safe auto-fix`, and
|
||||
`Autopilot` rather than transport shorthand or Pro-matrix labels, the Patrol
|
||||
header description derives from the same
|
||||
effective control state, and the advanced settings
|
||||
drawer stays limited to model, schedule, trigger, and user-level model checks
|
||||
sourced from the existing settings APIs rather than duplicating a second
|
||||
control-level chooser. Runtime or provider setup
|
||||
effective control state, and Settings > Pulse Intelligence > Patrol, which
|
||||
replaced the retired header settings drawer, stays limited to model,
|
||||
schedule, trigger, notification, and user-level model checks sourced from
|
||||
the existing settings APIs rather than duplicating a second control-level
|
||||
chooser. Runtime or provider setup
|
||||
readiness may suppress run, schedule, model, trigger, and provider-repair
|
||||
controls until Patrol can check infrastructure reliably, but it must not hide
|
||||
the Patrol mode selector or replace it with setup-first proof/status chrome.
|
||||
@@ -3322,7 +3323,7 @@ a new API state machine, queue contract, or verification-accounting field.
|
||||
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
|
||||
and the Patrol header in `frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx`, which outside setup-only states may summarize noteworthy API-owned `status.trigger_status` facts in one compact line and, unless a provider setup block shows `Fix setup` instead, links to Settings > Pulse Intelligence > Patrol, where the Patrol model, schedule, and trigger tuning are saved through `PUT /api/settings/ai/update`, so none of those settings or background-only runtime-policy pauses become the primary Patrol control decision and the header carries no schedule and model drawer
|
||||
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
|
||||
|
||||
@@ -1214,7 +1214,7 @@ Compact Coverage age cells use the existing metadata-size text and reclaim numer
|
||||
59. `frontend-modern/src/components/shared/FilterBar/index.ts`
|
||||
59c. `frontend-modern/src/components/shared/FilterBar/filterOptionPresentation.tsx`
|
||||
60. `frontend-modern/src/components/shared/TypeColumn.guardrails.test.ts`
|
||||
61. `frontend-modern/src/features/` (including Patrol presentation, where Patrol control starter counts are context only even when mirrored through `patrolAutonomy*` compatibility fields, successful direct Patrol control saves may record the content-free `patrol_control` starter only when paid control is available and the effective control posture changes and must then refresh Patrol status, findings, approvals, and run history before the operator waits for polling, legacy `proActivation*` starter aliases must not render a separate proof strip by themselves, Patrol control completed/resolved counts may only project backend-owned terminal proof, current active findings and pending approvals outrank historical completion proof in the primary operator state, selected run history must read as a Patrol run record rather than a findings filter or snapshot workflow, terminal verified/rejected outcomes with no active finding or pending approval must stay history detail without rendering a no-op proof strip, resolved-only issue history must not be promoted into current-work copy or actions, compact recurrence/trust counters must read as historical evidence rather than current issue state, Patrol-owned status/history evidence must keep the assessment visible when the broader intelligence summary is missing, Patrol work-group chips may group current approvals, failed actions, failed checks, recurring active issues, and stale scheduled protection but must not become a separate status/trust/proof strip, the Patrol route and page title must lead with Patrol while the default workspace underneath may use Open work and run history stays a deliberate secondary review surface, setup-only Patrol runtime failures must instead use `Fix Patrol setup` framing with a dedicated setup task and direct provider-settings action while suppressing generic issue-row chips and filter chrome, Patrol must not expose a generic Details/supporting-context panel for nearby activity, related patterns, or policy limits, locked-control copy must state the watch-only boundary in positive capability language by saying Patrol checks infrastructure and shows current issues, avoid repeating the same sentence across the header and control, and avoid repeatedly restating infrastructure-unchanged caveats or relying on disabled controls, compact Pro badges, Limits controls, or manual-review framing, `patrolControlValueState` decides whether a terminal decision is partial review context or verified value proof while `patrolAutonomyValueState` remains a compatibility mirror, legacy `proActivation*` fields are compatibility fallback only, native Patrol state must not load the operations-loop status projection to decide current work, local Patrol state must expose issue-backed `patrolWork*` evidence rather than legacy proof naming, Patrol mode labels remain domain copy that must describe backend-owned risk policy without creating page-local safety thresholds, the selected Patrol mode sentence must state the approval and policy boundary without adding a second limits panel or proof strip, the Patrol schedule and model drawer must stay separate from the always-visible Patrol mode selector instead of duplicating the four control choices or reintroducing save/apply configuration framing, and Patrol header refresh controls must call an explicit operator-refresh handler whose spinning/disabled state is separate from background polling and initial data loads)
|
||||
61. `frontend-modern/src/features/` (including Patrol presentation, where Patrol control starter counts are context only even when mirrored through `patrolAutonomy*` compatibility fields, successful direct Patrol control saves may record the content-free `patrol_control` starter only when paid control is available and the effective control posture changes and must then refresh Patrol status, findings, approvals, and run history before the operator waits for polling, legacy `proActivation*` starter aliases must not render a separate proof strip by themselves, Patrol control completed/resolved counts may only project backend-owned terminal proof, current active findings and pending approvals outrank historical completion proof in the primary operator state, selected run history must read as a Patrol run record rather than a findings filter or snapshot workflow, terminal verified/rejected outcomes with no active finding or pending approval must stay history detail without rendering a no-op proof strip, resolved-only issue history must not be promoted into current-work copy or actions, compact recurrence/trust counters must read as historical evidence rather than current issue state, Patrol-owned status/history evidence must keep the assessment visible when the broader intelligence summary is missing, Patrol work-group chips may group current approvals, failed actions, failed checks, recurring active issues, and stale scheduled protection but must not become a separate status/trust/proof strip, the Patrol route and page title must lead with Patrol while the default workspace underneath may use Open work and run history stays a deliberate secondary review surface, setup-only Patrol runtime failures must instead use `Fix Patrol setup` framing with a dedicated setup task and direct provider-settings action while suppressing generic issue-row chips and filter chrome, Patrol must not expose a generic Details/supporting-context panel for nearby activity, related patterns, or policy limits, locked-control copy must state the watch-only boundary in positive capability language by saying Patrol checks infrastructure and shows current issues, avoid repeating the same sentence across the header and control, and avoid repeatedly restating infrastructure-unchanged caveats or relying on disabled controls, compact Pro badges, Limits controls, or manual-review framing, `patrolControlValueState` decides whether a terminal decision is partial review context or verified value proof while `patrolAutonomyValueState` remains a compatibility mirror, legacy `proActivation*` fields are compatibility fallback only, native Patrol state must not load the operations-loop status projection to decide current work, local Patrol state must expose issue-backed `patrolWork*` evidence rather than legacy proof naming, Patrol mode labels remain domain copy that must describe backend-owned risk policy without creating page-local safety thresholds, the selected Patrol mode sentence must state the approval and policy boundary without adding a second limits panel or proof strip, Patrol schedule, trigger, and model settings must stay on Settings > Pulse Intelligence > Patrol, apart from the Patrol mode selector, instead of returning as a Patrol page drawer, that settings page must not duplicate the four control choices, and the Patrol page mode selector must not reintroduce save/apply configuration framing, and Patrol header refresh controls must call an explicit operator-refresh handler whose spinning/disabled state is separate from background polling and initial data loads)
|
||||
62. `frontend-modern/src/components/SetupWizard/SetupWizard.tsx`
|
||||
63. `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts`
|
||||
64. `frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx`
|
||||
@@ -4440,21 +4440,26 @@ Assistant and settings surfaces can expose provider repair without forking the
|
||||
model-list shell; callers own the destination, while the shared picker owns the
|
||||
button placement, labeling, close behavior, and keyboard-safe dropdown state.
|
||||
|
||||
The Patrol alert-trigger severity selector under
|
||||
`frontend-modern/src/features/patrol/` is built on the shared `FormSelect`
|
||||
primitive (label-for/id wiring, `selectBaseClass` styling hook) rather than a
|
||||
hand-rolled `<select>`, so its labeling and disabled-state affordances stay
|
||||
consistent with the rest of the AI settings surface. That selector belongs in the
|
||||
advanced Patrol settings disclosure, below the control policy that defines what
|
||||
Patrol may do.
|
||||
The Patrol alert-trigger severity selector on Settings > Pulse Intelligence >
|
||||
Patrol (`frontend-modern/src/components/Settings/AISettings.tsx`) is built on
|
||||
the shared `FormSelect` primitive (label-for/id wiring, `selectBaseClass`
|
||||
styling hook) rather than a hand-rolled `<select>`, so its labeling and
|
||||
disabled-state affordances stay consistent with the rest of the AI settings
|
||||
surface. It sits in that page's Triggers section under the `Alert-triggered
|
||||
Patrols` toggle, renders only while that toggle is on, and is saved with the
|
||||
rest of the page by `Save Patrol settings`. The Patrol page carries no trigger
|
||||
settings; its header's `Settings` link opens this page when setup is not
|
||||
blocking.
|
||||
|
||||
The advanced Patrol control toggles in the same surface bind each `Toggle`
|
||||
primitive's accessible name through `ariaLabelledBy` pointing at the row's
|
||||
heading span, so renaming a control's visible label (for example
|
||||
"Alert-Triggered Analysis" to "Container Update Risk") updates the accessible
|
||||
name automatically without a parallel `aria-label` string. New Patrol toggle
|
||||
copy must keep using the shared `Toggle` `ariaLabelledBy` wiring rather than
|
||||
hard-coding a divergent accessible name.
|
||||
The Patrol trigger and notification toggles on that page (`Alert-triggered
|
||||
Patrols`, `Anomaly-triggered Patrols`, `Container update risk`, `Finding
|
||||
notifications`) name each `Toggle` primitive through an explicit `ariaLabel`
|
||||
such as `Enable alert-triggered Patrols`, not `ariaLabelledBy`. Each
|
||||
accessible name must contain its row's visible label, so a change to a row's
|
||||
visible copy (for example "Alert-Triggered Analysis" to "Container update
|
||||
risk") must update its `ariaLabel` in the same change rather than leave a
|
||||
divergent accessible name. `AISettings.test.tsx` finds these toggles by name
|
||||
and checks that each name contains its visible label.
|
||||
|
||||
Kubernetes RBAC inventory (Roles, ClusterRoles, RoleBindings,
|
||||
ClusterRoleBindings) is part of the existing Kubernetes platform-page
|
||||
|
||||
@@ -367,8 +367,8 @@ or historical proof/counting for resolved-only work.
|
||||
summary.
|
||||
Runtime/setup findings are still active Patrol work, but setup-only runtime
|
||||
failures must read as one setup task rather than an infrastructure issue
|
||||
queue. The header may suppress run and schedule/model controls while setup is
|
||||
the only active work, but it must keep the Patrol mode selector visible
|
||||
queue. The header may suppress its run control and `Settings` link while setup
|
||||
is the only active work, but it must keep the Patrol mode selector available
|
||||
because the operator's autonomy boundary remains a primary product choice
|
||||
even before Patrol can run. The workspace must replace the generic findings
|
||||
row with a dedicated provider/runtime setup task, one current issue label,
|
||||
@@ -700,15 +700,19 @@ attention`, `approval needed`, `outcome verified`, `no active work`) instead
|
||||
Effective runtime blocks on event-triggered Patrol must pass through the same
|
||||
`status.trigger_status` presentation helper, but the default header and
|
||||
activity strip may surface them only when they explain an actionable manual
|
||||
Patrol block; background-only trigger pauses belong in secondary schedule and
|
||||
model diagnostics when manual Patrol still works.
|
||||
Patrol mode selection belongs to the always-visible header control;
|
||||
the header drawer is the secondary Schedule & model surface and may expose
|
||||
provider model, schedule, trigger tuning, and readiness errors, but it must
|
||||
not duplicate the four Patrol mode choices or reintroduce a save button
|
||||
for already auto-saving secondary fields. The provider model setting must lead with the effective
|
||||
Patrol/default model summary; the full model catalog is power-user detail
|
||||
behind an explicit change action, not the default content of the drawer.
|
||||
Patrol block; while manual Patrol still works, a background-only trigger
|
||||
pause stays off the header, and no other surface renders it today.
|
||||
Patrol mode selection belongs to the Patrol page header, which has no
|
||||
schedule and model drawer. The Patrol model, schedule, and trigger tuning
|
||||
live on Settings > Pulse Intelligence > Patrol, which the header's
|
||||
`Settings` link opens when setup is not blocking, and save together through
|
||||
that page's `Save Patrol settings` button. The same page runs the model
|
||||
readiness check as its own `Check Patrol model` action, separate from that
|
||||
save. That settings page must not duplicate the
|
||||
four Patrol mode choices; it points back to the Patrol page for them. The
|
||||
Patrol model field must lead with the current selection or `Use shared
|
||||
default`; the full model catalog is power-user detail that opens only from
|
||||
the picker, not the default content of the page.
|
||||
When Patrol mode is available, the Patrol mode selector must keep the
|
||||
default view to the selected mode and one short sentence. It must not render a
|
||||
secondary `Limits` disclosure, hard-limit matrix, or policy explainer on the
|
||||
@@ -1450,19 +1454,30 @@ surface for Patrol intelligence. This contract now owns that orchestration and
|
||||
presentation boundary while leaving shared transport and payload-shape
|
||||
ownership in the governed AI runtime and API contract surfaces.
|
||||
|
||||
The Patrol control panel (`PatrolIntelligenceHeader.tsx`,
|
||||
`usePatrolIntelligenceState.ts`) exposes the per-rule alert-trigger policy
|
||||
directly under the Alert-Triggered Patrols toggle. A minimum-severity selector
|
||||
("Investigate alerts at or above": Critical only / Warning and critical) renders
|
||||
only while alert triggers are enabled and persists through
|
||||
`AIAPI.updateSettings({ patrol_alert_trigger_min_severity })` with optimistic
|
||||
state and revert-on-error, mirroring the existing trigger-toggle handlers. The
|
||||
selector reads `patrol_alert_trigger_min_severity` from the settings response,
|
||||
defaulting to critical-only, and must keep using the shared AI settings shape
|
||||
rather than forking a patrol-local form.
|
||||
The per-rule alert-trigger policy lives on Settings > Pulse Intelligence >
|
||||
Patrol (`PatrolSettingsContent` in
|
||||
`frontend-modern/src/components/Settings/AISettings.tsx`, form state and save
|
||||
in `useAISettingsState.ts`), not on the Patrol page. The Patrol page header
|
||||
has no trigger controls. Outside setup-only states it may show a one-line
|
||||
`status.trigger_status` summary when there is something to note (queued
|
||||
triggers, busy mode, alert or anomaly triggers off) and links to that settings
|
||||
page, unless a provider setup block puts a `Fix setup` action in place of the
|
||||
link. In the settings page's Triggers section, a
|
||||
minimum-severity selector ("Investigate alerts at or above": Critical only /
|
||||
Warning and critical) sits under the `Alert-triggered Patrols` toggle and
|
||||
renders only while that toggle is on. The selector and the trigger toggles
|
||||
edit the settings form only, and nothing is written until `Save Patrol
|
||||
settings`, which sends `patrol_alert_trigger_min_severity` in the page's single
|
||||
`AIAPI.updateSettings` payload whenever it differs from the loaded value.
|
||||
There is no optimistic write and no revert: a rejected save raises an error
|
||||
toast and leaves the edit in the form to retry, and a successful save reloads
|
||||
the form from the response. The selector reads
|
||||
`patrol_alert_trigger_min_severity` from the settings response, defaulting to
|
||||
critical-only, and must keep using the shared AI settings form rather than
|
||||
forking a Patrol-local form or save path.
|
||||
|
||||
The Patrol page now keeps the Patrol mode policy inline on the main surface,
|
||||
not inside the secondary Schedule & model drawer: the first configurable decision remains
|
||||
The Patrol page keeps the Patrol mode policy on its own header, not on
|
||||
Settings > Pulse Intelligence > Patrol: the first configurable decision remains
|
||||
what Patrol may handle automatically (`Watch only`, `Ask first`,
|
||||
`Safe auto-fix`, `Autopilot`), and there must be only one visible chooser for
|
||||
that decision.
|
||||
@@ -1485,13 +1500,15 @@ Commercial, runtime, and documentation copy that describes this same decision
|
||||
must also use those visible labels and the umbrella name `Patrol mode`, not
|
||||
the retired `Only watch`, `Fix safe issues`, `Full control`, or generic
|
||||
`Patrol control level` vocabulary.
|
||||
Provider model, run schedule, trigger tuning, readiness checks, and saved
|
||||
readiness issues belong to the secondary Schedule & model drawer. Within that
|
||||
secondary drawer, the panel disambiguates the two alert-driven AI toggles by
|
||||
scope rather than by near-identical names. The genuinely general path is
|
||||
Alert-Triggered Patrols, which runs a focused Patrol investigation of the
|
||||
alert's own issue. The Pro-gated `AlertTriggeredAnalysis` toggle presents as
|
||||
"Container Update Risk" with copy scoped to container-update alerts, because the
|
||||
Provider model, run schedule, trigger tuning, readiness checks, and the saved
|
||||
readiness result belong on Settings > Pulse Intelligence > Patrol; a save whose
|
||||
response reports Patrol not ready or degraded raises a warning toast instead of
|
||||
a success toast. In that page's Triggers section, the two alert-driven AI
|
||||
toggles are told apart by scope rather than by near-identical names. The
|
||||
genuinely general path is `Alert-triggered Patrols`, which runs a focused
|
||||
Patrol investigation of the alert's own issue. The Pro-gated
|
||||
`AlertTriggeredAnalysis` toggle presents as
|
||||
"Container update risk" with copy scoped to container-update alerts, because the
|
||||
enterprise `AlertTriggeredAnalyzer` only assesses `docker-container-update`
|
||||
alerts and returns nil for every other alert type. New copy must keep the
|
||||
container-update toggle scoped to its real Docker-update-risk capability instead
|
||||
@@ -1544,11 +1561,13 @@ owns local toggle optimism, run-status orchestration, and Patrol-only copy,
|
||||
including runtime-availability messaging that stays Patrol-first in
|
||||
operator-facing shells and uses provider/API-key wording only for the actual
|
||||
provider settings boundary, but the underlying AI runtime catalog must stay shared
|
||||
with chat and AI settings. The advanced Patrol model selector must remain
|
||||
with chat and AI settings. The Patrol model selector on Settings > Pulse
|
||||
Intelligence > Patrol (`AIModelOverrideField` with `kind="patrol"`) reads that
|
||||
same shared catalog and must remain
|
||||
state-driven across async settings/catalog loading: a saved direct-provider
|
||||
model such as `deepseek:deepseek-v4-flash` must render as that provider model
|
||||
once the shared catalog supplies it, not fall back visually to the default
|
||||
model or an unrelated OpenRouter entry because the popover mounted after the
|
||||
model or an unrelated OpenRouter entry because the field rendered before the
|
||||
catalog request completed.
|
||||
The Patrol page now also treats Patrol runtime availability as a first-class
|
||||
render contract: any Patrol surface that presents runtime availability must
|
||||
@@ -1941,10 +1960,10 @@ plain-text renders: the latest-run segment must keep an explicit textual
|
||||
separator between run kind and result, so degraded entries read as
|
||||
`Scoped run · error` rather than collapsing into concatenated strings like
|
||||
`Scoped runerror`.
|
||||
Those runtime facts must stay aligned with Patrol mode copy. The
|
||||
header settings surface should expose alert-triggered and anomaly-triggered
|
||||
scoped patrols as separate controls, with the legacy aggregate event-trigger
|
||||
toggle treated as compatibility-only transport rather than the primary product
|
||||
Those runtime facts must stay aligned with Patrol mode copy. Settings > Pulse
|
||||
Intelligence > Patrol, not the Patrol header, exposes alert-triggered and
|
||||
anomaly-triggered scoped Patrols as separate controls, with the legacy aggregate
|
||||
event-trigger toggle treated as compatibility-only transport rather than the primary product
|
||||
model.
|
||||
The findings empty state must also stay subordinate to the Patrol header
|
||||
rather than mirroring its timing metadata. In the primary
|
||||
|
||||
@@ -525,6 +525,146 @@ describe('AISettings model loading error states', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the alert severity selector only while alert triggers are on and writes it on Save', async () => {
|
||||
getSettingsMock.mockResolvedValue({
|
||||
...baseSettings(),
|
||||
enabled: true,
|
||||
configured: true,
|
||||
patrol_interval_minutes: 180,
|
||||
patrol_alert_triggers_enabled: false,
|
||||
patrol_alert_trigger_min_severity: 'critical',
|
||||
});
|
||||
updateSettingsMock.mockImplementation(async (payload: Record<string, unknown>) => ({
|
||||
...baseSettings(),
|
||||
enabled: true,
|
||||
configured: true,
|
||||
...payload,
|
||||
}));
|
||||
|
||||
renderComponent('patrol');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText('Schedule')).toHaveValue('180');
|
||||
});
|
||||
const alertToggle = screen.getByLabelText('Enable alert-triggered Patrols');
|
||||
expect(alertToggle).toHaveAttribute('aria-pressed', 'false');
|
||||
expect(screen.queryByLabelText('Investigate alerts at or above')).toBeNull();
|
||||
|
||||
fireEvent.click(alertToggle);
|
||||
fireEvent.change(screen.getByLabelText('Investigate alerts at or above'), {
|
||||
target: { value: 'warning' },
|
||||
});
|
||||
fireEvent.click(alertToggle);
|
||||
expect(screen.queryByLabelText('Investigate alerts at or above')).toBeNull();
|
||||
fireEvent.click(alertToggle);
|
||||
expect(screen.getByLabelText('Investigate alerts at or above')).toHaveValue('warning');
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(updateSettingsMock).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Save Patrol settings/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateSettingsMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(updateSettingsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
patrol_alert_triggers_enabled: true,
|
||||
patrol_alert_trigger_min_severity: 'warning',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps an unsaved alert severity edit in the form when the Patrol save is rejected', async () => {
|
||||
getSettingsMock.mockResolvedValue({
|
||||
...baseSettings(),
|
||||
enabled: true,
|
||||
configured: true,
|
||||
patrol_interval_minutes: 180,
|
||||
patrol_alert_triggers_enabled: true,
|
||||
patrol_alert_trigger_min_severity: 'critical',
|
||||
});
|
||||
updateSettingsMock
|
||||
.mockRejectedValueOnce(new Error('Settings store unavailable'))
|
||||
.mockImplementation(async (payload: Record<string, unknown>) => ({
|
||||
...baseSettings(),
|
||||
enabled: true,
|
||||
configured: true,
|
||||
patrol_interval_minutes: 360,
|
||||
patrol_alert_triggers_enabled: true,
|
||||
patrol_alert_trigger_min_severity: 'critical',
|
||||
...payload,
|
||||
}));
|
||||
|
||||
renderComponent('patrol');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText('Schedule')).toHaveValue('180');
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('Investigate alerts at or above'), {
|
||||
target: { value: 'warning' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: /Save Patrol settings/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(notificationErrorMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Settings store unavailable'),
|
||||
);
|
||||
});
|
||||
expect(notificationSuccessMock).not.toHaveBeenCalled();
|
||||
expect(screen.getByLabelText('Investigate alerts at or above')).toHaveValue('warning');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Save Patrol settings/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateSettingsMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
expect(updateSettingsMock).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ patrol_alert_trigger_min_severity: 'warning' }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(notificationSuccessMock).toHaveBeenCalledWith('Patrol settings saved');
|
||||
});
|
||||
// The form reloads from the saved response, so an unchanged severity is
|
||||
// not sent again.
|
||||
expect(screen.getByLabelText('Schedule')).toHaveValue('360');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Save Patrol settings/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateSettingsMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
expect(updateSettingsMock.mock.calls[2][0]).not.toHaveProperty(
|
||||
'patrol_alert_trigger_min_severity',
|
||||
);
|
||||
});
|
||||
|
||||
it('names each Patrol trigger toggle with an accessible name containing its visible label', async () => {
|
||||
getSettingsMock.mockResolvedValue({
|
||||
...baseSettings(),
|
||||
enabled: true,
|
||||
configured: true,
|
||||
patrol_alert_triggers_enabled: true,
|
||||
patrol_finding_notifications_enabled: true,
|
||||
});
|
||||
|
||||
renderComponent('patrol');
|
||||
|
||||
await screen.findByLabelText('Enable alert-triggered Patrols');
|
||||
for (const visibleLabel of [
|
||||
'Alert-triggered Patrols',
|
||||
'Anomaly-triggered Patrols',
|
||||
'Container update risk',
|
||||
'Finding notifications',
|
||||
]) {
|
||||
const row = screen.getByText(visibleLabel).parentElement?.parentElement;
|
||||
expect(row, visibleLabel).toBeTruthy();
|
||||
const escaped = visibleLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
expect(
|
||||
within(row as HTMLElement).getByRole('button', { name: new RegExp(escaped, 'i') }),
|
||||
).toHaveAttribute('aria-pressed');
|
||||
}
|
||||
});
|
||||
|
||||
it('shows inline warning when getModels throws a network error', async () => {
|
||||
getSettingsMock.mockResolvedValue({
|
||||
...baseSettings(),
|
||||
|
||||
Reference in New Issue
Block a user