@oneuptime/common 14.0.1 → 14.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1090) hide show
  1. package/Models/DatabaseModels/AlertEpisodeMember.ts +8 -1
  2. package/Models/DatabaseModels/AutoRemediationSuggestion.ts +63 -0
  3. package/Models/DatabaseModels/IncidentEpisodeMember.ts +8 -1
  4. package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +9 -2
  5. package/Models/DatabaseModels/IncidentPublicNote.ts +9 -2
  6. package/Models/DatabaseModels/KubernetesCluster.ts +429 -0
  7. package/Models/DatabaseModels/Monitor.ts +48 -0
  8. package/Models/DatabaseModels/Runner.ts +5 -4
  9. package/Models/DatabaseModels/RunnerJob.ts +66 -0
  10. package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +10 -2
  11. package/Models/DatabaseModels/WorkflowVariable.ts +479 -2
  12. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +79 -25
  13. package/Server/API/AIInvestigationAPI.ts +341 -0
  14. package/Server/API/AutoRemediationAPI.ts +184 -2
  15. package/Server/API/CommonAPI.ts +70 -0
  16. package/Server/API/KubernetesClusterAiAccessAPI.ts +793 -0
  17. package/Server/API/MicrosoftTeamsAPI.ts +770 -315
  18. package/Server/API/MonitorAPI.ts +140 -0
  19. package/Server/API/OnCallScheduleTimelineAPI.ts +5 -21
  20. package/Server/API/SlackAPI.ts +447 -99
  21. package/Server/API/StatusPageAPI.ts +146 -26
  22. package/Server/API/TelemetryAPI.ts +216 -5
  23. package/Server/API/UserTwoFactorBackupCodeAPI.ts +1 -0
  24. package/Server/API/WorkflowVariableAPI.ts +156 -0
  25. package/Server/Infrastructure/GlobalCache.ts +35 -0
  26. package/Server/Infrastructure/Postgres/SchemaMigrations/1794300000000-AddOAuth2WorkflowVariables.ts +97 -0
  27. package/Server/Infrastructure/Postgres/SchemaMigrations/1794400000000-AddKubernetesClusterAiAccess.ts +129 -0
  28. package/Server/Infrastructure/Postgres/SchemaMigrations/1794500000000-AddIncomingEmailCustomAddressToMonitor.ts +25 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1794600000000-AddAuthTokenExpiresAtToWorkspaceProjectAuthToken.ts +20 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1794700000000-RemoveAppAccessTokenFromWorkspaceProjectAuthTokenMiscData.ts +38 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
  32. package/Server/Middleware/MasterAdminAuthorization.ts +28 -0
  33. package/Server/Middleware/TelemetryIngest.ts +47 -2
  34. package/Server/Middleware/UserAuthorization.ts +56 -2
  35. package/Server/Services/AIAgentService.ts +1 -0
  36. package/Server/Services/AccessTokenService.ts +121 -48
  37. package/Server/Services/AlertEpisodeMemberService.ts +9 -0
  38. package/Server/Services/AlertEpisodeService.ts +14 -1
  39. package/Server/Services/AlertService.ts +134 -22
  40. package/Server/Services/AutoRemediationRuleEngineService.ts +968 -39
  41. package/Server/Services/AutoRemediationRuleService.ts +118 -0
  42. package/Server/Services/BillingService.ts +1 -0
  43. package/Server/Services/InboundEmail/Providers/SendGridInboundProvider.ts +16 -22
  44. package/Server/Services/IncidentEpisodeMemberService.ts +9 -0
  45. package/Server/Services/IncidentEpisodePublicNoteService.ts +66 -0
  46. package/Server/Services/IncidentEpisodeRoleMemberService.ts +27 -0
  47. package/Server/Services/IncidentEpisodeService.ts +14 -1
  48. package/Server/Services/IncidentGroupingEngineService.ts +11 -0
  49. package/Server/Services/IncidentMemberService.ts +21 -0
  50. package/Server/Services/IncidentPublicNoteService.ts +63 -0
  51. package/Server/Services/IncidentService.ts +120 -20
  52. package/Server/Services/IncidentStateTimelineService.ts +15 -0
  53. package/Server/Services/IncidentTemplateService.ts +83 -3
  54. package/Server/Services/KubernetesClusterAiAccessService.ts +1943 -0
  55. package/Server/Services/KubernetesClusterService.ts +1570 -1
  56. package/Server/Services/LogAggregationService.ts +14 -6
  57. package/Server/Services/MonitorService.ts +651 -52
  58. package/Server/Services/MonitorStatusTimelineService.ts +639 -2
  59. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +2 -0
  60. package/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.ts +2 -0
  61. package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +2 -0
  62. package/Server/Services/OnCallDutyPolicyScheduleService.ts +3 -0
  63. package/Server/Services/OnCallNotificationAlertingService.ts +36 -12
  64. package/Server/Services/OnCallSetupReminderService.ts +1 -0
  65. package/Server/Services/ProbeService.ts +7 -1
  66. package/Server/Services/ProjectService.ts +11 -1
  67. package/Server/Services/RunbookCredentialService.ts +26 -1
  68. package/Server/Services/RunbookSecretService.ts +36 -0
  69. package/Server/Services/RunnerJobService.ts +941 -23
  70. package/Server/Services/RunnerService.ts +439 -6
  71. package/Server/Services/ScheduledMaintenancePublicNoteService.ts +66 -0
  72. package/Server/Services/ScheduledMaintenanceService.ts +124 -19
  73. package/Server/Services/ScheduledMaintenanceTemplateService.ts +82 -3
  74. package/Server/Services/ServiceLevelObjectiveService.ts +14 -1
  75. package/Server/Services/StatusPagePrivateUserService.ts +1 -0
  76. package/Server/Services/StatusPageService.ts +301 -74
  77. package/Server/Services/StatusPageSubscriberService.ts +4 -0
  78. package/Server/Services/TeamMemberService.ts +309 -7
  79. package/Server/Services/TraceAggregationService.ts +10 -6
  80. package/Server/Services/UserEmailService.ts +1 -0
  81. package/Server/Services/UserNotificationMethodAdminService.ts +1 -0
  82. package/Server/Services/UserNotificationRuleAdminService.ts +1 -0
  83. package/Server/Services/UserNotificationRuleService.ts +4 -0
  84. package/Server/Services/UserService.ts +122 -1
  85. package/Server/Services/WorkflowVariableService.ts +593 -8
  86. package/Server/Services/WorkspaceNotificationRuleService.ts +588 -8
  87. package/Server/Services/WorkspaceProjectAuthTokenService.ts +170 -1
  88. package/Server/Types/Database/QueryHelper.ts +22 -0
  89. package/Server/Types/Markdown.ts +502 -27
  90. package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +182 -0
  91. package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +525 -0
  92. package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +854 -0
  93. package/Server/Utils/AI/CodeFix/CodeFixAgentAuth.ts +22 -2
  94. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +1916 -307
  95. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +1601 -74
  96. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +139 -7
  97. package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +58 -1
  98. package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +54 -1
  99. package/Server/Utils/AI/Toolbox/AIActionTools.ts +14 -6
  100. package/Server/Utils/AI/Toolbox/ResourceTools.ts +4 -2
  101. package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +1341 -150
  102. package/Server/Utils/AutoRemediation/RemediationVerifier.ts +375 -4
  103. package/Server/Utils/Database/AffectedResourceRelations.ts +127 -0
  104. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +240 -1
  105. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +1 -0
  106. package/Server/Utils/Monitor/MonitorAlert.ts +83 -9
  107. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +5 -1
  108. package/Server/Utils/Monitor/MonitorIncident.ts +104 -9
  109. package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +124 -7
  110. package/Server/Utils/Monitor/ProbeMonitorsNotification.ts +407 -0
  111. package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +1 -0
  112. package/Server/Utils/PushNotificationUtil.ts +45 -0
  113. package/Server/Utils/Rules/OwnerRuleAssignment.ts +40 -3
  114. package/Server/Utils/Runbook/RunbookExecutePermission.ts +56 -6
  115. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +223 -33
  116. package/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.ts +559 -0
  117. package/Server/Utils/StartServer.ts +4 -3
  118. package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +528 -0
  119. package/Server/Utils/TwoFactorBackupCodeNotification.ts +1 -0
  120. package/Server/Utils/UserPermission/UserPermission.ts +16 -0
  121. package/Server/Utils/Workflow/OAuth2TokenClient.ts +704 -0
  122. package/Server/Utils/Workflow/WorkflowVariableOAuthToken.ts +318 -0
  123. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +47 -7
  124. package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +57 -16
  125. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +193 -214
  126. package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +57 -13
  127. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor.ts +54 -17
  128. package/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.ts +179 -161
  129. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +281 -167
  130. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsServiceUrl.ts +114 -0
  131. package/Server/Utils/Workspace/MicrosoftTeams/ReactionNoteSync.ts +895 -0
  132. package/Server/Utils/Workspace/Slack/Actions/ActionTypes.ts +7 -9
  133. package/Server/Utils/Workspace/Slack/Actions/Alert.ts +96 -227
  134. package/Server/Utils/Workspace/Slack/Actions/AlertEpisode.ts +93 -214
  135. package/Server/Utils/Workspace/Slack/Actions/Auth.ts +30 -3
  136. package/Server/Utils/Workspace/Slack/Actions/Authorization.ts +99 -0
  137. package/Server/Utils/Workspace/Slack/Actions/Incident.ts +117 -283
  138. package/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.ts +101 -196
  139. package/Server/Utils/Workspace/Slack/Actions/ReactionNote.ts +329 -0
  140. package/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.ts +111 -287
  141. package/Server/Utils/Workspace/Slack/Slack.ts +86 -17
  142. package/Server/Utils/Workspace/Slack/app-manifest.json +2 -0
  143. package/Server/Utils/Workspace/Workspace.ts +171 -54
  144. package/Server/Utils/Workspace/WorkspaceActionAuthorization.ts +252 -0
  145. package/Server/Utils/Workspace/WorkspaceBase.ts +39 -0
  146. package/Server/Utils/Workspace/WorkspaceOAuthState.ts +280 -0
  147. package/Server/Utils/Workspace/WorkspaceProjectReferenceValidator.ts +96 -0
  148. package/Server/Utils/Workspace/WorkspaceReactionNote.ts +620 -0
  149. package/Tests/App/Dashboard/AttributesJSONDashboardSurfaces.test.tsx +496 -0
  150. package/Tests/App/Dashboard/AutoRemediationRulesTableCommandRunners.test.tsx +152 -0
  151. package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +32 -0
  152. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +794 -0
  153. package/Tests/App/Dashboard/CreateOAuthWorkflowVariableModal.test.tsx +840 -0
  154. package/Tests/App/Dashboard/CreateWorkflowModal.test.tsx +653 -0
  155. package/Tests/App/Dashboard/DatabaseMonitorStepForm.test.tsx +197 -0
  156. package/Tests/App/Dashboard/DatabaseMonitorView.test.tsx +376 -0
  157. package/Tests/App/Dashboard/DisabledWarningRobustness.test.tsx +262 -0
  158. package/Tests/App/Dashboard/EventNotes.test.tsx +2080 -0
  159. package/Tests/App/Dashboard/EventNotesPages.test.tsx +486 -0
  160. package/Tests/App/Dashboard/EventNotesUtil.test.ts +933 -0
  161. package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +218 -0
  162. package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +665 -0
  163. package/Tests/App/Dashboard/IncidentEpisodeFeedPublicNoteNotifyDefault.test.tsx +494 -0
  164. package/Tests/App/Dashboard/IncidentEpisodeOverviewNotifyDefault.test.tsx +706 -0
  165. package/Tests/App/Dashboard/IncidentFeedPublicNoteNotifyDefault.test.tsx +472 -0
  166. package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +858 -0
  167. package/Tests/App/Dashboard/IncomingEmailAddressSettings.test.tsx +567 -0
  168. package/Tests/App/Dashboard/InvestigationClusterToolFormat.test.ts +241 -0
  169. package/Tests/App/Dashboard/InvestigationEvidenceKubectl.test.tsx +322 -0
  170. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +12 -0
  171. package/Tests/App/Dashboard/InvestigationPanelClusterAccess.test.tsx +476 -0
  172. package/Tests/App/Dashboard/InvestigationRunDetailsKubectl.test.tsx +383 -0
  173. package/Tests/App/Dashboard/KubectlActivitySummary.test.tsx +366 -0
  174. package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +2886 -0
  175. package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +2688 -0
  176. package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +915 -0
  177. package/Tests/App/Dashboard/LayerDateTimeFieldTimezoneName.test.tsx +66 -0
  178. package/Tests/App/Dashboard/LogMonitorPreview.test.tsx +174 -0
  179. package/Tests/App/Dashboard/MonitorFeedRefresh.test.tsx +279 -0
  180. package/Tests/App/Dashboard/MonitorMetricQueryConfig.test.ts +244 -0
  181. package/Tests/App/Dashboard/MonitorOpenWorkCard.test.tsx +381 -0
  182. package/Tests/App/Dashboard/MonitorOpenWorkHook.test.tsx +784 -0
  183. package/Tests/App/Dashboard/MonitorOverviewData.test.tsx +2199 -0
  184. package/Tests/App/Dashboard/MonitorOverviewHero.test.tsx +1169 -0
  185. package/Tests/App/Dashboard/MonitorOverviewInput.test.ts +1127 -0
  186. package/Tests/App/Dashboard/MonitorOverviewPage.test.tsx +2136 -0
  187. package/Tests/App/Dashboard/MonitorOverviewSelectReadable.test.ts +368 -0
  188. package/Tests/App/Dashboard/MonitorOverviewStatBar.test.tsx +526 -0
  189. package/Tests/App/Dashboard/MonitorOverviewSupportingCards.test.tsx +542 -0
  190. package/Tests/App/Dashboard/MonitorOwnersCombine.test.ts +299 -0
  191. package/Tests/App/Dashboard/MonitorProbesCard.test.tsx +602 -0
  192. package/Tests/App/Dashboard/MonitorResponseTimeCard.test.tsx +386 -0
  193. package/Tests/App/Dashboard/MonitorSetupAndConnectionCards.test.tsx +612 -0
  194. package/Tests/App/Dashboard/MonitorStatusChangesCard.test.tsx +257 -0
  195. package/Tests/App/Dashboard/MonitorSummaryTestMonitorButton.test.tsx +360 -1
  196. package/Tests/App/Dashboard/MonitorUptimeHistoryCard.test.tsx +835 -0
  197. package/Tests/App/Dashboard/MonitorUptimeSummaryHook.test.tsx +971 -0
  198. package/Tests/App/Dashboard/MonitorViewLayout.test.tsx +580 -0
  199. package/Tests/App/Dashboard/OnCallLayerRestrictionTimesPreserved.test.tsx +441 -0
  200. package/Tests/App/Dashboard/OnCallNavbar.test.tsx +214 -0
  201. package/Tests/App/Dashboard/OnCallSchedulePreviewCalendarDisplay.test.tsx +1389 -0
  202. package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +205 -1
  203. package/Tests/App/Dashboard/OnCallScheduleTimeInputsSettingsZone.test.tsx +198 -0
  204. package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +524 -0
  205. package/Tests/App/Dashboard/RemediationSuggestionCardApproval.test.tsx +702 -0
  206. package/Tests/App/Dashboard/RemoveUserFromProject.test.tsx +427 -0
  207. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +4 -0
  208. package/Tests/App/Dashboard/RumSessionReplaySideMenu.test.tsx +3 -1
  209. package/Tests/App/Dashboard/RunbookStepTypeMaps.test.ts +166 -0
  210. package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +622 -0
  211. package/Tests/App/Dashboard/ScheduledMaintenanceChangeStateNotifyDefault.test.tsx +1491 -0
  212. package/Tests/App/Dashboard/ScheduledMaintenanceFeedPublicNoteNotifyDefault.test.tsx +489 -0
  213. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewNotifyDefault.test.tsx +909 -0
  214. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +14 -0
  215. package/Tests/App/Dashboard/ScheduledMaintenanceWindowStatsTranslation.test.tsx +307 -0
  216. package/Tests/App/Dashboard/SecurityEventAttributeColumnPicker.test.tsx +542 -0
  217. package/Tests/App/Dashboard/SecurityEventConnectionFormModal.test.tsx +174 -0
  218. package/Tests/App/Dashboard/SecurityEventConnectionsEmptyState.test.tsx +973 -212
  219. package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +491 -52
  220. package/Tests/App/Dashboard/SecurityEventDetailPanel.test.tsx +301 -0
  221. package/Tests/App/Dashboard/SecurityEventListRow.test.tsx +158 -1
  222. package/Tests/App/Dashboard/SecurityEventProviderTile.test.tsx +869 -0
  223. package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +959 -94
  224. package/Tests/App/Dashboard/SecurityEventsNoResults.test.tsx +2 -0
  225. package/Tests/App/Dashboard/SecurityEventsViewer.test.tsx +274 -1
  226. package/Tests/App/Dashboard/TeamMembershipRemoval.test.ts +900 -0
  227. package/Tests/App/Dashboard/TeamMembershipRemovalPages.test.tsx +460 -0
  228. package/Tests/App/Dashboard/TimezoneElements.test.tsx +321 -0
  229. package/Tests/App/Dashboard/TimezoneSelectButton.test.tsx +634 -0
  230. package/Tests/App/Dashboard/TraceExplorer.test.tsx +260 -0
  231. package/Tests/App/Dashboard/TraceMonitorPreview.test.tsx +182 -0
  232. package/Tests/App/Dashboard/UserTimezoneInit.test.tsx +544 -0
  233. package/Tests/App/Dashboard/WorkflowVariableModals.test.tsx +1630 -0
  234. package/Tests/App/Dashboard/WorkflowVariableRoutes.test.tsx +603 -0
  235. package/Tests/App/Dashboard/WorkflowVariableTokenStatus.test.tsx +589 -0
  236. package/Tests/App/Dashboard/WorkflowVariableUtil.test.ts +1543 -0
  237. package/Tests/App/Dashboard/WorkflowVariableView.test.tsx +3227 -0
  238. package/Tests/App/Dashboard/WorkflowVariablesTable.test.tsx +1277 -395
  239. package/Tests/App/StatusPage/StatusPageOverviewMonitorGroupUptime.test.tsx +714 -0
  240. package/Tests/App/StatusPage/StatusPageOverviewUptimeBars.NewYork.test.tsx +1009 -0
  241. package/Tests/App/StatusPage/StatusPageOverviewUptimeBars.test.tsx +1092 -0
  242. package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +5 -0
  243. package/Tests/Models/DatabaseModels/KubernetesClusterAiAccessColumns.test.ts +520 -0
  244. package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +66 -2
  245. package/Tests/Models/DatabaseModels/WorkspaceProjectAuthTokenAccessControl.test.ts +236 -0
  246. package/Tests/Models/IncidentEpisodePublicNoteNotifyColumnContract.test.ts +103 -0
  247. package/Tests/Models/IncidentPublicNoteNotifyColumnContract.test.ts +96 -0
  248. package/Tests/Models/ScheduledMaintenancePublicNoteNotifyColumnContract.test.ts +111 -0
  249. package/Tests/Server/API/AIInvestigationAPI.test.ts +976 -0
  250. package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +836 -0
  251. package/Tests/Server/API/CommonAPIAssertCanCreateTable.test.ts +565 -0
  252. package/Tests/Server/API/CommonAPIAssertCanReadTable.test.ts +282 -0
  253. package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +1744 -0
  254. package/Tests/Server/API/LogHistogramAPI.test.ts +371 -0
  255. package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +125 -30
  256. package/Tests/Server/API/MicrosoftTeamsOAuthState.test.ts +1121 -0
  257. package/Tests/Server/API/MonitorUptimeSummaryAPI.test.ts +685 -0
  258. package/Tests/Server/API/SessionReplayAPI.test.ts +602 -1
  259. package/Tests/Server/API/SlackChannelCacheAPI.test.ts +358 -0
  260. package/Tests/Server/API/SlackOAuthState.test.ts +688 -0
  261. package/Tests/Server/API/StatusPageOverviewMonitorGroupUptime.test.ts +527 -0
  262. package/Tests/Server/API/StatusPageTestEmailReportAuth.test.ts +2 -0
  263. package/Tests/Server/API/StatusPageUptimeAPI.test.ts +427 -0
  264. package/Tests/Server/API/UserProjectsAPI.test.ts +4 -0
  265. package/Tests/Server/API/WorkflowVariableAPI.test.ts +404 -0
  266. package/Tests/Server/API/WorkspaceOAuthTestHelpers.ts +289 -0
  267. package/Tests/Server/API/WorkspaceSendTestNotificationRoutes.test.ts +2064 -0
  268. package/Tests/Server/Infrastructure/GlobalCache.test.ts +86 -0
  269. package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +589 -0
  270. package/Tests/Server/Infrastructure/Postgres/AddOAuth2WorkflowVariablesMigration.test.ts +254 -0
  271. package/Tests/Server/Middleware/BlockedUserMiddleware.test.ts +403 -0
  272. package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +13 -0
  273. package/Tests/Server/Middleware/PublicRouteUserMiddleware.test.ts +2 -0
  274. package/Tests/Server/Middleware/RemovedProjectMemberAuthorization.test.ts +469 -0
  275. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +77 -1
  276. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +211 -0
  277. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +358 -0
  278. package/Tests/Server/Middleware/UserAuthorization.test.ts +97 -0
  279. package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +2 -0
  280. package/Tests/Server/Services/AccessTokenProjectUserGrant.test.ts +14 -2
  281. package/Tests/Server/Services/AccessTokenRemovedMemberAccess.test.ts +1044 -0
  282. package/Tests/Server/Services/AddOwnersIdempotency.test.ts +5 -0
  283. package/Tests/Server/Services/AlertEpisodeMemberCreatePermission.test.ts +139 -0
  284. package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +1361 -0
  285. package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +375 -0
  286. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +2384 -0
  287. package/Tests/Server/Services/FindOwnersProjectMembership.test.ts +267 -0
  288. package/Tests/Server/Services/InboundEmail/SendGridInboundProviderAddress.test.ts +90 -0
  289. package/Tests/Server/Services/IncidentEpisodeMemberCreatePermission.test.ts +143 -0
  290. package/Tests/Server/Services/IncidentEpisodePublicNoteNotifyDefault.test.ts +805 -0
  291. package/Tests/Server/Services/IncidentPublicNoteNotifyDefault.test.ts +724 -0
  292. package/Tests/Server/Services/IncidentRoleProjectMembership.test.ts +233 -0
  293. package/Tests/Server/Services/KubernetesClusterAiAccessAgentRunnerAssignments.test.ts +372 -0
  294. package/Tests/Server/Services/KubernetesClusterAiAccessBindingGuard.test.ts +1876 -0
  295. package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +620 -0
  296. package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +421 -0
  297. package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +3662 -0
  298. package/Tests/Server/Services/KubernetesClusterAiAccessSubjectClusters.test.ts +405 -0
  299. package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +975 -0
  300. package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +497 -0
  301. package/Tests/Server/Services/KubernetesClusterServiceDeleteSettlesRemediation.test.ts +328 -0
  302. package/Tests/Server/Services/LogAggregationService.test.ts +99 -0
  303. package/Tests/Server/Services/MonitorServiceIncomingEmailCustomAddress.test.ts +447 -0
  304. package/Tests/Server/Services/MonitorServiceProbeFlagSync.test.ts +1334 -0
  305. package/Tests/Server/Services/MonitorServiceProbeStatusGrouping.test.ts +1640 -0
  306. package/Tests/Server/Services/MonitorServiceRefreshCurrentStatus.test.ts +225 -0
  307. package/Tests/Server/Services/MonitorStatusBridgeProbePath.test.ts +8 -8
  308. package/Tests/Server/Services/MonitorStatusTimelineDailyAggregates.test.ts +264 -1
  309. package/Tests/Server/Services/MonitorStatusTimelineMergedDowntime.test.ts +284 -0
  310. package/Tests/Server/Services/MonitorUptimeSummaryService.test.ts +1043 -0
  311. package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +5 -0
  312. package/Tests/Server/Services/OnCallUndeliverablePageOwnerMembership.test.ts +359 -0
  313. package/Tests/Server/Services/ProbeServiceDashboardLink.test.ts +191 -0
  314. package/Tests/Server/Services/ProjectOwnersProjectMembership.test.ts +221 -0
  315. package/Tests/Server/Services/RuleRun/IncidentAlertMaintenanceRuleEnginesRun.test.ts +5 -0
  316. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupARun.test.ts +28 -0
  317. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +9 -1
  318. package/Tests/Server/Services/RunnerJobEnqueueAiCommandAgentRunner.test.ts +230 -0
  319. package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +978 -0
  320. package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +866 -0
  321. package/Tests/Server/Services/RunnerJobSubmitResultRedaction.test.ts +322 -0
  322. package/Tests/Server/Services/RunnerJobSubmitResultUnstorableText.test.ts +357 -0
  323. package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +227 -0
  324. package/Tests/Server/Services/RunnerServiceAgentName.test.ts +581 -0
  325. package/Tests/Server/Services/ScheduledMaintenancePublicNoteNotifyDefault.test.ts +802 -0
  326. package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +53 -0
  327. package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +5 -0
  328. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +12 -0
  329. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +1176 -1
  330. package/Tests/Server/Services/TeamMemberBillingSeatSync.test.ts +9 -0
  331. package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +18 -0
  332. package/Tests/Server/Services/TeamMemberProjectMembership.test.ts +255 -0
  333. package/Tests/Server/Services/TeamMemberRemovalRevokesProjectAccess.test.ts +600 -0
  334. package/Tests/Server/Services/TeamMemberResourceCleanup.test.ts +731 -0
  335. package/Tests/Server/Services/TeamMemberWorkspaceAccountCleanup.test.ts +164 -0
  336. package/Tests/Server/Services/TraceAggregationService.test.ts +64 -0
  337. package/Tests/Server/Services/UserServiceBlockedUser.test.ts +293 -0
  338. package/Tests/Server/Services/WorkflowVariableOAuthHooks.test.ts +1016 -0
  339. package/Tests/Server/Services/WorkflowVariableOAuthPostgres.test.ts +543 -0
  340. package/Tests/Server/Services/WorkflowVariableRenameUniqueness.test.ts +16 -5
  341. package/Tests/Server/Services/WorkspaceNotificationRuleNoteDelivery.test.ts +473 -0
  342. package/Tests/Server/Services/WorkspaceNotificationRuleSendTestNotification.test.ts +3690 -0
  343. package/Tests/Server/Services/WorkspaceProjectAuthTokenMiscDataSecrets.test.ts +374 -0
  344. package/Tests/Server/Services/WorkspaceProjectAuthTokenSlackChannelCache.test.ts +143 -0
  345. package/Tests/Server/Types/Database/Permissions/WorkflowVariableOAuthColumnPermission.test.ts +250 -0
  346. package/Tests/Server/Types/Database/QueryHelperJsonContains.test.ts +96 -0
  347. package/Tests/Server/Types/Markdown.test.ts +1033 -0
  348. package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +285 -0
  349. package/Tests/Server/Utils/AI/CodeFix/CodeFixAgentAuth.test.ts +159 -1
  350. package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +752 -0
  351. package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +1840 -0
  352. package/Tests/Server/Utils/AI/KubectlJobRunner.test.ts +1067 -0
  353. package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +355 -0
  354. package/Tests/Server/Utils/AI/PostIncidentStatusUpdateTool.test.ts +496 -0
  355. package/Tests/Server/Utils/AI/RemediationCommandTools.test.ts +305 -0
  356. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +1365 -0
  357. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +639 -0
  358. package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +1585 -0
  359. package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +1065 -0
  360. package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +2138 -1
  361. package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +1035 -0
  362. package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +517 -0
  363. package/Tests/Server/Utils/AI/ResourceTools.test.ts +27 -1
  364. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +1371 -24
  365. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorRollbackRace.test.ts +962 -0
  366. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +518 -0
  367. package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +939 -2
  368. package/Tests/Server/Utils/Database/ProjectScopedRelationReferences.test.ts +371 -0
  369. package/Tests/Server/Utils/Monitor/AffectedResourceList.test.ts +64 -0
  370. package/Tests/Server/Utils/Monitor/DatabaseMetricObservationRemediation.test.ts +102 -0
  371. package/Tests/Server/Utils/Monitor/MonitorAlertCriteriaReferences.test.ts +362 -0
  372. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +55 -0
  373. package/Tests/Server/Utils/Monitor/MonitorEmailAddressRedaction.test.ts +255 -0
  374. package/Tests/Server/Utils/Monitor/MonitorIncidentCriteriaReferences.test.ts +347 -0
  375. package/Tests/Server/Utils/Monitor/MonitorIncidentMemberRoleMembership.test.ts +203 -0
  376. package/Tests/Server/Utils/Monitor/ProbeMonitorsNotification.test.ts +1147 -0
  377. package/Tests/Server/Utils/PushNotificationUtilCreators.test.ts +79 -0
  378. package/Tests/Server/Utils/ResourceFeed/ResourceFeedUtil.test.ts +21 -1
  379. package/Tests/Server/Utils/Rules/OwnerRuleAssignment.test.ts +155 -1
  380. package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +288 -0
  381. package/Tests/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.test.ts +304 -0
  382. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +40 -3
  383. package/Tests/Server/Utils/Workflow/OAuth2TokenClient.test.ts +702 -0
  384. package/Tests/Server/Utils/Workflow/OAuth2TokenClientHttpIntegration.test.ts +507 -0
  385. package/Tests/Server/Utils/Workflow/WorkflowVariableOAuthToken.test.ts +831 -0
  386. package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +63 -1
  387. package/Tests/Server/Utils/Workspace/MicrosoftTeamsAppAccessToken.test.ts +256 -0
  388. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +60 -0
  389. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChatActionAuthorization.test.ts +442 -0
  390. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +44 -2
  391. package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +55 -0
  392. package/Tests/Server/Utils/Workspace/MicrosoftTeamsReactionNoteSync.test.ts +1522 -0
  393. package/Tests/Server/Utils/Workspace/MicrosoftTeamsServiceUrl.test.ts +130 -0
  394. package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +4 -4
  395. package/Tests/Server/Utils/Workspace/Slack/SlackActionAuthorization.test.ts +691 -0
  396. package/Tests/Server/Utils/Workspace/Slack/SlackMessageByTimestamp.test.ts +253 -0
  397. package/Tests/Server/Utils/Workspace/Slack/SlackReactionNote.test.ts +574 -0
  398. package/Tests/Server/Utils/Workspace/WorkspaceActionAuthorization.test.ts +488 -0
  399. package/Tests/Server/Utils/Workspace/WorkspaceCreateProjectReferences.test.ts +861 -0
  400. package/Tests/Server/Utils/Workspace/WorkspaceMessageAuthor.test.ts +635 -0
  401. package/Tests/Server/Utils/Workspace/WorkspaceOAuthState.test.ts +548 -0
  402. package/Tests/Server/Utils/Workspace/WorkspaceReactionNote.test.ts +839 -0
  403. package/Tests/Server/Utils/Workspace/WorkspaceSendErrorMessage.test.ts +725 -0
  404. package/Tests/Types/AutoRemediation/AiRemediationCommandPlan.test.ts +283 -9
  405. package/Tests/Types/DateBrowserLocalWallClock.test.ts +280 -0
  406. package/Tests/Types/DateLocalShortMonthNameFallback.test.ts +389 -0
  407. package/Tests/Types/DateUserTimezone.test.ts +177 -4
  408. package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +306 -0
  409. package/Tests/Types/Kubernetes/KubernetesClusterAiAccess.test.ts +283 -0
  410. package/Tests/Types/Kubernetes/KubernetesRunnerPostureWriteScope.test.ts +68 -0
  411. package/Tests/Types/Monitor/DatabaseMonitor/SqlServerPlatform.test.ts +144 -0
  412. package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +160 -0
  413. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +53 -0
  414. package/Tests/Types/Runbook/RunbookStepType.test.ts +91 -16
  415. package/Tests/Types/Runner/RunnerLiveStatus.test.ts +14 -3
  416. package/Tests/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.test.ts +15 -0
  417. package/Tests/Types/StatusPage/PublicNoteSubscriberNotificationDefault.test.ts +1868 -0
  418. package/Tests/Types/Timezone.test.ts +5 -0
  419. package/Tests/Types/TimezoneAlias.test.ts +1034 -0
  420. package/Tests/Types/Workflow/JiraTemplateFixtures.ts +833 -0
  421. package/Tests/Types/Workflow/JiraTemplateScripts.test.ts +5414 -0
  422. package/Tests/Types/Workflow/JiraTemplateSimulation.test.ts +4803 -0
  423. package/Tests/Types/Workflow/JiraTemplates.test.ts +5429 -0
  424. package/Tests/Types/Workflow/Templates.test.ts +79 -4
  425. package/Tests/Types/Workflow/WorkflowVariableOAuth.test.ts +520 -0
  426. package/Tests/Types/Workspace/WorkspaceNoteReaction.test.ts +177 -0
  427. package/Tests/UI/Components/AttributesJSON/AttributesJSONView.test.tsx +390 -0
  428. package/Tests/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.test.tsx +566 -0
  429. package/Tests/UI/Components/AttributesJSON/JSONHighlight.test.ts +131 -0
  430. package/Tests/UI/Components/BasicFormDropdownNormalization.test.tsx +276 -1
  431. package/Tests/UI/Components/Calendar/Calendar.test.tsx +1611 -0
  432. package/Tests/UI/Components/Calendar/CalendarDayLayout.test.ts +1486 -0
  433. package/Tests/UI/Components/Calendar/CalendarDayLayoutDst.test.ts +632 -0
  434. package/Tests/UI/Components/Calendar/CalendarDst.test.tsx +388 -0
  435. package/Tests/UI/Components/Charts/ChartGroupPlotFloor.test.tsx +170 -0
  436. package/Tests/UI/Components/Charts/HistogramSelection.test.ts +282 -0
  437. package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +6 -0
  438. package/Tests/UI/Components/CustomTimeRangeModalTimezoneTranslation.test.tsx +285 -0
  439. package/Tests/UI/Components/Dropdown.test.tsx +999 -2
  440. package/Tests/UI/Components/Forms/FormFieldCheckboxTranslation.test.tsx +216 -0
  441. package/Tests/UI/Components/Forms/FormFieldTimezoneDescriptionTranslation.test.tsx +461 -0
  442. package/Tests/UI/Components/Forms/IncidentEpisodePublicNoteNotifyPayload.test.tsx +641 -0
  443. package/Tests/UI/Components/Forms/IncidentPublicNoteNotifyPayload.test.tsx +346 -0
  444. package/Tests/UI/Components/Forms/ScheduledMaintenancePublicNoteNotifyPayload.test.tsx +493 -0
  445. package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +911 -2
  446. package/Tests/UI/Components/Graphs/DayUptimeGraphTimezone.NewYork.test.tsx +730 -0
  447. package/Tests/UI/Components/Graphs/DayUptimeGraphTimezone.Tokyo.test.tsx +506 -0
  448. package/Tests/UI/Components/HistogramClickToZoom.test.tsx +619 -0
  449. package/Tests/UI/Components/LogDetailsPanelAttributesJSON.test.tsx +248 -0
  450. package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +47 -1
  451. package/Tests/UI/Components/LogsViewerSmallScreens.test.tsx +620 -0
  452. package/Tests/UI/Components/ModelPageModelChange.test.tsx +242 -3
  453. package/Tests/UI/Components/ModelTable/BaseModelTableDeleteConfirmation.test.tsx +521 -0
  454. package/Tests/UI/Components/MonitorGraphs/Uptime.test.tsx +406 -2
  455. package/Tests/UI/Components/MonitorGraphs/UptimeBarDayModal.test.tsx +169 -2
  456. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +266 -0
  457. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerSmallScreens.test.tsx +439 -0
  458. package/Tests/UI/Components/TimezoneDropdownLegacyValues.test.tsx +704 -0
  459. package/Tests/UI/Components/UseHistogramZoom.test.tsx +89 -1
  460. package/Tests/UI/Components/UseLogsHistogram.test.tsx +175 -0
  461. package/Tests/UI/Monitor/MonitorSummarySnapshotRender.test.tsx +5 -5
  462. package/Tests/UI/Telemetry/TelemetryDetailPanel.test.tsx +28 -0
  463. package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +51 -0
  464. package/Tests/UI/Utils/Timezone.test.ts +747 -33
  465. package/Tests/UI/Workspace/SendTestNotificationButton.test.tsx +1854 -0
  466. package/Tests/UI/Workspace/WorkspaceCardsSendTestNotification.test.tsx +1864 -0
  467. package/Tests/Utils/AI/InvestigationReport.test.ts +435 -7
  468. package/Tests/Utils/AiRemediation/KubectlAllowlistValidatorParity.test.ts +1084 -0
  469. package/Tests/Utils/AiRemediation/KubectlOutputRedactor.test.ts +2531 -0
  470. package/Tests/Utils/AiRemediation/KubectlPolicy.test.ts +1895 -0
  471. package/Tests/Utils/AiRemediation/KubectlPolicyAllowlist.test.ts +428 -0
  472. package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +828 -0
  473. package/Tests/Utils/AiRemediation/KubectlPolicyCustomResourceScope.test.ts +573 -0
  474. package/Tests/Utils/AiRemediation/KubectlPolicyDenials.test.ts +1525 -0
  475. package/Tests/Utils/AiRemediation/KubectlPolicyExposeOverrides.test.ts +275 -0
  476. package/Tests/Utils/AiRemediation/KubectlPolicyNodePatch.test.ts +428 -0
  477. package/Tests/Utils/AiRemediation/KubectlPolicyObjectWords.test.ts +195 -0
  478. package/Tests/Utils/AiRemediation/KubectlPolicyPatchParents.test.ts +637 -0
  479. package/Tests/Utils/AiRemediation/KubectlPolicyPodSecurityBaseline.test.ts +322 -0
  480. package/Tests/Utils/AiRemediation/KubectlPolicySafeWriteScope.test.ts +1026 -0
  481. package/Tests/Utils/AiRemediation/KubectlWaitBudget.test.ts +267 -0
  482. package/Tests/Utils/AiRemediation/KubectlWriteScope.test.ts +390 -0
  483. package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +777 -0
  484. package/Tests/Utils/AiRemediation/KubectlWriteScopeClusterScoped.test.ts +613 -0
  485. package/Tests/Utils/AiRemediation/KubectlWriteScopeEntryPoint.test.ts +576 -0
  486. package/Tests/Utils/AiRemediation/KubectlWriteScopeKubectlSpellings.test.ts +752 -0
  487. package/Tests/Utils/AiRemediation/KubectlWriteScopeObjectWords.test.ts +358 -0
  488. package/Tests/Utils/AiRemediation/KubectlWriteScopeOverrides.test.ts +293 -0
  489. package/Tests/Utils/AiRemediation/KubectlWriteScopeParityCases.ts +598 -0
  490. package/Tests/Utils/AiRemediation/KubectlWriteScopePolicyParity.test.ts +403 -0
  491. package/Tests/Utils/Dashboard/DashboardClockComponent.test.ts +154 -5
  492. package/Tests/Utils/Monitor/IncomingEmailMonitorAddress.test.ts +478 -0
  493. package/Tests/Utils/Monitor/MonitorCheckScheduleUtil.test.ts +646 -0
  494. package/Tests/Utils/Monitor/MonitorOverviewCriteriaUtil.test.ts +292 -0
  495. package/Tests/Utils/Monitor/MonitorOverviewFamily.test.ts +274 -0
  496. package/Tests/Utils/Monitor/MonitorOverviewPresentationUtil.test.ts +3100 -0
  497. package/Tests/Utils/Monitor/MonitorOverviewProbeUtil.test.ts +1412 -0
  498. package/Tests/Utils/Monitor/MonitorOverviewTargetUtil.test.ts +374 -0
  499. package/Tests/Utils/Monitor/MonitorStatusHistoryUtil.test.ts +332 -0
  500. package/Tests/Utils/Monitor/MonitorUptimeSummaryUtil.test.ts +946 -0
  501. package/Tests/Utils/Rum/SessionIdentity.test.ts +94 -0
  502. package/Tests/Utils/Rum/SessionReplayWafCompatibility.test.ts +840 -0
  503. package/Tests/Utils/Rum/SessionReplayWireEncoding.test.ts +208 -0
  504. package/Tests/Utils/Rum/UserFlow.test.ts +1010 -0
  505. package/Tests/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.test.ts +217 -0
  506. package/Tests/Utils/StatusPage/ResourceUptime.test.ts +1272 -1
  507. package/Tests/Utils/StatusPage/UptimeDailyAggregateTotals.test.ts +298 -0
  508. package/Tests/Utils/StatusPage/UptimeDailyAggregateUtil.test.ts +649 -1
  509. package/Tests/Utils/StatusPage/UptimeDailyAggregateWindow.test.ts +314 -0
  510. package/Tests/Utils/Telemetry/AttributesJSON.test.ts +454 -0
  511. package/Tests/Utils/Uptime/DayUptimeGraphUtil.test.ts +418 -2
  512. package/Tests/Utils/Uptime/DayUptimeGraphUtilMidnightDst.test.ts +160 -0
  513. package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.Auckland.test.ts +45 -0
  514. package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.Kolkata.test.ts +40 -0
  515. package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.London.test.ts +147 -0
  516. package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.NewYork.test.ts +48 -0
  517. package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezone.Tokyo.test.ts +39 -0
  518. package/Tests/Utils/Uptime/DayUptimeGraphUtilTimezoneCases.ts +1015 -0
  519. package/Tests/Utils/Uptime/UptimeUtil.test.ts +75 -0
  520. package/Tests/Utils/Uptime/UptimeUtilChronologicalOrder.test.ts +1279 -0
  521. package/Types/AutoRemediation/AiRemediationCommandPlan.ts +203 -5
  522. package/Types/Date.ts +121 -7
  523. package/Types/Email/EmailMessage.ts +8 -0
  524. package/Types/Email/EmailTemplateType.ts +1 -0
  525. package/Types/Exception/ExceptionMessages.ts +1 -0
  526. package/Types/Kubernetes/KubernetesClusterAiAccess.ts +488 -0
  527. package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +54 -0
  528. package/Types/Kubernetes/KubernetesClusterAiAccessToolNames.ts +17 -0
  529. package/Types/Monitor/DatabaseMonitor/DatabaseMonitorResponse.ts +9 -0
  530. package/Types/Monitor/DatabaseMonitor/SqlServerPlatform.ts +117 -0
  531. package/Types/Monitor/KubernetesAlertTemplates.ts +6 -5
  532. package/Types/Monitor/MonitorStepExceptionMonitor.ts +56 -0
  533. package/Types/Monitor/MonitorUptimeSummary.ts +113 -0
  534. package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +11 -0
  535. package/Types/Rum/SessionReplay.ts +55 -2
  536. package/Types/Rum/SessionReplayApi.ts +44 -1
  537. package/Types/Rum/UserFlow.ts +187 -0
  538. package/Types/Runbook/RunbookStepType.ts +10 -0
  539. package/Types/Runbook/RunnerJobOrigin.ts +15 -0
  540. package/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.ts +6 -2
  541. package/Types/StatusPage/MergedDowntimeTotals.ts +17 -0
  542. package/Types/StatusPage/PublicNoteSubscriberNotificationDefault.ts +148 -0
  543. package/Types/StatusPage/UptimeDailyAggregate.ts +15 -0
  544. package/Types/Telemetry/TelemetryIngestSurface.ts +9 -0
  545. package/Types/Timezone.ts +5 -0
  546. package/Types/TimezoneAlias.ts +385 -0
  547. package/Types/Workflow/Templates.ts +3041 -2
  548. package/Types/Workflow/WorkflowVariableOAuth.ts +476 -0
  549. package/Types/Workspace/WorkspaceNoteReaction.ts +132 -0
  550. package/Typings/Index.d.ts +24 -0
  551. package/UI/Components/AttributesJSON/AttributesJSONPreferences.ts +175 -0
  552. package/UI/Components/AttributesJSON/AttributesJSONView.tsx +138 -0
  553. package/UI/Components/AttributesJSON/AttributesViewToggle.tsx +79 -0
  554. package/UI/Components/AttributesJSON/BracesGlyph.tsx +30 -0
  555. package/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.tsx +491 -0
  556. package/UI/Components/AttributesJSON/JSONHighlight.ts +77 -0
  557. package/UI/Components/Calendar/Calendar.css +83 -0
  558. package/UI/Components/Calendar/Calendar.tsx +136 -7
  559. package/UI/Components/Calendar/CalendarDayLayout.ts +236 -0
  560. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +23 -1
  561. package/UI/Components/Charts/Utils/HistogramSelection.ts +117 -0
  562. package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +269 -0
  563. package/UI/Components/Charts/Utils/useHistogramZoom.ts +26 -2
  564. package/UI/Components/Date/CustomTimeRangeModal.tsx +26 -3
  565. package/UI/Components/Dropdown/Dropdown.tsx +58 -9
  566. package/UI/Components/Forms/Fields/FormField.tsx +49 -14
  567. package/UI/Components/Graphs/DayUptimeGraph.tsx +166 -25
  568. package/UI/Components/Graphs/UptimeBarTooltip.tsx +10 -2
  569. package/UI/Components/Graphs/UptimeDaySummary.tsx +8 -0
  570. package/UI/Components/LogsViewer/LogsViewer.tsx +52 -5
  571. package/UI/Components/LogsViewer/components/ColumnSelector.tsx +13 -2
  572. package/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.tsx +13 -1
  573. package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +91 -76
  574. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +15 -1
  575. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +28 -106
  576. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +54 -6
  577. package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +8 -2
  578. package/UI/Components/LogsViewer/useLogsHistogram.ts +39 -6
  579. package/UI/Components/ModelTable/BaseModelTable.tsx +66 -14
  580. package/UI/Components/MonitorGraphs/Uptime.tsx +12 -0
  581. package/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx +10 -2
  582. package/UI/Components/Page/ModelPage.tsx +59 -3
  583. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +69 -5
  584. package/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.tsx +12 -1
  585. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +15 -1
  586. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +36 -104
  587. package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +9 -1
  588. package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +19 -1
  589. package/UI/Utils/Timezone.ts +69 -28
  590. package/Utils/AI/InvestigationReport.ts +195 -25
  591. package/Utils/AiRemediation/KubectlOutputRedactor.ts +1724 -0
  592. package/Utils/AiRemediation/KubectlPolicy.ts +4136 -0
  593. package/Utils/AiRemediation/KubectlWaitBudget.ts +156 -0
  594. package/Utils/AiRemediation/KubectlWriteScope.ts +1964 -0
  595. package/Utils/Dashboard/Components/DashboardClockComponent.ts +7 -6
  596. package/Utils/Monitor/IncomingEmailMonitorAddress.ts +292 -0
  597. package/Utils/Monitor/MonitorCheckScheduleUtil.ts +654 -0
  598. package/Utils/Monitor/MonitorOverviewCriteriaUtil.ts +188 -0
  599. package/Utils/Monitor/MonitorOverviewFamily.ts +263 -0
  600. package/Utils/Monitor/MonitorOverviewPresentationUtil.ts +1958 -0
  601. package/Utils/Monitor/MonitorOverviewProbeUtil.ts +979 -0
  602. package/Utils/Monitor/MonitorOverviewTargetUtil.ts +299 -0
  603. package/Utils/Monitor/MonitorStatusHistoryUtil.ts +201 -0
  604. package/Utils/Monitor/MonitorUptimeSummaryUtil.ts +638 -0
  605. package/Utils/Rum/SessionIdentity.ts +44 -0
  606. package/Utils/Rum/SessionReplayWireEncoding.ts +137 -0
  607. package/Utils/Rum/UserFlow.ts +1527 -0
  608. package/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.ts +107 -0
  609. package/Utils/StatusPage/ResourceUptime.ts +99 -0
  610. package/Utils/StatusPage/UptimeDailyAggregateUtil.ts +197 -0
  611. package/Utils/Telemetry/AttributesJSON.ts +367 -0
  612. package/Utils/Uptime/DayUptimeGraphUtil.ts +165 -0
  613. package/Utils/Uptime/UptimeUtil.ts +122 -38
  614. package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js +8 -1
  615. package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js.map +1 -1
  616. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js +62 -0
  617. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js.map +1 -1
  618. package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js +8 -1
  619. package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js.map +1 -1
  620. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +10 -3
  621. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -1
  622. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +10 -3
  623. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
  624. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +434 -0
  625. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  626. package/build/dist/Models/DatabaseModels/Monitor.js +49 -0
  627. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  628. package/build/dist/Models/DatabaseModels/Runner.js +4 -4
  629. package/build/dist/Models/DatabaseModels/Runner.js.map +1 -1
  630. package/build/dist/Models/DatabaseModels/RunnerJob.js +66 -0
  631. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  632. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +11 -3
  633. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
  634. package/build/dist/Models/DatabaseModels/WorkflowVariable.js +487 -3
  635. package/build/dist/Models/DatabaseModels/WorkflowVariable.js.map +1 -1
  636. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js +64 -16
  637. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  638. package/build/dist/Server/API/AIInvestigationAPI.js +205 -0
  639. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  640. package/build/dist/Server/API/AutoRemediationAPI.js +115 -2
  641. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  642. package/build/dist/Server/API/CommonAPI.js +49 -0
  643. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  644. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +564 -0
  645. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -0
  646. package/build/dist/Server/API/MicrosoftTeamsAPI.js +465 -176
  647. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  648. package/build/dist/Server/API/MonitorAPI.js +107 -1
  649. package/build/dist/Server/API/MonitorAPI.js.map +1 -1
  650. package/build/dist/Server/API/OnCallScheduleTimelineAPI.js +5 -16
  651. package/build/dist/Server/API/OnCallScheduleTimelineAPI.js.map +1 -1
  652. package/build/dist/Server/API/SlackAPI.js +322 -78
  653. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  654. package/build/dist/Server/API/StatusPageAPI.js +123 -17
  655. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  656. package/build/dist/Server/API/TelemetryAPI.js +144 -5
  657. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  658. package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js +1 -0
  659. package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js.map +1 -1
  660. package/build/dist/Server/API/WorkflowVariableAPI.js +109 -0
  661. package/build/dist/Server/API/WorkflowVariableAPI.js.map +1 -0
  662. package/build/dist/Server/Infrastructure/GlobalCache.js +29 -0
  663. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  664. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794300000000-AddOAuth2WorkflowVariables.js +38 -0
  665. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794300000000-AddOAuth2WorkflowVariables.js.map +1 -0
  666. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794400000000-AddKubernetesClusterAiAccess.js +54 -0
  667. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794400000000-AddKubernetesClusterAiAccess.js.map +1 -0
  668. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794500000000-AddIncomingEmailCustomAddressToMonitor.js +14 -0
  669. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794500000000-AddIncomingEmailCustomAddressToMonitor.js.map +1 -0
  670. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794600000000-AddAuthTokenExpiresAtToWorkspaceProjectAuthToken.js +12 -0
  671. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794600000000-AddAuthTokenExpiresAtToWorkspaceProjectAuthToken.js.map +1 -0
  672. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794700000000-RemoveAppAccessTokenFromWorkspaceProjectAuthTokenMiscData.js +32 -0
  673. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794700000000-RemoveAppAccessTokenFromWorkspaceProjectAuthTokenMiscData.js.map +1 -0
  674. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
  675. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  676. package/build/dist/Server/Middleware/MasterAdminAuthorization.js +21 -0
  677. package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
  678. package/build/dist/Server/Middleware/TelemetryIngest.js +32 -2
  679. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  680. package/build/dist/Server/Middleware/UserAuthorization.js +39 -6
  681. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  682. package/build/dist/Server/Services/AIAgentService.js +1 -0
  683. package/build/dist/Server/Services/AIAgentService.js.map +1 -1
  684. package/build/dist/Server/Services/AccessTokenService.js +87 -42
  685. package/build/dist/Server/Services/AccessTokenService.js.map +1 -1
  686. package/build/dist/Server/Services/AlertEpisodeMemberService.js +4 -0
  687. package/build/dist/Server/Services/AlertEpisodeMemberService.js.map +1 -1
  688. package/build/dist/Server/Services/AlertEpisodeService.js +12 -1
  689. package/build/dist/Server/Services/AlertEpisodeService.js.map +1 -1
  690. package/build/dist/Server/Services/AlertService.js +110 -23
  691. package/build/dist/Server/Services/AlertService.js.map +1 -1
  692. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +716 -30
  693. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
  694. package/build/dist/Server/Services/AutoRemediationRuleService.js +100 -0
  695. package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
  696. package/build/dist/Server/Services/BillingService.js +1 -0
  697. package/build/dist/Server/Services/BillingService.js.map +1 -1
  698. package/build/dist/Server/Services/InboundEmail/Providers/SendGridInboundProvider.js +11 -16
  699. package/build/dist/Server/Services/InboundEmail/Providers/SendGridInboundProvider.js.map +1 -1
  700. package/build/dist/Server/Services/IncidentEpisodeMemberService.js +4 -0
  701. package/build/dist/Server/Services/IncidentEpisodeMemberService.js.map +1 -1
  702. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +48 -0
  703. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
  704. package/build/dist/Server/Services/IncidentEpisodeRoleMemberService.js +21 -0
  705. package/build/dist/Server/Services/IncidentEpisodeRoleMemberService.js.map +1 -1
  706. package/build/dist/Server/Services/IncidentEpisodeService.js +12 -1
  707. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  708. package/build/dist/Server/Services/IncidentGroupingEngineService.js +8 -0
  709. package/build/dist/Server/Services/IncidentGroupingEngineService.js.map +1 -1
  710. package/build/dist/Server/Services/IncidentMemberService.js +14 -0
  711. package/build/dist/Server/Services/IncidentMemberService.js.map +1 -1
  712. package/build/dist/Server/Services/IncidentPublicNoteService.js +48 -0
  713. package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
  714. package/build/dist/Server/Services/IncidentService.js +99 -21
  715. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  716. package/build/dist/Server/Services/IncidentStateTimelineService.js +12 -0
  717. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  718. package/build/dist/Server/Services/IncidentTemplateService.js +68 -4
  719. package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
  720. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +1470 -0
  721. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -0
  722. package/build/dist/Server/Services/KubernetesClusterService.js +1097 -0
  723. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  724. package/build/dist/Server/Services/LogAggregationService.js +14 -6
  725. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  726. package/build/dist/Server/Services/MonitorService.js +487 -48
  727. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  728. package/build/dist/Server/Services/MonitorStatusTimelineService.js +540 -3
  729. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  730. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +2 -0
  731. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  732. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js +2 -0
  733. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js.map +1 -1
  734. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +2 -0
  735. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
  736. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +3 -0
  737. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  738. package/build/dist/Server/Services/OnCallNotificationAlertingService.js +29 -7
  739. package/build/dist/Server/Services/OnCallNotificationAlertingService.js.map +1 -1
  740. package/build/dist/Server/Services/OnCallSetupReminderService.js +1 -0
  741. package/build/dist/Server/Services/OnCallSetupReminderService.js.map +1 -1
  742. package/build/dist/Server/Services/ProbeService.js +7 -1
  743. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  744. package/build/dist/Server/Services/ProjectService.js +11 -1
  745. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  746. package/build/dist/Server/Services/RunbookCredentialService.js +24 -0
  747. package/build/dist/Server/Services/RunbookCredentialService.js.map +1 -1
  748. package/build/dist/Server/Services/RunbookSecretService.js +44 -0
  749. package/build/dist/Server/Services/RunbookSecretService.js.map +1 -1
  750. package/build/dist/Server/Services/RunnerJobService.js +641 -21
  751. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  752. package/build/dist/Server/Services/RunnerService.js +344 -5
  753. package/build/dist/Server/Services/RunnerService.js.map +1 -1
  754. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +48 -0
  755. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
  756. package/build/dist/Server/Services/ScheduledMaintenanceService.js +102 -20
  757. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  758. package/build/dist/Server/Services/ScheduledMaintenanceTemplateService.js +67 -4
  759. package/build/dist/Server/Services/ScheduledMaintenanceTemplateService.js.map +1 -1
  760. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +12 -1
  761. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  762. package/build/dist/Server/Services/StatusPagePrivateUserService.js +1 -0
  763. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  764. package/build/dist/Server/Services/StatusPageService.js +230 -32
  765. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  766. package/build/dist/Server/Services/StatusPageSubscriberService.js +4 -0
  767. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  768. package/build/dist/Server/Services/TeamMemberService.js +270 -11
  769. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  770. package/build/dist/Server/Services/TraceAggregationService.js +10 -6
  771. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  772. package/build/dist/Server/Services/UserEmailService.js +1 -0
  773. package/build/dist/Server/Services/UserEmailService.js.map +1 -1
  774. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +1 -0
  775. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
  776. package/build/dist/Server/Services/UserNotificationRuleAdminService.js +1 -0
  777. package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
  778. package/build/dist/Server/Services/UserNotificationRuleService.js +4 -0
  779. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  780. package/build/dist/Server/Services/UserService.js +107 -1
  781. package/build/dist/Server/Services/UserService.js.map +1 -1
  782. package/build/dist/Server/Services/WorkflowVariableService.js +396 -7
  783. package/build/dist/Server/Services/WorkflowVariableService.js.map +1 -1
  784. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +463 -7
  785. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  786. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +144 -2
  787. package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
  788. package/build/dist/Server/Types/Database/QueryHelper.js +20 -0
  789. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  790. package/build/dist/Server/Types/Markdown.js +394 -27
  791. package/build/dist/Server/Types/Markdown.js.map +1 -1
  792. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +123 -0
  793. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -0
  794. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +344 -0
  795. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -0
  796. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +587 -0
  797. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -0
  798. package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentAuth.js +18 -2
  799. package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentAuth.js.map +1 -1
  800. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +1380 -239
  801. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  802. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +1099 -68
  803. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  804. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +88 -6
  805. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  806. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +45 -11
  807. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
  808. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +41 -11
  809. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
  810. package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js +11 -4
  811. package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js.map +1 -1
  812. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +4 -2
  813. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
  814. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +997 -141
  815. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
  816. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js +299 -4
  817. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js.map +1 -1
  818. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js +120 -0
  819. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js.map +1 -0
  820. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +160 -1
  821. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  822. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +1 -0
  823. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
  824. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +59 -12
  825. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  826. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +5 -1
  827. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  828. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +84 -22
  829. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  830. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +70 -5
  831. package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -1
  832. package/build/dist/Server/Utils/Monitor/ProbeMonitorsNotification.js +249 -0
  833. package/build/dist/Server/Utils/Monitor/ProbeMonitorsNotification.js.map +1 -0
  834. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1 -0
  835. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -1
  836. package/build/dist/Server/Utils/PushNotificationUtil.js +27 -0
  837. package/build/dist/Server/Utils/PushNotificationUtil.js.map +1 -1
  838. package/build/dist/Server/Utils/Rules/OwnerRuleAssignment.js +26 -3
  839. package/build/dist/Server/Utils/Rules/OwnerRuleAssignment.js.map +1 -1
  840. package/build/dist/Server/Utils/Runbook/RunbookExecutePermission.js +38 -3
  841. package/build/dist/Server/Utils/Runbook/RunbookExecutePermission.js.map +1 -1
  842. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +143 -23
  843. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  844. package/build/dist/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.js +406 -0
  845. package/build/dist/Server/Utils/SessionReplay/SessionReplayUserFlowReadService.js.map +1 -0
  846. package/build/dist/Server/Utils/StartServer.js +4 -3
  847. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  848. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +382 -0
  849. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -0
  850. package/build/dist/Server/Utils/TwoFactorBackupCodeNotification.js +1 -0
  851. package/build/dist/Server/Utils/TwoFactorBackupCodeNotification.js.map +1 -1
  852. package/build/dist/Server/Utils/UserPermission/UserPermission.js +10 -0
  853. package/build/dist/Server/Utils/UserPermission/UserPermission.js.map +1 -1
  854. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +447 -0
  855. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -0
  856. package/build/dist/Server/Utils/Workflow/WorkflowVariableOAuthToken.js +202 -0
  857. package/build/dist/Server/Utils/Workflow/WorkflowVariableOAuthToken.js.map +1 -0
  858. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +39 -1
  859. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  860. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +44 -4
  861. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -1
  862. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +150 -191
  863. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  864. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +44 -4
  865. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
  866. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor.js +39 -17
  867. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Monitor.js.map +1 -1
  868. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js +151 -145
  869. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js.map +1 -1
  870. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +221 -127
  871. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  872. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsServiceUrl.js +91 -0
  873. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsServiceUrl.js.map +1 -0
  874. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/ReactionNoteSync.js +673 -0
  875. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/ReactionNoteSync.js.map +1 -0
  876. package/build/dist/Server/Utils/Workspace/Slack/Actions/ActionTypes.js +3 -9
  877. package/build/dist/Server/Utils/Workspace/Slack/Actions/ActionTypes.js.map +1 -1
  878. package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js +65 -166
  879. package/build/dist/Server/Utils/Workspace/Slack/Actions/Alert.js.map +1 -1
  880. package/build/dist/Server/Utils/Workspace/Slack/Actions/AlertEpisode.js +63 -157
  881. package/build/dist/Server/Utils/Workspace/Slack/Actions/AlertEpisode.js.map +1 -1
  882. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js +23 -3
  883. package/build/dist/Server/Utils/Workspace/Slack/Actions/Auth.js.map +1 -1
  884. package/build/dist/Server/Utils/Workspace/Slack/Actions/Authorization.js +92 -0
  885. package/build/dist/Server/Utils/Workspace/Slack/Actions/Authorization.js.map +1 -0
  886. package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js +81 -210
  887. package/build/dist/Server/Utils/Workspace/Slack/Actions/Incident.js.map +1 -1
  888. package/build/dist/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.js +68 -138
  889. package/build/dist/Server/Utils/Workspace/Slack/Actions/IncidentEpisode.js.map +1 -1
  890. package/build/dist/Server/Utils/Workspace/Slack/Actions/ReactionNote.js +240 -0
  891. package/build/dist/Server/Utils/Workspace/Slack/Actions/ReactionNote.js.map +1 -0
  892. package/build/dist/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.js +85 -212
  893. package/build/dist/Server/Utils/Workspace/Slack/Actions/ScheduledMaintenance.js.map +1 -1
  894. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +60 -17
  895. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  896. package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +2 -0
  897. package/build/dist/Server/Utils/Workspace/Workspace.js +134 -41
  898. package/build/dist/Server/Utils/Workspace/Workspace.js.map +1 -1
  899. package/build/dist/Server/Utils/Workspace/WorkspaceActionAuthorization.js +197 -0
  900. package/build/dist/Server/Utils/Workspace/WorkspaceActionAuthorization.js.map +1 -0
  901. package/build/dist/Server/Utils/Workspace/WorkspaceBase.js +32 -0
  902. package/build/dist/Server/Utils/Workspace/WorkspaceBase.js.map +1 -1
  903. package/build/dist/Server/Utils/Workspace/WorkspaceOAuthState.js +199 -0
  904. package/build/dist/Server/Utils/Workspace/WorkspaceOAuthState.js.map +1 -0
  905. package/build/dist/Server/Utils/Workspace/WorkspaceProjectReferenceValidator.js +91 -0
  906. package/build/dist/Server/Utils/Workspace/WorkspaceProjectReferenceValidator.js.map +1 -0
  907. package/build/dist/Server/Utils/Workspace/WorkspaceReactionNote.js +450 -0
  908. package/build/dist/Server/Utils/Workspace/WorkspaceReactionNote.js.map +1 -0
  909. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +103 -5
  910. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
  911. package/build/dist/Types/Date.js +97 -7
  912. package/build/dist/Types/Date.js.map +1 -1
  913. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  914. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  915. package/build/dist/Types/Exception/ExceptionMessages.js +1 -0
  916. package/build/dist/Types/Exception/ExceptionMessages.js.map +1 -1
  917. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +245 -0
  918. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -0
  919. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +49 -0
  920. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -0
  921. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessToolNames.js +16 -0
  922. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessToolNames.js.map +1 -0
  923. package/build/dist/Types/Monitor/DatabaseMonitor/SqlServerPlatform.js +104 -0
  924. package/build/dist/Types/Monitor/DatabaseMonitor/SqlServerPlatform.js.map +1 -0
  925. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +6 -5
  926. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
  927. package/build/dist/Types/Monitor/MonitorStepExceptionMonitor.js +32 -0
  928. package/build/dist/Types/Monitor/MonitorStepExceptionMonitor.js.map +1 -1
  929. package/build/dist/Types/Monitor/MonitorUptimeSummary.js +52 -0
  930. package/build/dist/Types/Monitor/MonitorUptimeSummary.js.map +1 -0
  931. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +9 -1
  932. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
  933. package/build/dist/Types/Rum/SessionReplay.js +52 -2
  934. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  935. package/build/dist/Types/Rum/SessionReplayApi.js +11 -0
  936. package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -1
  937. package/build/dist/Types/Rum/UserFlow.js +115 -0
  938. package/build/dist/Types/Rum/UserFlow.js.map +1 -0
  939. package/build/dist/Types/Runbook/RunbookStepType.js +10 -0
  940. package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
  941. package/build/dist/Types/Runbook/RunnerJobOrigin.js +14 -0
  942. package/build/dist/Types/Runbook/RunnerJobOrigin.js.map +1 -1
  943. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js +2 -2
  944. package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js.map +1 -1
  945. package/build/dist/Types/StatusPage/MergedDowntimeTotals.js +2 -0
  946. package/build/dist/Types/StatusPage/MergedDowntimeTotals.js.map +1 -0
  947. package/build/dist/Types/StatusPage/PublicNoteSubscriberNotificationDefault.js +65 -0
  948. package/build/dist/Types/StatusPage/PublicNoteSubscriberNotificationDefault.js.map +1 -0
  949. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +8 -0
  950. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
  951. package/build/dist/Types/Timezone.js +5 -0
  952. package/build/dist/Types/Timezone.js.map +1 -1
  953. package/build/dist/Types/TimezoneAlias.js +340 -0
  954. package/build/dist/Types/TimezoneAlias.js.map +1 -0
  955. package/build/dist/Types/Workflow/Templates.js +2694 -2
  956. package/build/dist/Types/Workflow/Templates.js.map +1 -1
  957. package/build/dist/Types/Workflow/WorkflowVariableOAuth.js +314 -0
  958. package/build/dist/Types/Workflow/WorkflowVariableOAuth.js.map +1 -0
  959. package/build/dist/Types/Workspace/WorkspaceNoteReaction.js +98 -0
  960. package/build/dist/Types/Workspace/WorkspaceNoteReaction.js.map +1 -0
  961. package/build/dist/UI/Components/AttributesJSON/AttributesJSONPreferences.js +95 -0
  962. package/build/dist/UI/Components/AttributesJSON/AttributesJSONPreferences.js.map +1 -0
  963. package/build/dist/UI/Components/AttributesJSON/AttributesJSONView.js +44 -0
  964. package/build/dist/UI/Components/AttributesJSON/AttributesJSONView.js.map +1 -0
  965. package/build/dist/UI/Components/AttributesJSON/AttributesViewToggle.js +27 -0
  966. package/build/dist/UI/Components/AttributesJSON/AttributesViewToggle.js.map +1 -0
  967. package/build/dist/UI/Components/AttributesJSON/BracesGlyph.js +9 -0
  968. package/build/dist/UI/Components/AttributesJSON/BracesGlyph.js.map +1 -0
  969. package/build/dist/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.js +260 -0
  970. package/build/dist/UI/Components/AttributesJSON/CopyAttributesAsJSONButton.js.map +1 -0
  971. package/build/dist/UI/Components/AttributesJSON/JSONHighlight.js +56 -0
  972. package/build/dist/UI/Components/AttributesJSON/JSONHighlight.js.map +1 -0
  973. package/build/dist/UI/Components/Calendar/Calendar.js +94 -7
  974. package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
  975. package/build/dist/UI/Components/Calendar/CalendarDayLayout.js +136 -0
  976. package/build/dist/UI/Components/Calendar/CalendarDayLayout.js.map +1 -0
  977. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +1 -1
  978. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  979. package/build/dist/UI/Components/Charts/Utils/HistogramSelection.js +72 -0
  980. package/build/dist/UI/Components/Charts/Utils/HistogramSelection.js.map +1 -0
  981. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +178 -0
  982. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -0
  983. package/build/dist/UI/Components/Charts/Utils/useHistogramZoom.js +13 -1
  984. package/build/dist/UI/Components/Charts/Utils/useHistogramZoom.js.map +1 -1
  985. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +20 -4
  986. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -1
  987. package/build/dist/UI/Components/Dropdown/Dropdown.js +33 -9
  988. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  989. package/build/dist/UI/Components/Forms/Fields/FormField.js +36 -13
  990. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  991. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +117 -11
  992. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  993. package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js +5 -1
  994. package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js.map +1 -1
  995. package/build/dist/UI/Components/Graphs/UptimeDaySummary.js.map +1 -1
  996. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +36 -8
  997. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  998. package/build/dist/UI/Components/LogsViewer/components/ColumnSelector.js +10 -2
  999. package/build/dist/UI/Components/LogsViewer/components/ColumnSelector.js.map +1 -1
  1000. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js +9 -1
  1001. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js.map +1 -1
  1002. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +16 -15
  1003. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
  1004. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +1 -1
  1005. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  1006. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +14 -74
  1007. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  1008. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +22 -5
  1009. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  1010. package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +9 -2
  1011. package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -1
  1012. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +16 -5
  1013. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -1
  1014. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +23 -8
  1015. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  1016. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +7 -1
  1017. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  1018. package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js +5 -1
  1019. package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js.map +1 -1
  1020. package/build/dist/UI/Components/Page/ModelPage.js +37 -3
  1021. package/build/dist/UI/Components/Page/ModelPage.js.map +1 -1
  1022. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +27 -6
  1023. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  1024. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js +7 -1
  1025. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js.map +1 -1
  1026. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +1 -1
  1027. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  1028. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +17 -77
  1029. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  1030. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +10 -1
  1031. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
  1032. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +15 -1
  1033. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -1
  1034. package/build/dist/UI/Utils/Timezone.js +52 -17
  1035. package/build/dist/UI/Utils/Timezone.js.map +1 -1
  1036. package/build/dist/Utils/AI/InvestigationReport.js +116 -21
  1037. package/build/dist/Utils/AI/InvestigationReport.js.map +1 -1
  1038. package/build/dist/Utils/AiRemediation/KubectlOutputRedactor.js +1209 -0
  1039. package/build/dist/Utils/AiRemediation/KubectlOutputRedactor.js.map +1 -0
  1040. package/build/dist/Utils/AiRemediation/KubectlPolicy.js +2973 -0
  1041. package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -0
  1042. package/build/dist/Utils/AiRemediation/KubectlWaitBudget.js +99 -0
  1043. package/build/dist/Utils/AiRemediation/KubectlWaitBudget.js.map +1 -0
  1044. package/build/dist/Utils/AiRemediation/KubectlWriteScope.js +1429 -0
  1045. package/build/dist/Utils/AiRemediation/KubectlWriteScope.js.map +1 -0
  1046. package/build/dist/Utils/Dashboard/Components/DashboardClockComponent.js +7 -6
  1047. package/build/dist/Utils/Dashboard/Components/DashboardClockComponent.js.map +1 -1
  1048. package/build/dist/Utils/Monitor/IncomingEmailMonitorAddress.js +214 -0
  1049. package/build/dist/Utils/Monitor/IncomingEmailMonitorAddress.js.map +1 -0
  1050. package/build/dist/Utils/Monitor/MonitorCheckScheduleUtil.js +471 -0
  1051. package/build/dist/Utils/Monitor/MonitorCheckScheduleUtil.js.map +1 -0
  1052. package/build/dist/Utils/Monitor/MonitorOverviewCriteriaUtil.js +107 -0
  1053. package/build/dist/Utils/Monitor/MonitorOverviewCriteriaUtil.js.map +1 -0
  1054. package/build/dist/Utils/Monitor/MonitorOverviewFamily.js +191 -0
  1055. package/build/dist/Utils/Monitor/MonitorOverviewFamily.js.map +1 -0
  1056. package/build/dist/Utils/Monitor/MonitorOverviewPresentationUtil.js +1259 -0
  1057. package/build/dist/Utils/Monitor/MonitorOverviewPresentationUtil.js.map +1 -0
  1058. package/build/dist/Utils/Monitor/MonitorOverviewProbeUtil.js +593 -0
  1059. package/build/dist/Utils/Monitor/MonitorOverviewProbeUtil.js.map +1 -0
  1060. package/build/dist/Utils/Monitor/MonitorOverviewTargetUtil.js +195 -0
  1061. package/build/dist/Utils/Monitor/MonitorOverviewTargetUtil.js.map +1 -0
  1062. package/build/dist/Utils/Monitor/MonitorStatusHistoryUtil.js +127 -0
  1063. package/build/dist/Utils/Monitor/MonitorStatusHistoryUtil.js.map +1 -0
  1064. package/build/dist/Utils/Monitor/MonitorUptimeSummaryUtil.js +464 -0
  1065. package/build/dist/Utils/Monitor/MonitorUptimeSummaryUtil.js.map +1 -0
  1066. package/build/dist/Utils/Rum/SessionIdentity.js +31 -1
  1067. package/build/dist/Utils/Rum/SessionIdentity.js.map +1 -1
  1068. package/build/dist/Utils/Rum/SessionReplayWireEncoding.js +128 -0
  1069. package/build/dist/Utils/Rum/SessionReplayWireEncoding.js.map +1 -0
  1070. package/build/dist/Utils/Rum/UserFlow.js +941 -0
  1071. package/build/dist/Utils/Rum/UserFlow.js.map +1 -0
  1072. package/build/dist/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.js +82 -0
  1073. package/build/dist/Utils/StatusPage/MonitorGroupMergedDowntimeUtil.js.map +1 -0
  1074. package/build/dist/Utils/StatusPage/ResourceUptime.js +46 -0
  1075. package/build/dist/Utils/StatusPage/ResourceUptime.js.map +1 -1
  1076. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js +148 -0
  1077. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js.map +1 -1
  1078. package/build/dist/Utils/Telemetry/AttributesJSON.js +245 -0
  1079. package/build/dist/Utils/Telemetry/AttributesJSON.js.map +1 -0
  1080. package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js +103 -0
  1081. package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js.map +1 -1
  1082. package/build/dist/Utils/Uptime/UptimeUtil.js +91 -27
  1083. package/build/dist/Utils/Uptime/UptimeUtil.js.map +1 -1
  1084. package/jest.config.json +2 -1
  1085. package/package.json +2 -1
  1086. package/tsconfig.tests.json +36 -0
  1087. package/Tests/UI/Components/EmptyState/EmptyStateGuideLinks.test.tsx +0 -337
  1088. package/UI/Components/EmptyState/EmptyStateGuideLinks.tsx +0 -124
  1089. package/build/dist/UI/Components/EmptyState/EmptyStateGuideLinks.js +0 -47
  1090. package/build/dist/UI/Components/EmptyState/EmptyStateGuideLinks.js.map +0 -1
@@ -0,0 +1,2688 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ jest,
8
+ test,
9
+ } from "@jest/globals";
10
+ import {
11
+ act,
12
+ cleanup,
13
+ fireEvent,
14
+ render,
15
+ screen,
16
+ waitFor,
17
+ within,
18
+ } from "@testing-library/react";
19
+ import React from "react";
20
+ import { MemoryRouter, Route as PageRoute, Routes } from "react-router-dom";
21
+ import KubernetesClusterAI, {
22
+ AI_ACCESS_STATUS_POLL_INTERVAL_MS,
23
+ KUBERNETES_AGENT_HELM_NAMESPACE,
24
+ KUBERNETES_AGENT_HELM_RELEASE,
25
+ REMEDIATION_MODE_LABELS,
26
+ canPickKubernetesCredential,
27
+ getAiAccessClusterWideCommandNote,
28
+ getAiAccessHelmCommands,
29
+ getAiAccessScopedCommandNote,
30
+ getKubernetesCredentialPermissionTitles,
31
+ getRemediationModeFieldDescription,
32
+ parseStatus,
33
+ } from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/AI";
34
+ import { getKubernetesInstallationMarkdown } from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/DocumentationMarkdown";
35
+ import PageMap from "../../../../App/FeatureSet/Dashboard/src/Utils/PageMap";
36
+ import RouteMap from "../../../../App/FeatureSet/Dashboard/src/Utils/RouteMap";
37
+ import KubernetesCluster from "../../../Models/DatabaseModels/KubernetesCluster";
38
+ import RunbookCredential from "../../../Models/DatabaseModels/RunbookCredential";
39
+ import Runner from "../../../Models/DatabaseModels/Runner";
40
+ import RunnerJob from "../../../Models/DatabaseModels/RunnerJob";
41
+ import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
42
+ import HTTPResponse from "../../../Types/API/HTTPResponse";
43
+ import Route from "../../../Types/API/Route";
44
+ import ListResult from "../../../Types/BaseDatabase/ListResult";
45
+ import { JSONObject } from "../../../Types/JSON";
46
+ import {
47
+ KubernetesAiAccessGap,
48
+ KubernetesAiRemediationMode,
49
+ KubernetesClusterAiAccessStatus,
50
+ } from "../../../Types/Kubernetes/KubernetesClusterAiAccess";
51
+ import ObjectID from "../../../Types/ObjectID";
52
+ import Permission from "../../../Types/Permission";
53
+ import RunbookCredentialType from "../../../Types/Runbook/RunbookCredentialType";
54
+ import RunbookStepType from "../../../Types/Runbook/RunbookStepType";
55
+ import RunnerJobOrigin from "../../../Types/Runbook/RunnerJobOrigin";
56
+ import RunnerJobStatus from "../../../Types/Runbook/RunnerJobStatus";
57
+ import API from "../../../UI/Utils/API/API";
58
+ import ModelAPI from "../../../UI/Utils/ModelAPI/ModelAPI";
59
+ import PermissionUtil from "../../../UI/Utils/Permission";
60
+ import User from "../../../UI/Utils/User";
61
+ import { goTo, PROJECT_ID } from "./SideMenuHarness";
62
+
63
+ jest.mock("react-i18next", () => {
64
+ return {
65
+ useTranslation: () => {
66
+ return {
67
+ t: (value: string): string => {
68
+ return value;
69
+ },
70
+ };
71
+ },
72
+ };
73
+ });
74
+
75
+ /*
76
+ * The cluster's AI page renders for real on its real route, with the
77
+ * status endpoint, the model API and the permission snapshot stubbed. The
78
+ * page is one screen made of pieces that each failed in review in a
79
+ * different way: the summary tile read a status field that does not exist,
80
+ * a background poll wiped the whole page, the commands table asked for the
81
+ * wrong columns (and lacked the ones ModelTable requires), the edit modal
82
+ * loaded Runner and credential lists its users were not allowed to read
83
+ * and let any cluster editor switch on unattended fixes, the helm command
84
+ * named another release, and the test button and the command history
85
+ * failed for roles that may open the page.
86
+ */
87
+
88
+ // Real components fetch; give the waits room on a loaded CI box.
89
+ const WAIT_TIMEOUT: number = 20000;
90
+
91
+ const CLUSTER_ID: ObjectID = new ObjectID(
92
+ "44444444-0000-4000-8000-000000000004",
93
+ );
94
+ const RUNNER_ID: string = "55555555-0000-4000-8000-000000000005";
95
+ const RUN_ID: string = "66666666-0000-4000-8000-000000000006";
96
+ const CREDENTIAL_ID: string = "77777777-0000-4000-8000-000000000007";
97
+ const AI_PAGE_PATH: string = `/dashboard/${PROJECT_ID}/kubernetes/${CLUSTER_ID.toString()}/ai`;
98
+
99
+ const INVESTIGATION_ON_TEXT: string =
100
+ "AI runs read-only kubectl (get, describe, logs, events, top) while investigating.";
101
+ const INVESTIGATION_OFF_TEXT: string =
102
+ "Off — AI investigates with OneUptime data only.";
103
+ const STATUS_CARD_TITLE: string = "OneUptime AI access to this cluster";
104
+ const SETTINGS_CARD_TITLE: string = "AI access settings";
105
+
106
+ const BASE_PERMISSIONS: Array<Permission> = [
107
+ Permission.Public,
108
+ Permission.User,
109
+ Permission.CurrentUser,
110
+ ];
111
+
112
+ const MEMBER_PERMISSIONS: Array<Permission> = [
113
+ ...BASE_PERMISSIONS,
114
+ Permission.ProjectMember,
115
+ ];
116
+
117
+ // May loosen AI access (unattended modes, allowlist, bindings) and read everything.
118
+ const ADMIN_PERMISSIONS: Array<Permission> = [
119
+ ...BASE_PERMISSIONS,
120
+ Permission.ProjectAdmin,
121
+ ];
122
+
123
+ const SET_IMAGE_PATTERN: string = "kubectl set image deployment/web * -n web";
124
+ const PATCH_PATTERN: string = "kubectl patch deployment/web -n web -p *";
125
+ /*
126
+ * A broad but valid allowlist entry: deleting any Deployment in any
127
+ * namespace. These tests used "kubectl * * * -n *" until KubectlPolicy made
128
+ * a wildcard verb invalid; the form now refuses that entry outright.
129
+ */
130
+ const BROAD_PATTERN: string = "kubectl delete deployment * -n *";
131
+ const OTHER_CLUSTER_RUNNER_ID: string = "55555555-0000-4000-8000-00000000000e";
132
+ const HOST_RUNNER_ID: string = "55555555-0000-4000-8000-00000000000f";
133
+ const DISABLED_RUNNER_ID: string = "55555555-0000-4000-8000-000000000010";
134
+ const RENAMED_AGENT_RUNNER_ID: string = "55555555-0000-4000-8000-000000000011";
135
+ const SSH_CREDENTIAL_ID: string = "77777777-0000-4000-8000-000000000008";
136
+ const STAGING_CREDENTIAL_ID: string = "77777777-0000-4000-8000-000000000009";
137
+
138
+ function makeStatus(
139
+ overrides: Partial<KubernetesClusterAiAccessStatus> = {},
140
+ ): KubernetesClusterAiAccessStatus {
141
+ return {
142
+ clusterId: CLUSTER_ID.toString(),
143
+ clusterName: "prod-east",
144
+ clusterIdentifier: "prod-east",
145
+ runner: {
146
+ id: RUNNER_ID,
147
+ name: "kubernetes-agent/prod-east",
148
+ isOnline: true,
149
+ lastAliveAt: "2026-09-22T10:00:00.000Z",
150
+ canRunAiCommands: true,
151
+ posture: {
152
+ inCluster: true,
153
+ allowWrites: false,
154
+ kubectlVersion: "v1.31.0",
155
+ },
156
+ },
157
+ accessMethod: "in_cluster",
158
+ kubectlAllowlist: [],
159
+ isInvestigationEnabled: true,
160
+ isInvestigationReady: true,
161
+ remediationMode: KubernetesAiRemediationMode.RequireApproval,
162
+ isRemediationReady: true,
163
+ gaps: [],
164
+ evaluatedAt: "2026-09-22T10:00:30.000Z",
165
+ ...overrides,
166
+ };
167
+ }
168
+
169
+ function statusResponse(
170
+ status: KubernetesClusterAiAccessStatus,
171
+ ): HTTPResponse<JSONObject> {
172
+ return new HTTPResponse<JSONObject>(200, status as unknown as JSONObject, {});
173
+ }
174
+
175
+ type ClusterOverrides = {
176
+ [Key in keyof KubernetesCluster]?: KubernetesCluster[Key] | unknown;
177
+ };
178
+
179
+ function makeCluster(overrides: ClusterOverrides = {}): KubernetesCluster {
180
+ const runner: Runner = Object.assign(new Runner(), {
181
+ _id: RUNNER_ID,
182
+ name: "kubernetes-agent/prod-east",
183
+ });
184
+ return Object.assign(
185
+ new KubernetesCluster(),
186
+ {
187
+ _id: CLUSTER_ID.toString(),
188
+ isAiInvestigationEnabled: true,
189
+ aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
190
+ aiKubectlCommandAllowlist: [],
191
+ aiAccessRunner: runner,
192
+ },
193
+ overrides,
194
+ );
195
+ }
196
+
197
+ // The posture an in-cluster agent Runner reports for the cluster it runs in.
198
+ function agentHostInfo(clusterIdentifier: string): JSONObject {
199
+ return {
200
+ kubernetes: { inCluster: true, allowWrites: false, clusterIdentifier },
201
+ };
202
+ }
203
+
204
+ /*
205
+ * Every kind of Runner a project holds: this cluster's agent, another
206
+ * cluster's agent, a host Runner that runs AI commands, one that does not,
207
+ * and another cluster's agent whose name lost the "kubernetes-agent/"
208
+ * marker (a rename from before the server refused one) — an agent by its
209
+ * posture, the server's rule. Only the first and third can serve this
210
+ * cluster. The agents report the cluster they run in, which is how the
211
+ * picker tells them apart.
212
+ */
213
+ function makeProjectRunners(): Array<Runner> {
214
+ return [
215
+ Object.assign(new Runner(), {
216
+ _id: RUNNER_ID,
217
+ name: "kubernetes-agent/prod-east",
218
+ canRunAiCommands: true,
219
+ hostInfo: agentHostInfo("prod-east"),
220
+ }),
221
+ Object.assign(new Runner(), {
222
+ _id: OTHER_CLUSTER_RUNNER_ID,
223
+ name: "kubernetes-agent/prod-eu",
224
+ canRunAiCommands: true,
225
+ hostInfo: agentHostInfo("prod-eu"),
226
+ }),
227
+ Object.assign(new Runner(), {
228
+ _id: HOST_RUNNER_ID,
229
+ name: "bash-runner",
230
+ canRunAiCommands: true,
231
+ }),
232
+ Object.assign(new Runner(), {
233
+ _id: DISABLED_RUNNER_ID,
234
+ name: "ops-runner",
235
+ canRunAiCommands: false,
236
+ }),
237
+ Object.assign(new Runner(), {
238
+ _id: RENAMED_AGENT_RUNNER_ID,
239
+ name: "prod-eu-kubectl",
240
+ canRunAiCommands: true,
241
+ hostInfo: agentHostInfo("prod-eu"),
242
+ }),
243
+ ];
244
+ }
245
+
246
+ function assignedTo(...runnerIds: Array<string>): Array<Runner> {
247
+ return runnerIds.map((runnerId: string): Runner => {
248
+ return Object.assign(new Runner(), { _id: runnerId });
249
+ });
250
+ }
251
+
252
+ /*
253
+ * A Kubernetes credential assigned to the host Runner, one assigned to a
254
+ * Runner that is not listed, and an SSH key.
255
+ */
256
+ function makeProjectCredentials(): Array<RunbookCredential> {
257
+ return [
258
+ Object.assign(new RunbookCredential(), {
259
+ _id: CREDENTIAL_ID,
260
+ name: "prod-east token",
261
+ credentialType: RunbookCredentialType.Kubernetes,
262
+ runners: assignedTo(HOST_RUNNER_ID),
263
+ }),
264
+ Object.assign(new RunbookCredential(), {
265
+ _id: STAGING_CREDENTIAL_ID,
266
+ name: "staging token",
267
+ credentialType: RunbookCredentialType.Kubernetes,
268
+ runners: assignedTo(DISABLED_RUNNER_ID),
269
+ }),
270
+ Object.assign(new RunbookCredential(), {
271
+ _id: SSH_CREDENTIAL_ID,
272
+ name: "bastion ssh key",
273
+ credentialType: RunbookCredentialType.SSH,
274
+ runners: assignedTo(HOST_RUNNER_ID),
275
+ }),
276
+ ];
277
+ }
278
+
279
+ // The cluster bound to the host Runner (outside the cluster).
280
+ function hostRunnerBinding(): ClusterOverrides {
281
+ return {
282
+ aiAccessRunner: Object.assign(new Runner(), {
283
+ _id: HOST_RUNNER_ID,
284
+ name: "bash-runner",
285
+ }),
286
+ };
287
+ }
288
+
289
+ type JobOverrides = {
290
+ [Key in keyof RunnerJob]?: RunnerJob[Key] | undefined;
291
+ };
292
+
293
+ function makeJob(id: string, overrides: JobOverrides = {}): RunnerJob {
294
+ return Object.assign(
295
+ new RunnerJob(),
296
+ {
297
+ _id: id,
298
+ createdAt: new Date("2026-09-22T09:59:00Z"),
299
+ origin: RunnerJobOrigin.AiInvestigation,
300
+ stepType: RunbookStepType.Kubectl,
301
+ status: RunnerJobStatus.Succeeded,
302
+ payload: { displayCommand: `kubectl get pods -n web-${id}` },
303
+ exitCode: 0,
304
+ },
305
+ overrides,
306
+ );
307
+ }
308
+
309
+ interface ListRequest {
310
+ modelType?: unknown;
311
+ query?: Record<string, unknown> | undefined;
312
+ select?: Record<string, unknown> | undefined;
313
+ }
314
+
315
+ let postSpy: ReturnType<typeof jest.spyOn>;
316
+ let getListSpy: ReturnType<typeof jest.spyOn>;
317
+ let getItemSpy: ReturnType<typeof jest.spyOn>;
318
+ let updateByIdSpy: ReturnType<typeof jest.spyOn>;
319
+ let jobs: Array<RunnerJob> = [];
320
+
321
+ function serveStatus(status: KubernetesClusterAiAccessStatus): void {
322
+ postSpy.mockImplementation(async (): Promise<HTTPResponse<JSONObject>> => {
323
+ return statusResponse(status);
324
+ });
325
+ }
326
+
327
+ /*
328
+ * A status request that fails the way a transient 502 or a dropped
329
+ * connection does. Thrown from inside the async body rather than returned
330
+ * as a pre-built rejected promise: zone.js (pulled in by the telemetry util
331
+ * the page imports) reports the latter as an unhandled rejection in the
332
+ * microtask between construction and the await that catches it, which
333
+ * fills a passing run with alarming console noise.
334
+ */
335
+ async function failStatusRequest(): Promise<HTTPResponse<JSONObject>> {
336
+ throw new Error("Network Error");
337
+ }
338
+
339
+ function listRequests(): Array<ListRequest> {
340
+ return getListSpy.mock.calls.map((call: Array<unknown>): ListRequest => {
341
+ return (call[0] || {}) as ListRequest;
342
+ });
343
+ }
344
+
345
+ function jobListRequest(): ListRequest | undefined {
346
+ return listRequests().find((request: ListRequest): boolean => {
347
+ return request.modelType === RunnerJob;
348
+ });
349
+ }
350
+
351
+ function listRequestsFor(modelType: unknown): Array<ListRequest> {
352
+ return listRequests().filter((request: ListRequest): boolean => {
353
+ return request.modelType === modelType;
354
+ });
355
+ }
356
+
357
+ // The requests the page sent to one of its two custom routes.
358
+ function postsTo(route: string): Array<JSONObject> {
359
+ return postSpy.mock.calls
360
+ .map((call: Array<unknown>): JSONObject => {
361
+ return (call[0] || {}) as JSONObject;
362
+ })
363
+ .filter((request: JSONObject): boolean => {
364
+ return String(request["url"]).endsWith(route);
365
+ });
366
+ }
367
+
368
+ // Answers the status route with `status` and the test route with `test`.
369
+ function serveStatusAndTest(
370
+ status: KubernetesClusterAiAccessStatus,
371
+ test: () => Promise<HTTPResponse<JSONObject> | HTTPErrorResponse>,
372
+ ): void {
373
+ postSpy.mockImplementation(
374
+ async (
375
+ request: unknown,
376
+ ): Promise<HTTPResponse<JSONObject> | HTTPErrorResponse> => {
377
+ const url: string = String((request as JSONObject)["url"]);
378
+ if (url.endsWith("/kubernetes-cluster/ai-access/test")) {
379
+ return await test();
380
+ }
381
+ return statusResponse(status);
382
+ },
383
+ );
384
+ }
385
+
386
+ function updateRequests(): Array<JSONObject> {
387
+ return updateByIdSpy.mock.calls.map((call: Array<unknown>): JSONObject => {
388
+ return (call[0] || {}) as JSONObject;
389
+ });
390
+ }
391
+
392
+ function textOf(testId: string): string {
393
+ return screen.getByTestId(testId).textContent || "";
394
+ }
395
+
396
+ function grant(permissions: Array<Permission>): void {
397
+ jest.spyOn(User, "isMasterAdmin").mockReturnValue(false);
398
+ jest.spyOn(PermissionUtil, "getAllPermissions").mockReturnValue(permissions);
399
+ jest.spyOn(PermissionUtil, "getGlobalPermissions").mockReturnValue(null);
400
+ jest.spyOn(PermissionUtil, "getProjectPermissions").mockReturnValue({
401
+ projectId: new ObjectID(PROJECT_ID),
402
+ userId: ObjectID.generate(),
403
+ permissions: permissions.map((permission: Permission) => {
404
+ return {
405
+ permission: permission,
406
+ labelIds: [],
407
+ _type: "UserPermission",
408
+ };
409
+ }),
410
+ _type: "UserTenantAccessPermission",
411
+ } as unknown as ReturnType<typeof PermissionUtil.getProjectPermissions>);
412
+ }
413
+
414
+ function openAiPage(): void {
415
+ goTo(AI_PAGE_PATH);
416
+
417
+ render(
418
+ <MemoryRouter initialEntries={[AI_PAGE_PATH]}>
419
+ <Routes>
420
+ <PageRoute
421
+ path={String(RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_AI])}
422
+ element={
423
+ <KubernetesClusterAI
424
+ pageRoute={RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_AI] as Route}
425
+ currentProject={null}
426
+ hasPaymentMethod={true}
427
+ />
428
+ }
429
+ />
430
+ </Routes>
431
+ </MemoryRouter>,
432
+ );
433
+ }
434
+
435
+ async function findText(text: string | RegExp): Promise<HTMLElement> {
436
+ return await screen.findByText(text, {}, { timeout: WAIT_TIMEOUT });
437
+ }
438
+
439
+ async function openEditModal(): Promise<HTMLElement> {
440
+ await findText(SETTINGS_CARD_TITLE);
441
+ fireEvent.click(await findText("Edit AI access"));
442
+ const dialog: HTMLElement = await screen.findByRole(
443
+ "dialog",
444
+ {},
445
+ { timeout: WAIT_TIMEOUT },
446
+ );
447
+ // The form is on screen once its first field label is.
448
+ await within(dialog).findByText(
449
+ "Let AI investigate with kubectl",
450
+ {},
451
+ { timeout: WAIT_TIMEOUT },
452
+ );
453
+ return dialog;
454
+ }
455
+
456
+ // Serves the settings the edit modal reads when it opens.
457
+ function serveCluster(overrides: ClusterOverrides = {}): void {
458
+ getItemSpy.mockImplementation(async (): Promise<KubernetesCluster> => {
459
+ return makeCluster(overrides);
460
+ });
461
+ }
462
+
463
+ /*
464
+ * The open react-select menu's options. Native <option>s — the commands
465
+ * table's page-size picker — also have the option role and are left out.
466
+ */
467
+ function menuOptions(): Array<HTMLElement> {
468
+ return screen
469
+ .queryAllByRole("option")
470
+ .filter((option: HTMLElement): boolean => {
471
+ return option.tagName !== "OPTION";
472
+ });
473
+ }
474
+
475
+ // Opens a react-select dropdown in the dialog and returns its option texts.
476
+ async function openDropdown(
477
+ dialog: HTMLElement,
478
+ name: RegExp,
479
+ ): Promise<Array<string>> {
480
+ const combobox: HTMLElement = within(dialog).getByRole("combobox", {
481
+ name,
482
+ });
483
+ fireEvent.keyDown(combobox, { key: "ArrowDown", code: "ArrowDown" });
484
+ await waitFor(
485
+ () => {
486
+ expect(menuOptions().length).toBeGreaterThan(0);
487
+ },
488
+ { timeout: WAIT_TIMEOUT },
489
+ );
490
+ return menuOptions().map((option: HTMLElement): string => {
491
+ return option.textContent || "";
492
+ });
493
+ }
494
+
495
+ async function pickOption(
496
+ dialog: HTMLElement,
497
+ name: RegExp,
498
+ optionText: string,
499
+ ): Promise<void> {
500
+ await openDropdown(dialog, name);
501
+ const option: HTMLElement | undefined = menuOptions().find(
502
+ (candidate: HTMLElement): boolean => {
503
+ return candidate.textContent === optionText;
504
+ },
505
+ );
506
+ if (!option) {
507
+ throw new Error(`No option "${optionText}" in the ${name} dropdown.`);
508
+ }
509
+ fireEvent.click(option);
510
+ }
511
+
512
+ /*
513
+ * Clicks the investigation switch once the form has been seeded with the
514
+ * saved value. The Toggle picks up its value in an effect after its first
515
+ * paint, so a click in that instant would toggle the unseeded default.
516
+ */
517
+ async function toggleSwitch(
518
+ dialog: HTMLElement,
519
+ testId: string,
520
+ from: boolean,
521
+ ): Promise<void> {
522
+ const toggle: HTMLElement = within(dialog).getByTestId(testId);
523
+ await waitFor(
524
+ () => {
525
+ expect(toggle).toHaveAttribute("aria-checked", String(from));
526
+ },
527
+ { timeout: WAIT_TIMEOUT },
528
+ );
529
+ fireEvent.click(toggle);
530
+ expect(toggle).toHaveAttribute("aria-checked", String(!from));
531
+ }
532
+
533
+ /*
534
+ * By its test id: the modal can also hold "Unbind the Runner" and "Unbind
535
+ * the Kubernetes credential" switches.
536
+ */
537
+ async function toggleInvestigation(
538
+ dialog: HTMLElement,
539
+ from: boolean,
540
+ ): Promise<void> {
541
+ await toggleSwitch(dialog, "ai-investigation-field", from);
542
+ }
543
+
544
+ async function setAllowlistText(
545
+ dialog: HTMLElement,
546
+ value: string,
547
+ ): Promise<void> {
548
+ const field: HTMLElement = await within(dialog).findByTestId(
549
+ "kubectl-allowlist-field",
550
+ {},
551
+ { timeout: WAIT_TIMEOUT },
552
+ );
553
+ fireEvent.change(field, { target: { value } });
554
+ }
555
+
556
+ async function waitForOneUpdate(): Promise<JSONObject> {
557
+ await waitFor(
558
+ () => {
559
+ expect(updateByIdSpy).toHaveBeenCalledTimes(1);
560
+ },
561
+ { timeout: WAIT_TIMEOUT },
562
+ );
563
+ return updateRequests()[0]!["data"] as JSONObject;
564
+ }
565
+
566
+ function saveEditModal(dialog: HTMLElement): void {
567
+ fireEvent.click(within(dialog).getByTestId("modal-footer-submit-button"));
568
+ }
569
+
570
+ async function findDialogTitled(title: string): Promise<HTMLElement> {
571
+ const heading: HTMLElement = await findText(title);
572
+ const dialog: HTMLElement | null = heading.closest('[role="dialog"]');
573
+ if (!dialog) {
574
+ throw new Error(`"${title}" is not inside a dialog.`);
575
+ }
576
+ return dialog;
577
+ }
578
+
579
+ beforeEach(() => {
580
+ window.localStorage.clear();
581
+ window.sessionStorage.clear();
582
+ grant(MEMBER_PERMISSIONS);
583
+
584
+ postSpy = jest.spyOn(API, "post");
585
+ serveStatus(makeStatus());
586
+
587
+ jobs = [];
588
+ getListSpy = jest.spyOn(ModelAPI, "getList");
589
+ getListSpy.mockImplementation(
590
+ async (args: unknown): Promise<ListResult<RunnerJob>> => {
591
+ const request: ListRequest = args as ListRequest;
592
+ if (request.modelType === RunnerJob) {
593
+ return { data: jobs, count: jobs.length, skip: 0, limit: 10 };
594
+ }
595
+ if (request.modelType === RunbookCredential) {
596
+ const credentials: Array<RunbookCredential> = makeProjectCredentials();
597
+ return {
598
+ data: credentials as unknown as Array<RunnerJob>,
599
+ count: credentials.length,
600
+ skip: 0,
601
+ limit: 10,
602
+ };
603
+ }
604
+ if (request.modelType === Runner) {
605
+ const runners: Array<Runner> = makeProjectRunners();
606
+ return {
607
+ data: runners as unknown as Array<RunnerJob>,
608
+ count: runners.length,
609
+ skip: 0,
610
+ limit: 10,
611
+ };
612
+ }
613
+ return { data: [], count: 0, skip: 0, limit: 10 };
614
+ },
615
+ );
616
+
617
+ getItemSpy = jest.spyOn(ModelAPI, "getItem");
618
+ serveCluster();
619
+ updateByIdSpy = jest.spyOn(ModelAPI, "updateById");
620
+ updateByIdSpy.mockImplementation(
621
+ async (): Promise<HTTPResponse<JSONObject>> => {
622
+ return new HTTPResponse<JSONObject>(200, {}, {});
623
+ },
624
+ );
625
+ jest.spyOn(ModelAPI, "getCommonHeaders").mockReturnValue({});
626
+ });
627
+
628
+ afterEach(() => {
629
+ cleanup();
630
+ jest.useRealTimers();
631
+ jest.restoreAllMocks();
632
+ });
633
+
634
+ describe("parseStatus", () => {
635
+ test("accepts the status shape the API returns and nothing looser", () => {
636
+ expect(parseStatus(makeStatus())).toEqual(makeStatus());
637
+ expect(parseStatus(null)).toBeNull();
638
+ expect(parseStatus("ready")).toBeNull();
639
+ expect(parseStatus([makeStatus()])).toBeNull();
640
+ expect(parseStatus({ ...makeStatus(), clusterId: 7 })).toBeNull();
641
+ expect(parseStatus({ ...makeStatus(), gaps: null })).toBeNull();
642
+ });
643
+ });
644
+
645
+ describe("remediation mode labels", () => {
646
+ test("give every mode its own plain-words label", () => {
647
+ const modes: Array<KubernetesAiRemediationMode> = Object.values(
648
+ KubernetesAiRemediationMode,
649
+ );
650
+ const labels: Array<string> = modes.map(
651
+ (mode: KubernetesAiRemediationMode): string => {
652
+ return REMEDIATION_MODE_LABELS[mode];
653
+ },
654
+ );
655
+
656
+ expect(modes.length).toBeGreaterThanOrEqual(4);
657
+ for (const label of labels) {
658
+ expect(typeof label).toBe("string");
659
+ expect(label.trim().length).toBeGreaterThan(0);
660
+ }
661
+ expect(new Set(labels).size).toBe(labels.length);
662
+ // Both unattended modes say fixes run on their own.
663
+ expect(
664
+ REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.Automatic],
665
+ ).toMatch(/run on their own/);
666
+ expect(
667
+ REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.BypassApproval],
668
+ ).toMatch(/runs on its own/);
669
+ expect(
670
+ REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.Disabled],
671
+ ).toMatch(/Off/);
672
+ });
673
+
674
+ /*
675
+ * The copy must match the canonical description on
676
+ * KubernetesAiRemediationMode (and what RemediationExecutionRunner does):
677
+ * an Automatic round that could only find riskier fixes ends by proposing
678
+ * them for one-click approval. The page said they were "left for you"
679
+ * and that "only a follow-up round" proposed them. This pin replaces the
680
+ * old one, which required "left for you".
681
+ */
682
+ test("Automatic says a riskier fix is proposed for one-click approval", () => {
683
+ const label: string =
684
+ REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.Automatic];
685
+ expect(label).toMatch(/one-click approval/);
686
+ expect(label).not.toMatch(/left for you/);
687
+
688
+ const description: string = getRemediationModeFieldDescription();
689
+ expect(description).toMatch(
690
+ /when the round could only find riskier fixes, it ends by proposing exactly those for one-click approval/,
691
+ );
692
+ expect(description).not.toMatch(/only a follow-up round/);
693
+ expect(description).not.toMatch(
694
+ /leaves the exact command in its recommendations/,
695
+ );
696
+ // A named Job is not recreated by anything: deleting one is riskier.
697
+ expect(description).not.toMatch(/delete a named pod or job/);
698
+ expect(description).toContain("delete a named pod");
699
+ });
700
+
701
+ /*
702
+ * Bypass approval does ask in a few places: protected namespaces, node
703
+ * drains and taints, and a tripped circuit breaker. "Nobody is asked"
704
+ * was false.
705
+ */
706
+ test("Bypass approval names what still asks a human", () => {
707
+ const label: string =
708
+ REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.BypassApproval];
709
+ expect(label).not.toMatch(/nobody is asked/);
710
+ for (const exception of [
711
+ "protected namespaces",
712
+ "drain",
713
+ "circuit breaker",
714
+ ]) {
715
+ expect(label).toContain(exception);
716
+ }
717
+
718
+ const description: string = getRemediationModeFieldDescription();
719
+ expect(description).not.toMatch(/nobody is ever asked/);
720
+ for (const namespace of ["kube-system", "kube-public", "kube-node-lease"]) {
721
+ expect(description).toContain(namespace);
722
+ }
723
+ /*
724
+ * The canonical comment's words ("a node drain and a node taint"),
725
+ * plus a patch of a node, which round four holds to the same rule:
726
+ * a taint written as `kubectl patch node` is the same change.
727
+ */
728
+ expect(description).toMatch(
729
+ /a node drain, a node taint and a patch of a node always need a human/,
730
+ );
731
+ expect(description).toMatch(/circuit breaker/);
732
+ expect(description).toMatch(/never changes its own namespace/);
733
+ // Bypass approval's other exception: another unattended round.
734
+ expect(description).toMatch(
735
+ /another unattended round already holds the cluster/,
736
+ );
737
+ expect(label).toContain("another unattended round");
738
+ });
739
+ });
740
+
741
+ describe("credential picker permission", () => {
742
+ test("is offered only to users who may read Runner credentials", () => {
743
+ expect(canPickKubernetesCredential()).toBe(false);
744
+
745
+ grant([...MEMBER_PERMISSIONS, Permission.ReadRunbookCredential]);
746
+ expect(canPickKubernetesCredential()).toBe(true);
747
+
748
+ grant([Permission.Public, Permission.User, Permission.ProjectAdmin]);
749
+ expect(canPickKubernetesCredential()).toBe(true);
750
+ });
751
+
752
+ test("is always offered to a master admin", () => {
753
+ jest.spyOn(User, "isMasterAdmin").mockReturnValue(true);
754
+ expect(canPickKubernetesCredential()).toBe(true);
755
+ });
756
+
757
+ test("is withheld while the permission snapshot has not landed", () => {
758
+ grant([]);
759
+ expect(canPickKubernetesCredential()).toBe(false);
760
+ });
761
+
762
+ test("names the permissions that would unlock it", () => {
763
+ const titles: Array<string> = getKubernetesCredentialPermissionTitles();
764
+ expect(titles.length).toBeGreaterThan(0);
765
+ expect(titles.join(", ")).toMatch(/Runbook Credential/);
766
+ });
767
+ });
768
+
769
+ describe("Kubernetes cluster AI page", () => {
770
+ describe("investigation tile", () => {
771
+ test("says investigation is on when the status says it is", async () => {
772
+ serveStatus(makeStatus({ isInvestigationEnabled: true }));
773
+ openAiPage();
774
+
775
+ expect(await findText(INVESTIGATION_ON_TEXT)).toBeInTheDocument();
776
+ expect(
777
+ screen.queryByText(INVESTIGATION_OFF_TEXT),
778
+ ).not.toBeInTheDocument();
779
+ expect(screen.getByText("Investigation: ready")).toBeInTheDocument();
780
+ });
781
+
782
+ test("says investigation is off when the status says it is", async () => {
783
+ serveStatus(
784
+ makeStatus({
785
+ isInvestigationEnabled: false,
786
+ isInvestigationReady: false,
787
+ }),
788
+ );
789
+ openAiPage();
790
+
791
+ expect(await findText(INVESTIGATION_OFF_TEXT)).toBeInTheDocument();
792
+ expect(screen.queryByText(INVESTIGATION_ON_TEXT)).not.toBeInTheDocument();
793
+ expect(screen.getByText("Investigation: off")).toBeInTheDocument();
794
+ });
795
+
796
+ test("shows the remediation mode in the same words as the settings dropdown", async () => {
797
+ serveStatus(
798
+ makeStatus({
799
+ remediationMode: KubernetesAiRemediationMode.BypassApproval,
800
+ }),
801
+ );
802
+ openAiPage();
803
+
804
+ expect(
805
+ await findText("Remediation: bypass approval"),
806
+ ).toBeInTheDocument();
807
+ expect(
808
+ screen.getAllByText(
809
+ REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.BypassApproval],
810
+ ).length,
811
+ ).toBeGreaterThan(0);
812
+ });
813
+ });
814
+
815
+ describe("status polling", () => {
816
+ /*
817
+ * The page read its cluster id as a fresh ObjectID on every render, so
818
+ * the memoized fetch was recreated each render and the load effect
819
+ * re-ran after every answer: an unbounded loop of status requests.
820
+ */
821
+ test("requests the status once on load, not once per render", async () => {
822
+ openAiPage();
823
+
824
+ expect(await findText(STATUS_CARD_TITLE)).toBeInTheDocument();
825
+ expect(await findText(SETTINGS_CARD_TITLE)).toBeInTheDocument();
826
+ await findText(
827
+ "OneUptime AI has not run any kubectl commands on this cluster yet.",
828
+ );
829
+
830
+ expect(postSpy).toHaveBeenCalledTimes(1);
831
+ });
832
+
833
+ test("shows the error page only when the first load fails", async () => {
834
+ postSpy.mockImplementation(failStatusRequest);
835
+ openAiPage();
836
+
837
+ expect(await findText("Network Error")).toBeInTheDocument();
838
+ expect(screen.queryByText(STATUS_CARD_TITLE)).not.toBeInTheDocument();
839
+ expect(
840
+ screen.queryByTestId("ai-access-refresh-warning"),
841
+ ).not.toBeInTheDocument();
842
+ });
843
+
844
+ /*
845
+ * The finding: an operator mid-edit lost the modal and the whole page to
846
+ * a transient 502 on the 30 s poll. The last good status must stay on
847
+ * screen, with a warning that says so, until a later poll succeeds.
848
+ */
849
+ test("keeps the last good status and warns inline when a background poll fails", async () => {
850
+ jest.useFakeTimers();
851
+ postSpy
852
+ .mockResolvedValueOnce(statusResponse(makeStatus()))
853
+ .mockImplementationOnce(failStatusRequest)
854
+ .mockResolvedValue(statusResponse(makeStatus()));
855
+
856
+ openAiPage();
857
+ expect(await findText(STATUS_CARD_TITLE)).toBeInTheDocument();
858
+ expect(await findText(SETTINGS_CARD_TITLE)).toBeInTheDocument();
859
+
860
+ await act(async () => {
861
+ jest.advanceTimersByTime(AI_ACCESS_STATUS_POLL_INTERVAL_MS);
862
+ });
863
+
864
+ const warning: HTMLElement = await screen.findByTestId(
865
+ "ai-access-refresh-warning",
866
+ {},
867
+ { timeout: WAIT_TIMEOUT },
868
+ );
869
+ expect(warning).toHaveTextContent(
870
+ "Could not refresh the AI access status",
871
+ );
872
+ expect(warning).toHaveTextContent("Network Error");
873
+ expect(warning).toHaveTextContent("Showing the last status from");
874
+ expect(screen.getByText(STATUS_CARD_TITLE)).toBeInTheDocument();
875
+ expect(screen.getByText(SETTINGS_CARD_TITLE)).toBeInTheDocument();
876
+ expect(screen.getByText(INVESTIGATION_ON_TEXT)).toBeInTheDocument();
877
+ expect(postSpy).toHaveBeenCalledTimes(2);
878
+
879
+ await act(async () => {
880
+ jest.advanceTimersByTime(AI_ACCESS_STATUS_POLL_INTERVAL_MS);
881
+ });
882
+
883
+ await waitFor(
884
+ () => {
885
+ expect(
886
+ screen.queryByTestId("ai-access-refresh-warning"),
887
+ ).not.toBeInTheDocument();
888
+ },
889
+ { timeout: WAIT_TIMEOUT },
890
+ );
891
+ expect(screen.getByText(STATUS_CARD_TITLE)).toBeInTheDocument();
892
+ expect(postSpy).toHaveBeenCalledTimes(3);
893
+ });
894
+
895
+ /*
896
+ * The exact scene from the review: the operator is inside "Edit AI
897
+ * access" when a poll fails. The modal must stay open through the
898
+ * failure and through the recovery that follows.
899
+ */
900
+ test("keeps an open edit modal through a failed poll and its recovery", async () => {
901
+ jest.useFakeTimers();
902
+ // Every poll after the first load fails until the test says otherwise.
903
+ postSpy
904
+ .mockResolvedValueOnce(statusResponse(makeStatus()))
905
+ .mockImplementation(failStatusRequest);
906
+
907
+ openAiPage();
908
+ const dialog: HTMLElement = await openEditModal();
909
+
910
+ await act(async () => {
911
+ jest.advanceTimersByTime(AI_ACCESS_STATUS_POLL_INTERVAL_MS);
912
+ });
913
+
914
+ const warning: HTMLElement = await screen.findByTestId(
915
+ "ai-access-refresh-warning",
916
+ {},
917
+ { timeout: WAIT_TIMEOUT },
918
+ );
919
+ expect(warning).toHaveTextContent("Network Error");
920
+ expect(screen.getByRole("dialog")).toBe(dialog);
921
+ expect(
922
+ within(dialog).getByText("Let AI investigate with kubectl"),
923
+ ).toBeInTheDocument();
924
+ expect(screen.getByText(SETTINGS_CARD_TITLE)).toBeInTheDocument();
925
+
926
+ serveStatus(makeStatus());
927
+ await act(async () => {
928
+ jest.advanceTimersByTime(AI_ACCESS_STATUS_POLL_INTERVAL_MS);
929
+ });
930
+
931
+ await waitFor(
932
+ () => {
933
+ expect(
934
+ screen.queryByTestId("ai-access-refresh-warning"),
935
+ ).not.toBeInTheDocument();
936
+ },
937
+ { timeout: WAIT_TIMEOUT },
938
+ );
939
+ expect(screen.getByRole("dialog")).toBe(dialog);
940
+ expect(
941
+ within(dialog).getByText("Let AI investigate with kubectl"),
942
+ ).toBeInTheDocument();
943
+ });
944
+
945
+ test("keeps the page when a poll returns a status it cannot read", async () => {
946
+ jest.useFakeTimers();
947
+ postSpy
948
+ .mockResolvedValueOnce(statusResponse(makeStatus()))
949
+ .mockResolvedValue(
950
+ new HTTPResponse<JSONObject>(200, { unexpected: true }, {}),
951
+ );
952
+
953
+ openAiPage();
954
+ expect(await findText(STATUS_CARD_TITLE)).toBeInTheDocument();
955
+
956
+ await act(async () => {
957
+ jest.advanceTimersByTime(AI_ACCESS_STATUS_POLL_INTERVAL_MS);
958
+ });
959
+
960
+ expect(
961
+ await screen.findByTestId(
962
+ "ai-access-refresh-warning",
963
+ {},
964
+ { timeout: WAIT_TIMEOUT },
965
+ ),
966
+ ).toBeInTheDocument();
967
+ expect(screen.getByText(STATUS_CARD_TITLE)).toBeInTheDocument();
968
+ });
969
+ });
970
+
971
+ describe("commands table", () => {
972
+ test("asks for every column its cells read and labels rows from them", async () => {
973
+ jobs = [
974
+ makeJob("investigation", { aiRunId: new ObjectID(RUN_ID) }),
975
+ makeJob("access-test"),
976
+ makeJob("failed", {
977
+ aiRunId: new ObjectID(RUN_ID),
978
+ status: RunnerJobStatus.Failed,
979
+ exitCode: 1,
980
+ errorMessage: "pods is forbidden: User cannot list resource",
981
+ }),
982
+ ];
983
+ openAiPage();
984
+
985
+ expect(
986
+ await findText("kubectl get pods -n web-investigation"),
987
+ ).toBeInTheDocument();
988
+ expect(
989
+ screen.getByText("kubectl get pods -n web-access-test"),
990
+ ).toBeInTheDocument();
991
+
992
+ // Whole-cell matches only: the card description also says "(read-only)".
993
+ const whyCells: Array<string> = screen
994
+ .getAllByText(/^(Investigation|Access test) \(read-only\)$/)
995
+ .map((cell: HTMLElement): string => {
996
+ return cell.textContent || "";
997
+ });
998
+ expect(whyCells).toEqual([
999
+ "Investigation (read-only)",
1000
+ "Access test (read-only)",
1001
+ "Investigation (read-only)",
1002
+ ]);
1003
+
1004
+ expect(screen.getByText("Failed (exit 1)")).toBeInTheDocument();
1005
+ expect(screen.getAllByText("Succeeded (exit 0)").length).toBe(2);
1006
+ expect(
1007
+ screen.getByText("pods is forbidden: User cannot list resource"),
1008
+ ).toBeInTheDocument();
1009
+
1010
+ const request: ListRequest | undefined = jobListRequest();
1011
+ expect(request?.select).toEqual(
1012
+ expect.objectContaining({
1013
+ createdAt: true,
1014
+ payload: true,
1015
+ origin: true,
1016
+ status: true,
1017
+ aiRunId: true,
1018
+ exitCode: true,
1019
+ errorMessage: true,
1020
+ }),
1021
+ );
1022
+ expect(request?.select).not.toHaveProperty("output");
1023
+ expect(request?.query).toEqual(
1024
+ expect.objectContaining({
1025
+ stepType: RunbookStepType.Kubectl,
1026
+ }),
1027
+ );
1028
+ expect(String(request?.query?.["kubernetesClusterId"])).toBe(
1029
+ CLUSTER_ID.toString(),
1030
+ );
1031
+ });
1032
+
1033
+ test("labels a remediation command and tells the empty state apart", async () => {
1034
+ jobs = [
1035
+ makeJob("fix", {
1036
+ origin: RunnerJobOrigin.AiRemediation,
1037
+ aiRunId: new ObjectID(RUN_ID),
1038
+ payload: {
1039
+ displayCommand: "kubectl rollout restart deployment/web -n web",
1040
+ },
1041
+ }),
1042
+ ];
1043
+ openAiPage();
1044
+
1045
+ const commandCell: HTMLElement = await findText(
1046
+ "kubectl rollout restart deployment/web -n web",
1047
+ );
1048
+ // Scoped to the row: the summary tile above is also headed "Remediation".
1049
+ const row: HTMLElement | null = commandCell.closest("tr");
1050
+ expect(row).not.toBeNull();
1051
+ expect(
1052
+ within(row as HTMLElement).getByText("Remediation"),
1053
+ ).toBeInTheDocument();
1054
+ expect(
1055
+ within(row as HTMLElement).queryByText(
1056
+ /^(Investigation|Access test) \(read-only\)$/,
1057
+ ),
1058
+ ).not.toBeInTheDocument();
1059
+ });
1060
+
1061
+ test("renders the empty state without crashing when nothing ran yet", async () => {
1062
+ openAiPage();
1063
+
1064
+ expect(
1065
+ await findText(
1066
+ "OneUptime AI has not run any kubectl commands on this cluster yet.",
1067
+ ),
1068
+ ).toBeInTheDocument();
1069
+ });
1070
+ });
1071
+
1072
+ /*
1073
+ * Binding a Runner or a credential, the higher unattended modes and new
1074
+ * allowlist patterns need KUBERNETES_AI_ACCESS_ADMIN_PERMISSIONS (the
1075
+ * server refuses them otherwise). The pickers are exercised with a user
1076
+ * who holds the admin set; the tightening a member may do is below.
1077
+ */
1078
+ describe("edit modal pickers", () => {
1079
+ test("omits the credential picker for an admin-set holder who may not read Runner credentials", async () => {
1080
+ grant([...MEMBER_PERMISSIONS, Permission.EditAutoRemediationRule]);
1081
+ openAiPage();
1082
+
1083
+ expect(
1084
+ await screen.findByTestId(
1085
+ "kubernetes-credential-permission-note",
1086
+ {},
1087
+ { timeout: WAIT_TIMEOUT },
1088
+ ),
1089
+ ).toHaveTextContent("Runbook Credential");
1090
+
1091
+ const dialog: HTMLElement = await openEditModal();
1092
+
1093
+ expect(
1094
+ await within(dialog).findByText(
1095
+ "Runner",
1096
+ {},
1097
+ { timeout: WAIT_TIMEOUT },
1098
+ ),
1099
+ ).toBeInTheDocument();
1100
+ expect(
1101
+ within(dialog).queryByText("Kubernetes credential"),
1102
+ ).not.toBeInTheDocument();
1103
+ expect(
1104
+ within(dialog).getByTestId(
1105
+ "kubernetes-credential-picker-permission-note",
1106
+ ),
1107
+ ).toHaveTextContent("Read Runbook Credential");
1108
+ expect(
1109
+ within(dialog).queryByText(/do not have permission/i),
1110
+ ).not.toBeInTheDocument();
1111
+
1112
+ expect(listRequestsFor(Runner).length).toBeGreaterThan(0);
1113
+ expect(listRequestsFor(RunbookCredential)).toHaveLength(0);
1114
+ });
1115
+
1116
+ /*
1117
+ * The finding (dashboard-ai-page-7): the picker offered every
1118
+ * Kubernetes credential, whatever Runner it was assigned to.
1119
+ */
1120
+ test("offers an admin only the Kubernetes credentials assigned to the chosen Runner", async () => {
1121
+ grant(ADMIN_PERMISSIONS);
1122
+ serveCluster(hostRunnerBinding());
1123
+ openAiPage();
1124
+
1125
+ await findText(SETTINGS_CARD_TITLE);
1126
+ expect(
1127
+ screen.queryByTestId("kubernetes-credential-permission-note"),
1128
+ ).not.toBeInTheDocument();
1129
+
1130
+ const dialog: HTMLElement = await openEditModal();
1131
+
1132
+ expect(
1133
+ await within(dialog).findByText(
1134
+ "Kubernetes credential",
1135
+ {},
1136
+ { timeout: WAIT_TIMEOUT },
1137
+ ),
1138
+ ).toBeInTheDocument();
1139
+
1140
+ const requests: Array<ListRequest> = listRequestsFor(RunbookCredential);
1141
+ expect(requests).toHaveLength(1);
1142
+ expect(requests[0]!.query).toEqual({
1143
+ credentialType: RunbookCredentialType.Kubernetes,
1144
+ });
1145
+ // The assignments are read so the options can follow the Runner.
1146
+ expect(requests[0]!.select).toEqual(
1147
+ expect.objectContaining({ runners: { _id: true } }),
1148
+ );
1149
+
1150
+ const options: Array<string> = await openDropdown(
1151
+ dialog,
1152
+ /^Kubernetes credential/,
1153
+ );
1154
+ expect(options).toEqual(["prod-east token"]);
1155
+ expect(
1156
+ within(dialog).getByText(/assigned to "bash-runner" are listed/),
1157
+ ).toBeInTheDocument();
1158
+ });
1159
+
1160
+ test("offers no credential for this cluster's in-cluster Runner, and says why", async () => {
1161
+ grant(ADMIN_PERMISSIONS);
1162
+ openAiPage();
1163
+ const dialog: HTMLElement = await openEditModal();
1164
+
1165
+ expect(
1166
+ await within(dialog).findByText(
1167
+ /No credential can be chosen: "kubernetes-agent\/prod-east" is an in-cluster Runner/,
1168
+ {},
1169
+ { timeout: WAIT_TIMEOUT },
1170
+ ),
1171
+ ).toBeInTheDocument();
1172
+
1173
+ // Choosing the host Runner brings its credentials back.
1174
+ await pickOption(dialog, /^Runner/, "bash-runner");
1175
+ expect(
1176
+ await within(dialog).findByText(
1177
+ /assigned to "bash-runner" are listed/,
1178
+ {},
1179
+ { timeout: WAIT_TIMEOUT },
1180
+ ),
1181
+ ).toBeInTheDocument();
1182
+ const options: Array<string> = await openDropdown(
1183
+ dialog,
1184
+ /^Kubernetes credential/,
1185
+ );
1186
+ expect(options).toEqual(["prod-east token"]);
1187
+ });
1188
+
1189
+ test("refuses to save this cluster's in-cluster Runner together with a credential", async () => {
1190
+ grant(ADMIN_PERMISSIONS);
1191
+ serveCluster({
1192
+ ...hostRunnerBinding(),
1193
+ aiAccessCredential: Object.assign(new RunbookCredential(), {
1194
+ _id: CREDENTIAL_ID,
1195
+ name: "prod-east token",
1196
+ }),
1197
+ });
1198
+ openAiPage();
1199
+ const dialog: HTMLElement = await openEditModal();
1200
+ await within(dialog).findByText("Runner", {}, { timeout: WAIT_TIMEOUT });
1201
+
1202
+ await pickOption(
1203
+ dialog,
1204
+ /^Runner/,
1205
+ "kubernetes-agent/prod-east (in-cluster Runner for this cluster)",
1206
+ );
1207
+ saveEditModal(dialog);
1208
+
1209
+ const error: HTMLElement = await screen.findByTestId(
1210
+ "ai-access-save-error",
1211
+ {},
1212
+ { timeout: WAIT_TIMEOUT },
1213
+ );
1214
+ expect(error).toHaveTextContent("is an in-cluster Runner");
1215
+ expect(error).toHaveTextContent("Clear the Kubernetes credential");
1216
+ expect(updateByIdSpy).not.toHaveBeenCalled();
1217
+ });
1218
+
1219
+ /*
1220
+ * The finding: the Runner picker listed every project Runner — another
1221
+ * cluster's in-cluster agent (which can only reach its own cluster, and
1222
+ * which this cluster's own agent then never takes the binding back
1223
+ * from) and Runners with AI commands off.
1224
+ */
1225
+ test("offers only Runners that can serve this cluster", async () => {
1226
+ grant(ADMIN_PERMISSIONS);
1227
+ openAiPage();
1228
+ const dialog: HTMLElement = await openEditModal();
1229
+ await within(dialog).findByText("Runner", {}, { timeout: WAIT_TIMEOUT });
1230
+
1231
+ const request: ListRequest = listRequestsFor(Runner)[0]!;
1232
+ expect(request.select).toEqual(
1233
+ expect.objectContaining({
1234
+ name: true,
1235
+ canRunAiCommands: true,
1236
+ // The posture tells this cluster's agent from another's.
1237
+ hostInfo: true,
1238
+ }),
1239
+ );
1240
+
1241
+ const options: Array<string> = await openDropdown(dialog, /^Runner/);
1242
+ expect(options).toEqual([
1243
+ "kubernetes-agent/prod-east (in-cluster Runner for this cluster)",
1244
+ "bash-runner",
1245
+ ]);
1246
+ });
1247
+
1248
+ /*
1249
+ * The finding (dashboard-ai-page-2): for a cluster identifier long
1250
+ * enough that the server shortens the Runner name with a hash, the
1251
+ * picker hid this cluster's own Runner.
1252
+ */
1253
+ test("offers this cluster's in-cluster Runner when the server shortened its name", async () => {
1254
+ const longIdentifier: string =
1255
+ "arn:aws:eks:ap-southeast-2:123456789012:cluster/payments-platform-production-blue-green";
1256
+ const shortenedName: string = `kubernetes-agent/${longIdentifier.slice(0, 74)}-fe7fc1b3`;
1257
+ grant(ADMIN_PERMISSIONS);
1258
+ serveStatus(makeStatus({ clusterIdentifier: longIdentifier }));
1259
+ getListSpy.mockImplementation(async (args: unknown): Promise<unknown> => {
1260
+ const request: ListRequest = args as ListRequest;
1261
+ const data: Array<unknown> =
1262
+ request.modelType === Runner
1263
+ ? [
1264
+ Object.assign(new Runner(), {
1265
+ _id: RUNNER_ID,
1266
+ name: shortenedName,
1267
+ canRunAiCommands: true,
1268
+ hostInfo: agentHostInfo(longIdentifier),
1269
+ }),
1270
+ ]
1271
+ : [];
1272
+ return { data, count: data.length, skip: 0, limit: 10 };
1273
+ });
1274
+ serveCluster({
1275
+ aiAccessRunner: Object.assign(new Runner(), {
1276
+ _id: RUNNER_ID,
1277
+ name: shortenedName,
1278
+ }),
1279
+ });
1280
+ openAiPage();
1281
+ const dialog: HTMLElement = await openEditModal();
1282
+ await within(dialog).findByText("Runner", {}, { timeout: WAIT_TIMEOUT });
1283
+
1284
+ const options: Array<string> = await openDropdown(dialog, /^Runner/);
1285
+ expect(options).toEqual([
1286
+ `${shortenedName} (in-cluster Runner for this cluster)`,
1287
+ ]);
1288
+ });
1289
+
1290
+ test("keeps a Runner bound by mistake visible, saying why it cannot serve the cluster", async () => {
1291
+ grant(ADMIN_PERMISSIONS);
1292
+ serveCluster({
1293
+ aiAccessRunner: Object.assign(new Runner(), {
1294
+ _id: OTHER_CLUSTER_RUNNER_ID,
1295
+ name: "kubernetes-agent/prod-eu",
1296
+ }),
1297
+ });
1298
+ openAiPage();
1299
+ const dialog: HTMLElement = await openEditModal();
1300
+ await within(dialog).findByText("Runner", {}, { timeout: WAIT_TIMEOUT });
1301
+
1302
+ const options: Array<string> = await openDropdown(dialog, /^Runner/);
1303
+ expect(options).toContain(
1304
+ "kubernetes-agent/prod-eu (currently bound — runs inside another cluster and can only reach that cluster)",
1305
+ );
1306
+ expect(options).not.toContain("ops-runner");
1307
+ });
1308
+
1309
+ /*
1310
+ * The finding: SettingsAdmin, SettingsMember and EditKubernetesCluster
1311
+ * may edit the cluster but not read Runners, and the modal's Runner
1312
+ * list request failed the whole form with a permission error.
1313
+ */
1314
+ test("never requests Runners or credentials for a Settings admin, and explains why", async () => {
1315
+ grant([...BASE_PERMISSIONS, Permission.SettingsAdmin]);
1316
+ openAiPage();
1317
+
1318
+ expect(
1319
+ await screen.findByTestId(
1320
+ "kubernetes-runner-permission-note",
1321
+ {},
1322
+ { timeout: WAIT_TIMEOUT },
1323
+ ),
1324
+ ).toHaveTextContent("Read Runbook Agent");
1325
+
1326
+ const dialog: HTMLElement = await openEditModal();
1327
+
1328
+ expect(within(dialog).queryByText("Runner")).not.toBeInTheDocument();
1329
+ expect(
1330
+ within(dialog).queryByText("Kubernetes credential"),
1331
+ ).not.toBeInTheDocument();
1332
+ expect(
1333
+ within(dialog).queryByText(/do not have permission/i),
1334
+ ).not.toBeInTheDocument();
1335
+ expect(within(dialog).getByText("AI remediation")).toBeInTheDocument();
1336
+ expect(
1337
+ within(dialog).getByTestId("ai-investigation-field"),
1338
+ ).toBeInTheDocument();
1339
+ expect(
1340
+ within(dialog).getByTestId("kubernetes-ai-access-admin-note"),
1341
+ ).toBeInTheDocument();
1342
+
1343
+ expect(listRequestsFor(Runner)).toHaveLength(0);
1344
+ expect(listRequestsFor(RunbookCredential)).toHaveLength(0);
1345
+ });
1346
+
1347
+ test("an admin-set holder who may read credentials but not Runners gets the credential picker, and may unbind the Runner", async () => {
1348
+ grant([
1349
+ ...BASE_PERMISSIONS,
1350
+ Permission.EditKubernetesCluster,
1351
+ Permission.ReadKubernetesCluster,
1352
+ Permission.EditAutoRemediationRule,
1353
+ Permission.ReadRunbookCredential,
1354
+ ]);
1355
+ openAiPage();
1356
+ const dialog: HTMLElement = await openEditModal();
1357
+
1358
+ expect(
1359
+ await within(dialog).findByText(
1360
+ "Kubernetes credential",
1361
+ {},
1362
+ { timeout: WAIT_TIMEOUT },
1363
+ ),
1364
+ ).toBeInTheDocument();
1365
+ expect(within(dialog).queryByText("Runner")).not.toBeInTheDocument();
1366
+ expect(
1367
+ within(dialog).getByTestId("kubernetes-runner-picker-permission-note"),
1368
+ ).toHaveTextContent("Read Runbook Agent");
1369
+ expect(
1370
+ within(dialog).getByTestId("ai-access-clear-runner-field"),
1371
+ ).toBeInTheDocument();
1372
+ expect(listRequestsFor(RunbookCredential)).toHaveLength(1);
1373
+ expect(listRequestsFor(Runner)).toHaveLength(0);
1374
+ });
1375
+
1376
+ test("one picker's failed list leaves the other picker and the form working", async () => {
1377
+ grant(ADMIN_PERMISSIONS);
1378
+ const listImplementation: (args: unknown) => Promise<unknown> =
1379
+ getListSpy.getMockImplementation() as (
1380
+ args: unknown,
1381
+ ) => Promise<unknown>;
1382
+ getListSpy.mockImplementation(async (args: unknown): Promise<unknown> => {
1383
+ if ((args as ListRequest).modelType === Runner) {
1384
+ throw new Error("You do not have permissions to read Runner.");
1385
+ }
1386
+ return await listImplementation(args);
1387
+ });
1388
+ openAiPage();
1389
+ const dialog: HTMLElement = await openEditModal();
1390
+
1391
+ expect(
1392
+ await within(dialog).findByText(
1393
+ "Kubernetes credential",
1394
+ {},
1395
+ { timeout: WAIT_TIMEOUT },
1396
+ ),
1397
+ ).toBeInTheDocument();
1398
+ expect(within(dialog).queryByText("Runner")).not.toBeInTheDocument();
1399
+ expect(
1400
+ within(dialog).getByText(/The Runner list could not be loaded/),
1401
+ ).toBeInTheDocument();
1402
+ });
1403
+ });
1404
+
1405
+ /*
1406
+ * The finding: every cluster editor — SettingsMember, SettingsAdmin,
1407
+ * EditKubernetesCluster, ProjectMember — could switch a cluster to
1408
+ * Bypass approval, or Automatic with a "kubectl *" allowlist, and so
1409
+ * remove an approval they could not give. The server refuses those
1410
+ * writes without KUBERNETES_AI_ACCESS_ADMIN_PERMISSIONS; the page must
1411
+ * not offer them, and must not re-send them unchanged either.
1412
+ */
1413
+ describe("who may loosen AI access", () => {
1414
+ test("a member is offered only Off and Ask for approval on an Ask-for-approval cluster, and told why", async () => {
1415
+ openAiPage();
1416
+
1417
+ expect(
1418
+ await screen.findByTestId(
1419
+ "kubernetes-ai-access-admin-note",
1420
+ {},
1421
+ { timeout: WAIT_TIMEOUT },
1422
+ ),
1423
+ ).toHaveTextContent("Edit Auto Remediation Rule");
1424
+
1425
+ const dialog: HTMLElement = await openEditModal();
1426
+
1427
+ expect(
1428
+ within(dialog).getByTestId("kubernetes-ai-access-admin-note"),
1429
+ ).toHaveTextContent("Project Admin");
1430
+ // Nothing saved to remove: no allowlist field.
1431
+ expect(
1432
+ within(dialog).queryByText("kubectl allowlist (Automatic mode)"),
1433
+ ).not.toBeInTheDocument();
1434
+ expect(within(dialog).queryByText("Runner")).not.toBeInTheDocument();
1435
+
1436
+ const options: Array<string> = await openDropdown(
1437
+ dialog,
1438
+ /^AI remediation/,
1439
+ );
1440
+ expect(options).toEqual([
1441
+ REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.Disabled],
1442
+ REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.RequireApproval],
1443
+ ]);
1444
+ expect(listRequestsFor(Runner)).toHaveLength(0);
1445
+ expect(listRequestsFor(RunbookCredential)).toHaveLength(0);
1446
+ });
1447
+
1448
+ test("an admin is offered every mode and the allowlist", async () => {
1449
+ grant(ADMIN_PERMISSIONS);
1450
+ openAiPage();
1451
+ await findText(SETTINGS_CARD_TITLE);
1452
+ expect(
1453
+ screen.queryByTestId("kubernetes-ai-access-admin-note"),
1454
+ ).not.toBeInTheDocument();
1455
+
1456
+ const dialog: HTMLElement = await openEditModal();
1457
+ expect(
1458
+ await within(dialog).findByText(
1459
+ "kubectl allowlist (Automatic mode)",
1460
+ {},
1461
+ { timeout: WAIT_TIMEOUT },
1462
+ ),
1463
+ ).toBeInTheDocument();
1464
+
1465
+ const options: Array<string> = await openDropdown(
1466
+ dialog,
1467
+ /^AI remediation/,
1468
+ );
1469
+ expect(options).toEqual(
1470
+ Object.values(KubernetesAiRemediationMode).map(
1471
+ (mode: KubernetesAiRemediationMode): string => {
1472
+ return REMEDIATION_MODE_LABELS[mode];
1473
+ },
1474
+ ),
1475
+ );
1476
+ });
1477
+
1478
+ test("a member on an Automatic cluster sees the current mode but no higher one", async () => {
1479
+ serveCluster({
1480
+ aiRemediationMode: KubernetesAiRemediationMode.Automatic,
1481
+ });
1482
+ openAiPage();
1483
+ const dialog: HTMLElement = await openEditModal();
1484
+
1485
+ const options: Array<string> = await openDropdown(
1486
+ dialog,
1487
+ /^AI remediation/,
1488
+ );
1489
+ expect(options).toEqual([
1490
+ REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.Disabled],
1491
+ REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.RequireApproval],
1492
+ `${REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.Automatic]} (current)`,
1493
+ ]);
1494
+ });
1495
+
1496
+ /*
1497
+ * The finding (dashboard-ai-page-4, XP-7): Bypass approval -> Automatic
1498
+ * is a tightening the server accepts from every cluster editor, and the
1499
+ * page hid Automatic from them.
1500
+ */
1501
+ test("a member on a Bypass-approval cluster may step down to Automatic", async () => {
1502
+ serveCluster({
1503
+ aiRemediationMode: KubernetesAiRemediationMode.BypassApproval,
1504
+ });
1505
+ openAiPage();
1506
+ const dialog: HTMLElement = await openEditModal();
1507
+
1508
+ const options: Array<string> = await openDropdown(
1509
+ dialog,
1510
+ /^AI remediation/,
1511
+ );
1512
+ expect(options).toEqual([
1513
+ REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.Disabled],
1514
+ REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.RequireApproval],
1515
+ REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.Automatic],
1516
+ `${REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.BypassApproval]} (current)`,
1517
+ ]);
1518
+
1519
+ fireEvent.click(
1520
+ menuOptions().find((option: HTMLElement): boolean => {
1521
+ return (
1522
+ option.textContent ===
1523
+ REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.Automatic]
1524
+ );
1525
+ })!,
1526
+ );
1527
+ saveEditModal(dialog);
1528
+
1529
+ expect(await waitForOneUpdate()).toEqual({
1530
+ aiRemediationMode: KubernetesAiRemediationMode.Automatic,
1531
+ });
1532
+ expect(
1533
+ screen.queryByTestId("ai-access-save-error"),
1534
+ ).not.toBeInTheDocument();
1535
+ // A step down is not a new risk: no confirmation either.
1536
+ expect(
1537
+ screen.queryByText("Let riskier changes run without approval?"),
1538
+ ).not.toBeInTheDocument();
1539
+ });
1540
+
1541
+ test("a member may remove an allowlist pattern", async () => {
1542
+ serveCluster({
1543
+ aiRemediationMode: KubernetesAiRemediationMode.Automatic,
1544
+ aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN, PATCH_PATTERN],
1545
+ });
1546
+ openAiPage();
1547
+ const dialog: HTMLElement = await openEditModal();
1548
+
1549
+ expect(
1550
+ within(dialog).getByText(/You can remove patterns or clear the list/),
1551
+ ).toBeInTheDocument();
1552
+ await setAllowlistText(dialog, SET_IMAGE_PATTERN);
1553
+ saveEditModal(dialog);
1554
+
1555
+ expect(await waitForOneUpdate()).toEqual({
1556
+ aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN],
1557
+ });
1558
+ });
1559
+
1560
+ test("a member may clear the allowlist", async () => {
1561
+ serveCluster({
1562
+ aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN],
1563
+ });
1564
+ openAiPage();
1565
+ const dialog: HTMLElement = await openEditModal();
1566
+
1567
+ await setAllowlistText(dialog, "");
1568
+ saveEditModal(dialog);
1569
+
1570
+ expect(await waitForOneUpdate()).toEqual({
1571
+ aiKubectlCommandAllowlist: [],
1572
+ });
1573
+ });
1574
+
1575
+ test("a member who adds or edits a pattern is told it needs the admin set, and nothing is sent", async () => {
1576
+ serveCluster({
1577
+ aiRemediationMode: KubernetesAiRemediationMode.Automatic,
1578
+ aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN],
1579
+ });
1580
+ openAiPage();
1581
+ const dialog: HTMLElement = await openEditModal();
1582
+
1583
+ await setAllowlistText(
1584
+ dialog,
1585
+ "kubectl set image deployment/web * -n prod",
1586
+ );
1587
+ saveEditModal(dialog);
1588
+
1589
+ expect(
1590
+ await within(dialog).findByText(
1591
+ /is not in the saved allowlist/,
1592
+ {},
1593
+ { timeout: WAIT_TIMEOUT },
1594
+ ),
1595
+ ).toHaveTextContent("Edit Auto Remediation Rule");
1596
+ expect(updateByIdSpy).not.toHaveBeenCalled();
1597
+ });
1598
+
1599
+ /*
1600
+ * The finding (SIA-4): the server and the gap copy tell every cluster
1601
+ * editor they may clear the Runner, and the page offered them no way
1602
+ * to — Settings roles and EditKubernetesCluster cannot even list
1603
+ * Runners.
1604
+ */
1605
+ for (const [role, permissions] of [
1606
+ ["a member", MEMBER_PERMISSIONS],
1607
+ ["a Settings member", [...BASE_PERMISSIONS, Permission.SettingsMember]],
1608
+ [
1609
+ "an EditKubernetesCluster editor",
1610
+ [
1611
+ ...BASE_PERMISSIONS,
1612
+ Permission.EditKubernetesCluster,
1613
+ Permission.ReadKubernetesCluster,
1614
+ ],
1615
+ ],
1616
+ ] as Array<[string, Array<Permission>]>) {
1617
+ test(`${role} may unbind the Runner without listing Runners`, async () => {
1618
+ grant(permissions);
1619
+ openAiPage();
1620
+ const dialog: HTMLElement = await openEditModal();
1621
+
1622
+ expect(
1623
+ within(dialog).getByText(/Bound now: kubernetes-agent\/prod-east/),
1624
+ ).toBeInTheDocument();
1625
+ await toggleSwitch(dialog, "ai-access-clear-runner-field", false);
1626
+ saveEditModal(dialog);
1627
+
1628
+ expect(await waitForOneUpdate()).toEqual({ aiAccessRunnerId: null });
1629
+ expect(listRequestsFor(Runner)).toHaveLength(0);
1630
+ });
1631
+ }
1632
+
1633
+ test("a member may unbind the credential", async () => {
1634
+ serveCluster({
1635
+ ...hostRunnerBinding(),
1636
+ aiAccessCredential: Object.assign(new RunbookCredential(), {
1637
+ _id: CREDENTIAL_ID,
1638
+ name: "prod-east token",
1639
+ }),
1640
+ });
1641
+ openAiPage();
1642
+ const dialog: HTMLElement = await openEditModal();
1643
+
1644
+ await toggleSwitch(dialog, "ai-access-clear-credential-field", false);
1645
+ saveEditModal(dialog);
1646
+
1647
+ expect(await waitForOneUpdate()).toEqual({
1648
+ aiAccessCredentialId: null,
1649
+ });
1650
+ expect(listRequestsFor(RunbookCredential)).toHaveLength(0);
1651
+ });
1652
+
1653
+ test("with nothing bound, there is nothing to unbind", async () => {
1654
+ serveCluster({ aiAccessRunner: undefined });
1655
+ openAiPage();
1656
+ const dialog: HTMLElement = await openEditModal();
1657
+
1658
+ expect(
1659
+ within(dialog).queryByTestId("ai-access-clear-runner-field"),
1660
+ ).not.toBeInTheDocument();
1661
+ expect(
1662
+ within(dialog).queryByTestId("ai-access-clear-credential-field"),
1663
+ ).not.toBeInTheDocument();
1664
+ });
1665
+
1666
+ /*
1667
+ * ModelForm submitted every field, so a member who only switched
1668
+ * investigation off on an Automatic cluster with an allowlist and a
1669
+ * bound Runner re-sent all three — and the server refused the save of
1670
+ * a change they were allowed to make.
1671
+ */
1672
+ test("a member who only switches investigation off sends only that", async () => {
1673
+ serveCluster({
1674
+ aiRemediationMode: KubernetesAiRemediationMode.Automatic,
1675
+ aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN],
1676
+ });
1677
+ openAiPage();
1678
+ const dialog: HTMLElement = await openEditModal();
1679
+
1680
+ await toggleInvestigation(dialog, true);
1681
+ saveEditModal(dialog);
1682
+
1683
+ await waitFor(
1684
+ () => {
1685
+ expect(updateByIdSpy).toHaveBeenCalledTimes(1);
1686
+ },
1687
+ { timeout: WAIT_TIMEOUT },
1688
+ );
1689
+ const request: JSONObject = updateRequests()[0]!;
1690
+ expect(request["data"]).toEqual({ isAiInvestigationEnabled: false });
1691
+ expect(String(request["id"])).toBe(CLUSTER_ID.toString());
1692
+ expect(request["modelType"]).toBe(KubernetesCluster);
1693
+
1694
+ /*
1695
+ * Saved: the modal closes and the status is read again. The re-read
1696
+ * is a passive effect of the same state update that closes the
1697
+ * modal, so it can land a tick after the dialog is gone: wait for
1698
+ * it, rather than assume it has already run (a suite run once saw
1699
+ * one status post here).
1700
+ */
1701
+ await waitFor(
1702
+ () => {
1703
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
1704
+ },
1705
+ { timeout: WAIT_TIMEOUT },
1706
+ );
1707
+ await waitFor(
1708
+ () => {
1709
+ expect(
1710
+ postsTo("/kubernetes-cluster/ai-access/status").length,
1711
+ ).toBeGreaterThanOrEqual(2);
1712
+ },
1713
+ { timeout: WAIT_TIMEOUT },
1714
+ );
1715
+ });
1716
+
1717
+ test("a member can still switch remediation off", async () => {
1718
+ serveCluster({
1719
+ aiRemediationMode: KubernetesAiRemediationMode.Automatic,
1720
+ });
1721
+ openAiPage();
1722
+ const dialog: HTMLElement = await openEditModal();
1723
+
1724
+ await pickOption(
1725
+ dialog,
1726
+ /^AI remediation/,
1727
+ REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.Disabled],
1728
+ );
1729
+ saveEditModal(dialog);
1730
+
1731
+ expect(await waitForOneUpdate()).toEqual({
1732
+ aiRemediationMode: KubernetesAiRemediationMode.Disabled,
1733
+ });
1734
+ });
1735
+
1736
+ test("an untouched form saves nothing", async () => {
1737
+ grant(ADMIN_PERMISSIONS);
1738
+ openAiPage();
1739
+ const dialog: HTMLElement = await openEditModal();
1740
+ await within(dialog).findByText("Runner", {}, { timeout: WAIT_TIMEOUT });
1741
+
1742
+ saveEditModal(dialog);
1743
+
1744
+ await waitFor(
1745
+ () => {
1746
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
1747
+ },
1748
+ { timeout: WAIT_TIMEOUT },
1749
+ );
1750
+ expect(updateByIdSpy).not.toHaveBeenCalled();
1751
+ });
1752
+
1753
+ test("a server refusal is shown and the modal stays open", async () => {
1754
+ updateByIdSpy.mockImplementation(async (): Promise<never> => {
1755
+ throw new HTTPErrorResponse(
1756
+ 422,
1757
+ {
1758
+ error: "You do not have permission to switch on unattended fixes.",
1759
+ },
1760
+ {},
1761
+ );
1762
+ });
1763
+ openAiPage();
1764
+ const dialog: HTMLElement = await openEditModal();
1765
+
1766
+ await toggleInvestigation(dialog, true);
1767
+ saveEditModal(dialog);
1768
+
1769
+ const error: HTMLElement = await screen.findByTestId(
1770
+ "ai-access-save-error",
1771
+ {},
1772
+ { timeout: WAIT_TIMEOUT },
1773
+ );
1774
+ expect(error).toHaveTextContent(
1775
+ "You do not have permission to switch on unattended fixes.",
1776
+ );
1777
+ expect(screen.getByRole("dialog")).toBe(dialog);
1778
+ });
1779
+ });
1780
+
1781
+ describe("confirming unattended riskier changes", () => {
1782
+ test("Bypass approval is saved only after a confirmation that names what it unlocks", async () => {
1783
+ grant(ADMIN_PERMISSIONS);
1784
+ openAiPage();
1785
+ const dialog: HTMLElement = await openEditModal();
1786
+
1787
+ await pickOption(
1788
+ dialog,
1789
+ /^AI remediation/,
1790
+ REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.BypassApproval],
1791
+ );
1792
+ saveEditModal(dialog);
1793
+
1794
+ const confirm: HTMLElement = await findDialogTitled(
1795
+ "Turn on Bypass approval?",
1796
+ );
1797
+ expect(confirm).toHaveTextContent("set image");
1798
+ expect(confirm).toHaveTextContent("drain");
1799
+ expect(confirm).toHaveTextContent("kube-system");
1800
+ expect(updateByIdSpy).not.toHaveBeenCalled();
1801
+
1802
+ fireEvent.click(within(confirm).getByText("Confirm and save"));
1803
+
1804
+ expect(await waitForOneUpdate()).toEqual({
1805
+ aiRemediationMode: KubernetesAiRemediationMode.BypassApproval,
1806
+ });
1807
+ });
1808
+
1809
+ test("cancelling the confirmation saves nothing and keeps the form", async () => {
1810
+ grant(ADMIN_PERMISSIONS);
1811
+ openAiPage();
1812
+ const dialog: HTMLElement = await openEditModal();
1813
+
1814
+ await setAllowlistText(dialog, BROAD_PATTERN);
1815
+ saveEditModal(dialog);
1816
+
1817
+ const confirm: HTMLElement = await findDialogTitled(
1818
+ "Let riskier changes run without approval?",
1819
+ );
1820
+ expect(confirm).toHaveTextContent(`"${BROAD_PATTERN}"`);
1821
+ fireEvent.click(within(confirm).getByText("Cancel"));
1822
+
1823
+ await waitFor(
1824
+ () => {
1825
+ expect(
1826
+ screen.queryByText("Let riskier changes run without approval?"),
1827
+ ).not.toBeInTheDocument();
1828
+ },
1829
+ { timeout: WAIT_TIMEOUT },
1830
+ );
1831
+ expect(updateByIdSpy).not.toHaveBeenCalled();
1832
+ expect(screen.getByRole("dialog")).toBe(dialog);
1833
+ });
1834
+
1835
+ /*
1836
+ * "kubectl delete deployment * -n *" auto-approves deleting any
1837
+ * Deployment in any non-protected namespace; the old whole-string
1838
+ * check let such an entry through without a confirmation because it
1839
+ * spells out "-n".
1840
+ */
1841
+ test("a broad allowlist is saved after confirmation, one pattern per line", async () => {
1842
+ grant(ADMIN_PERMISSIONS);
1843
+ openAiPage();
1844
+ const dialog: HTMLElement = await openEditModal();
1845
+
1846
+ await setAllowlistText(
1847
+ dialog,
1848
+ `${SET_IMAGE_PATTERN}\n\n${BROAD_PATTERN}\n`,
1849
+ );
1850
+ saveEditModal(dialog);
1851
+
1852
+ const confirm: HTMLElement = await findDialogTitled(
1853
+ "Let riskier changes run without approval?",
1854
+ );
1855
+ expect(confirm).toHaveTextContent(
1856
+ "a wildcard for an object, the namespace, a selector or a --from source",
1857
+ );
1858
+ fireEvent.click(within(confirm).getByText("Confirm and save"));
1859
+
1860
+ expect(await waitForOneUpdate()).toEqual({
1861
+ aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN, BROAD_PATTERN],
1862
+ });
1863
+ });
1864
+
1865
+ test("a narrow allowlist saves without a confirmation", async () => {
1866
+ grant(ADMIN_PERMISSIONS);
1867
+ openAiPage();
1868
+ const dialog: HTMLElement = await openEditModal();
1869
+
1870
+ await setAllowlistText(dialog, SET_IMAGE_PATTERN);
1871
+ saveEditModal(dialog);
1872
+
1873
+ expect(await waitForOneUpdate()).toEqual({
1874
+ aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN],
1875
+ });
1876
+ expect(
1877
+ screen.queryByText("Let riskier changes run without approval?"),
1878
+ ).not.toBeInTheDocument();
1879
+ });
1880
+
1881
+ /*
1882
+ * Validity is KubectlPolicy's: a bare "kubectl" names no command. (A
1883
+ * pattern without the leading "kubectl" is valid — the matcher reads
1884
+ * it — so it is no longer the example here.)
1885
+ */
1886
+ test("an allowlist pattern the matcher cannot use is refused in the form", async () => {
1887
+ grant(ADMIN_PERMISSIONS);
1888
+ openAiPage();
1889
+ const dialog: HTMLElement = await openEditModal();
1890
+
1891
+ await setAllowlistText(dialog, `${SET_IMAGE_PATTERN}\nkubectl`);
1892
+ saveEditModal(dialog);
1893
+
1894
+ expect(
1895
+ await within(dialog).findByText(
1896
+ /^Pattern 2: /,
1897
+ {},
1898
+ { timeout: WAIT_TIMEOUT },
1899
+ ),
1900
+ ).toBeInTheDocument();
1901
+ expect(updateByIdSpy).not.toHaveBeenCalled();
1902
+ });
1903
+
1904
+ test("explains how patterns match", async () => {
1905
+ grant(ADMIN_PERMISSIONS);
1906
+ openAiPage();
1907
+ const dialog: HTMLElement = await openEditModal();
1908
+
1909
+ const description: HTMLElement = within(dialog).getByText(
1910
+ /\* matches exactly one word/,
1911
+ );
1912
+ expect(description).toHaveTextContent("flags must be written out");
1913
+ expect(description).toHaveTextContent("One pattern per line");
1914
+ expect(description).toHaveTextContent('a leading "kubectl" is optional');
1915
+ /*
1916
+ * KubectlPolicy refuses a wildcard verb and (from round three) a
1917
+ * one-word entry; the help said a wildcard verb was merely broad.
1918
+ */
1919
+ expect(description).toHaveTextContent(
1920
+ "Write the verb (and the subcommand of rollout, set or create) out, never as *, and use more than one word",
1921
+ );
1922
+ expect(description).toHaveTextContent(
1923
+ "A wildcard for the object or the namespace pre-approves a whole class of changes",
1924
+ );
1925
+ expect(description).not.toHaveTextContent("A wildcard for the verb");
1926
+ });
1927
+ });
1928
+
1929
+ describe("allowlist in effect", () => {
1930
+ test("the Remediation tile lists the patterns the policy actually uses", async () => {
1931
+ serveStatus(
1932
+ makeStatus({
1933
+ remediationMode: KubernetesAiRemediationMode.Automatic,
1934
+ kubectlAllowlist: [SET_IMAGE_PATTERN],
1935
+ }),
1936
+ );
1937
+ openAiPage();
1938
+
1939
+ const tile: HTMLElement = await screen.findByTestId(
1940
+ "kubectl-allowlist-in-effect",
1941
+ {},
1942
+ { timeout: WAIT_TIMEOUT },
1943
+ );
1944
+ expect(tile).toHaveTextContent(SET_IMAGE_PATTERN);
1945
+ expect(tile).toHaveTextContent("also run on their own");
1946
+ // The banner says so too, and that any other riskier fix asks.
1947
+ expect(
1948
+ screen.getByText(/plus riskier ones that match the kubectl allowlist/),
1949
+ ).toHaveTextContent(
1950
+ "Any other riskier fix is proposed for your one-click approval.",
1951
+ );
1952
+ });
1953
+
1954
+ test("an empty allowlist shows nothing extra", async () => {
1955
+ serveStatus(
1956
+ makeStatus({
1957
+ remediationMode: KubernetesAiRemediationMode.Automatic,
1958
+ kubectlAllowlist: [],
1959
+ }),
1960
+ );
1961
+ openAiPage();
1962
+ await findText(STATUS_CARD_TITLE);
1963
+
1964
+ expect(
1965
+ screen.queryByTestId("kubectl-allowlist-in-effect"),
1966
+ ).not.toBeInTheDocument();
1967
+ expect(
1968
+ screen.getByText(
1969
+ /and apply safe fixes on its own\. A riskier fix is proposed for your one-click approval\.$/,
1970
+ ),
1971
+ ).toBeInTheDocument();
1972
+ });
1973
+
1974
+ test("a stored allowlist the policy cannot use is flagged", async () => {
1975
+ serveStatus(makeStatus({ kubectlAllowlist: [] }));
1976
+ getItemSpy.mockImplementation(async (): Promise<KubernetesCluster> => {
1977
+ return makeCluster({ aiKubectlCommandAllowlist: { a: "b" } });
1978
+ });
1979
+ openAiPage();
1980
+
1981
+ expect(
1982
+ await screen.findByTestId(
1983
+ "kubectl-allowlist-ignored-warning",
1984
+ {},
1985
+ { timeout: WAIT_TIMEOUT },
1986
+ ),
1987
+ ).toHaveTextContent("no allowlist pattern is in effect");
1988
+ });
1989
+
1990
+ test("a usable stored allowlist is not flagged", async () => {
1991
+ serveStatus(makeStatus({ kubectlAllowlist: [SET_IMAGE_PATTERN] }));
1992
+ serveCluster({ aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN] });
1993
+ openAiPage();
1994
+
1995
+ await findText(SETTINGS_CARD_TITLE);
1996
+ await waitFor(
1997
+ () => {
1998
+ expect(screen.getAllByText(SET_IMAGE_PATTERN).length).toBe(2);
1999
+ },
2000
+ { timeout: WAIT_TIMEOUT },
2001
+ );
2002
+ expect(
2003
+ screen.queryByTestId("kubectl-allowlist-ignored-warning"),
2004
+ ).not.toBeInTheDocument();
2005
+ });
2006
+ });
2007
+
2008
+ describe("connect a Runner", () => {
2009
+ const noRunnerGap: KubernetesAiAccessGap = {
2010
+ code: "no_runner_bound",
2011
+ title: "No Runner can reach this cluster",
2012
+ description:
2013
+ "OneUptime AI runs kubectl through a Runner. None is bound to this cluster yet.",
2014
+ nextStep:
2015
+ "Upgrade the Kubernetes agent with --set aiAccess.enabled=true to install an in-cluster Runner (one command), or bind an existing Runner and a Kubernetes credential on this cluster's AI page.",
2016
+ blocks: "both",
2017
+ };
2018
+
2019
+ // The server's gap when this cluster's in-cluster Runner is registered.
2020
+ const agentNotSelectedGap: KubernetesAiAccessGap = {
2021
+ code: "no_runner_bound",
2022
+ title: "The in-cluster Runner is installed but not selected",
2023
+ description:
2024
+ 'Runner "kubernetes-agent/prod-east", this cluster\'s in-cluster Runner, is registered and online, but no Runner is bound to this cluster, so OneUptime AI does not use it.',
2025
+ nextStep:
2026
+ 'Select the kubernetes-agent Runner "kubernetes-agent/prod-east" as this cluster\'s Runner on this page (leave the credential empty). No helm change is needed.',
2027
+ blocks: "both",
2028
+ };
2029
+
2030
+ function installReleaseAndNamespace(): {
2031
+ release: string;
2032
+ namespace: string;
2033
+ } {
2034
+ const match: RegExpMatchArray | null = getKubernetesInstallationMarkdown({
2035
+ clusterName: "prod-east",
2036
+ oneuptimeUrl: "https://oneuptime.example.com",
2037
+ apiKey: "key",
2038
+ }).match(
2039
+ /helm install (\S+) oneuptime\/kubernetes-agent[\s\\]+--namespace (\S+)/,
2040
+ );
2041
+ if (!match) {
2042
+ throw new Error("The install markdown has no helm install command.");
2043
+ }
2044
+ return { release: match[1]!, namespace: match[2]! };
2045
+ }
2046
+
2047
+ test("the default command matches the Dashboard's install and grants read-only access", async () => {
2048
+ serveStatus(makeStatus({ runner: null, gaps: [noRunnerGap] }));
2049
+ openAiPage();
2050
+
2051
+ await findText("Connect a Runner (one command)");
2052
+ const readOnly: string = textOf("ai-access-helm-command");
2053
+ const install: { release: string; namespace: string } =
2054
+ installReleaseAndNamespace();
2055
+
2056
+ expect(install).toEqual({
2057
+ release: KUBERNETES_AGENT_HELM_RELEASE,
2058
+ namespace: KUBERNETES_AGENT_HELM_NAMESPACE,
2059
+ });
2060
+ expect(readOnly).toContain("helm repo update");
2061
+ expect(readOnly.indexOf("helm repo update")).toBeLessThan(
2062
+ readOnly.indexOf("helm upgrade"),
2063
+ );
2064
+ expect(readOnly).toContain(
2065
+ `helm upgrade ${install.release} oneuptime/kubernetes-agent`,
2066
+ );
2067
+ expect(readOnly).toContain(`--namespace ${install.namespace}`);
2068
+ expect(readOnly).toContain("--set aiAccess.enabled=true");
2069
+ expect(readOnly).not.toContain("aiAccess.remediation.enabled=true");
2070
+ expect(screen.queryByText(/Drop the last line/)).not.toBeInTheDocument();
2071
+ });
2072
+
2073
+ test("an unbound cluster shows the card and a locked test button", async () => {
2074
+ serveStatus(makeStatus({ runner: null, gaps: [noRunnerGap] }));
2075
+ openAiPage();
2076
+
2077
+ expect(
2078
+ await findText("Connect a Runner (one command)"),
2079
+ ).toBeInTheDocument();
2080
+ expect(
2081
+ screen.getByText("Bind a Runner first — there is nothing to test yet."),
2082
+ ).toBeInTheDocument();
2083
+ expect(screen.getByTestId("ai-access-test-button")).toBeDisabled();
2084
+ });
2085
+
2086
+ /*
2087
+ * Known follow-up 7: with this cluster's in-cluster Runner installed
2088
+ * but not selected, the helm card told the operator to run a command
2089
+ * that changes nothing.
2090
+ */
2091
+ test("an installed but unselected in-cluster Runner asks to select it, not to run helm", async () => {
2092
+ serveStatus(makeStatus({ runner: null, gaps: [agentNotSelectedGap] }));
2093
+ openAiPage();
2094
+
2095
+ const card: HTMLElement = await screen.findByTestId(
2096
+ "ai-access-select-agent-runner",
2097
+ {},
2098
+ { timeout: WAIT_TIMEOUT },
2099
+ );
2100
+ expect(card).toHaveTextContent(
2101
+ 'Select the kubernetes-agent Runner "kubernetes-agent/prod-east"',
2102
+ );
2103
+ // A member cannot choose a Runner: the card says what it takes.
2104
+ expect(card).toHaveTextContent("Edit Auto Remediation Rule");
2105
+ expect(
2106
+ screen.queryByText("Connect a Runner (one command)"),
2107
+ ).not.toBeInTheDocument();
2108
+ expect(
2109
+ screen.queryByTestId("ai-access-helm-command"),
2110
+ ).not.toBeInTheDocument();
2111
+ });
2112
+
2113
+ test("an admin is pointed at Edit AI access to select the installed Runner", async () => {
2114
+ grant(ADMIN_PERMISSIONS);
2115
+ serveStatus(makeStatus({ runner: null, gaps: [agentNotSelectedGap] }));
2116
+ openAiPage();
2117
+
2118
+ expect(
2119
+ await screen.findByTestId(
2120
+ "ai-access-select-agent-runner",
2121
+ {},
2122
+ { timeout: WAIT_TIMEOUT },
2123
+ ),
2124
+ ).toHaveTextContent("Open Edit AI access below and choose it");
2125
+ });
2126
+
2127
+ test("a ready cluster in Bypass approval shows the qualified banner and no connect card", async () => {
2128
+ serveStatus(
2129
+ makeStatus({
2130
+ remediationMode: KubernetesAiRemediationMode.BypassApproval,
2131
+ gaps: [],
2132
+ }),
2133
+ );
2134
+ openAiPage();
2135
+
2136
+ const banner: HTMLElement = await findText(
2137
+ /apply every fix the policy allows on its own/,
2138
+ );
2139
+ expect(banner).toHaveTextContent("protected namespace");
2140
+ expect(banner).toHaveTextContent("drain");
2141
+ expect(banner).toHaveTextContent("a node taint");
2142
+ // Round four: a patch of a node asks a human in Bypass approval too.
2143
+ expect(banner).toHaveTextContent("a patch of a node still asks a human");
2144
+ expect(banner).toHaveTextContent("circuit breaker");
2145
+ // The canonical comment's second proposal case.
2146
+ expect(banner).toHaveTextContent(
2147
+ "while another unattended round holds this cluster",
2148
+ );
2149
+ expect(banner).not.toHaveTextContent("without asking anyone");
2150
+ expect(banner).not.toHaveTextContent("only a write");
2151
+ expect(
2152
+ screen.queryByText("Connect a Runner (one command)"),
2153
+ ).not.toBeInTheDocument();
2154
+ });
2155
+
2156
+ test("a Runner bound to another cluster shows its gap, not the card", async () => {
2157
+ serveStatus(
2158
+ makeStatus({
2159
+ gaps: [
2160
+ {
2161
+ code: "runner_cluster_mismatch",
2162
+ title: "The bound Runner runs in another cluster",
2163
+ description: "kubernetes-agent/prod-eu runs inside prod-eu.",
2164
+ nextStep: "Bind this cluster's own Runner on this page.",
2165
+ blocks: "both",
2166
+ },
2167
+ ],
2168
+ }),
2169
+ );
2170
+ openAiPage();
2171
+
2172
+ expect(
2173
+ await findText("The bound Runner runs in another cluster"),
2174
+ ).toBeInTheDocument();
2175
+ expect(
2176
+ screen.getByText("Bind this cluster's own Runner on this page."),
2177
+ ).toBeInTheDocument();
2178
+ expect(
2179
+ screen.queryByText("Connect a Runner (one command)"),
2180
+ ).not.toBeInTheDocument();
2181
+ });
2182
+
2183
+ // Known follow-up 7: this gap renders like every other, with its next step.
2184
+ test("a credential on another cluster's in-cluster Runner shows its gap and next step", async () => {
2185
+ serveStatus(
2186
+ makeStatus({
2187
+ runner: {
2188
+ id: OTHER_CLUSTER_RUNNER_ID,
2189
+ name: "kubernetes-agent/prod-eu",
2190
+ isOnline: true,
2191
+ canRunAiCommands: true,
2192
+ posture: {
2193
+ inCluster: true,
2194
+ allowWrites: false,
2195
+ clusterIdentifier: "prod-eu",
2196
+ },
2197
+ },
2198
+ accessMethod: "none",
2199
+ isInvestigationReady: false,
2200
+ isRemediationReady: false,
2201
+ gaps: [
2202
+ {
2203
+ code: "credential_on_agent_runner",
2204
+ title: "An in-cluster Runner cannot carry a credential",
2205
+ description:
2206
+ 'Runner "kubernetes-agent/prod-eu" is the in-cluster Runner of cluster "prod-eu".',
2207
+ nextStep:
2208
+ "Create a Runner under Project Settings → Runners, assign the Kubernetes credential to it and select both on this page — or install the in-cluster Runner on THIS cluster with --set aiAccess.enabled=true, which needs no credential.",
2209
+ blocks: "both",
2210
+ },
2211
+ ],
2212
+ }),
2213
+ );
2214
+ openAiPage();
2215
+
2216
+ expect(
2217
+ await findText("An in-cluster Runner cannot carry a credential"),
2218
+ ).toBeInTheDocument();
2219
+ expect(
2220
+ screen.getByText(/Create a Runner under Project Settings → Runners/),
2221
+ ).toBeInTheDocument();
2222
+ expect(
2223
+ screen.queryByText("Connect a Runner (one command)"),
2224
+ ).not.toBeInTheDocument();
2225
+ });
2226
+ });
2227
+
2228
+ /*
2229
+ * The finding (dashboard-ai-page-3): the write-access command lived in
2230
+ * the connect card, so it vanished once the recommended read-only
2231
+ * Runner connected; it granted write RBAC cluster-wide with no scoped
2232
+ * form, no `helm repo update` and understated copy.
2233
+ */
2234
+ describe("letting AI apply fixes", () => {
2235
+ const writeAccessGap: KubernetesAiAccessGap = {
2236
+ code: "remediation_write_access_missing",
2237
+ title: "The in-cluster Runner is read-only",
2238
+ description:
2239
+ "The Kubernetes agent was installed without write access, so kubectl changes would be refused by the cluster.",
2240
+ nextStep:
2241
+ "Upgrade the agent with --set aiAccess.remediation.enabled=true to grant the Runner's ServiceAccount the write verbs OneUptime AI may use.",
2242
+ blocks: "remediation",
2243
+ };
2244
+
2245
+ test("a connected read-only in-cluster Runner still gets complete write-access commands", async () => {
2246
+ serveStatus(
2247
+ makeStatus({
2248
+ remediationMode: KubernetesAiRemediationMode.RequireApproval,
2249
+ isRemediationReady: false,
2250
+ gaps: [writeAccessGap],
2251
+ }),
2252
+ );
2253
+ openAiPage();
2254
+
2255
+ const section: HTMLElement = await screen.findByTestId(
2256
+ "ai-access-remediation-setup",
2257
+ {},
2258
+ { timeout: WAIT_TIMEOUT },
2259
+ );
2260
+ expect(section).toHaveTextContent(
2261
+ "The in-cluster Runner has read-only RBAC",
2262
+ );
2263
+ expect(
2264
+ screen.queryByText("Connect a Runner (one command)"),
2265
+ ).not.toBeInTheDocument();
2266
+
2267
+ const scoped: string = textOf(
2268
+ "ai-access-helm-remediation-scoped-command",
2269
+ );
2270
+ expect(scoped.startsWith("helm repo update")).toBe(true);
2271
+ expect(scoped).toContain(
2272
+ `helm upgrade ${KUBERNETES_AGENT_HELM_RELEASE} oneuptime/kubernetes-agent`,
2273
+ );
2274
+ expect(scoped).toContain("--set aiAccess.remediation.enabled=true");
2275
+ expect(scoped).toContain("aiAccess.remediation.namespaces=");
2276
+ expect(scoped).toContain("aiAccess.remediation.nodeOperations=false");
2277
+ expect(scoped).toBe(getAiAccessHelmCommands().enableRemediationScoped);
2278
+
2279
+ const clusterWide: string = textOf("ai-access-helm-remediation-command");
2280
+ expect(clusterWide.startsWith("helm repo update")).toBe(true);
2281
+ expect(clusterWide).toContain("--set aiAccess.remediation.enabled=true");
2282
+ /*
2283
+ * Round four: the reset that works under --reuse-values. `=null` kept
2284
+ * a stored list there, and failed the schema on installs from a
2285
+ * chart without aiAccess.
2286
+ */
2287
+ expect(clusterWide).toContain(
2288
+ "--set-json 'aiAccess.remediation.namespaces=[]'",
2289
+ );
2290
+ expect(clusterWide).not.toContain("namespaces=null");
2291
+ expect(clusterWide).toBe(getAiAccessHelmCommands().enableRemediation);
2292
+
2293
+ /*
2294
+ * Under each command, what the chart and the server do with the list:
2295
+ * every namespace must already exist, the empty-list `--set-json`
2296
+ * resets a stored list (`=null` does not, under --reuse-values), and
2297
+ * a write outside it is refused before it reaches the Runner.
2298
+ */
2299
+ const scopedNote: string = textOf(
2300
+ "ai-access-helm-remediation-scoped-note",
2301
+ );
2302
+ expect(scopedNote).toBe(getAiAccessScopedCommandNote());
2303
+ expect(scopedNote).toContain("must already exist");
2304
+ expect(scopedNote).toContain(
2305
+ "--set-json 'aiAccess.remediation.namespaces=[]' resets it to cluster-wide",
2306
+ );
2307
+ expect(scopedNote).toContain(
2308
+ "--set aiAccess.remediation.namespaces=null does not reset a stored list under --reuse-values",
2309
+ );
2310
+ expect(scopedNote).toContain("refused when it is proposed or approved");
2311
+ const clusterWideNote: string = textOf("ai-access-helm-remediation-note");
2312
+ expect(clusterWideNote).toBe(getAiAccessClusterWideCommandNote());
2313
+ expect(clusterWideNote).toContain(
2314
+ "--set-json 'aiAccess.remediation.namespaces=[]' resets a namespace list stored on the release",
2315
+ );
2316
+ expect(clusterWideNote).not.toContain(
2317
+ "--set aiAccess.remediation.namespaces=null resets",
2318
+ );
2319
+
2320
+ const disclosure: HTMLElement = screen.getByTestId(
2321
+ "ai-access-write-disclosure",
2322
+ );
2323
+ expect(disclosure).toHaveTextContent("any image as any ServiceAccount");
2324
+ expect(disclosure).toHaveTextContent("kube-system");
2325
+ });
2326
+
2327
+ test("stays on the page for a Runner that already writes, saying where", async () => {
2328
+ serveStatus(
2329
+ makeStatus({
2330
+ runner: {
2331
+ ...makeStatus().runner!,
2332
+ posture: {
2333
+ inCluster: true,
2334
+ allowWrites: true,
2335
+ writeNamespaces: ["web", "api"],
2336
+ kubectlVersion: "v1.31.0",
2337
+ },
2338
+ },
2339
+ }),
2340
+ );
2341
+ openAiPage();
2342
+
2343
+ expect(
2344
+ await screen.findByTestId(
2345
+ "ai-access-runner-write-access",
2346
+ {},
2347
+ { timeout: WAIT_TIMEOUT },
2348
+ ),
2349
+ ).toHaveTextContent("writes allowed in web, api");
2350
+ expect(
2351
+ screen.getByTestId("ai-access-remediation-setup"),
2352
+ ).toHaveTextContent(
2353
+ "The in-cluster Runner reports: writes allowed in web, api.",
2354
+ );
2355
+ expect(
2356
+ screen.getByTestId("ai-access-helm-remediation-scoped-command"),
2357
+ ).toBeInTheDocument();
2358
+ });
2359
+
2360
+ test("an external Runner is told its credential's RBAC decides", async () => {
2361
+ serveStatus(
2362
+ makeStatus({
2363
+ runner: {
2364
+ id: HOST_RUNNER_ID,
2365
+ name: "bash-runner",
2366
+ isOnline: true,
2367
+ canRunAiCommands: true,
2368
+ },
2369
+ accessMethod: "credential",
2370
+ credentialId: CREDENTIAL_ID,
2371
+ credentialName: "prod-east token",
2372
+ }),
2373
+ );
2374
+ openAiPage();
2375
+
2376
+ const setup: HTMLElement = await screen.findByTestId(
2377
+ "ai-access-remediation-setup",
2378
+ {},
2379
+ { timeout: WAIT_TIMEOUT },
2380
+ );
2381
+ expect(setup).toHaveTextContent("bounded by that credential's RBAC");
2382
+ /*
2383
+ * Round four: an ordinary Runner reports the write limits it was
2384
+ * started with, so the page names them — and that a fix outside
2385
+ * them is refused before it reaches the Runner — instead of leaving
2386
+ * everything to the credential's RBAC.
2387
+ */
2388
+ expect(setup).toHaveTextContent(
2389
+ "the write limits the Runner was started with, if any (ONEUPTIME_KUBECTL_WRITE_NAMESPACES, ONEUPTIME_KUBECTL_ALLOW_NODE_OPERATIONS=false)",
2390
+ );
2391
+ expect(setup).toHaveTextContent(
2392
+ "The Runner reports those limits, so a fix outside them is refused when it is proposed or approved, before it reaches the Runner.",
2393
+ );
2394
+ expect(
2395
+ screen.queryByTestId("ai-access-runner-write-access"),
2396
+ ).not.toBeInTheDocument();
2397
+ });
2398
+ });
2399
+
2400
+ describe("access test", () => {
2401
+ const TEST_ROUTE: string = "/kubernetes-cluster/ai-access/test";
2402
+
2403
+ /*
2404
+ * The finding: the button was enabled for everyone once a Runner was
2405
+ * bound, but the route needs edit access to the cluster; a reader got
2406
+ * a red "You do not have permission to change this cluster's AI
2407
+ * access" for a read-only test.
2408
+ */
2409
+ test("is locked, with the reason, for a user who may only read the cluster", async () => {
2410
+ grant([...BASE_PERMISSIONS, Permission.ReadKubernetesCluster]);
2411
+ openAiPage();
2412
+
2413
+ const button: HTMLElement = await screen.findByTestId(
2414
+ "ai-access-test-button",
2415
+ {},
2416
+ { timeout: WAIT_TIMEOUT },
2417
+ );
2418
+ expect(button).toBeDisabled();
2419
+ expect(textOf("ai-access-test-permission-note")).toMatch(
2420
+ /Running the access test needs permission to edit this cluster/,
2421
+ );
2422
+ expect(textOf("ai-access-test-permission-note")).toContain(
2423
+ "Edit Kubernetes Cluster",
2424
+ );
2425
+
2426
+ fireEvent.click(button);
2427
+ expect(postsTo(TEST_ROUTE)).toHaveLength(0);
2428
+ });
2429
+
2430
+ test("is hidden while the permission snapshot has not landed", async () => {
2431
+ grant([]);
2432
+ openAiPage();
2433
+
2434
+ await findText(STATUS_CARD_TITLE);
2435
+ expect(
2436
+ screen.queryByTestId("ai-access-test-button"),
2437
+ ).not.toBeInTheDocument();
2438
+ expect(
2439
+ screen.queryByTestId("ai-access-test-permission-note"),
2440
+ ).not.toBeInTheDocument();
2441
+ });
2442
+
2443
+ test("runs once for a member and shows every command's result", async () => {
2444
+ let answer: (value: HTTPResponse<JSONObject>) => void = (): void => {
2445
+ // replaced below
2446
+ };
2447
+ serveStatusAndTest(
2448
+ makeStatus(),
2449
+ (): Promise<HTTPResponse<JSONObject>> => {
2450
+ return new Promise(
2451
+ (resolve: (value: HTTPResponse<JSONObject>) => void) => {
2452
+ answer = resolve;
2453
+ },
2454
+ );
2455
+ },
2456
+ );
2457
+ openAiPage();
2458
+
2459
+ const button: HTMLElement = await screen.findByTestId(
2460
+ "ai-access-test-button",
2461
+ {},
2462
+ { timeout: WAIT_TIMEOUT },
2463
+ );
2464
+ expect(button).not.toBeDisabled();
2465
+ expect(
2466
+ screen.queryByTestId("ai-access-test-permission-note"),
2467
+ ).not.toBeInTheDocument();
2468
+
2469
+ fireEvent.click(button);
2470
+ fireEvent.click(button);
2471
+
2472
+ await waitFor(
2473
+ () => {
2474
+ expect(screen.getByTestId("ai-access-test-button")).toBeDisabled();
2475
+ },
2476
+ { timeout: WAIT_TIMEOUT },
2477
+ );
2478
+ expect(postsTo(TEST_ROUTE)).toHaveLength(1);
2479
+ expect(postsTo(TEST_ROUTE)[0]!["data"]).toEqual({
2480
+ clusterId: CLUSTER_ID.toString(),
2481
+ });
2482
+
2483
+ await act(async () => {
2484
+ answer(
2485
+ new HTTPResponse<JSONObject>(
2486
+ 200,
2487
+ {
2488
+ ok: true,
2489
+ message: "OneUptime AI can run kubectl on prod-east.",
2490
+ results: [
2491
+ {
2492
+ command: "kubectl version",
2493
+ succeeded: true,
2494
+ exitCode: 0,
2495
+ output: "Server Version: v1.31.0",
2496
+ errorMessage: null,
2497
+ },
2498
+ ],
2499
+ status: makeStatus({
2500
+ lastVerifiedAt: "2026-09-22T10:05:00.000Z",
2501
+ }) as unknown as JSONObject,
2502
+ },
2503
+ {},
2504
+ ),
2505
+ );
2506
+ });
2507
+
2508
+ expect(await findText("Access works")).toBeInTheDocument();
2509
+ expect(screen.getByText("succeeded")).toBeInTheDocument();
2510
+ expect(screen.getByText("Server Version: v1.31.0")).toBeInTheDocument();
2511
+ expect(
2512
+ screen.getByText(/Last successful kubectl command:/),
2513
+ ).toBeInTheDocument();
2514
+ expect(screen.getByTestId("ai-access-test-button")).not.toBeDisabled();
2515
+ });
2516
+
2517
+ test("shows a failed command with its exit code", async () => {
2518
+ serveStatusAndTest(
2519
+ makeStatus(),
2520
+ async (): Promise<HTTPResponse<JSONObject>> => {
2521
+ return new HTTPResponse<JSONObject>(
2522
+ 200,
2523
+ {
2524
+ ok: false,
2525
+ message: "kubectl could not run successfully.",
2526
+ results: [
2527
+ {
2528
+ command: "kubectl version",
2529
+ succeeded: false,
2530
+ exitCode: 1,
2531
+ output: "",
2532
+ errorMessage: "forbidden",
2533
+ },
2534
+ ],
2535
+ },
2536
+ {},
2537
+ );
2538
+ },
2539
+ );
2540
+ openAiPage();
2541
+
2542
+ fireEvent.click(
2543
+ await screen.findByTestId(
2544
+ "ai-access-test-button",
2545
+ {},
2546
+ { timeout: WAIT_TIMEOUT },
2547
+ ),
2548
+ );
2549
+
2550
+ expect(await findText("Access is not working yet")).toBeInTheDocument();
2551
+ expect(screen.getByText("failed (exit 1)")).toBeInTheDocument();
2552
+ expect(screen.getByText("forbidden")).toBeInTheDocument();
2553
+ });
2554
+
2555
+ test("shows an error the server returns", async () => {
2556
+ serveStatusAndTest(makeStatus(), async (): Promise<HTTPErrorResponse> => {
2557
+ return new HTTPErrorResponse(
2558
+ 400,
2559
+ { error: "Kubernetes cluster not found." },
2560
+ {},
2561
+ );
2562
+ });
2563
+ openAiPage();
2564
+
2565
+ fireEvent.click(
2566
+ await screen.findByTestId(
2567
+ "ai-access-test-button",
2568
+ {},
2569
+ { timeout: WAIT_TIMEOUT },
2570
+ ),
2571
+ );
2572
+
2573
+ const error: HTMLElement = await screen.findByTestId(
2574
+ "ai-access-test-error",
2575
+ {},
2576
+ { timeout: WAIT_TIMEOUT },
2577
+ );
2578
+ expect(error).toHaveTextContent("The test could not run");
2579
+ expect(error).toHaveTextContent("Kubernetes cluster not found.");
2580
+ });
2581
+
2582
+ test("a permission refusal talks about running the test, not changing settings", async () => {
2583
+ serveStatusAndTest(makeStatus(), async (): Promise<HTTPErrorResponse> => {
2584
+ return new HTTPErrorResponse(
2585
+ 422,
2586
+ {
2587
+ error:
2588
+ "You do not have permission to change this cluster's AI access.",
2589
+ },
2590
+ {},
2591
+ );
2592
+ });
2593
+ openAiPage();
2594
+
2595
+ fireEvent.click(
2596
+ await screen.findByTestId(
2597
+ "ai-access-test-button",
2598
+ {},
2599
+ { timeout: WAIT_TIMEOUT },
2600
+ ),
2601
+ );
2602
+
2603
+ const error: HTMLElement = await screen.findByTestId(
2604
+ "ai-access-test-error",
2605
+ {},
2606
+ { timeout: WAIT_TIMEOUT },
2607
+ );
2608
+ expect(error).toHaveTextContent(
2609
+ "Running the access test needs permission to edit this cluster",
2610
+ );
2611
+ expect(error).toHaveTextContent("Nothing on the cluster");
2612
+ expect(error).not.toHaveTextContent("change this cluster's AI access");
2613
+ });
2614
+ });
2615
+
2616
+ /*
2617
+ * The finding: Settings roles and ReadKubernetesCluster may open this
2618
+ * page but not read RunnerJob rows, so the command history showed a
2619
+ * permission error. RunnerJob's ACL is not widened; they get an
2620
+ * explanation and no failing request.
2621
+ */
2622
+ describe("command history permission", () => {
2623
+ for (const permission of [
2624
+ Permission.SettingsAdmin,
2625
+ Permission.SettingsViewer,
2626
+ Permission.ReadKubernetesCluster,
2627
+ ]) {
2628
+ test(`explains instead of failing for ${permission}`, async () => {
2629
+ grant([...BASE_PERMISSIONS, permission]);
2630
+ openAiPage();
2631
+
2632
+ const note: HTMLElement = await screen.findByTestId(
2633
+ "kubectl-jobs-permission-note",
2634
+ {},
2635
+ { timeout: WAIT_TIMEOUT },
2636
+ );
2637
+ expect(note).toHaveTextContent("Runbook Viewer");
2638
+ expect(note).toHaveTextContent("Viewer");
2639
+ expect(
2640
+ screen.getByText("Commands OneUptime AI ran on this cluster"),
2641
+ ).toBeInTheDocument();
2642
+ expect(jobListRequest()).toBeUndefined();
2643
+ });
2644
+ }
2645
+
2646
+ for (const permission of [Permission.ProjectMember, Permission.Viewer]) {
2647
+ test(`shows the table for ${permission}`, async () => {
2648
+ grant([...BASE_PERMISSIONS, permission]);
2649
+ openAiPage();
2650
+
2651
+ await findText(
2652
+ "OneUptime AI has not run any kubectl commands on this cluster yet.",
2653
+ );
2654
+ expect(
2655
+ screen.queryByTestId("kubectl-jobs-permission-note"),
2656
+ ).not.toBeInTheDocument();
2657
+ const request: ListRequest | undefined = jobListRequest();
2658
+ expect(request?.query).toEqual(
2659
+ expect.objectContaining({ stepType: RunbookStepType.Kubectl }),
2660
+ );
2661
+ expect(String(request?.query?.["kubernetesClusterId"])).toBe(
2662
+ CLUSTER_ID.toString(),
2663
+ );
2664
+ });
2665
+ }
2666
+ });
2667
+
2668
+ describe("settings edit button", () => {
2669
+ test("is locked, with the reason, for a user who may only read the cluster", async () => {
2670
+ grant([...BASE_PERMISSIONS, Permission.ReadKubernetesCluster]);
2671
+ openAiPage();
2672
+
2673
+ await findText(SETTINGS_CARD_TITLE);
2674
+ const button: HTMLElement | null = (
2675
+ await findText("Edit AI access")
2676
+ ).closest("button");
2677
+ expect(button).not.toBeNull();
2678
+ expect(button).toBeDisabled();
2679
+
2680
+ fireEvent.click(button as HTMLElement);
2681
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
2682
+ // A reader is not told about loosening permissions they could not use.
2683
+ expect(
2684
+ screen.queryByTestId("kubernetes-ai-access-admin-note"),
2685
+ ).not.toBeInTheDocument();
2686
+ });
2687
+ });
2688
+ });