mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Remove test-only Patrol autonomy save helpers
resolvePatrolAutonomyLevelForSave and resolvePatrolAutonomySettingsForSave
had no production caller; only their own unit tests imported them. Patrol
mode saves go through handleAutonomyChange and
acknowledgeAndActivateAutopilot, whose payload type (PatrolAutonomyUpdate)
has no full_mode_unlocked field, and both reconcile from
GET /api/ai/patrol/autonomy afterwards. Rollup already dropped both
helpers, so the production build is byte-identical.
Several contract sentences described the helpers' model rather than the
live path: the browser clearing stale full-mode unlock state before a
monitor-only save, sending full_mode_unlocked:false when locked, and
sending full_mode_unlocked:true for Autopilot. The API does that work.
The monitor-only handler and the paid SaveAutonomySettings adapter clear
the unlock and the Autopilot activation on every save below Autopilot,
and the autonomy gate derives the compatibility flag from a recorded
acknowledgement. Rewrite those sentences in patrol-intelligence,
api-contracts, frontend-primitives and ai-runtime to say what the header,
the state owner and the API each do. Two neighbouring api-contracts
sentences were also wrong about the same save: a `full` request without
a current acknowledgement gets 409 acknowledgement_required from the gate
before any licence check, and the Patrol mode dialog they name was
removed in 27ba85ddc6.
Replace the deleted helper tests with tests of the live locked path. The
page test loads stale paid state (Safe auto-fix plus the compatibility
unlock) under a runtime lock and checks that the header presents Watch
only, that the paid choices are disabled and save nothing, and that
Watch only sends exactly autonomy_level, investigation_budget and
investigation_timeout_sec. A source pin keeps the state owner's own lock
guards, which the disabled buttons otherwise hide from the page test.
This commit is contained in:
@@ -4195,10 +4195,14 @@ resolve canonical/source IDs and unique aliases before collection, reject
|
||||
human-facing labels in the settings payload must frame the operator boundary
|
||||
as Patrol mode rather than Patrol configuration.
|
||||
Monitor-only Patrol autonomy saves are part of the same runtime gate:
|
||||
when the safe-remediation extension or entitlement is unavailable, both the
|
||||
browser state owner and `internal/api/ai_handlers.go` must clear stale
|
||||
full-mode unlock state while clamping autonomy back to `monitor`, so paid
|
||||
remediation permission cannot survive through a free runtime save.
|
||||
when the safe-remediation extension or entitlement is unavailable, the
|
||||
API must clear stale full-mode unlock and Autopilot activation state while
|
||||
clamping autonomy back to `monitor` (the monitor-only handler in
|
||||
`internal/api/ai_handlers.go` without the extension, the
|
||||
`SaveAutonomySettings` adapter in `internal/api/router_routes_ai_relay.go`
|
||||
behind the paid handler), and the browser state owner must refuse a paid
|
||||
level when a save starts, so paid remediation permission cannot survive
|
||||
through a free runtime save.
|
||||
Event-triggered Patrol runtime policy is also part of this gate: when local
|
||||
development background automation or another runtime policy pauses automatic
|
||||
alert/anomaly-triggered checks, `internal/ai` must expose an explicit
|
||||
|
||||
@@ -3347,8 +3347,8 @@ a new API state machine, queue contract, or verification-accounting field.
|
||||
and the Patrol runtime-remediation destination shared with the AI settings endpoint, so run-history runtime-failure actions and runtime-finding actions may reuse the governed provider-settings route while still presenting that destination in Patrol as provider configuration instead of generic `AI Settings` copy
|
||||
and the Patrol route-shell destination itself, so the thin page shell at `frontend-modern/src/pages/AIIntelligence.tsx` may continue to bridge the shared AI-runtime payload boundary while exposing `/patrol` as the canonical product route and keeping retired `/ai` browser entry points unregistered
|
||||
and the Patrol route-shell accessibility boundary, so brand icons in `frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx` stay decorative when the same heading already exposes visible Patrol text, preventing duplicate accessible names such as `Pulse Patrol Patrol`
|
||||
and the Patrol mode selector boundary, so the default header and the
|
||||
Patrol mode dialog compose the shared
|
||||
and the Patrol mode selector boundary, so the header's `Mode and automation`
|
||||
disclosure composes the shared
|
||||
`frontend-modern/src/components/shared/FilterButtonGroup.tsx` primitive for
|
||||
the visible `Watch only` / `Ask first` / `Safe auto-fix` /
|
||||
`Autopilot` presentation while the API contract remains the sole owner of
|
||||
@@ -4028,14 +4028,20 @@ the authoritative analysis outcome.
|
||||
self-hosted v6 GA UI
|
||||
and the Patrol autonomy save contract, so Community/free runtime payloads
|
||||
may persist only `monitor` autonomy settings through
|
||||
`/api/ai/patrol/autonomy`, while `approval`, `assisted`, and `full` return
|
||||
the canonical license-required response instead of a generic save failure,
|
||||
and Patrol frontend state owners must clamp stale paid autonomy to `monitor`
|
||||
and send `full_mode_unlocked:false` before submitting that endpoint when the
|
||||
safe-remediation entitlement is not effective. The monitor-only backend path
|
||||
must likewise clear stale full-mode unlock state in its response and
|
||||
persistence layer instead of preserving paid remediation state through a free
|
||||
configuration save
|
||||
`/api/ai/patrol/autonomy`, while `approval`, `assisted`, and an
|
||||
acknowledged `full` return the canonical license-required response instead
|
||||
of a generic save failure (the shared acknowledgement gate refuses a `full`
|
||||
request without a current Autopilot acknowledgement first, for example with
|
||||
`409 acknowledgement_required` when it carries none), and the Patrol
|
||||
frontend state owner must
|
||||
refuse any level other than `monitor` when a save starts while the
|
||||
safe-remediation entitlement is not effective; its payload carries only
|
||||
`autonomy_level`, the investigation budget and timeout, and (for Autopilot)
|
||||
`acknowledgement_id`, never `full_mode_unlocked`. The monitor-only backend
|
||||
path owns the unlock clamp:
|
||||
it must clear stale full-mode unlock and Autopilot activation state in its
|
||||
response and persistence layer instead of preserving paid remediation state
|
||||
through a free configuration save
|
||||
and the Patrol settings-save readiness contract, so
|
||||
`/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
|
||||
@@ -9641,19 +9647,22 @@ configuration from paid remediation autonomy. `GET /api/ai/patrol/autonomy`
|
||||
continues to clamp effective Community autonomy to `monitor`, and
|
||||
`PUT /api/ai/patrol/autonomy` in the open-source/free adapter must accept and
|
||||
persist only `monitor` settings while returning the canonical license-required
|
||||
payload for investigation or remediation autonomy levels. Frontend Patrol state
|
||||
owners must not rely on that 402 as normal control flow for stale local state:
|
||||
when the current entitlement locks safe remediation, they submit `monitor` even
|
||||
if older persisted settings or a previous entitlement left `approval`,
|
||||
`assisted`, or `full` in memory.
|
||||
payload for investigation or remediation autonomy levels (a `full` request
|
||||
reaches that adapter only with a current Autopilot acknowledgement; otherwise
|
||||
the shared acknowledgement gate refuses it first, for example with
|
||||
`409 acknowledgement_required` when it carries none). Frontend Patrol
|
||||
state owners must not rely on that 402 as normal control flow for stale local
|
||||
state: when the current entitlement locks safe remediation, the Patrol header
|
||||
presents `monitor` as the effective mode and the state owner starts no save but
|
||||
an explicit `monitor` choice, even if older persisted settings or a previous
|
||||
entitlement left `approval`, `assisted`, or `full` in memory.
|
||||
After a successful persisted update whose effective mode actually crosses from
|
||||
`monitor` to `approval`, `assisted`, or eligible `full`, the shared API mutation
|
||||
boundary invokes the bounded Patrol actionable-finding reconciliation pass.
|
||||
The pass runs only after `ApplyPatrolAutonomyConfig` has published the new
|
||||
runtime authority; failed writes, Community clamps, repeated paid-mode saves,
|
||||
budget-only edits, and demotions must not start backlog work.
|
||||
The Patrol header and configuration-dialog presentation for that API boundary
|
||||
must compose the shared
|
||||
The Patrol header presentation for that API boundary must compose the shared
|
||||
`frontend-modern/src/components/shared/FilterButtonGroup.tsx` selector: the
|
||||
endpoint contract still owns the accepted autonomy values and license-required
|
||||
response shape, while the frontend owns only the option mapping,
|
||||
|
||||
@@ -3185,9 +3185,13 @@ Agent`), with the plain-language source phrase available through accessible
|
||||
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
|
||||
governed fixes are locked, the Patrol header presents `Watch only` as the
|
||||
effective mode with no enabled paid choice, and the Patrol state owner
|
||||
refuses any other level when a save starts. Its autonomy payload
|
||||
never carries `full_mode_unlocked`: `PUT /api/ai/patrol/autonomy` itself
|
||||
clears stale full-mode unlock and Autopilot activation state on every save
|
||||
below Autopilot, 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
|
||||
|
||||
@@ -1631,12 +1631,14 @@ after a page render: recoverable Patrol provider/model settings saves must
|
||||
persist and echo structured readiness cause metadata, manual run requests must
|
||||
return the structured readiness reason if a stale UI still submits, and
|
||||
scheduled or scoped alert/anomaly runs must skip before calling the model while
|
||||
preserving the blocked reason and cause in Patrol status. The Patrol
|
||||
control state owner must also clamp stale investigation/remediation
|
||||
autonomy back to findings-only `monitor` and clear stale full-mode unlock state
|
||||
before persisting Patrol control when the safe-remediation entitlement is not
|
||||
effective, so an expired or downgraded plan cannot turn a recoverable control
|
||||
review into a Pro-only save failure. Patrol renders no inline error
|
||||
preserving the blocked reason and cause in Patrol status. When the
|
||||
safe-remediation entitlement is not effective, the Patrol header must present
|
||||
findings-only `monitor` as the effective mode and the Patrol control state
|
||||
owner must refuse any investigation/remediation level when a save starts;
|
||||
the `monitor` save it does send is accepted without the entitlement, and the
|
||||
API clears stale full-mode unlock and Autopilot activation state on it, so an
|
||||
expired or downgraded plan cannot turn a recoverable control review into a
|
||||
Pro-only save failure. Patrol renders no inline error
|
||||
surface for these failures today: switching Patrol on or off while it is not
|
||||
ready raises a fixed warning notification, a failed Patrol mode save raises an error
|
||||
notification, and neither offers an Assistant handoff. A provider or model save
|
||||
@@ -1651,8 +1653,10 @@ 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
|
||||
acknowledgement. The shared primitive owns pressed-state semantics,
|
||||
Autopilot level opens the acknowledgement dialog and saves `full` only with a
|
||||
recorded `acknowledgement_id` (the API derives the compatibility
|
||||
`full_mode_unlocked` value itself), while saving any lower level lets the API
|
||||
clear the Autopilot activation. The shared primitive owns pressed-state semantics,
|
||||
disabled-option behavior, and active/inactive selector styling.
|
||||
That same Patrol-owned presentation rule also applies to the findings empty
|
||||
state: `frontend-modern/src/components/AI/FindingsPanel.tsx` must not treat
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "32fad38aa486ac116f885cb77e27317ea351c957",
|
||||
"verified_at": "2026-10-07T23:52:39Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts": "789f7e4a6f28e07d6d1d28abc29843ab02a90eef4f787fa024f0ba383abc8563"
|
||||
},
|
||||
"routes": [
|
||||
"/patrol (header 'Mode and automation' disclosure)",
|
||||
"/patrol (Activate Autopilot acknowledgement dialog)"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 844
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Removal of the test-only resolvePatrolAutonomyLevelForSave / resolvePatrolAutonomySettingsForSave exports; no production code called them.",
|
||||
"Embedded production builds (vite build into internal/api/frontend-modern/dist, go build) of the parent 32fad38aa4 and of this change, run on pulse-dev because local memory pressure refused heavy work. The two dists are byte-identical (187 files, every sha256 equal): Rollup had already dropped both helpers.",
|
||||
"Each build on its own private DEMO_MODE mock stack with a fresh data dir, one at a time; the same Playwright script ran against both at both widths. Results compared field by field: 6 sessions, 80 recorded fields, 0 differences.",
|
||||
"Paid entitlement (ai_autofix added to /api/license/runtime-capabilities via page.route): all four Patrol mode choices enabled; Ask first, Watch only and Autopilot each saved through PUT /api/ai/patrol/autonomy (stubbed 200 plus a GET reconcile rewrite); Autopilot opened the acknowledgement dialog with no PUT until 'Record acknowledgement and activate' (one stubbed acknowledgement POST). Every PUT body carried only autonomy_level, investigation_budget, investigation_timeout_sec and, for Autopilot, acknowledgement_id; none carried full_mode_unlocked.",
|
||||
"Runtime lock under demo presentation (ai_autofix blocked with paid_runtime_required): demo forces hideCommercial, so the header shows Watch only with no mode selector and no save path.",
|
||||
"Runtime lock with the demo presentation flags cleared in the browser (GET /api/security/status rewritten): 'Pulse Pro runtime required' notice, Watch only selected, Ask first / Safe auto-fix / Autopilot disabled; forced clicks on them sent no PUT and opened no Autopilot dialog; Watch only sent exactly autonomy_level monitor plus the investigation budget and timeout.",
|
||||
"Console/network noise identical on both builds and unrelated to the change: 403 POST /api/ai/workflow-prompts/activity (demo write guard on the starter record) in the paid sessions, 404 GET /api/license/commercial-posture in the cleared-presentation sessions."
|
||||
],
|
||||
"interactions": [
|
||||
"Demo auto sign-in; open /patrol, expand 'Mode and automation', read the selector's pressed and disabled states.",
|
||||
"Paid: click Ask first, then Watch only, then Autopilot (re-clicking only while the previous save was still reconciling), tick the acknowledgement checkbox and click 'Record acknowledgement and activate'.",
|
||||
"Locked (selector visible): force-click Ask first, Safe auto-fix and Autopilot, then click Watch only.",
|
||||
"Desktop 1440x900 and phone 390x844 contexts; screenshots of the initial, Autopilot dialog and final states inspected."
|
||||
]
|
||||
}
|
||||
@@ -5,8 +5,6 @@ import {
|
||||
getPatrolSavedReadinessWarning,
|
||||
patrolStartFailureMessage,
|
||||
recordPatrolControlStarterActivity,
|
||||
resolvePatrolAutonomyLevelForSave,
|
||||
resolvePatrolAutonomySettingsForSave,
|
||||
resolvePatrolBlockedActionCause,
|
||||
} from '../usePatrolIntelligenceState';
|
||||
import patrolIntelligenceStateSource from '../usePatrolIntelligenceState.ts?raw';
|
||||
@@ -80,6 +78,17 @@ describe('usePatrolIntelligenceState', () => {
|
||||
expect(patrolIntelligenceStateSource).toContain('if (requestId === refreshRequestId) {');
|
||||
});
|
||||
|
||||
it('refuses paid Patrol modes in the state owner while governed fixes are locked', () => {
|
||||
// The header disables paid choices too; these guards keep the state owner
|
||||
// from starting a paid save if a caller reaches it anyway.
|
||||
expect(patrolIntelligenceStateSource).toContain(
|
||||
"if (controlLocked && level !== 'monitor') return;",
|
||||
);
|
||||
expect(patrolIntelligenceStateSource).toContain(
|
||||
'if (isUpdatingAutonomy() || autoFixLocked()) return;',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps browser/network start failures distinct from backend rejections', () => {
|
||||
expect(patrolStartFailureMessage(new TypeError('Failed to fetch'))).toBe(
|
||||
'Could not reach Pulse to start Patrol: Failed to fetch',
|
||||
@@ -206,78 +215,6 @@ describe('usePatrolIntelligenceState', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('resolvePatrolAutonomyLevelForSave', () => {
|
||||
it('clamps stale paid autonomy to monitor when governed fixes are locked', () => {
|
||||
expect(resolvePatrolAutonomyLevelForSave('full', true, true)).toBe('monitor');
|
||||
expect(resolvePatrolAutonomyLevelForSave('assisted', false, true)).toBe('monitor');
|
||||
expect(resolvePatrolAutonomyLevelForSave('approval', false, true)).toBe('monitor');
|
||||
});
|
||||
|
||||
it('preserves paid autonomy choices when governed fixes are available', () => {
|
||||
expect(resolvePatrolAutonomyLevelForSave('assisted', false, false)).toBe('assisted');
|
||||
expect(resolvePatrolAutonomyLevelForSave('assisted', true, false)).toBe('assisted');
|
||||
expect(resolvePatrolAutonomyLevelForSave('full', true, false)).toBe('full');
|
||||
expect(resolvePatrolAutonomyLevelForSave('full', false, false)).toBe('assisted');
|
||||
expect(resolvePatrolAutonomyLevelForSave('approval', false, false)).toBe('approval');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolvePatrolAutonomySettingsForSave', () => {
|
||||
it('clears stale full-mode state when governed fixes are locked', () => {
|
||||
expect(
|
||||
resolvePatrolAutonomySettingsForSave({
|
||||
level: 'full',
|
||||
fullModeUnlocked: true,
|
||||
autoFixLocked: true,
|
||||
}),
|
||||
).toEqual({ autonomyLevel: 'monitor', fullModeUnlocked: false });
|
||||
});
|
||||
|
||||
it('does not carry full-mode state into non-remediation modes', () => {
|
||||
expect(
|
||||
resolvePatrolAutonomySettingsForSave({
|
||||
level: 'monitor',
|
||||
fullModeUnlocked: true,
|
||||
autoFixLocked: false,
|
||||
}),
|
||||
).toEqual({ autonomyLevel: 'monitor', fullModeUnlocked: false });
|
||||
|
||||
expect(
|
||||
resolvePatrolAutonomySettingsForSave({
|
||||
level: 'approval',
|
||||
fullModeUnlocked: true,
|
||||
autoFixLocked: false,
|
||||
}),
|
||||
).toEqual({ autonomyLevel: 'approval', fullModeUnlocked: false });
|
||||
});
|
||||
|
||||
it('keeps full control tied to the explicit full selection', () => {
|
||||
expect(
|
||||
resolvePatrolAutonomySettingsForSave({
|
||||
level: 'assisted',
|
||||
fullModeUnlocked: true,
|
||||
autoFixLocked: false,
|
||||
}),
|
||||
).toEqual({ autonomyLevel: 'assisted', fullModeUnlocked: false });
|
||||
|
||||
expect(
|
||||
resolvePatrolAutonomySettingsForSave({
|
||||
level: 'full',
|
||||
fullModeUnlocked: true,
|
||||
autoFixLocked: false,
|
||||
}),
|
||||
).toEqual({ autonomyLevel: 'full', fullModeUnlocked: true });
|
||||
|
||||
expect(
|
||||
resolvePatrolAutonomySettingsForSave({
|
||||
level: 'full',
|
||||
fullModeUnlocked: false,
|
||||
autoFixLocked: false,
|
||||
}),
|
||||
).toEqual({ autonomyLevel: 'assisted', fullModeUnlocked: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPatrolSavedReadinessWarning', () => {
|
||||
it('stays quiet when the saved settings do not block Patrol readiness', () => {
|
||||
expect(getPatrolSavedReadinessWarning(null)).toBeNull();
|
||||
|
||||
@@ -86,36 +86,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');
|
||||
|
||||
export function resolvePatrolAutonomyLevelForSave(
|
||||
level: PatrolAutonomyLevel,
|
||||
fullModeUnlocked: boolean,
|
||||
autoFixLocked: boolean,
|
||||
): PatrolAutonomyLevel {
|
||||
if (autoFixLocked) return 'monitor';
|
||||
if (level === 'full') {
|
||||
return fullModeUnlocked ? 'full' : 'assisted';
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
export function resolvePatrolAutonomySettingsForSave({
|
||||
level,
|
||||
fullModeUnlocked,
|
||||
autoFixLocked,
|
||||
}: {
|
||||
level: PatrolAutonomyLevel;
|
||||
fullModeUnlocked: boolean;
|
||||
autoFixLocked: boolean;
|
||||
}): { autonomyLevel: PatrolAutonomyLevel; fullModeUnlocked: boolean } {
|
||||
const canUseFullMode = !autoFixLocked && level === 'full' && fullModeUnlocked;
|
||||
const autonomyLevel = resolvePatrolAutonomyLevelForSave(level, canUseFullMode, autoFixLocked);
|
||||
|
||||
return {
|
||||
autonomyLevel,
|
||||
fullModeUnlocked: autonomyLevel === 'full',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A Patrol on/off save whose response reports Patrol not ready was still
|
||||
* saved, so it raises a warning rather than an error.
|
||||
|
||||
@@ -1249,6 +1249,49 @@ describe('AIIntelligence entitlement gating', () => {
|
||||
expect(screen.queryByText(/Install the Pulse Pro runtime/)).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('link', { name: 'Open Pro downloads' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('saves only Watch only, without an unlock field, while governed fixes are locked', async () => {
|
||||
useLicensedCommunityRuntime();
|
||||
// A previous entitlement left a paid mode and the compatibility unlock in memory.
|
||||
getPatrolAutonomySettingsMock.mockResolvedValue(
|
||||
defaultPatrolAutonomySettings({
|
||||
autonomy_level: 'assisted',
|
||||
requested_autonomy_level: 'assisted',
|
||||
effective_autonomy_level: 'assisted',
|
||||
full_mode_unlocked: true,
|
||||
}),
|
||||
);
|
||||
|
||||
render(() => <AIIntelligence />);
|
||||
|
||||
const modes = within(await screen.findByRole('group', { name: 'Patrol mode' }));
|
||||
await waitFor(() => expect(getPatrolAutonomySettingsMock).toHaveBeenCalled());
|
||||
await getPatrolAutonomySettingsMock.mock.results[0]?.value;
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(modes.getByRole('button', { name: 'Watch only' })).toHaveAttribute(
|
||||
'aria-pressed',
|
||||
'true',
|
||||
);
|
||||
for (const paidMode of ['Ask first', 'Safe auto-fix', 'Autopilot']) {
|
||||
expect(modes.getByRole('button', { name: paidMode })).toBeDisabled();
|
||||
fireEvent.click(modes.getByRole('button', { name: paidMode }));
|
||||
}
|
||||
expect(screen.queryByRole('dialog', { name: 'Activate Autopilot' })).toBeNull();
|
||||
expect(updatePatrolAutonomySettingsMock).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(modes.getByRole('button', { name: 'Watch only' }));
|
||||
|
||||
// The browser sends no full_mode_unlocked field; the API clears stale
|
||||
// unlock and Autopilot activation state on every non-Autopilot save.
|
||||
await waitFor(() => {
|
||||
expect(updatePatrolAutonomySettingsMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(updatePatrolAutonomySettingsMock).toHaveBeenCalledWith({
|
||||
autonomy_level: 'monitor',
|
||||
investigation_budget: 15,
|
||||
investigation_timeout_sec: 300,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the Patrol model catalog out of the operator page', async () => {
|
||||
|
||||
Reference in New Issue
Block a user