mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 12:42:49 +00:00
Remove test-only Patrol autonomy save helpers (#2740)
Stacked on #2695 (branch contains its head32fad38aa4). Once #2695 merges, this PR's diff is the single commit776c2774ba. ## What `resolvePatrolAutonomyLevelForSave` and `resolvePatrolAutonomySettingsForSave` in `features/patrol/usePatrolIntelligenceState.ts` 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`. This PR deletes both helpers and their tests. Contract prose in patrol-intelligence, api-contracts, frontend-primitives and ai-runtime 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. `HandleUpdatePatrolAutonomyMonitorOnly` and the paid handler's `SaveAutonomySettings` adapter clear the unlock and Autopilot activation on every save below Autopilot, and `GatePatrolAutonomyUpdate` derives the compatibility flag from a recorded acknowledgement. The prose now says what the header, the state owner and the API each do. Three neighbouring api-contracts sentences about the same save were also corrected: - a `full` request without a current acknowledgement is refused by the gate before any licence check; - the "Patrol mode dialog" (removed in27ba85ddc6) is now the header's `Mode and automation` disclosure, with the same wording as #2691; - the lock is checked "when a save starts", not "never submits", because the Autopilot flow checks it only at entry. The deleted helper tests are replaced by tests of the live locked path. A page test loads stale paid state (Safe auto-fix plus the compatibility unlock) under a runtime lock. It checks that Watch only is presented, 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. ## Verification - Production build: parent and this change produce byte-identical dists (187 files), because Rollup had already dropped both helpers. - Browser pass on pulse-dev, before and after, one DEMO_MODE stack at a time, 1440x900 and 390x844. Covered paid saves (Ask first, Watch only, Autopilot through the acknowledgement dialog) and the runtime lock with and without the demo presentation override. 6 sessions and 80 recorded fields, with 0 differences between before and after. No PUT ever carried `full_mode_unlocked`. Receipt `frontend-modern/browser-verification/994121d9684bd9f0.json`. - `tsc --noEmit` and the Patrol hook, page, header, workspace, commercial-boundary and resource-boundary suites pass (84 tests). - Negative controls, each failing the intended test: - header showing the stale paid mode while locked; - payload carrying `full_mode_unlocked`; - hook lock guard removed; - paid choices enabled while locked.
This commit is contained in:
@@ -4198,10 +4198,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
|
||||
|
||||
@@ -3348,8 +3348,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
|
||||
@@ -4036,14 +4036,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
|
||||
@@ -9803,19 +9809,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,
|
||||
|
||||
@@ -3207,9 +3207,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
|
||||
|
||||
@@ -1633,12 +1633,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
|
||||
@@ -1653,8 +1655,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.
|
||||
|
||||
@@ -1242,6 +1242,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