test(frontend): repin four suites to the governed action review and autopilot acknowledgement flows

Build and Test has been red since 2026-07-11 because these tests still
pinned the pre-review-flow behavior:

- apiErrorStatus guardrail expected 3 triggerResourceCommand returns in
  monitoring.ts; the governed container update flow moved two callers out.
- ResourceDetailDrawer docker test drove the retired click-twice confirm;
  the drawer now plans the action and opens ActionReviewDialog, so the
  test pins surface attribution and the dialog handoff instead (the
  dialog's own suite covers approve/execute).
- AIIntelligence patrol tests mocked the old autonomy settings shape and
  expected Autopilot to switch modes directly; settings now carry
  requested/effective levels plus autopilot_acknowledgement, and the
  Autopilot button opens the acknowledgement dialog without saving.
- infrastructureSummaryCache boundary test measured age with real timers,
  so any millisecond tick between write and read flipped the strict >
  verdict; the clock is now frozen for the boundary assertion.
This commit is contained in:
rcourtman
2026-07-17 14:30:06 +01:00
parent f8e8135a32
commit 63c2118a16
4 changed files with 95 additions and 43 deletions
@@ -149,7 +149,7 @@ describe('API error-status guardrails', () => {
/const response = await apiFetch\(url,\s*\{\s*method: 'POST',\s*\}\);/g,
) ?? [],
).toHaveLength(1);
expect(monitoringSource.match(/return triggerResourceCommand</g) ?? []).toHaveLength(3);
expect(monitoringSource.match(/return triggerResourceCommand</g) ?? []).toHaveLength(1);
expect(monitoringSource.match(/await runResourceAction\(url\);/g) ?? []).toHaveLength(3);
expect(monitoringSource.match(/parseOptionalSuccessAPIResponse</g) ?? []).toHaveLength(1);
expect(discoverySource).toContain('assertAPIResponseOK(response,');
@@ -53,6 +53,42 @@ vi.mock('@/api/resourceActions', () => ({
state: 'completed',
result: { success: true },
}),
getAction: vi.fn().mockResolvedValue({
audit: {
id: 'detail-action-1',
createdAt: '2026-06-12T20:00:00Z',
updatedAt: '2026-06-12T20:00:00Z',
state: 'pending_approval',
decisionRevision: 0,
request: {
requestId: 'detail-request-1',
resourceId: 'app-container-web',
capabilityName: 'restart',
reason: 'restart Docker container edge-web from the resource details.',
requestedBy: 'ui:resource-detail',
},
plan: {
actionId: 'detail-action-1',
requestId: 'detail-request-1',
allowed: true,
requiresApproval: true,
approvalPolicy: 'admin',
rollbackAvailable: false,
expiresAt: '2026-06-12T20:05:00Z',
policyDecision: {
version: 0,
status: 'legacy_unknown',
scope: { orgId: '', resourceId: '', capabilityName: '' },
authorities: [],
approvalRequirement: { version: 0, floor: 'admin', quorum: 1, disallowRequester: false },
planningAllowed: false,
requiresApproval: true,
},
},
verificationOutcome: { status: 'unknown' },
},
events: [],
}),
},
}));
@@ -115,7 +151,7 @@ describe('ResourceDetailDrawer for Docker containers', () => {
));
const restartButton = screen.getByRole('button', {
name: 'Restart edge-web through governed action',
name: 'Review restart for edge-web',
});
expect(restartButton.closest('[data-docker-container-actions-surface]')).toHaveAttribute(
'data-docker-container-actions-surface',
@@ -123,29 +159,21 @@ describe('ResourceDetailDrawer for Docker containers', () => {
);
fireEvent.click(restartButton);
fireEvent.click(screen.getByRole('button', { name: 'Click again to restart edge-web' }));
await waitFor(() =>
expect(ResourceActionsAPI.planAction).toHaveBeenCalledWith(
expect.objectContaining({
resourceId: 'app-container-web',
capabilityName: 'restart',
reason: expect.stringContaining('from the resource details'),
requestedBy: 'ui:resource-detail',
}),
),
);
await waitFor(() =>
expect(ResourceActionsAPI.executeAction).toHaveBeenCalledWith(
'detail-action-1',
expect.stringContaining('from the resource details'),
),
);
expect(ResourceActionsAPI.decideAction).toHaveBeenCalledWith(
'detail-action-1',
'approved',
expect.stringContaining('restart Docker container edge-web'),
);
await waitFor(() => expect(onResourceActionSettled).toHaveBeenCalledTimes(1));
expect(await screen.findByRole('dialog', { name: 'Restart' })).toBeInTheDocument();
expect(ResourceActionsAPI.decideAction).not.toHaveBeenCalled();
expect(ResourceActionsAPI.executeAction).not.toHaveBeenCalled();
expect(onResourceActionSettled).not.toHaveBeenCalled();
});
it('adds a metrics history tab for app-containers with a metrics target', async () => {
@@ -456,6 +456,31 @@ const defaultAgentCapabilitiesManifest = () => ({
capabilities: [],
});
const defaultPatrolAutonomySettings = (overrides: Record<string, unknown> = {}) => ({
autonomy_level: 'monitor',
requested_autonomy_level: 'monitor',
effective_autonomy_level: 'monitor',
full_mode_unlocked: false,
autopilot_acknowledgement: {
code: 'not_requested',
active: false,
currentVersion: 1,
acceptedScope: [],
acceptedLimits: {
policyAllowlistRequired: true,
emergencyStopHonored: true,
approvalFloorsHonored: true,
verificationReconciledWhenSupported: true,
evidenceClassDisclosed: true,
inconclusiveOutcomeAllowed: true,
executionSuccessIsNotOutcomeTruth: true,
},
},
investigation_budget: 15,
investigation_timeout_sec: 300,
...overrides,
});
const defaultOperationsLoopStatus = (overrides: Record<string, unknown> = {}) => ({
nextAction: 'run_patrol',
progressLabel: 'Run Patrol to produce actionable issue evidence.',
@@ -547,17 +572,9 @@ describe('AIIntelligence entitlement gating', () => {
getCorrelationsMock.mockReset();
getPatrolStatusMock.mockResolvedValue(defaultPatrolStatus());
getPatrolAutonomySettingsMock.mockResolvedValue({
autonomy_level: 'monitor',
full_mode_unlocked: false,
investigation_budget: 15,
investigation_timeout_sec: 300,
});
getPatrolAutonomySettingsMock.mockResolvedValue(defaultPatrolAutonomySettings());
updatePatrolAutonomySettingsMock.mockResolvedValue({
settings: {
autonomy_level: 'monitor',
full_mode_unlocked: false,
},
settings: defaultPatrolAutonomySettings(),
});
triggerPatrolRunMock.mockResolvedValue(undefined);
getPatrolRunHistoryMock.mockResolvedValue([]);
@@ -1205,13 +1222,11 @@ describe('AIIntelligence entitlement gating', () => {
fireEvent.click(screen.getByRole('button', { name: 'Autopilot' }));
await waitFor(() => {
expect(
screen.getByText(
'Patrol can act automatically within policy and still asks when approval is required.',
),
).toBeInTheDocument();
});
// Autopilot no longer switches directly; it opens the acknowledgement
// dialog and the mode stays where it was until the user records one.
expect(await screen.findByRole('dialog', { name: 'Activate Autopilot' })).toBeInTheDocument();
expect(updatePatrolAutonomySettingsMock).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: 'Close Autopilot acknowledgement' }));
expect(screen.getByRole('link', { name: 'Open Patrol settings' })).toHaveAttribute(
'href',
@@ -1248,10 +1263,11 @@ describe('AIIntelligence entitlement gating', () => {
return {};
});
updatePatrolAutonomySettingsMock.mockResolvedValue({
settings: {
settings: defaultPatrolAutonomySettings({
autonomy_level: 'approval',
full_mode_unlocked: false,
},
requested_autonomy_level: 'approval',
effective_autonomy_level: 'approval',
}),
});
render(() => <AIIntelligence />);
@@ -1268,7 +1284,8 @@ describe('AIIntelligence entitlement gating', () => {
expect(updatePatrolAutonomySettingsMock).toHaveBeenCalledWith(
expect.objectContaining({
autonomy_level: 'approval',
full_mode_unlocked: false,
investigation_budget: 15,
investigation_timeout_sec: 300,
}),
);
});
@@ -324,13 +324,20 @@ describe('readInfrastructureSummaryCache — hit / miss / expiry / empty boundar
it('returns a cache hit when age equals maxAgeMs exactly (strict > boundary)', () => {
// `Date.now() - parsed.cachedAt > maxAgeMs` uses strict >, so an entry that
// is exactly maxAgeMs old must still be a hit (boundary is inclusive).
const maxAge = 60_000;
const cachedAt = now() - maxAge;
storePayload(
'1h',
buildPayload({ cachedAt, charts: undefined }),
);
expect(readInfrastructureSummaryCache('1h', maxAge)).not.toBeNull();
// Frozen clock: with real timers a millisecond tick between the write and
// the read pushes the age past maxAgeMs and flips the verdict.
vi.useFakeTimers();
try {
const maxAge = 60_000;
const cachedAt = now() - maxAge;
storePayload(
'1h',
buildPayload({ cachedAt, charts: undefined }),
);
expect(readInfrastructureSummaryCache('1h', maxAge)).not.toBeNull();
} finally {
vi.useRealTimers();
}
});
it('returns an empty map (cache hit, but empty) when charts is absent', () => {