@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,2886 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ jest,
7
+ test,
8
+ } from "@jest/globals";
9
+ import {
10
+ AI_ACCESS_EXAMPLE_WRITE_NAMESPACES,
11
+ AiAccessHelmCommands,
12
+ KUBERNETES_AGENT_HELM_NAMESPACE,
13
+ KUBERNETES_AGENT_HELM_RELEASE,
14
+ KubernetesAiAccessConfirmation,
15
+ KubernetesAiAccessOfferedFields,
16
+ KubernetesAiAccessSavedSettings,
17
+ KubernetesAiCredentialDirectoryEntry,
18
+ KubernetesAiRunnerDirectoryEntry,
19
+ REMEDIATION_MODES_BY_AUTONOMY,
20
+ REMEDIATION_MODE_LABELS,
21
+ SavedKubectlAllowlist,
22
+ buildKubernetesAiCredentialDirectory,
23
+ buildKubernetesAiCredentialOptions,
24
+ buildKubernetesAiRunnerDirectory,
25
+ buildKubernetesAiRunnerOptions,
26
+ canConfigureUnattendedKubernetesAiAccess,
27
+ canPickKubernetesRunner,
28
+ canReadKubectlJobs,
29
+ describeRunnerWriteAccess,
30
+ getAccessTestPermissionGate,
31
+ getAccessTestPermissionMessage,
32
+ getAiAccessClusterWideCommandNote,
33
+ getAiAccessConnectCardMode,
34
+ getAiAccessHelmCommands,
35
+ getAiAccessScopedCommandNote,
36
+ getAiAccessWriteDisclosure,
37
+ getEveryModeProtectionsSentence,
38
+ getKubectlAllowlistRemovalOnlyError,
39
+ getKubectlJobsPermissionTitles,
40
+ getKubernetesAiAccessAdminPermissionTitles,
41
+ getKubernetesAiAccessBindingError,
42
+ getKubernetesAiAccessChosenRunnerId,
43
+ getKubernetesAiAccessConfirmation,
44
+ getKubernetesAiAccessEditCapabilities,
45
+ getKubernetesAiAccessLooseningChanges,
46
+ getKubernetesAiAccessOfferedFields,
47
+ getKubernetesAiAccessSettingsChanges,
48
+ getKubernetesAiAccessSettingsInitialValues,
49
+ getKubernetesAiCredentialAssignmentError,
50
+ getKubernetesAiCredentialFieldDescription,
51
+ getKubernetesRunnerPermissionTitles,
52
+ getRemediationModeFieldDescription,
53
+ isRemediationModeOpenToEveryEditor,
54
+ isThisClustersAgentRunner,
55
+ normalizeSavedKubectlAllowlist,
56
+ parseKubectlAllowlistText,
57
+ readKubernetesAiAccessSavedSettings,
58
+ readStoredKubectlAllowlist,
59
+ validateKubectlAllowlistText,
60
+ } from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/AI";
61
+ import { getKubernetesInstallationMarkdown } from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/DocumentationMarkdown";
62
+ import { isKubernetesAgentRunnerRow } from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesAgentRunner";
63
+ import KubernetesCluster from "../../../Models/DatabaseModels/KubernetesCluster";
64
+ import RunbookCredential from "../../../Models/DatabaseModels/RunbookCredential";
65
+ import Runner from "../../../Models/DatabaseModels/Runner";
66
+ import { AiRemediationCommandPolicyVerdict } from "../../../Types/AutoRemediation/AiRemediationCommandPlan";
67
+ import { JSONObject } from "../../../Types/JSON";
68
+ import {
69
+ KubernetesAiAccessGap,
70
+ KubernetesAiRemediationMode,
71
+ KubernetesClusterAiAccessStatus,
72
+ PROTECTED_KUBERNETES_NAMESPACES,
73
+ } from "../../../Types/Kubernetes/KubernetesClusterAiAccess";
74
+ import { KUBERNETES_AI_ACCESS_ADMIN_PERMISSIONS } from "../../../Types/Kubernetes/KubernetesClusterAiAccessPermissions";
75
+ import ObjectID from "../../../Types/ObjectID";
76
+ import Permission from "../../../Types/Permission";
77
+ import RunbookCredentialType from "../../../Types/Runbook/RunbookCredentialType";
78
+ import { DropdownOption } from "../../../UI/Components/Dropdown/Dropdown";
79
+ import FormValues from "../../../UI/Components/Forms/Types/FormValues";
80
+ import { PermissionGateResult } from "../../../UI/Utils/PermissionGate";
81
+ import PermissionUtil from "../../../UI/Utils/Permission";
82
+ import User from "../../../UI/Utils/User";
83
+ import KubectlPolicy, {
84
+ KUBECTL_ALLOWLIST_MAX_PATTERNS,
85
+ KUBECTL_ALLOWLIST_MAX_PATTERN_LENGTH,
86
+ } from "../../../Utils/AiRemediation/KubectlPolicy";
87
+ import fs from "fs";
88
+ import path from "path";
89
+
90
+ /*
91
+ * The pure pieces behind the cluster AI page's settings: who may loosen
92
+ * what AI does (relative to the saved settings, as the server decides it),
93
+ * what an edit actually sends, when a save is confirmed first, how the
94
+ * allowlist is read and checked (by KubectlPolicy, the matcher's own
95
+ * rules), which Runners and credentials the pickers offer, which card helps
96
+ * connect a Runner, and the helm upgrades. The rendered page is covered by
97
+ * KubernetesClusterAiPage.test.tsx; the server-side parity checks by
98
+ * KubernetesClusterAiPageServerParity.test.ts.
99
+ */
100
+
101
+ const PROJECT_ID: string = "8f2a1b3c-4d5e-4f60-9a7b-1c2d3e4f5a6b";
102
+ const RUNNER_ID: string = "55555555-0000-4000-8000-000000000005";
103
+ const OTHER_RUNNER_ID: string = "55555555-0000-4000-8000-000000000006";
104
+ const CREDENTIAL_ID: string = "77777777-0000-4000-8000-000000000007";
105
+
106
+ const BASE_PERMISSIONS: Array<Permission> = [
107
+ Permission.Public,
108
+ Permission.User,
109
+ Permission.CurrentUser,
110
+ ];
111
+
112
+ const SET_IMAGE_PATTERN: string = "kubectl set image deployment/web * -n web";
113
+ const PATCH_PATTERN: string = "kubectl patch deployment/web -n web -p *";
114
+ /*
115
+ * A broad but valid entry: deleting any Deployment in any namespace. The
116
+ * fixtures used "kubectl * * * -n *" until KubectlPolicy made a wildcard
117
+ * verb invalid (round two): such an entry is refused where it is typed and
118
+ * skipped by the matcher, so it is never broad and never confirmed.
119
+ */
120
+ const BROAD_PATTERN: string = "kubectl delete deployment * -n *";
121
+ // Wildcard verbs (and a lone "*"): invalid, refused by the form.
122
+ const VERB_WILDCARD_PATTERNS: Array<string> = [
123
+ "*",
124
+ "kubectl *",
125
+ "kubectl * * * -n *",
126
+ "* * -n web",
127
+ ];
128
+
129
+ // A line continuation left dangling at the end of a command.
130
+ const TRAILING_CONTINUATION_REGEX: RegExp = /\\\s*$/;
131
+ const CHART_VERSION_FLAG_REGEX: RegExp = /--version/;
132
+ const OLD_CHART_VERSION_REGEX: RegExp = /0\.7\.0/;
133
+ const MATCHES_NEARLY_EVERYTHING_REGEX: RegExp = /nearly/;
134
+ const ANY_IMAGE_ANY_SERVICE_ACCOUNT_REGEX: RegExp =
135
+ /any image as any ServiceAccount/;
136
+
137
+ /*
138
+ * The reset of aiAccess.remediation.namespaces that works under
139
+ * `helm upgrade --reuse-values`, as the page prints it. `--set ...=null`
140
+ * does not: Helm drops a null override whose key the release's stored
141
+ * values hold (the stored list survives), and on a release from a chart
142
+ * without aiAccess the null reaches the values schema and fails the
143
+ * upgrade. Reproduced with the real helm binary against a stored release;
144
+ * helm-unittest renders from values files and cannot show it.
145
+ */
146
+ const EMPTY_LIST_RESET_FLAG: string =
147
+ "--set-json 'aiAccess.remediation.namespaces=[]'";
148
+ // A backslash-newline line continuation, which the shell reads as a space.
149
+ const LINE_CONTINUATION_REGEX: RegExp = /\\\n/g;
150
+ // One shell word: a run of unquoted, single-quoted or double-quoted parts.
151
+ const SHELL_WORD_REGEX: RegExp = /(?:[^\s'"]+|'[^']*'|"[^"]*")+/g;
152
+ // The quotes around one quoted part of a shell word.
153
+ const SHELL_QUOTED_PART_REGEX: RegExp = /'([^']*)'|"([^"]*)"/g;
154
+ // A --set value that sets the namespace list to null.
155
+ const NULL_NAMESPACES_VALUE_REGEX: RegExp =
156
+ /^aiAccess\.remediation\.namespaces=null$/;
157
+
158
+ /*
159
+ * The argv of the `helm upgrade` line in a copy-paste command, the way a
160
+ * POSIX shell splits it: line continuations joined, quotes removed. Only
161
+ * what the page's commands use (no escapes inside quotes, no expansion).
162
+ */
163
+ function getHelmUpgradeArgv(command: string): Array<string> {
164
+ const line: string | undefined = command
165
+ .replace(LINE_CONTINUATION_REGEX, " ")
166
+ .split("\n")
167
+ .find((candidate: string): boolean => {
168
+ return candidate.trim().startsWith("helm upgrade ");
169
+ });
170
+ if (!line) {
171
+ return [];
172
+ }
173
+ return (line.match(SHELL_WORD_REGEX) || []).map((word: string): string => {
174
+ return word.replace(
175
+ SHELL_QUOTED_PART_REGEX,
176
+ (_quoted: string, single?: string, double?: string): string => {
177
+ return single ?? double ?? "";
178
+ },
179
+ );
180
+ });
181
+ }
182
+
183
+ // The value each occurrence of `flag` takes in an argv.
184
+ function getFlagValues(argv: Array<string>, flag: string): Array<string> {
185
+ const values: Array<string> = [];
186
+ argv.forEach((word: string, index: number) => {
187
+ if (word === flag && index + 1 < argv.length) {
188
+ values.push(argv[index + 1]!);
189
+ }
190
+ });
191
+ return values;
192
+ }
193
+
194
+ function grant(
195
+ permissions: Array<Permission>,
196
+ blocked: Array<Permission> = [],
197
+ ): void {
198
+ jest.spyOn(User, "isMasterAdmin").mockReturnValue(false);
199
+ jest
200
+ .spyOn(PermissionUtil, "getAllPermissions")
201
+ .mockReturnValue([...permissions, ...blocked]);
202
+ jest.spyOn(PermissionUtil, "getGlobalPermissions").mockReturnValue(null);
203
+ jest.spyOn(PermissionUtil, "getProjectPermissions").mockReturnValue({
204
+ projectId: new ObjectID(PROJECT_ID),
205
+ userId: ObjectID.generate(),
206
+ permissions: [
207
+ ...permissions.map((permission: Permission) => {
208
+ return {
209
+ permission: permission,
210
+ labelIds: [],
211
+ _type: "UserPermission",
212
+ };
213
+ }),
214
+ ...blocked.map((permission: Permission) => {
215
+ return {
216
+ permission: permission,
217
+ labelIds: [],
218
+ isBlockPermission: true,
219
+ _type: "UserPermission",
220
+ };
221
+ }),
222
+ ],
223
+ _type: "UserTenantAccessPermission",
224
+ } as unknown as ReturnType<typeof PermissionUtil.getProjectPermissions>);
225
+ }
226
+
227
+ // The permission snapshot has not landed yet: nothing stored at all.
228
+ function grantNothingYet(): void {
229
+ jest.spyOn(User, "isMasterAdmin").mockReturnValue(false);
230
+ jest.spyOn(PermissionUtil, "getAllPermissions").mockReturnValue([]);
231
+ jest.spyOn(PermissionUtil, "getGlobalPermissions").mockReturnValue(null);
232
+ jest.spyOn(PermissionUtil, "getProjectPermissions").mockReturnValue(null);
233
+ }
234
+
235
+ function makeSaved(
236
+ overrides: Partial<KubernetesAiAccessSavedSettings> = {},
237
+ ): KubernetesAiAccessSavedSettings {
238
+ return {
239
+ isAiInvestigationEnabled: true,
240
+ aiRemediationMode: KubernetesAiRemediationMode.Automatic,
241
+ aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN],
242
+ aiAccessRunnerId: RUNNER_ID,
243
+ aiAccessRunnerName: "kubernetes-agent/prod-east",
244
+ aiAccessCredentialId: null,
245
+ aiAccessCredentialName: null,
246
+ ...overrides,
247
+ };
248
+ }
249
+
250
+ const NOTHING_OFFERED: KubernetesAiAccessOfferedFields = {
251
+ allowlist: false,
252
+ allowlistRemoveOnly: false,
253
+ runner: false,
254
+ credential: false,
255
+ runnerClear: false,
256
+ credentialClear: false,
257
+ };
258
+
259
+ const EVERYTHING_OFFERED: KubernetesAiAccessOfferedFields = {
260
+ allowlist: true,
261
+ allowlistRemoveOnly: false,
262
+ runner: true,
263
+ credential: true,
264
+ runnerClear: false,
265
+ credentialClear: false,
266
+ };
267
+
268
+ // What a cluster editor without the admin set and without the pickers gets.
269
+ const TIGHTEN_ONLY_OFFERED: KubernetesAiAccessOfferedFields = {
270
+ allowlist: true,
271
+ allowlistRemoveOnly: true,
272
+ runner: false,
273
+ credential: false,
274
+ runnerClear: true,
275
+ credentialClear: true,
276
+ };
277
+
278
+ type SettingsFormValues = FormValues<{
279
+ isAiInvestigationEnabled: boolean;
280
+ aiRemediationMode: string;
281
+ kubectlAllowlistText: string;
282
+ aiAccessRunnerId: string;
283
+ aiAccessCredentialId: string;
284
+ clearAiAccessRunner: boolean;
285
+ clearAiAccessCredential: boolean;
286
+ }>;
287
+
288
+ // What the form holds for a saved state, with the given edits applied.
289
+ function formValues(
290
+ saved: KubernetesAiAccessSavedSettings,
291
+ edits: Record<string, unknown> = {},
292
+ ): SettingsFormValues {
293
+ return {
294
+ ...getKubernetesAiAccessSettingsInitialValues(saved),
295
+ ...edits,
296
+ } as SettingsFormValues;
297
+ }
298
+
299
+ // A Runner row; `clusterIdentifier` gives it an in-cluster agent posture.
300
+ function makeRunner(
301
+ id: string,
302
+ name: string,
303
+ canRunAiCommands: boolean = true,
304
+ clusterIdentifier?: string,
305
+ ): Runner {
306
+ return Object.assign(new Runner(), {
307
+ _id: id,
308
+ name,
309
+ canRunAiCommands,
310
+ ...(clusterIdentifier
311
+ ? {
312
+ hostInfo: {
313
+ kubernetes: {
314
+ inCluster: true,
315
+ allowWrites: false,
316
+ clusterIdentifier,
317
+ },
318
+ },
319
+ }
320
+ : {}),
321
+ });
322
+ }
323
+
324
+ function makeCredential(
325
+ id: string,
326
+ name: string,
327
+ runnerIds: Array<string> | undefined,
328
+ credentialType: RunbookCredentialType = RunbookCredentialType.Kubernetes,
329
+ ): RunbookCredential {
330
+ return Object.assign(new RunbookCredential(), {
331
+ _id: id,
332
+ name,
333
+ credentialType,
334
+ ...(runnerIds
335
+ ? {
336
+ runners: runnerIds.map((runnerId: string): Runner => {
337
+ return Object.assign(new Runner(), { _id: runnerId });
338
+ }),
339
+ }
340
+ : {}),
341
+ });
342
+ }
343
+
344
+ function optionValues(options: Array<DropdownOption>): Array<string> {
345
+ return options.map((option: DropdownOption): string => {
346
+ return String(option.value);
347
+ });
348
+ }
349
+
350
+ function optionLabel(
351
+ options: Array<DropdownOption>,
352
+ value: string,
353
+ ): string | undefined {
354
+ return options.find((option: DropdownOption): boolean => {
355
+ return option.value === value;
356
+ })?.label;
357
+ }
358
+
359
+ beforeEach(() => {
360
+ grant([...BASE_PERMISSIONS, Permission.ProjectMember]);
361
+ });
362
+
363
+ afterEach(() => {
364
+ jest.restoreAllMocks();
365
+ });
366
+
367
+ describe("helm upgrades", () => {
368
+ /*
369
+ * The finding: the page printed `helm upgrade oneuptime-agent ...
370
+ * --namespace oneuptime-kubernetes-agent`, while the Dashboard's own
371
+ * install instructions install `kubernetes-agent` into `oneuptime-agent`.
372
+ * helm answers a missing release with "has no deployed releases", so a
373
+ * cluster installed the way the product said could not connect. The pair
374
+ * is now one exported constant pair in DocumentationMarkdown.ts.
375
+ */
376
+ const installMarkdown: string = getKubernetesInstallationMarkdown({
377
+ clusterName: "prod-east",
378
+ oneuptimeUrl: "https://oneuptime.example.com",
379
+ apiKey: "key",
380
+ });
381
+
382
+ function allCommands(): Array<string> {
383
+ const commands: AiAccessHelmCommands = getAiAccessHelmCommands();
384
+ return Object.values(commands);
385
+ }
386
+
387
+ function installPairs(): Array<{ release: string; namespace: string }> {
388
+ const pairs: Array<{ release: string; namespace: string }> = [];
389
+ const pattern: RegExp =
390
+ /helm (?:install|upgrade) (\S+) oneuptime\/kubernetes-agent[\s\\]+--namespace (\S+)/g;
391
+ let match: RegExpExecArray | null = pattern.exec(installMarkdown);
392
+ while (match) {
393
+ pairs.push({ release: match[1]!, namespace: match[2]! });
394
+ match = pattern.exec(installMarkdown);
395
+ }
396
+ return pairs;
397
+ }
398
+
399
+ test("uses the release and namespace of the Dashboard's install instructions", () => {
400
+ const pairs: Array<{ release: string; namespace: string }> = installPairs();
401
+
402
+ // Harness guard: the parse found the install commands to compare with.
403
+ expect(pairs.length).toBeGreaterThan(0);
404
+
405
+ for (const pair of pairs) {
406
+ expect(pair).toEqual({
407
+ release: KUBERNETES_AGENT_HELM_RELEASE,
408
+ namespace: KUBERNETES_AGENT_HELM_NAMESPACE,
409
+ });
410
+ }
411
+
412
+ for (const command of allCommands()) {
413
+ expect(command).toContain(
414
+ `helm upgrade ${pairs[0]!.release} oneuptime/kubernetes-agent`,
415
+ );
416
+ expect(command).toContain(`--namespace ${pairs[0]!.namespace}`);
417
+ expect(command).not.toContain("oneuptime-kubernetes-agent");
418
+ expect(command).toContain("--reuse-values");
419
+ }
420
+ });
421
+
422
+ test("the install markdown names the namespace and release only through the shared pair", () => {
423
+ // Every -n / --namespace / uninstall in the instructions uses the pair.
424
+ const namespaces: Array<string> = Array.from(
425
+ installMarkdown.matchAll(/(?:--namespace|-n) ([\w.-]+)/g),
426
+ ).map((match: RegExpMatchArray): string => {
427
+ return match[1]!;
428
+ });
429
+ expect(namespaces.length).toBeGreaterThan(5);
430
+ for (const namespace of namespaces) {
431
+ expect(namespace).toBe(KUBERNETES_AGENT_HELM_NAMESPACE);
432
+ }
433
+ expect(installMarkdown).toContain(
434
+ `helm uninstall ${KUBERNETES_AGENT_HELM_RELEASE} --namespace ${KUBERNETES_AGENT_HELM_NAMESPACE}`,
435
+ );
436
+ expect(installMarkdown).toContain(
437
+ `deployment/${KUBERNETES_AGENT_HELM_RELEASE}`,
438
+ );
439
+ });
440
+
441
+ /*
442
+ * Every command, not only the read-only one: the write-access commands
443
+ * are offered on their own, so an operator who runs one directly on an
444
+ * old chart index would otherwise hit "Additional property aiAccess is
445
+ * not allowed".
446
+ */
447
+ test("every command updates the chart index first", () => {
448
+ expect(allCommands()).toHaveLength(3);
449
+ for (const command of allCommands()) {
450
+ expect(command.startsWith("helm repo update\n")).toBe(true);
451
+ expect(command.indexOf("helm repo update")).toBeLessThan(
452
+ command.indexOf("helm upgrade"),
453
+ );
454
+ }
455
+ });
456
+
457
+ test("the default command grants read-only access", () => {
458
+ const readOnly: string = getAiAccessHelmCommands().readOnly;
459
+ expect(readOnly).toContain("--set aiAccess.enabled=true");
460
+ expect(readOnly).not.toContain("remediation");
461
+ });
462
+
463
+ test("the recommended write-access command binds the write role only in the listed namespaces and keeps fixes off nodes", () => {
464
+ const scoped: string = getAiAccessHelmCommands().enableRemediationScoped;
465
+ expect(scoped).toContain("--set aiAccess.enabled=true");
466
+ expect(scoped).toContain("--set aiAccess.remediation.enabled=true");
467
+ expect(scoped).toContain(
468
+ `--set "aiAccess.remediation.namespaces=${AI_ACCESS_EXAMPLE_WRITE_NAMESPACES}"`,
469
+ );
470
+ expect(scoped).toContain("--set aiAccess.remediation.nodeOperations=false");
471
+
472
+ const clusterWide: string = getAiAccessHelmCommands().enableRemediation;
473
+ expect(clusterWide).toContain("--set aiAccess.remediation.enabled=true");
474
+ /*
475
+ * Replaces the pin that the cluster-wide command names no namespaces:
476
+ * under --reuse-values that left a list stored by the scoped command in
477
+ * force, so "write access across the cluster" bound the role only
478
+ * where the old list said. It now resets the list — with an empty JSON
479
+ * list, not `=null` (round four: under --reuse-values Helm dropped the
480
+ * null and kept a stored list, and on an install from a chart without
481
+ * aiAccess the null failed the values schema, so the command failed
482
+ * for every existing install).
483
+ */
484
+ expect(clusterWide).toContain(EMPTY_LIST_RESET_FLAG);
485
+ expect(clusterWide).not.toContain("namespaces=null");
486
+ expect(clusterWide).not.toContain(AI_ACCESS_EXAMPLE_WRITE_NAMESPACES);
487
+ expect(clusterWide).not.toContain("nodeOperations");
488
+ });
489
+
490
+ /*
491
+ * What helm receives, not only what the page prints: the shell hands
492
+ * `--set-json` one word, `aiAccess.remediation.namespaces=[]` (the single
493
+ * quotes keep a shell such as zsh from reading `[]` as a glob), whose
494
+ * value is an empty JSON list — a value the chart's schema (namespaces is
495
+ * an array) accepts and that replaces a stored list under --reuse-values.
496
+ */
497
+ test("the cluster-wide command hands helm an empty JSON list for the namespaces", () => {
498
+ const argv: Array<string> = getHelmUpgradeArgv(
499
+ getAiAccessHelmCommands().enableRemediation,
500
+ );
501
+ expect(argv).toContain("--reuse-values");
502
+
503
+ const setJson: Array<string> = getFlagValues(argv, "--set-json");
504
+ expect(setJson).toEqual(["aiAccess.remediation.namespaces=[]"]);
505
+ const assignment: string = setJson[0]!;
506
+ const separator: number = assignment.indexOf("=");
507
+ expect(assignment.slice(0, separator)).toBe(
508
+ "aiAccess.remediation.namespaces",
509
+ );
510
+ expect(JSON.parse(assignment.slice(separator + 1))).toEqual([]);
511
+
512
+ // Negative control: the helper does read a null reset where one is.
513
+ const withNull: Array<string> = getHelmUpgradeArgv(
514
+ "helm repo update\nhelm upgrade r c --reuse-values \\\n --set aiAccess.remediation.namespaces=null",
515
+ );
516
+ expect(
517
+ getFlagValues(withNull, "--set").some((setting: string): boolean => {
518
+ return NULL_NAMESPACES_VALUE_REGEX.test(setting);
519
+ }),
520
+ ).toBe(true);
521
+ });
522
+
523
+ /*
524
+ * No command the page prints pairs --reuse-values (which every one of
525
+ * them uses) with a null namespace list: that pairing is the one that
526
+ * keeps a stored list, or fails the upgrade on an install from a chart
527
+ * without aiAccess.
528
+ */
529
+ test("no command pairs --reuse-values with a null namespace list", () => {
530
+ expect(allCommands()).toHaveLength(3);
531
+ for (const command of allCommands()) {
532
+ const argv: Array<string> = getHelmUpgradeArgv(command);
533
+ expect({
534
+ command,
535
+ reuseValues: argv.includes("--reuse-values"),
536
+ nullNamespaces: getFlagValues(argv, "--set").some(
537
+ (setting: string): boolean => {
538
+ return NULL_NAMESPACES_VALUE_REGEX.test(setting);
539
+ },
540
+ ),
541
+ nullText: command.includes("namespaces=null"),
542
+ }).toEqual({
543
+ command,
544
+ reuseValues: true,
545
+ nullNamespaces: false,
546
+ nullText: false,
547
+ });
548
+ }
549
+ });
550
+
551
+ /*
552
+ * What the page says under the two write-access commands: every listed
553
+ * namespace must already exist (the chart never creates one, and a
554
+ * missing one fails the whole upgrade), the empty-list `--set-json`
555
+ * resets a stored list (and `=null` does not, under --reuse-values),
556
+ * turning node operations back on takes `=true` under --reuse-values,
557
+ * and a write outside the list is refused before it reaches the Runner.
558
+ */
559
+ test("the scoped command's note says what the chart and the server do with the list", () => {
560
+ const note: string = getAiAccessScopedCommandNote();
561
+ expect(note).toContain(AI_ACCESS_EXAMPLE_WRITE_NAMESPACES);
562
+ expect(note).toContain("Every namespace you list must already exist");
563
+ expect(note).toContain("never creates a namespace");
564
+ expect(note).toContain('namespaces "<name>" not found');
565
+ expect(note).toContain(
566
+ `${EMPTY_LIST_RESET_FLAG} resets it to cluster-wide, as the next command does.`,
567
+ );
568
+ // The round-three claim, which Helm does not honour under --reuse-values.
569
+ expect(note).not.toContain("=null resets it to cluster-wide");
570
+ expect(note).toContain(
571
+ "--set aiAccess.remediation.namespaces=null does not reset a stored list under --reuse-values",
572
+ );
573
+ expect(note).toContain(
574
+ "it only works with --reset-then-reuse-values (Helm 3.14+) in place of --reuse-values",
575
+ );
576
+ expect(note).toContain(
577
+ "a fix anywhere else is refused when it is proposed or approved, and the Runner refuses it again",
578
+ );
579
+ expect(note).toContain("set it to true to let AI cordon");
580
+ /*
581
+ * Round four: a patch of a node joins the drain and the taint (a taint
582
+ * written as a node patch is the same change), and it still waits for
583
+ * a human when node operations are turned on.
584
+ */
585
+ expect(note).toContain(
586
+ "a drain, a taint or a patch of a node still waits for a human",
587
+ );
588
+ // The old advice: dropping the line keeps a stored false under --reuse-values.
589
+ expect(note).not.toContain("leave that line out");
590
+ });
591
+
592
+ test("the cluster-wide command's note says the list is reset and node operations are kept", () => {
593
+ const note: string = getAiAccessClusterWideCommandNote();
594
+ expect(note.startsWith(`${EMPTY_LIST_RESET_FLAG} resets`)).toBe(true);
595
+ expect(note).toContain("bound cluster-wide");
596
+ expect(note).toContain("--set-json needs Helm 3.10 or later");
597
+ expect(note).toContain(
598
+ "--set aiAccess.remediation.namespaces=null does not reset a stored list under --reuse-values",
599
+ );
600
+ expect(note).toContain("--reset-then-reuse-values (Helm 3.14+)");
601
+ expect(note).toContain("Node operations keep the release's setting");
602
+ });
603
+
604
+ test("every command is complete, never a line to append", () => {
605
+ for (const command of allCommands()) {
606
+ // No trailing line continuation: the shell would wait for more input.
607
+ expect(TRAILING_CONTINUATION_REGEX.test(command)).toBe(false);
608
+
609
+ // Every continued line is followed by a real one.
610
+ const lines: Array<string> = command.split("\n");
611
+ lines.forEach((line: string, index: number) => {
612
+ if (line.trimEnd().endsWith("\\")) {
613
+ expect((lines[index + 1] || "").trim().length).toBeGreaterThan(0);
614
+ }
615
+ });
616
+ }
617
+ });
618
+
619
+ test("names no chart version (published charts carry the OneUptime version)", () => {
620
+ for (const command of allCommands()) {
621
+ expect(command).not.toMatch(CHART_VERSION_FLAG_REGEX);
622
+ expect(command).not.toMatch(OLD_CHART_VERSION_REGEX);
623
+ }
624
+ });
625
+
626
+ /*
627
+ * The chart docs must say what write RBAC amounts to wherever write
628
+ * access is offered; this page is where customers copy the command.
629
+ */
630
+ test("the write-access disclosure says what patch access amounts to and where the policy holds the line", () => {
631
+ const disclosure: string = getAiAccessWriteDisclosure();
632
+ expect(disclosure).toMatch(ANY_IMAGE_ANY_SERVICE_ACCOUNT_REGEX);
633
+ expect(disclosure).toContain("reading its Secrets");
634
+ expect(disclosure).toContain("aiAccess.remediation.namespaces");
635
+ expect(disclosure).toContain("aiAccess.remediation.nodeOperations=false");
636
+ expect(disclosure).toContain("own namespace");
637
+ for (const namespace of PROTECTED_KUBERNETES_NAMESPACES) {
638
+ expect(disclosure).toContain(namespace);
639
+ }
640
+ // Refused up front (the toolkit, the approval route, the enqueue), not only on the Runner.
641
+ expect(disclosure).toContain(
642
+ "a write anywhere else is refused when it is proposed or approved, and again by the Runner",
643
+ );
644
+ expect(disclosure).not.toContain(
645
+ "and the Runner refuses a write anywhere else.",
646
+ );
647
+ });
648
+ });
649
+
650
+ describe("the bound Runner's write access", () => {
651
+ test("says where an in-cluster Runner may write", () => {
652
+ expect(describeRunnerWriteAccess(undefined)).toBeNull();
653
+ // A Runner outside the cluster: its credential's RBAC decides.
654
+ expect(describeRunnerWriteAccess({ inCluster: false })).toBeNull();
655
+ expect(
656
+ describeRunnerWriteAccess({ inCluster: true, allowWrites: false }),
657
+ ).toBe("read-only RBAC");
658
+ // Absent: an older Runner that did not say.
659
+ expect(
660
+ describeRunnerWriteAccess({ inCluster: true, allowWrites: true }),
661
+ ).toBe("writes allowed");
662
+ expect(
663
+ describeRunnerWriteAccess({
664
+ inCluster: true,
665
+ allowWrites: true,
666
+ writeNamespaces: [],
667
+ }),
668
+ ).toBe("writes allowed cluster-wide");
669
+ expect(
670
+ describeRunnerWriteAccess({
671
+ inCluster: true,
672
+ allowWrites: true,
673
+ writeNamespaces: ["web", "api"],
674
+ allowNodeOperations: false,
675
+ }),
676
+ ).toBe("writes allowed in web, api, no node operations");
677
+ });
678
+ });
679
+
680
+ describe("the connect card", () => {
681
+ const baseStatus: KubernetesClusterAiAccessStatus = {
682
+ clusterId: "c1",
683
+ clusterName: "prod-east",
684
+ clusterIdentifier: "prod-east",
685
+ runner: {
686
+ id: RUNNER_ID,
687
+ name: "kubernetes-agent/prod-east",
688
+ isOnline: true,
689
+ canRunAiCommands: true,
690
+ posture: { inCluster: true, allowWrites: false },
691
+ },
692
+ accessMethod: "in_cluster",
693
+ kubectlAllowlist: [],
694
+ isInvestigationEnabled: true,
695
+ isInvestigationReady: true,
696
+ remediationMode: KubernetesAiRemediationMode.RequireApproval,
697
+ isRemediationReady: false,
698
+ gaps: [],
699
+ evaluatedAt: "2026-09-22T10:00:00.000Z",
700
+ };
701
+
702
+ function gap(
703
+ code: KubernetesAiAccessGap["code"],
704
+ nextStep: string,
705
+ ): KubernetesAiAccessGap {
706
+ return { code, title: code, description: code, nextStep, blocks: "both" };
707
+ }
708
+
709
+ test("offers the helm upgrade while no Runner is bound and none is installed", () => {
710
+ expect(
711
+ getAiAccessConnectCardMode({
712
+ ...baseStatus,
713
+ runner: null,
714
+ gaps: [
715
+ gap(
716
+ "no_runner_bound",
717
+ "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.",
718
+ ),
719
+ ],
720
+ }),
721
+ ).toBe("connect");
722
+ expect(
723
+ getAiAccessConnectCardMode({
724
+ ...baseStatus,
725
+ gaps: [gap("runner_missing", "Reload this page.")],
726
+ }),
727
+ ).toBe("connect");
728
+ });
729
+
730
+ /*
731
+ * Known follow-up 7: the in-cluster Runner is registered but not
732
+ * selected. Re-running helm would change nothing, so the helm card is
733
+ * replaced by "select it".
734
+ */
735
+ test("asks to select the Runner when this cluster's in-cluster Runner is installed but not selected", () => {
736
+ expect(
737
+ getAiAccessConnectCardMode({
738
+ ...baseStatus,
739
+ runner: null,
740
+ gaps: [
741
+ gap(
742
+ "no_runner_bound",
743
+ '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.',
744
+ ),
745
+ ],
746
+ }),
747
+ ).toBe("select_agent_runner");
748
+ });
749
+
750
+ test("shows no connect card once a Runner is bound, whatever else is missing", () => {
751
+ expect(getAiAccessConnectCardMode(baseStatus)).toBe("none");
752
+ expect(
753
+ getAiAccessConnectCardMode({
754
+ ...baseStatus,
755
+ gaps: [
756
+ gap(
757
+ "credential_on_agent_runner",
758
+ "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.",
759
+ ),
760
+ gap(
761
+ "remediation_write_access_missing",
762
+ "Upgrade the agent with --set aiAccess.remediation.enabled=true.",
763
+ ),
764
+ ],
765
+ }),
766
+ ).toBe("none");
767
+ });
768
+ });
769
+
770
+ describe("kubectl allowlist text", () => {
771
+ test("is one pattern per line, blank lines and extra spaces dropped", () => {
772
+ expect(
773
+ parseKubectlAllowlistText(
774
+ "kubectl rollout restart deployment/web -n web\n\n kubectl scale deployment/api --replicas=* -n api \r\n",
775
+ ),
776
+ ).toEqual([
777
+ "kubectl rollout restart deployment/web -n web",
778
+ "kubectl scale deployment/api --replicas=* -n api",
779
+ ]);
780
+ expect(parseKubectlAllowlistText("")).toEqual([]);
781
+ expect(parseKubectlAllowlistText(undefined)).toEqual([]);
782
+ expect(parseKubectlAllowlistText(["kubectl x"])).toEqual([]);
783
+ });
784
+
785
+ test("accepts kubectl patterns, including an empty list", () => {
786
+ expect(validateKubectlAllowlistText("")).toBeNull();
787
+ expect(validateKubectlAllowlistText(SET_IMAGE_PATTERN)).toBeNull();
788
+ expect(
789
+ validateKubectlAllowlistText(`${SET_IMAGE_PATTERN}\n${PATCH_PATTERN}`),
790
+ ).toBeNull();
791
+ // Broad but well-formed: accepted here, confirmed on save.
792
+ expect(validateKubectlAllowlistText(BROAD_PATTERN)).toBeNull();
793
+ expect(
794
+ validateKubectlAllowlistText("kubectl set image deployment/* * -n *"),
795
+ ).toBeNull();
796
+ });
797
+
798
+ /*
799
+ * KubectlPolicy made a wildcard verb invalid: it would pre-approve every
800
+ * KIND of change, and the matcher skips it. The form refuses it with the
801
+ * policy's words; this replaces the pin that accepted "*" and
802
+ * "kubectl * * * -n *" as broad but well-formed.
803
+ */
804
+ test("refuses a wildcard verb, which the matcher never reads", () => {
805
+ for (const pattern of VERB_WILDCARD_PATTERNS) {
806
+ expect({
807
+ pattern,
808
+ error: validateKubectlAllowlistText(pattern),
809
+ }).toEqual({
810
+ pattern,
811
+ error: `Pattern 1: ${KubectlPolicy.describeAllowlistPatternProblem(pattern)}`,
812
+ });
813
+ expect(validateKubectlAllowlistText(pattern)).toMatch(/verb/);
814
+ }
815
+ });
816
+
817
+ /*
818
+ * The matcher reads a pattern with or without the leading "kubectl" (it
819
+ * tokenizes patterns like commands), so the form accepts both; the old
820
+ * form refused "set image …" although the matcher would have used it.
821
+ */
822
+ test("accepts a pattern without the leading kubectl, as the matcher does", () => {
823
+ expect(
824
+ validateKubectlAllowlistText("set image deployment/web * -n web"),
825
+ ).toBeNull();
826
+ });
827
+
828
+ test("refuses a bare kubectl, which names no command", () => {
829
+ expect(validateKubectlAllowlistText("kubectl")).toMatch(/^Pattern 1: /);
830
+ // The old message taught the whole-string glob reading.
831
+ expect(validateKubectlAllowlistText("kubectl")).not.toMatch(
832
+ /matched against the whole command/,
833
+ );
834
+ // Negative control: a real command right next to it passes.
835
+ expect(validateKubectlAllowlistText("kubectl get pods")).toBeNull();
836
+ expect(
837
+ validateKubectlAllowlistText(`${SET_IMAGE_PATTERN}\nkubectl`),
838
+ ).toMatch(/^Pattern 2:/);
839
+ });
840
+
841
+ test("refuses a pattern the matcher cannot split into words", () => {
842
+ expect(
843
+ validateKubectlAllowlistText(
844
+ `kubectl patch deployment web -p '{"a":1} -n web`,
845
+ ),
846
+ ).toMatch(/^Pattern 1: /);
847
+ });
848
+
849
+ test("refuses more patterns, or longer ones, than the policy reads", () => {
850
+ const tooMany: string = Array.from(
851
+ { length: KUBECTL_ALLOWLIST_MAX_PATTERNS + 1 },
852
+ (_value: unknown, index: number): string => {
853
+ return `kubectl rollout restart deployment/web-${index} -n web`;
854
+ },
855
+ ).join("\n");
856
+ expect(validateKubectlAllowlistText(tooMany)).toMatch(/At most 100/);
857
+
858
+ const exactlyMax: string = tooMany.split("\n").slice(1).join("\n");
859
+ expect(validateKubectlAllowlistText(exactlyMax)).toBeNull();
860
+
861
+ const tooLong: string = `kubectl ${"x".repeat(
862
+ KUBECTL_ALLOWLIST_MAX_PATTERN_LENGTH,
863
+ )}`;
864
+ expect(validateKubectlAllowlistText(tooLong)).toMatch(/at most 500/);
865
+ });
866
+
867
+ /*
868
+ * One definition of validity: the form refuses a line exactly when
869
+ * KubectlPolicy says the matcher cannot use it.
870
+ */
871
+ test("refuses a line exactly when KubectlPolicy.describeAllowlistPatternProblem does", () => {
872
+ for (const pattern of [
873
+ "kubectl",
874
+ "*foo bar",
875
+ "set image deployment/web * -n web",
876
+ "kubectl get pods",
877
+ SET_IMAGE_PATTERN,
878
+ "Kubectl scale deployment/web --replicas=3 -n web",
879
+ "* * -n web",
880
+ `kubectl patch deployment web -p '{"a":1} -n web`,
881
+ `kubectl ${"x".repeat(KUBECTL_ALLOWLIST_MAX_PATTERN_LENGTH)}`,
882
+ ]) {
883
+ const policyProblem: string | null =
884
+ KubectlPolicy.describeAllowlistPatternProblem(
885
+ parseKubectlAllowlistText(pattern)[0],
886
+ );
887
+ expect({
888
+ pattern,
889
+ refused: validateKubectlAllowlistText(pattern) !== null,
890
+ }).toEqual({ pattern, refused: policyProblem !== null });
891
+ }
892
+ });
893
+ });
894
+
895
+ describe("the stored allowlist", () => {
896
+ test("is read the way the server reads it", () => {
897
+ const cases: Array<{ value: unknown; expected: SavedKubectlAllowlist }> = [
898
+ { value: null, expected: { patterns: [], isClean: true } },
899
+ { value: undefined, expected: { patterns: [], isClean: true } },
900
+ { value: [], expected: { patterns: [], isClean: true } },
901
+ {
902
+ value: [SET_IMAGE_PATTERN],
903
+ expected: { patterns: [SET_IMAGE_PATTERN], isClean: true },
904
+ },
905
+ {
906
+ value: JSON.stringify([SET_IMAGE_PATTERN]),
907
+ expected: { patterns: [SET_IMAGE_PATTERN], isClean: true },
908
+ },
909
+ // Unusable: the server uses these as an empty allowlist.
910
+ {
911
+ value: { patterns: [SET_IMAGE_PATTERN] },
912
+ expected: { patterns: [], isClean: false },
913
+ },
914
+ { value: 42, expected: { patterns: [], isClean: false } },
915
+ {
916
+ value: [1, "", SET_IMAGE_PATTERN],
917
+ expected: { patterns: [SET_IMAGE_PATTERN], isClean: false },
918
+ },
919
+ /*
920
+ * A string that is not JSON is ONE pattern to the server (its
921
+ * readStoredKubectlAllowlist and the status's normalizeAllowlist),
922
+ * so the policy uses it; it used to read here as no pattern at all.
923
+ */
924
+ {
925
+ value: "kubectl *",
926
+ expected: { patterns: ["kubectl *"], isClean: false },
927
+ },
928
+ ];
929
+
930
+ for (const testCase of cases) {
931
+ expect(normalizeSavedKubectlAllowlist(testCase.value)).toEqual(
932
+ testCase.expected,
933
+ );
934
+ }
935
+ });
936
+
937
+ test("keeps the stored spelling where the form collapses whitespace", () => {
938
+ expect(
939
+ readStoredKubectlAllowlist([" kubectl scale deployment/web ", ""]),
940
+ ).toEqual(["kubectl scale deployment/web"]);
941
+ expect(
942
+ normalizeSavedKubectlAllowlist([" kubectl scale deployment/web "])
943
+ .patterns,
944
+ ).toEqual(["kubectl scale deployment/web"]);
945
+ });
946
+ });
947
+
948
+ describe("reading the saved settings", () => {
949
+ test("takes the binding from the FK column or the relation", () => {
950
+ const fromRelation: KubernetesAiAccessSavedSettings =
951
+ readKubernetesAiAccessSavedSettings(
952
+ Object.assign(new KubernetesCluster(), {
953
+ isAiInvestigationEnabled: true,
954
+ aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
955
+ aiAccessRunner: Object.assign(new Runner(), {
956
+ _id: RUNNER_ID,
957
+ name: "kubernetes-agent/prod-east",
958
+ }),
959
+ }),
960
+ );
961
+ expect(fromRelation.aiAccessRunnerId).toBe(RUNNER_ID);
962
+ expect(fromRelation.aiAccessRunnerName).toBe("kubernetes-agent/prod-east");
963
+ expect(fromRelation.aiAccessCredentialId).toBeNull();
964
+
965
+ const fromColumn: KubernetesAiAccessSavedSettings =
966
+ readKubernetesAiAccessSavedSettings(
967
+ Object.assign(new KubernetesCluster(), {
968
+ aiAccessRunnerId: new ObjectID(RUNNER_ID),
969
+ aiAccessCredentialId: new ObjectID(CREDENTIAL_ID),
970
+ }),
971
+ );
972
+ expect(fromColumn.aiAccessRunnerId).toBe(RUNNER_ID);
973
+ expect(fromColumn.aiAccessCredentialId).toBe(CREDENTIAL_ID);
974
+ // Missing switches read as the column defaults.
975
+ expect(fromColumn.isAiInvestigationEnabled).toBe(false);
976
+ expect(fromColumn.aiRemediationMode).toBe(
977
+ KubernetesAiRemediationMode.Disabled,
978
+ );
979
+ });
980
+
981
+ test("seeds the form with the allowlist as lines and both clear switches off", () => {
982
+ const initial: SettingsFormValues =
983
+ getKubernetesAiAccessSettingsInitialValues(
984
+ makeSaved({
985
+ aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN, "kubectl *"],
986
+ }),
987
+ );
988
+ expect(initial.kubectlAllowlistText).toBe(
989
+ `${SET_IMAGE_PATTERN}\nkubectl *`,
990
+ );
991
+ expect(initial.clearAiAccessRunner).toBe(false);
992
+ expect(initial.clearAiAccessCredential).toBe(false);
993
+ });
994
+ });
995
+
996
+ describe("what the edit modal offers", () => {
997
+ test("an admin with both pickers gets the pickers and a free allowlist", () => {
998
+ expect(
999
+ getKubernetesAiAccessOfferedFields({
1000
+ saved: makeSaved({ aiKubectlCommandAllowlist: [] }),
1001
+ canConfigureUnattended: true,
1002
+ isRunnerPickerAvailable: true,
1003
+ isCredentialPickerAvailable: true,
1004
+ }),
1005
+ ).toEqual(EVERYTHING_OFFERED);
1006
+ });
1007
+
1008
+ /*
1009
+ * The finding (SIA-4, XP-7): the server, the docs and the gap copy
1010
+ * promise every cluster editor they may clear the Runner, credential and
1011
+ * allowlist, and the page offered them none of it.
1012
+ */
1013
+ test("a cluster editor without the admin set may remove patterns and unbind what is bound", () => {
1014
+ expect(
1015
+ getKubernetesAiAccessOfferedFields({
1016
+ saved: makeSaved({ aiAccessCredentialId: CREDENTIAL_ID }),
1017
+ canConfigureUnattended: false,
1018
+ isRunnerPickerAvailable: false,
1019
+ isCredentialPickerAvailable: false,
1020
+ }),
1021
+ ).toEqual(TIGHTEN_ONLY_OFFERED);
1022
+ });
1023
+
1024
+ test("nothing to remove or unbind means no allowlist field and no clear switch", () => {
1025
+ expect(
1026
+ getKubernetesAiAccessOfferedFields({
1027
+ saved: makeSaved({
1028
+ aiKubectlCommandAllowlist: [],
1029
+ aiAccessRunnerId: null,
1030
+ aiAccessRunnerName: null,
1031
+ }),
1032
+ canConfigureUnattended: false,
1033
+ isRunnerPickerAvailable: false,
1034
+ isCredentialPickerAvailable: false,
1035
+ }),
1036
+ ).toEqual({
1037
+ ...NOTHING_OFFERED,
1038
+ allowlistRemoveOnly: true,
1039
+ });
1040
+
1041
+ // An unclean stored allowlist is offered, so it can be cleared.
1042
+ expect(
1043
+ getKubernetesAiAccessOfferedFields({
1044
+ saved: makeSaved({ aiKubectlCommandAllowlist: { a: 1 } }),
1045
+ canConfigureUnattended: false,
1046
+ isRunnerPickerAvailable: false,
1047
+ isCredentialPickerAvailable: false,
1048
+ }).allowlist,
1049
+ ).toBe(true);
1050
+ });
1051
+
1052
+ test("an admin who may not list Runners may still unbind the bound one", () => {
1053
+ const offered: KubernetesAiAccessOfferedFields =
1054
+ getKubernetesAiAccessOfferedFields({
1055
+ saved: makeSaved(),
1056
+ canConfigureUnattended: true,
1057
+ isRunnerPickerAvailable: false,
1058
+ isCredentialPickerAvailable: true,
1059
+ });
1060
+ expect(offered.runner).toBe(false);
1061
+ expect(offered.runnerClear).toBe(true);
1062
+ expect(offered.credential).toBe(true);
1063
+ expect(offered.credentialClear).toBe(false);
1064
+ });
1065
+ });
1066
+
1067
+ describe("which modes a cluster editor without the admin set may choose", () => {
1068
+ test("every mode at or below the saved one", () => {
1069
+ function openModes(saved: KubernetesAiRemediationMode): Array<string> {
1070
+ return REMEDIATION_MODES_BY_AUTONOMY.filter(
1071
+ (mode: KubernetesAiRemediationMode): boolean => {
1072
+ return isRemediationModeOpenToEveryEditor(mode, saved);
1073
+ },
1074
+ );
1075
+ }
1076
+
1077
+ expect(openModes(KubernetesAiRemediationMode.Disabled)).toEqual([
1078
+ KubernetesAiRemediationMode.Disabled,
1079
+ KubernetesAiRemediationMode.RequireApproval,
1080
+ ]);
1081
+ expect(openModes(KubernetesAiRemediationMode.Automatic)).toEqual([
1082
+ KubernetesAiRemediationMode.Disabled,
1083
+ KubernetesAiRemediationMode.RequireApproval,
1084
+ KubernetesAiRemediationMode.Automatic,
1085
+ ]);
1086
+ // Bypass approval -> Automatic is a step down, open to every editor.
1087
+ expect(openModes(KubernetesAiRemediationMode.BypassApproval)).toEqual(
1088
+ REMEDIATION_MODES_BY_AUTONOMY,
1089
+ );
1090
+ });
1091
+ });
1092
+
1093
+ describe("what an edit sends", () => {
1094
+ /*
1095
+ * The finding: ModelForm submits every field, so an editor who only
1096
+ * switched investigation off re-sent the unchanged Automatic mode, the
1097
+ * allowlist and the Runner binding — writes the server refuses without
1098
+ * the admin permissions. Only the change may be sent.
1099
+ */
1100
+ test("an editor who only toggles investigation sends only that", () => {
1101
+ const saved: KubernetesAiAccessSavedSettings = makeSaved();
1102
+ for (const offered of [
1103
+ NOTHING_OFFERED,
1104
+ EVERYTHING_OFFERED,
1105
+ TIGHTEN_ONLY_OFFERED,
1106
+ ]) {
1107
+ expect(
1108
+ getKubernetesAiAccessSettingsChanges({
1109
+ saved,
1110
+ values: formValues(saved, { isAiInvestigationEnabled: false }),
1111
+ offered,
1112
+ }),
1113
+ ).toEqual({ isAiInvestigationEnabled: false });
1114
+ }
1115
+ });
1116
+
1117
+ test("an untouched form sends nothing", () => {
1118
+ const saved: KubernetesAiAccessSavedSettings = makeSaved({
1119
+ aiAccessCredentialId: CREDENTIAL_ID,
1120
+ });
1121
+ expect(
1122
+ getKubernetesAiAccessSettingsChanges({
1123
+ saved,
1124
+ values: formValues(saved),
1125
+ offered: EVERYTHING_OFFERED,
1126
+ }),
1127
+ ).toEqual({});
1128
+ expect(
1129
+ getKubernetesAiAccessSettingsChanges({
1130
+ saved,
1131
+ values: formValues(saved),
1132
+ offered: TIGHTEN_ONLY_OFFERED,
1133
+ }),
1134
+ ).toEqual({});
1135
+ });
1136
+
1137
+ test("tightening the mode sends the mode", () => {
1138
+ const saved: KubernetesAiAccessSavedSettings = makeSaved();
1139
+ expect(
1140
+ getKubernetesAiAccessSettingsChanges({
1141
+ saved,
1142
+ values: formValues(saved, {
1143
+ aiRemediationMode: KubernetesAiRemediationMode.Disabled,
1144
+ }),
1145
+ offered: NOTHING_OFFERED,
1146
+ }),
1147
+ ).toEqual({ aiRemediationMode: KubernetesAiRemediationMode.Disabled });
1148
+ });
1149
+
1150
+ test("reads a dropdown value handed over as an option", () => {
1151
+ const saved: KubernetesAiAccessSavedSettings = makeSaved();
1152
+ expect(
1153
+ getKubernetesAiAccessSettingsChanges({
1154
+ saved,
1155
+ values: formValues(saved, {
1156
+ aiRemediationMode: {
1157
+ value: KubernetesAiRemediationMode.RequireApproval,
1158
+ label: "Ask",
1159
+ },
1160
+ }),
1161
+ offered: NOTHING_OFFERED,
1162
+ }),
1163
+ ).toEqual({
1164
+ aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
1165
+ });
1166
+ });
1167
+
1168
+ test("ignores a mode that is not a mode", () => {
1169
+ const saved: KubernetesAiAccessSavedSettings = makeSaved();
1170
+ expect(
1171
+ getKubernetesAiAccessSettingsChanges({
1172
+ saved,
1173
+ values: formValues(saved, { aiRemediationMode: "automatic" }),
1174
+ offered: EVERYTHING_OFFERED,
1175
+ }),
1176
+ ).toEqual({});
1177
+ });
1178
+
1179
+ test("sends the allowlist only when its patterns changed", () => {
1180
+ const saved: KubernetesAiAccessSavedSettings = makeSaved();
1181
+
1182
+ // Reformatting is not a change.
1183
+ expect(
1184
+ getKubernetesAiAccessSettingsChanges({
1185
+ saved,
1186
+ values: formValues(saved, {
1187
+ kubectlAllowlistText: `\n ${SET_IMAGE_PATTERN.replace(/ /g, " ")} \n\n`,
1188
+ }),
1189
+ offered: EVERYTHING_OFFERED,
1190
+ }),
1191
+ ).toEqual({});
1192
+
1193
+ expect(
1194
+ getKubernetesAiAccessSettingsChanges({
1195
+ saved,
1196
+ values: formValues(saved, {
1197
+ kubectlAllowlistText: `${SET_IMAGE_PATTERN}\n${PATCH_PATTERN}`,
1198
+ }),
1199
+ offered: EVERYTHING_OFFERED,
1200
+ }),
1201
+ ).toEqual({
1202
+ aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN, PATCH_PATTERN],
1203
+ });
1204
+
1205
+ // Clearing sends an empty list.
1206
+ expect(
1207
+ getKubernetesAiAccessSettingsChanges({
1208
+ saved,
1209
+ values: formValues(saved, { kubectlAllowlistText: "" }),
1210
+ offered: EVERYTHING_OFFERED,
1211
+ }),
1212
+ ).toEqual({ aiKubectlCommandAllowlist: [] });
1213
+ });
1214
+
1215
+ /*
1216
+ * The server decides "does this write add a pattern?" on the stored
1217
+ * strings. A line the form shows collapsed is sent in its stored
1218
+ * spelling, or removing one line would read as adding another.
1219
+ */
1220
+ test("sends every kept line in its stored spelling", () => {
1221
+ const oddlySpaced: string =
1222
+ "kubectl scale deployment/web --replicas=* -n web";
1223
+ const saved: KubernetesAiAccessSavedSettings = makeSaved({
1224
+ aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN, oddlySpaced],
1225
+ });
1226
+
1227
+ expect(
1228
+ getKubernetesAiAccessSettingsChanges({
1229
+ saved,
1230
+ values: formValues(saved, {
1231
+ kubectlAllowlistText:
1232
+ "kubectl scale deployment/web --replicas=* -n web",
1233
+ }),
1234
+ offered: TIGHTEN_ONLY_OFFERED,
1235
+ }),
1236
+ ).toEqual({ aiKubectlCommandAllowlist: [oddlySpaced] });
1237
+ });
1238
+
1239
+ test("replaces a stored allowlist the policy cannot use", () => {
1240
+ const saved: KubernetesAiAccessSavedSettings = makeSaved({
1241
+ aiKubectlCommandAllowlist: { patterns: [SET_IMAGE_PATTERN] },
1242
+ });
1243
+ expect(
1244
+ getKubernetesAiAccessSettingsChanges({
1245
+ saved,
1246
+ values: formValues(saved),
1247
+ offered: EVERYTHING_OFFERED,
1248
+ }),
1249
+ ).toEqual({ aiKubectlCommandAllowlist: [] });
1250
+
1251
+ // Not offered: never written, whatever it holds.
1252
+ expect(
1253
+ getKubernetesAiAccessSettingsChanges({
1254
+ saved,
1255
+ values: formValues(saved, { kubectlAllowlistText: "kubectl *" }),
1256
+ offered: NOTHING_OFFERED,
1257
+ }),
1258
+ ).toEqual({});
1259
+ });
1260
+
1261
+ test("binds, re-binds and clears the Runner and credential only when offered", () => {
1262
+ const saved: KubernetesAiAccessSavedSettings = makeSaved({
1263
+ aiAccessCredentialId: CREDENTIAL_ID,
1264
+ });
1265
+
1266
+ expect(
1267
+ getKubernetesAiAccessSettingsChanges({
1268
+ saved,
1269
+ values: formValues(saved, {
1270
+ aiAccessRunnerId: OTHER_RUNNER_ID,
1271
+ aiAccessCredentialId: null,
1272
+ }),
1273
+ offered: EVERYTHING_OFFERED,
1274
+ }),
1275
+ ).toEqual({
1276
+ aiAccessRunnerId: OTHER_RUNNER_ID,
1277
+ aiAccessCredentialId: null,
1278
+ });
1279
+
1280
+ expect(
1281
+ getKubernetesAiAccessSettingsChanges({
1282
+ saved,
1283
+ values: formValues(saved, { aiAccessRunnerId: "" }),
1284
+ offered: EVERYTHING_OFFERED,
1285
+ }),
1286
+ ).toEqual({ aiAccessRunnerId: null });
1287
+
1288
+ expect(
1289
+ getKubernetesAiAccessSettingsChanges({
1290
+ saved,
1291
+ values: formValues(saved, {
1292
+ aiAccessRunnerId: OTHER_RUNNER_ID,
1293
+ aiAccessCredentialId: null,
1294
+ }),
1295
+ offered: NOTHING_OFFERED,
1296
+ }),
1297
+ ).toEqual({});
1298
+ });
1299
+
1300
+ test("a clear switch sends null and nothing else, and only when it is on", () => {
1301
+ const saved: KubernetesAiAccessSavedSettings = makeSaved({
1302
+ aiAccessCredentialId: CREDENTIAL_ID,
1303
+ });
1304
+
1305
+ expect(
1306
+ getKubernetesAiAccessSettingsChanges({
1307
+ saved,
1308
+ values: formValues(saved, {
1309
+ clearAiAccessRunner: true,
1310
+ clearAiAccessCredential: true,
1311
+ kubectlAllowlistText: "",
1312
+ }),
1313
+ offered: TIGHTEN_ONLY_OFFERED,
1314
+ }),
1315
+ ).toEqual({
1316
+ aiAccessRunnerId: null,
1317
+ aiAccessCredentialId: null,
1318
+ aiKubectlCommandAllowlist: [],
1319
+ });
1320
+
1321
+ // Switches off: nothing is sent.
1322
+ expect(
1323
+ getKubernetesAiAccessSettingsChanges({
1324
+ saved,
1325
+ values: formValues(saved),
1326
+ offered: TIGHTEN_ONLY_OFFERED,
1327
+ }),
1328
+ ).toEqual({});
1329
+
1330
+ // A clear-only offer can never send an id, whatever the form holds.
1331
+ expect(
1332
+ getKubernetesAiAccessSettingsChanges({
1333
+ saved,
1334
+ values: formValues(saved, {
1335
+ aiAccessRunnerId: OTHER_RUNNER_ID,
1336
+ aiAccessCredentialId: "77777777-0000-4000-8000-00000000000a",
1337
+ clearAiAccessRunner: false,
1338
+ }),
1339
+ offered: TIGHTEN_ONLY_OFFERED,
1340
+ }),
1341
+ ).toEqual({});
1342
+
1343
+ // Not offered: the switch is ignored.
1344
+ expect(
1345
+ getKubernetesAiAccessSettingsChanges({
1346
+ saved,
1347
+ values: formValues(saved, { clearAiAccessRunner: true }),
1348
+ offered: NOTHING_OFFERED,
1349
+ }),
1350
+ ).toEqual({});
1351
+ });
1352
+
1353
+ test("the chosen Runner follows the picker, the clear switch, or the saved binding", () => {
1354
+ const saved: KubernetesAiAccessSavedSettings = makeSaved();
1355
+ expect(
1356
+ getKubernetesAiAccessChosenRunnerId({
1357
+ saved,
1358
+ values: formValues(saved, {
1359
+ aiAccessRunnerId: { value: OTHER_RUNNER_ID, label: "x" },
1360
+ }),
1361
+ offered: EVERYTHING_OFFERED,
1362
+ }),
1363
+ ).toBe(OTHER_RUNNER_ID);
1364
+ expect(
1365
+ getKubernetesAiAccessChosenRunnerId({
1366
+ saved,
1367
+ values: formValues(saved, { clearAiAccessRunner: true }),
1368
+ offered: TIGHTEN_ONLY_OFFERED,
1369
+ }),
1370
+ ).toBeNull();
1371
+ expect(
1372
+ getKubernetesAiAccessChosenRunnerId({
1373
+ saved,
1374
+ values: formValues(saved),
1375
+ offered: NOTHING_OFFERED,
1376
+ }),
1377
+ ).toBe(RUNNER_ID);
1378
+ });
1379
+ });
1380
+
1381
+ /*
1382
+ * The server's rule (KubernetesClusterService.getAiAccessLoosening) is
1383
+ * RELATIVE to the saved settings; the page used an absolute one — any
1384
+ * unattended mode, any non-empty allowlist — and so hid tightening the
1385
+ * server accepts. KubernetesClusterAiPageServerParity.test.ts runs the same
1386
+ * cases through the server.
1387
+ */
1388
+ describe("loosening changes", () => {
1389
+ test("moving the mode up to an unattended mode loosens; moving it down never does", () => {
1390
+ const fromAutomatic: KubernetesAiAccessSavedSettings = makeSaved();
1391
+ const fromBypass: KubernetesAiAccessSavedSettings = makeSaved({
1392
+ aiRemediationMode: KubernetesAiRemediationMode.BypassApproval,
1393
+ });
1394
+ const fromOff: KubernetesAiAccessSavedSettings = makeSaved({
1395
+ aiRemediationMode: KubernetesAiRemediationMode.Disabled,
1396
+ });
1397
+
1398
+ expect(
1399
+ getKubernetesAiAccessLooseningChanges({
1400
+ saved: fromAutomatic,
1401
+ changes: {
1402
+ aiRemediationMode: KubernetesAiRemediationMode.BypassApproval,
1403
+ },
1404
+ }),
1405
+ ).toEqual(["switching AI remediation to Bypass approval"]);
1406
+ expect(
1407
+ getKubernetesAiAccessLooseningChanges({
1408
+ saved: fromOff,
1409
+ changes: { aiRemediationMode: KubernetesAiRemediationMode.Automatic },
1410
+ }),
1411
+ ).toEqual(["switching AI remediation to Automatic"]);
1412
+
1413
+ // Bypass approval -> Automatic is a tightening.
1414
+ expect(
1415
+ getKubernetesAiAccessLooseningChanges({
1416
+ saved: fromBypass,
1417
+ changes: { aiRemediationMode: KubernetesAiRemediationMode.Automatic },
1418
+ }),
1419
+ ).toEqual([]);
1420
+ for (const mode of [
1421
+ KubernetesAiRemediationMode.Disabled,
1422
+ KubernetesAiRemediationMode.RequireApproval,
1423
+ ]) {
1424
+ expect(
1425
+ getKubernetesAiAccessLooseningChanges({
1426
+ saved: fromOff,
1427
+ changes: { aiRemediationMode: mode },
1428
+ }),
1429
+ ).toEqual([]);
1430
+ }
1431
+ });
1432
+
1433
+ test("adding a pattern loosens; removing patterns or clearing the list never does", () => {
1434
+ const saved: KubernetesAiAccessSavedSettings = makeSaved({
1435
+ aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN, PATCH_PATTERN],
1436
+ });
1437
+
1438
+ expect(
1439
+ getKubernetesAiAccessLooseningChanges({
1440
+ saved,
1441
+ changes: { aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN] },
1442
+ }),
1443
+ ).toEqual([]);
1444
+ expect(
1445
+ getKubernetesAiAccessLooseningChanges({
1446
+ saved,
1447
+ changes: { aiKubectlCommandAllowlist: [] },
1448
+ }),
1449
+ ).toEqual([]);
1450
+ expect(
1451
+ getKubernetesAiAccessLooseningChanges({
1452
+ saved,
1453
+ changes: { aiKubectlCommandAllowlist: null },
1454
+ }),
1455
+ ).toEqual([]);
1456
+
1457
+ expect(
1458
+ getKubernetesAiAccessLooseningChanges({
1459
+ saved,
1460
+ changes: {
1461
+ aiKubectlCommandAllowlist: [
1462
+ SET_IMAGE_PATTERN,
1463
+ "kubectl delete pod * -n web",
1464
+ ],
1465
+ },
1466
+ }),
1467
+ ).toEqual([
1468
+ 'adding the kubectl allowlist pattern "kubectl delete pod * -n web"',
1469
+ ]);
1470
+
1471
+ // Editing a pattern into another string is adding one.
1472
+ expect(
1473
+ getKubernetesAiAccessLooseningChanges({
1474
+ saved,
1475
+ changes: {
1476
+ aiKubectlCommandAllowlist: [
1477
+ "kubectl set image deployment/web * -n prod",
1478
+ ],
1479
+ },
1480
+ }),
1481
+ ).toHaveLength(1);
1482
+ });
1483
+
1484
+ test("binding another Runner or a credential loosens; unbinding or re-sending the bound one never does", () => {
1485
+ const saved: KubernetesAiAccessSavedSettings = makeSaved({
1486
+ aiAccessCredentialId: CREDENTIAL_ID,
1487
+ });
1488
+
1489
+ expect(
1490
+ getKubernetesAiAccessLooseningChanges({
1491
+ saved,
1492
+ changes: { aiAccessRunnerId: OTHER_RUNNER_ID },
1493
+ }),
1494
+ ).toEqual(["binding a different Runner"]);
1495
+ expect(
1496
+ getKubernetesAiAccessLooseningChanges({
1497
+ saved: makeSaved(),
1498
+ changes: { aiAccessCredentialId: CREDENTIAL_ID },
1499
+ }),
1500
+ ).toEqual(["binding a Kubernetes credential"]);
1501
+
1502
+ for (const changes of [
1503
+ { aiAccessRunnerId: null },
1504
+ { aiAccessCredentialId: null },
1505
+ { aiAccessRunnerId: RUNNER_ID },
1506
+ { aiAccessCredentialId: CREDENTIAL_ID },
1507
+ { isAiInvestigationEnabled: true },
1508
+ { isAiInvestigationEnabled: false },
1509
+ ] as Array<JSONObject>) {
1510
+ expect(getKubernetesAiAccessLooseningChanges({ saved, changes })).toEqual(
1511
+ [],
1512
+ );
1513
+ }
1514
+ });
1515
+ });
1516
+
1517
+ describe("the remove-only allowlist", () => {
1518
+ const stored: Array<string> = [
1519
+ SET_IMAGE_PATTERN,
1520
+ "kubectl scale deployment/web --replicas=* -n web",
1521
+ ];
1522
+
1523
+ test("accepts removing lines, clearing the list and re-spacing a kept line", () => {
1524
+ for (const text of [
1525
+ SET_IMAGE_PATTERN,
1526
+ "",
1527
+ `${SET_IMAGE_PATTERN}\nkubectl scale deployment/web --replicas=* -n web`,
1528
+ ]) {
1529
+ expect(
1530
+ getKubectlAllowlistRemovalOnlyError({ text, storedValue: stored }),
1531
+ ).toBeNull();
1532
+ }
1533
+ });
1534
+
1535
+ test("refuses a new line, or an edited one, naming the permissions that would allow it", () => {
1536
+ const error: string | null = getKubectlAllowlistRemovalOnlyError({
1537
+ text: `${SET_IMAGE_PATTERN}\nkubectl set image deployment/web * -n prod`,
1538
+ storedValue: stored,
1539
+ });
1540
+ expect(error).toMatch(
1541
+ /^Pattern 2 \("kubectl set image deployment\/web \* -n prod"\)/,
1542
+ );
1543
+ for (const title of getKubernetesAiAccessAdminPermissionTitles()) {
1544
+ expect(error).toContain(title);
1545
+ }
1546
+ });
1547
+ });
1548
+
1549
+ /*
1550
+ * The confirmation is the UI's safety net for an allowlist that behaves
1551
+ * like Bypass approval. It used a whole-string glob reading ("*",
1552
+ * "kubectl *" were broad; anything with a literal "-n" was not), while the
1553
+ * matcher is word by word: "kubectl * * * -n *" auto-approves deleting any
1554
+ * Deployment in any non-protected namespace and was never confirmed. Broad
1555
+ * is now KubectlPolicy.isBroadAllowlistPattern, decided on the matcher's
1556
+ * own tokens, and the parity check below runs the real matcher.
1557
+ */
1558
+ describe("confirming a save that lets riskier changes run unattended", () => {
1559
+ function expectNamesWhatItUnlocks(
1560
+ confirmation: KubernetesAiAccessConfirmation | null,
1561
+ ): void {
1562
+ expect(confirmation).not.toBeNull();
1563
+ const text: string = confirmation!.description;
1564
+ for (const word of ["set image", "patch", "drain", "deleting workloads"]) {
1565
+ expect(text).toContain(word);
1566
+ }
1567
+ // What still never runs unattended is said too.
1568
+ expect(text).toMatch(/never run/);
1569
+ expect(text).toContain("kube-system");
1570
+ // The old copy taught the whole-string glob reading.
1571
+ expect(text).not.toMatch(MATCHES_NEARLY_EVERYTHING_REGEX);
1572
+ }
1573
+
1574
+ test("is asked before Bypass approval", () => {
1575
+ const confirmation: KubernetesAiAccessConfirmation | null =
1576
+ getKubernetesAiAccessConfirmation({
1577
+ saved: makeSaved({
1578
+ aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
1579
+ }),
1580
+ changes: {
1581
+ aiRemediationMode: KubernetesAiRemediationMode.BypassApproval,
1582
+ },
1583
+ });
1584
+ expectNamesWhatItUnlocks(confirmation);
1585
+ expect(confirmation!.title).toMatch(/Bypass approval/);
1586
+ });
1587
+
1588
+ test("is asked before a pattern with a wildcard for an object or the namespace", () => {
1589
+ for (const broad of [
1590
+ BROAD_PATTERN,
1591
+ "kubectl delete * * -n *",
1592
+ "kubectl delete deployment *",
1593
+ "kubectl set image deployment/* * -n *",
1594
+ "kubectl set image deployment/* * --namespace=*",
1595
+ "kubectl patch deployment * -p * -n *",
1596
+ "kubectl rollout restart deployment -n *",
1597
+ // Without the leading "kubectl", as the matcher reads it too.
1598
+ "set image deployment/web * -n *",
1599
+ ]) {
1600
+ const confirmation: KubernetesAiAccessConfirmation | null =
1601
+ getKubernetesAiAccessConfirmation({
1602
+ saved: makeSaved(),
1603
+ changes: {
1604
+ aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN, broad],
1605
+ },
1606
+ });
1607
+ expectNamesWhatItUnlocks(confirmation);
1608
+ expect(confirmation!.description).toContain(`"${broad}"`);
1609
+ expect(confirmation!.description).toContain("In Automatic mode");
1610
+ expect(confirmation!.description).toContain(
1611
+ "a wildcard for an object, the namespace, a selector or a --from source",
1612
+ );
1613
+ // A wildcard verb is invalid, so the copy no longer names it.
1614
+ expect(confirmation!.description).not.toContain("for the verb");
1615
+ }
1616
+ });
1617
+
1618
+ test("is never asked for a wildcard verb, which the form refuses first", () => {
1619
+ for (const pattern of VERB_WILDCARD_PATTERNS) {
1620
+ expect({
1621
+ pattern,
1622
+ confirmation: getKubernetesAiAccessConfirmation({
1623
+ saved: makeSaved(),
1624
+ changes: { aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN, pattern] },
1625
+ }),
1626
+ }).toEqual({ pattern, confirmation: null });
1627
+ }
1628
+ });
1629
+
1630
+ test("warns ahead when the broad allowlist is saved outside Automatic mode", () => {
1631
+ const confirmation: KubernetesAiAccessConfirmation | null =
1632
+ getKubernetesAiAccessConfirmation({
1633
+ saved: makeSaved({
1634
+ aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
1635
+ aiKubectlCommandAllowlist: [],
1636
+ }),
1637
+ changes: { aiKubectlCommandAllowlist: [BROAD_PATTERN] },
1638
+ });
1639
+ expectNamesWhatItUnlocks(confirmation);
1640
+ expect(confirmation!.description).toContain("Once this cluster is");
1641
+ });
1642
+
1643
+ test("is asked when Automatic mode switches on over a saved broad allowlist", () => {
1644
+ expectNamesWhatItUnlocks(
1645
+ getKubernetesAiAccessConfirmation({
1646
+ saved: makeSaved({
1647
+ aiRemediationMode: KubernetesAiRemediationMode.Disabled,
1648
+ aiKubectlCommandAllowlist: [BROAD_PATTERN],
1649
+ }),
1650
+ changes: { aiRemediationMode: KubernetesAiRemediationMode.Automatic },
1651
+ }),
1652
+ );
1653
+ });
1654
+
1655
+ test("is not asked for anything else", () => {
1656
+ const quiet: Array<{
1657
+ saved: KubernetesAiAccessSavedSettings;
1658
+ changes: JSONObject;
1659
+ }> = [
1660
+ // Narrow allowlist, any mode change short of Bypass.
1661
+ {
1662
+ saved: makeSaved(),
1663
+ changes: { aiRemediationMode: KubernetesAiRemediationMode.Disabled },
1664
+ },
1665
+ {
1666
+ saved: makeSaved({
1667
+ aiRemediationMode: KubernetesAiRemediationMode.Disabled,
1668
+ }),
1669
+ changes: { aiRemediationMode: KubernetesAiRemediationMode.Automatic },
1670
+ },
1671
+ // Narrow patterns: one object in one namespace, a wildcard only in a value.
1672
+ {
1673
+ saved: makeSaved(),
1674
+ changes: {
1675
+ aiKubectlCommandAllowlist: [
1676
+ SET_IMAGE_PATTERN,
1677
+ "kubectl delete deployment web -n prod",
1678
+ "kubectl rollout restart deployment/web -n web",
1679
+ "kubectl scale deployment/web --replicas=* -n web",
1680
+ ],
1681
+ },
1682
+ },
1683
+ // An already-saved broad pattern is not re-confirmed on unrelated edits.
1684
+ {
1685
+ saved: makeSaved({ aiKubectlCommandAllowlist: [BROAD_PATTERN] }),
1686
+ changes: { isAiInvestigationEnabled: false },
1687
+ },
1688
+ {
1689
+ saved: makeSaved({ aiKubectlCommandAllowlist: [BROAD_PATTERN] }),
1690
+ changes: {
1691
+ aiKubectlCommandAllowlist: [BROAD_PATTERN, SET_IMAGE_PATTERN],
1692
+ },
1693
+ },
1694
+ // Re-sent in its stored spelling: still the saved pattern.
1695
+ {
1696
+ saved: makeSaved({
1697
+ aiKubectlCommandAllowlist: ["kubectl delete deployment * -n *"],
1698
+ }),
1699
+ changes: {
1700
+ aiKubectlCommandAllowlist: ["kubectl delete deployment * -n *"],
1701
+ },
1702
+ },
1703
+ // Leaving Bypass approval — to Automatic too — or clearing the allowlist.
1704
+ {
1705
+ saved: makeSaved({
1706
+ aiRemediationMode: KubernetesAiRemediationMode.BypassApproval,
1707
+ }),
1708
+ changes: {
1709
+ aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
1710
+ },
1711
+ },
1712
+ {
1713
+ saved: makeSaved({
1714
+ aiRemediationMode: KubernetesAiRemediationMode.BypassApproval,
1715
+ aiKubectlCommandAllowlist: [BROAD_PATTERN],
1716
+ }),
1717
+ changes: { aiRemediationMode: KubernetesAiRemediationMode.Automatic },
1718
+ },
1719
+ {
1720
+ saved: makeSaved({ aiKubectlCommandAllowlist: [BROAD_PATTERN] }),
1721
+ changes: { aiKubectlCommandAllowlist: [] },
1722
+ },
1723
+ ];
1724
+
1725
+ for (const testCase of quiet) {
1726
+ expect({
1727
+ changes: testCase.changes,
1728
+ confirmation: getKubernetesAiAccessConfirmation(testCase),
1729
+ }).toEqual({ changes: testCase.changes, confirmation: null });
1730
+ }
1731
+ });
1732
+
1733
+ /*
1734
+ * The parity check the finding asked for, run on the real matcher. Every
1735
+ * probe names an object and namespaces nobody would type into a pattern,
1736
+ * so a pattern that auto-approves a probe reaches objects and namespaces
1737
+ * it does not name. Each such pattern must be confirmed; each narrow
1738
+ * pattern must neither be confirmed nor reach the probes.
1739
+ */
1740
+ describe("agrees with the matcher that actually runs", () => {
1741
+ const PROBES: Array<string> = [];
1742
+ for (const namespaceFlag of [
1743
+ "",
1744
+ " -n zz-probe-ns",
1745
+ " -n zz-probe-ns-2",
1746
+ " --namespace zz-probe-ns",
1747
+ " --namespace=zz-probe-ns",
1748
+ ]) {
1749
+ PROBES.push(`kubectl delete deployment zz-probe-obj${namespaceFlag}`);
1750
+ PROBES.push(`kubectl delete statefulset zz-probe-obj${namespaceFlag}`);
1751
+ PROBES.push(
1752
+ `kubectl set image deployment/zz-probe-obj c=zz:1${namespaceFlag}`,
1753
+ );
1754
+ }
1755
+ PROBES.push(
1756
+ "kubectl set env deployment/zz-probe-obj A=B -n zz-probe-ns",
1757
+ `kubectl patch deployment zz-probe-obj -p '{"spec":{"replicas":2}}' -n zz-probe-ns`,
1758
+ "kubectl scale deployment/zz-probe-obj --replicas=0 -n zz-probe-ns",
1759
+ "kubectl delete deployment zz-probe-obj zz-probe-obj-2 -n zz-probe-ns",
1760
+ "kubectl rollout restart deployment -n zz-probe-ns",
1761
+ "kubectl drain zz-probe-obj",
1762
+ "kubectl taint nodes zz-probe-obj k=v:NoSchedule",
1763
+ );
1764
+
1765
+ // Probes the empty allowlist leaves for a human: what a pattern could promote.
1766
+ const promotable: Array<string> = PROBES.filter(
1767
+ (command: string): boolean => {
1768
+ return (
1769
+ KubectlPolicy.evaluateForAutoExecution({
1770
+ command,
1771
+ allowlistPatterns: [],
1772
+ }).verdict === AiRemediationCommandPolicyVerdict.RequiresApproval
1773
+ );
1774
+ },
1775
+ );
1776
+
1777
+ function promotedBy(pattern: string): Array<string> {
1778
+ return promotable.filter((command: string): boolean => {
1779
+ return (
1780
+ KubectlPolicy.evaluateForAutoExecution({
1781
+ command,
1782
+ allowlistPatterns: [pattern],
1783
+ }).verdict === AiRemediationCommandPolicyVerdict.AutoApproved
1784
+ );
1785
+ });
1786
+ }
1787
+
1788
+ function confirmsFor(pattern: string): boolean {
1789
+ return (
1790
+ getKubernetesAiAccessConfirmation({
1791
+ saved: makeSaved({ aiKubectlCommandAllowlist: [] }),
1792
+ changes: { aiKubectlCommandAllowlist: [pattern] },
1793
+ }) !== null
1794
+ );
1795
+ }
1796
+
1797
+ const CANDIDATES: Array<string> = [
1798
+ "*",
1799
+ "kubectl *",
1800
+ "kubectl * *",
1801
+ "kubectl * * *",
1802
+ "kubectl * * * *",
1803
+ "kubectl * * -n *",
1804
+ "kubectl * * * -n *",
1805
+ "kubectl * * * * -n *",
1806
+ "kubectl * * * -p * -n *",
1807
+ "kubectl * * * --namespace *",
1808
+ "kubectl * * * --namespace=*",
1809
+ "* * * -n *",
1810
+ "kubectl delete * * -n *",
1811
+ "kubectl delete deployment * -n *",
1812
+ "kubectl delete deployment *",
1813
+ "kubectl delete statefulset * -n *",
1814
+ "kubectl set image deployment/* * -n *",
1815
+ "kubectl set image deployment/* * --namespace=*",
1816
+ "kubectl set env deployment/* * -n zz-probe-ns",
1817
+ "kubectl patch deployment * -p * -n *",
1818
+ "kubectl rollout restart deployment -n *",
1819
+ "kubectl set image deployment/* * -n zz-probe-ns",
1820
+ "kubectl scale deployment/* --replicas=* -n zz-probe-ns",
1821
+ SET_IMAGE_PATTERN,
1822
+ "kubectl delete deployment web -n prod",
1823
+ "kubectl rollout restart deployment/web -n web",
1824
+ "kubectl scale deployment/web --replicas=* -n web",
1825
+ ];
1826
+
1827
+ const NARROW: Array<string> = [
1828
+ SET_IMAGE_PATTERN,
1829
+ "kubectl delete deployment web -n prod",
1830
+ "kubectl rollout restart deployment/web -n web",
1831
+ "kubectl scale deployment/web --replicas=* -n web",
1832
+ ];
1833
+
1834
+ test("harness guard: the probes are riskier changes a pattern can promote", () => {
1835
+ expect(promotable.length).toBeGreaterThanOrEqual(15);
1836
+ /*
1837
+ * Broad entries that really do promote probes (the wildcard-verb
1838
+ * entries this guard used promote nothing since KubectlPolicy made
1839
+ * them invalid).
1840
+ */
1841
+ expect(promotedBy(BROAD_PATTERN).length).toBeGreaterThan(0);
1842
+ expect(promotedBy("kubectl delete * * -n *").length).toBeGreaterThan(0);
1843
+ expect(
1844
+ promotedBy("kubectl patch deployment * -p * -n *").length,
1845
+ ).toBeGreaterThan(0);
1846
+ // An invalid entry promotes nothing: the matcher skips it.
1847
+ for (const pattern of VERB_WILDCARD_PATTERNS) {
1848
+ expect({ pattern, promoted: promotedBy(pattern) }).toEqual({
1849
+ pattern,
1850
+ promoted: [],
1851
+ });
1852
+ }
1853
+ });
1854
+
1855
+ test("every pattern that reaches objects or namespaces it does not name is confirmed", () => {
1856
+ for (const pattern of CANDIDATES) {
1857
+ const promoted: Array<string> = promotedBy(pattern);
1858
+ if (promoted.length === 0) {
1859
+ continue;
1860
+ }
1861
+ expect({ pattern, promoted, confirmed: confirmsFor(pattern) }).toEqual({
1862
+ pattern,
1863
+ promoted,
1864
+ confirmed: true,
1865
+ });
1866
+ }
1867
+ });
1868
+
1869
+ test("narrow patterns are neither confirmed nor reach other objects or namespaces", () => {
1870
+ for (const pattern of NARROW) {
1871
+ expect({
1872
+ pattern,
1873
+ confirmed: confirmsFor(pattern),
1874
+ promoted: promotedBy(pattern),
1875
+ }).toEqual({ pattern, confirmed: false, promoted: [] });
1876
+ }
1877
+ });
1878
+ });
1879
+ });
1880
+
1881
+ describe("Runner picker options", () => {
1882
+ const runners: Array<Runner> = [
1883
+ makeRunner("r-this", "kubernetes-agent/prod-east", true, "prod-east"),
1884
+ makeRunner("r-other-cluster", "kubernetes-agent/prod-eu", true, "prod-eu"),
1885
+ makeRunner("r-host", "bash-runner"),
1886
+ makeRunner("r-off", "ops-runner", false),
1887
+ ];
1888
+
1889
+ test("offer only Runners that can serve this cluster, its own agent first", () => {
1890
+ const options: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
1891
+ runners,
1892
+ clusterIdentifier: "prod-east",
1893
+ boundRunnerId: null,
1894
+ boundRunnerName: null,
1895
+ });
1896
+
1897
+ expect(optionValues(options)).toEqual(["r-this", "r-host"]);
1898
+ expect(options[0]!.label).toMatch(/in-cluster Runner for this cluster/);
1899
+ });
1900
+
1901
+ test("match this cluster's agent Runner whatever the identifier's casing", () => {
1902
+ expect(
1903
+ optionValues(
1904
+ buildKubernetesAiRunnerOptions({
1905
+ runners,
1906
+ clusterIdentifier: "Prod-East",
1907
+ boundRunnerId: null,
1908
+ boundRunnerName: null,
1909
+ }),
1910
+ ),
1911
+ ).toEqual(["r-this", "r-host"]);
1912
+ });
1913
+
1914
+ test("offer no agent Runner when the cluster has no identifier", () => {
1915
+ expect(
1916
+ optionValues(
1917
+ buildKubernetesAiRunnerOptions({
1918
+ runners,
1919
+ clusterIdentifier: undefined,
1920
+ boundRunnerId: null,
1921
+ boundRunnerName: null,
1922
+ }),
1923
+ ),
1924
+ ).toEqual(["r-host"]);
1925
+ });
1926
+
1927
+ /*
1928
+ * The finding (dashboard-ai-page-2): the server names an agent Runner
1929
+ * "kubernetes-agent/<id>" only while that fits 100 characters, and
1930
+ * "kubernetes-agent/<head>-<hash>" beyond. The picker rebuilt the name
1931
+ * from the identifier, so for an identifier of 84+ characters it hid
1932
+ * this cluster's own Runner, or called it another cluster's. It now
1933
+ * matches the Runner's posture, as the server does.
1934
+ */
1935
+ describe("a cluster identifier long enough for the server to shorten the Runner name", () => {
1936
+ const longIdentifier: string =
1937
+ "arn:aws:eks:ap-southeast-2:123456789012:cluster/payments-platform-production-blue-green";
1938
+ const otherLongIdentifier: string =
1939
+ "arn:aws:eks:ap-southeast-2:123456789012:cluster/payments-platform-producti-other-region";
1940
+ const head: string = `kubernetes-agent/${longIdentifier.slice(0, 74)}`;
1941
+ const thisRunner: Runner = makeRunner(
1942
+ "r-long",
1943
+ `${head}-fe7fc1b3`,
1944
+ true,
1945
+ longIdentifier,
1946
+ );
1947
+ // Same head, another hash: another long-named cluster's Runner.
1948
+ const otherRunner: Runner = makeRunner(
1949
+ "r-long-other",
1950
+ `${head}-0a1b2c3d`,
1951
+ true,
1952
+ otherLongIdentifier,
1953
+ );
1954
+
1955
+ test("harness guard: the identifier is past the length the name keeps whole", () => {
1956
+ expect(longIdentifier.length).toBe(87);
1957
+ expect(`kubernetes-agent/${longIdentifier}`.length).toBeGreaterThan(100);
1958
+ expect(thisRunner.name!.endsWith(longIdentifier)).toBe(false);
1959
+ });
1960
+
1961
+ test("lists this cluster's Runner first, as its in-cluster Runner, bound or not", () => {
1962
+ for (const boundRunnerId of [null, "r-long"]) {
1963
+ const options: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
1964
+ runners: [
1965
+ otherRunner,
1966
+ thisRunner,
1967
+ makeRunner("r-host", "bash-runner"),
1968
+ ],
1969
+ clusterIdentifier: longIdentifier,
1970
+ boundRunnerId,
1971
+ boundRunnerName: boundRunnerId ? thisRunner.name! : null,
1972
+ });
1973
+ expect(optionValues(options)).toEqual(["r-long", "r-host"]);
1974
+ expect(options[0]!.label).toBe(
1975
+ `${thisRunner.name} (in-cluster Runner for this cluster)`,
1976
+ );
1977
+ }
1978
+ });
1979
+
1980
+ test("still calls another long-named cluster's Runner another cluster's", () => {
1981
+ const options: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
1982
+ runners: [otherRunner, thisRunner],
1983
+ clusterIdentifier: longIdentifier,
1984
+ boundRunnerId: "r-long-other",
1985
+ boundRunnerName: otherRunner.name!,
1986
+ });
1987
+ expect(optionLabel(options, "r-long-other")).toMatch(
1988
+ /currently bound — runs inside another cluster/,
1989
+ );
1990
+ });
1991
+ });
1992
+
1993
+ test("an agent-named Runner whose posture names another cluster, or none, is not this cluster's", () => {
1994
+ expect(
1995
+ isThisClustersAgentRunner(
1996
+ makeRunner("a", "kubernetes-agent/prod-east", true, "prod-eu"),
1997
+ "prod-east",
1998
+ ),
1999
+ ).toBe(false);
2000
+ expect(
2001
+ isThisClustersAgentRunner(
2002
+ makeRunner("a", "kubernetes-agent/prod-east"),
2003
+ "prod-east",
2004
+ ),
2005
+ ).toBe(false);
2006
+ /*
2007
+ * A row without the name marker that reports an agent posture for this
2008
+ * cluster IS this cluster's agent: only the kubernetes-agent Runner
2009
+ * binary reports such a posture, and RunnerService.isKubernetesAgent
2010
+ * RunnerRow / isKubernetesAgentRunnerOfCluster read it so. This replaces
2011
+ * the pin that called it "not an agent row" (the page read the name
2012
+ * alone); a Runner in a pod that names no cluster is still not one.
2013
+ */
2014
+ expect(
2015
+ isThisClustersAgentRunner(
2016
+ makeRunner("a", "office-runner", true, "prod-east"),
2017
+ "prod-east",
2018
+ ),
2019
+ ).toBe(true);
2020
+ expect(
2021
+ isThisClustersAgentRunner(
2022
+ Object.assign(new Runner(), {
2023
+ _id: "a",
2024
+ name: "office-runner",
2025
+ hostInfo: { kubernetes: { inCluster: true } },
2026
+ }),
2027
+ "prod-east",
2028
+ ),
2029
+ ).toBe(false);
2030
+ });
2031
+
2032
+ test("keep the bound Runner, saying why it cannot serve the cluster", () => {
2033
+ const boundToOther: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
2034
+ runners,
2035
+ clusterIdentifier: "prod-east",
2036
+ boundRunnerId: "r-other-cluster",
2037
+ boundRunnerName: "kubernetes-agent/prod-eu",
2038
+ });
2039
+ expect(optionValues(boundToOther)).toContain("r-other-cluster");
2040
+ expect(optionLabel(boundToOther, "r-other-cluster")).toMatch(
2041
+ /currently bound — runs inside another cluster/,
2042
+ );
2043
+
2044
+ const boundToOff: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
2045
+ runners,
2046
+ clusterIdentifier: "prod-east",
2047
+ boundRunnerId: "r-off",
2048
+ boundRunnerName: "ops-runner",
2049
+ });
2050
+ expect(optionLabel(boundToOff, "r-off")).toMatch(
2051
+ /Runs AI Remediation Commands/,
2052
+ );
2053
+
2054
+ const boundToUnlisted: Array<DropdownOption> =
2055
+ buildKubernetesAiRunnerOptions({
2056
+ runners,
2057
+ clusterIdentifier: "prod-east",
2058
+ boundRunnerId: "r-gone",
2059
+ boundRunnerName: "old-runner",
2060
+ });
2061
+ expect(boundToUnlisted[0]).toEqual({
2062
+ value: "r-gone",
2063
+ label: "old-runner (currently bound)",
2064
+ });
2065
+ });
2066
+
2067
+ test("the directory names each Runner and says which are agent rows", () => {
2068
+ const directory: Record<string, KubernetesAiRunnerDirectoryEntry> =
2069
+ buildKubernetesAiRunnerDirectory(runners);
2070
+ expect(directory["r-this"]).toEqual({
2071
+ name: "kubernetes-agent/prod-east",
2072
+ isAgent: true,
2073
+ });
2074
+ expect(directory["r-host"]).toEqual({
2075
+ name: "bash-runner",
2076
+ isAgent: false,
2077
+ });
2078
+ });
2079
+ });
2080
+
2081
+ /*
2082
+ * The finding (dashboard-ai-page-7): the picker offered every Kubernetes
2083
+ * credential, so an admin could save a Runner with a credential that is
2084
+ * not assigned to it (the status then blocks everything with a
2085
+ * credential_missing gap), or a credential for a kubernetes-agent Runner,
2086
+ * which is never given one.
2087
+ */
2088
+ describe("credential picker options", () => {
2089
+ const credentials: Array<RunbookCredential> = [
2090
+ makeCredential("c-ssh", "ssh key", ["r-host"], RunbookCredentialType.SSH),
2091
+ makeCredential("c-host", "prod-east token", ["r-host"]),
2092
+ makeCredential("c-other", "staging token", ["r-other-host"]),
2093
+ makeCredential("c-both", "shared token", ["r-host", "r-other-host"]),
2094
+ ];
2095
+
2096
+ const HOST_RUNNER: { id: string; name: string } = {
2097
+ id: "r-host",
2098
+ name: "bash-runner",
2099
+ };
2100
+
2101
+ test("offer only Kubernetes credentials assigned to the chosen Runner", () => {
2102
+ expect(
2103
+ optionValues(
2104
+ buildKubernetesAiCredentialOptions({
2105
+ credentials,
2106
+ boundCredentialId: null,
2107
+ boundCredentialName: null,
2108
+ runner: HOST_RUNNER,
2109
+ }),
2110
+ ),
2111
+ ).toEqual(["c-host", "c-both"]);
2112
+
2113
+ // Negative control: another Runner gets its own credentials.
2114
+ expect(
2115
+ optionValues(
2116
+ buildKubernetesAiCredentialOptions({
2117
+ credentials,
2118
+ boundCredentialId: null,
2119
+ boundCredentialName: null,
2120
+ runner: { id: "r-other-host", name: "other" },
2121
+ }),
2122
+ ),
2123
+ ).toEqual(["c-other", "c-both"]);
2124
+ });
2125
+
2126
+ test("offer none for a kubernetes-agent Runner, or before a Runner is chosen, and say why", () => {
2127
+ for (const runner of [
2128
+ { id: "r-this", name: "kubernetes-agent/prod-east" },
2129
+ { id: null, name: null },
2130
+ ]) {
2131
+ expect(
2132
+ buildKubernetesAiCredentialOptions({
2133
+ credentials,
2134
+ boundCredentialId: null,
2135
+ boundCredentialName: null,
2136
+ runner,
2137
+ }),
2138
+ ).toEqual([]);
2139
+ }
2140
+
2141
+ expect(
2142
+ getKubernetesAiCredentialFieldDescription({
2143
+ id: "r-this",
2144
+ name: "kubernetes-agent/prod-east",
2145
+ }),
2146
+ ).toMatch(
2147
+ /No credential can be chosen: "kubernetes-agent\/prod-east" is an in-cluster Runner/,
2148
+ );
2149
+ expect(
2150
+ getKubernetesAiCredentialFieldDescription({ id: null, name: null }),
2151
+ ).toMatch(/Choose the Runner first/);
2152
+ expect(getKubernetesAiCredentialFieldDescription(HOST_RUNNER)).toMatch(
2153
+ /assigned to "bash-runner"/,
2154
+ );
2155
+ });
2156
+
2157
+ test("keep the bound credential, saying why it does not fit", () => {
2158
+ const forHost: Array<DropdownOption> = buildKubernetesAiCredentialOptions({
2159
+ credentials,
2160
+ boundCredentialId: "c-other",
2161
+ boundCredentialName: "staging token",
2162
+ runner: HOST_RUNNER,
2163
+ });
2164
+ expect(optionValues(forHost)).toEqual(["c-host", "c-other", "c-both"]);
2165
+ expect(optionLabel(forHost, "c-other")).toBe(
2166
+ "staging token (currently bound — not assigned to bash-runner)",
2167
+ );
2168
+
2169
+ const forAgent: Array<DropdownOption> = buildKubernetesAiCredentialOptions({
2170
+ credentials,
2171
+ boundCredentialId: "c-host",
2172
+ boundCredentialName: "prod-east token",
2173
+ runner: { id: "r-this", name: "kubernetes-agent/prod-east" },
2174
+ });
2175
+ expect(forAgent).toHaveLength(1);
2176
+ expect(optionLabel(forAgent, "c-host")).toMatch(
2177
+ /currently bound — kubernetes-agent\/prod-east is an in-cluster Runner and is never given a credential/,
2178
+ );
2179
+
2180
+ expect(
2181
+ optionLabel(
2182
+ buildKubernetesAiCredentialOptions({
2183
+ credentials,
2184
+ boundCredentialId: "c-ssh",
2185
+ boundCredentialName: "ssh key",
2186
+ runner: HOST_RUNNER,
2187
+ }),
2188
+ "c-ssh",
2189
+ ),
2190
+ ).toMatch(/not a Kubernetes credential/);
2191
+
2192
+ expect(
2193
+ buildKubernetesAiCredentialOptions({
2194
+ credentials,
2195
+ boundCredentialId: "c-gone",
2196
+ boundCredentialName: null,
2197
+ runner: HOST_RUNNER,
2198
+ })[0],
2199
+ ).toEqual({
2200
+ value: "c-gone",
2201
+ label: "The bound credential (currently bound)",
2202
+ });
2203
+ });
2204
+
2205
+ test("offer a credential whose assignments could not be read", () => {
2206
+ expect(
2207
+ optionValues(
2208
+ buildKubernetesAiCredentialOptions({
2209
+ credentials: [makeCredential("c-unknown", "unknown", undefined)],
2210
+ boundCredentialId: null,
2211
+ boundCredentialName: null,
2212
+ runner: HOST_RUNNER,
2213
+ }),
2214
+ ),
2215
+ ).toEqual(["c-unknown"]);
2216
+ });
2217
+ });
2218
+
2219
+ describe("a Runner and credential that cannot work together", () => {
2220
+ test("a credential not assigned to the Runner is refused, naming both", () => {
2221
+ const error: string | null = getKubernetesAiCredentialAssignmentError({
2222
+ runner: { id: "r-host", name: "bash-runner" },
2223
+ credentialId: "c-other",
2224
+ credentialName: "staging token",
2225
+ credentialRunnerIds: ["r-other-host"],
2226
+ });
2227
+ expect(error).toMatch(
2228
+ /"staging token" is not assigned to Runner "bash-runner"/,
2229
+ );
2230
+
2231
+ // Negative controls: assigned, unknown assignments, or no credential.
2232
+ expect(
2233
+ getKubernetesAiCredentialAssignmentError({
2234
+ runner: { id: "r-host", name: "bash-runner" },
2235
+ credentialId: "c-host",
2236
+ credentialName: "prod-east token",
2237
+ credentialRunnerIds: ["r-host"],
2238
+ }),
2239
+ ).toBeNull();
2240
+ expect(
2241
+ getKubernetesAiCredentialAssignmentError({
2242
+ runner: { id: "r-host", name: "bash-runner" },
2243
+ credentialId: "c-host",
2244
+ credentialName: "prod-east token",
2245
+ credentialRunnerIds: undefined,
2246
+ }),
2247
+ ).toBeNull();
2248
+ expect(
2249
+ getKubernetesAiCredentialAssignmentError({
2250
+ runner: { id: "r-host", name: "bash-runner" },
2251
+ credentialId: null,
2252
+ credentialName: null,
2253
+ credentialRunnerIds: undefined,
2254
+ }),
2255
+ ).toBeNull();
2256
+ });
2257
+
2258
+ test("a kubernetes-agent Runner with a credential asks for the credential to be cleared", () => {
2259
+ const error: string | null = getKubernetesAiCredentialAssignmentError({
2260
+ runner: { id: "r-this", name: "kubernetes-agent/prod-east" },
2261
+ credentialId: "c-host",
2262
+ credentialName: "prod-east token",
2263
+ // Even assigned: an agent Runner is never given a credential.
2264
+ credentialRunnerIds: ["r-this"],
2265
+ });
2266
+ expect(error).toMatch(/is an in-cluster Runner/);
2267
+ expect(error).toMatch(/Clear the Kubernetes credential/);
2268
+ expect(error).not.toMatch(/not assigned/);
2269
+ });
2270
+
2271
+ describe("on save", () => {
2272
+ const runners: Record<string, KubernetesAiRunnerDirectoryEntry> =
2273
+ buildKubernetesAiRunnerDirectory([
2274
+ makeRunner("r-this", "kubernetes-agent/prod-east", true, "prod-east"),
2275
+ makeRunner("r-host", "bash-runner"),
2276
+ makeRunner("r-other-host", "other-runner"),
2277
+ ]);
2278
+ const credentials: Record<string, KubernetesAiCredentialDirectoryEntry> =
2279
+ buildKubernetesAiCredentialDirectory([
2280
+ makeCredential("c-host", "prod-east token", ["r-host"]),
2281
+ makeCredential("c-other", "staging token", ["r-other-host"]),
2282
+ ]);
2283
+ const saved: KubernetesAiAccessSavedSettings = makeSaved({
2284
+ aiAccessRunnerId: "r-host",
2285
+ aiAccessRunnerName: "bash-runner",
2286
+ aiAccessCredentialId: "c-host",
2287
+ aiAccessCredentialName: "prod-east token",
2288
+ });
2289
+
2290
+ test("refuses setting a credential the Runner cannot use", () => {
2291
+ expect(
2292
+ getKubernetesAiAccessBindingError({
2293
+ saved,
2294
+ changes: { aiAccessCredentialId: "c-other" },
2295
+ runners,
2296
+ credentials,
2297
+ }),
2298
+ ).toMatch(/"staging token" is not assigned to Runner "bash-runner"/);
2299
+ });
2300
+
2301
+ test("refuses binding a Runner the kept credential is not assigned to", () => {
2302
+ expect(
2303
+ getKubernetesAiAccessBindingError({
2304
+ saved,
2305
+ changes: { aiAccessRunnerId: "r-other-host" },
2306
+ runners,
2307
+ credentials,
2308
+ }),
2309
+ ).toMatch(/"prod-east token" is not assigned to Runner "other-runner"/);
2310
+ expect(
2311
+ getKubernetesAiAccessBindingError({
2312
+ saved,
2313
+ changes: { aiAccessRunnerId: "r-this" },
2314
+ runners,
2315
+ credentials,
2316
+ }),
2317
+ ).toMatch(/Clear the Kubernetes credential/);
2318
+ });
2319
+
2320
+ test("never refuses unbinding, a matching pair, or an unrelated edit", () => {
2321
+ for (const changes of [
2322
+ { aiAccessRunnerId: null },
2323
+ { aiAccessCredentialId: null },
2324
+ { aiAccessRunnerId: "r-this", aiAccessCredentialId: null },
2325
+ { aiAccessRunnerId: "r-other-host", aiAccessCredentialId: "c-other" },
2326
+ { isAiInvestigationEnabled: false },
2327
+ ] as Array<JSONObject>) {
2328
+ expect({
2329
+ changes,
2330
+ error: getKubernetesAiAccessBindingError({
2331
+ saved,
2332
+ changes,
2333
+ runners,
2334
+ credentials,
2335
+ }),
2336
+ }).toEqual({ changes, error: null });
2337
+ }
2338
+ });
2339
+ });
2340
+ });
2341
+
2342
+ describe("who may loosen a cluster's AI access", () => {
2343
+ test("uses the same permissions as an unattended auto-remediation rule", () => {
2344
+ expect(KUBERNETES_AI_ACCESS_ADMIN_PERMISSIONS).toEqual(
2345
+ expect.arrayContaining([
2346
+ Permission.ProjectOwner,
2347
+ Permission.ProjectAdmin,
2348
+ Permission.EditAutoRemediationRule,
2349
+ ]),
2350
+ );
2351
+
2352
+ for (const permission of KUBERNETES_AI_ACCESS_ADMIN_PERMISSIONS) {
2353
+ grant([...BASE_PERMISSIONS, permission]);
2354
+ expect(canConfigureUnattendedKubernetesAiAccess()).toBe(true);
2355
+ }
2356
+
2357
+ expect(getKubernetesAiAccessAdminPermissionTitles()).toEqual(
2358
+ expect.arrayContaining([
2359
+ "Project Owner",
2360
+ "Project Admin",
2361
+ "Edit Auto Remediation Rule",
2362
+ ]),
2363
+ );
2364
+ });
2365
+
2366
+ test("is refused to every other cluster editor", () => {
2367
+ for (const permission of [
2368
+ Permission.ProjectMember,
2369
+ Permission.SettingsAdmin,
2370
+ Permission.SettingsMember,
2371
+ Permission.EditKubernetesCluster,
2372
+ Permission.ReadRunbookCredential,
2373
+ ]) {
2374
+ grant([...BASE_PERMISSIONS, permission]);
2375
+ expect(canConfigureUnattendedKubernetesAiAccess()).toBe(false);
2376
+ }
2377
+ });
2378
+
2379
+ test("reads a block row as a denial, not a grant", () => {
2380
+ grant(
2381
+ [...BASE_PERMISSIONS, Permission.ProjectMember],
2382
+ [Permission.ProjectAdmin, Permission.EditAutoRemediationRule],
2383
+ );
2384
+ expect(canConfigureUnattendedKubernetesAiAccess()).toBe(false);
2385
+ });
2386
+
2387
+ test("is allowed to a master admin and withheld before the snapshot lands", () => {
2388
+ grantNothingYet();
2389
+ expect(canConfigureUnattendedKubernetesAiAccess()).toBe(false);
2390
+
2391
+ jest.spyOn(User, "isMasterAdmin").mockReturnValue(true);
2392
+ expect(canConfigureUnattendedKubernetesAiAccess()).toBe(true);
2393
+ });
2394
+
2395
+ test("decides what the edit modal offers", () => {
2396
+ grant([...BASE_PERMISSIONS, Permission.ProjectAdmin]);
2397
+ expect(getKubernetesAiAccessEditCapabilities()).toEqual({
2398
+ canConfigureUnattended: true,
2399
+ canPickRunner: true,
2400
+ canPickCredential: true,
2401
+ });
2402
+
2403
+ // May loosen and list Runners, may not read credentials.
2404
+ grant([
2405
+ ...BASE_PERMISSIONS,
2406
+ Permission.ProjectMember,
2407
+ Permission.EditAutoRemediationRule,
2408
+ ]);
2409
+ expect(getKubernetesAiAccessEditCapabilities()).toEqual({
2410
+ canConfigureUnattended: true,
2411
+ canPickRunner: true,
2412
+ canPickCredential: false,
2413
+ });
2414
+
2415
+ // May loosen, but may not list Runners.
2416
+ grant([
2417
+ ...BASE_PERMISSIONS,
2418
+ Permission.EditAutoRemediationRule,
2419
+ Permission.EditKubernetesCluster,
2420
+ Permission.ReadKubernetesCluster,
2421
+ ]);
2422
+ expect(getKubernetesAiAccessEditCapabilities()).toEqual({
2423
+ canConfigureUnattended: true,
2424
+ canPickRunner: false,
2425
+ canPickCredential: false,
2426
+ });
2427
+
2428
+ // Reading credentials alone does not let a member bind one.
2429
+ grant([
2430
+ ...BASE_PERMISSIONS,
2431
+ Permission.ProjectMember,
2432
+ Permission.ReadRunbookCredential,
2433
+ ]);
2434
+ expect(getKubernetesAiAccessEditCapabilities()).toEqual({
2435
+ canConfigureUnattended: false,
2436
+ canPickRunner: false,
2437
+ canPickCredential: false,
2438
+ });
2439
+ });
2440
+ });
2441
+
2442
+ describe("Runner picker permission", () => {
2443
+ test("is withheld from cluster editors who may not read Runners", () => {
2444
+ for (const permissions of [
2445
+ [Permission.SettingsAdmin],
2446
+ [Permission.SettingsMember],
2447
+ [Permission.EditKubernetesCluster, Permission.ReadKubernetesCluster],
2448
+ ]) {
2449
+ grant([...BASE_PERMISSIONS, ...permissions]);
2450
+ expect(canPickKubernetesRunner()).toBe(false);
2451
+ }
2452
+ });
2453
+
2454
+ test("is offered to Runner readers and a master admin", () => {
2455
+ for (const permission of [
2456
+ Permission.ProjectMember,
2457
+ Permission.ReadRunner,
2458
+ Permission.RunbookViewer,
2459
+ ]) {
2460
+ grant([...BASE_PERMISSIONS, permission]);
2461
+ expect(canPickKubernetesRunner()).toBe(true);
2462
+ }
2463
+
2464
+ grantNothingYet();
2465
+ expect(canPickKubernetesRunner()).toBe(false);
2466
+
2467
+ jest.spyOn(User, "isMasterAdmin").mockReturnValue(true);
2468
+ expect(canPickKubernetesRunner()).toBe(true);
2469
+ });
2470
+
2471
+ test("names the permissions that would unlock it", () => {
2472
+ expect(getKubernetesRunnerPermissionTitles()).toEqual(
2473
+ expect.arrayContaining(["Read Runbook Agent", "Project Member"]),
2474
+ );
2475
+ });
2476
+ });
2477
+
2478
+ describe("command history permission", () => {
2479
+ test("is withheld from roles that may open the page but not read Runner jobs", () => {
2480
+ for (const permission of [
2481
+ Permission.SettingsAdmin,
2482
+ Permission.SettingsMember,
2483
+ Permission.SettingsViewer,
2484
+ Permission.ReadKubernetesCluster,
2485
+ Permission.EditKubernetesCluster,
2486
+ ]) {
2487
+ grant([...BASE_PERMISSIONS, permission]);
2488
+ expect(canReadKubectlJobs()).toBe(false);
2489
+ }
2490
+ });
2491
+
2492
+ test("is granted to Runner job readers and a master admin", () => {
2493
+ for (const permission of [
2494
+ Permission.ProjectMember,
2495
+ Permission.Viewer,
2496
+ Permission.RunbookViewer,
2497
+ Permission.ReadRunbookExecution,
2498
+ ]) {
2499
+ grant([...BASE_PERMISSIONS, permission]);
2500
+ expect(canReadKubectlJobs()).toBe(true);
2501
+ }
2502
+
2503
+ jest.spyOn(User, "isMasterAdmin").mockReturnValue(true);
2504
+ expect(canReadKubectlJobs()).toBe(true);
2505
+ });
2506
+
2507
+ test("names the permissions that would unlock it", () => {
2508
+ expect(getKubectlJobsPermissionTitles()).toEqual(
2509
+ expect.arrayContaining(["Viewer", "Runbook Viewer"]),
2510
+ );
2511
+ });
2512
+ });
2513
+
2514
+ describe("access test permission", () => {
2515
+ test("requires edit access to the cluster, as the /test route does", () => {
2516
+ grant([...BASE_PERMISSIONS, Permission.ReadKubernetesCluster]);
2517
+ const refused: PermissionGateResult = getAccessTestPermissionGate();
2518
+ expect(refused.isAllowed).toBe(false);
2519
+ expect(refused.disabledReason).toMatch(/Edit Kubernetes Cluster/);
2520
+
2521
+ for (const permission of [
2522
+ Permission.ProjectMember,
2523
+ Permission.SettingsMember,
2524
+ Permission.EditKubernetesCluster,
2525
+ ]) {
2526
+ grant([...BASE_PERMISSIONS, permission]);
2527
+ expect(getAccessTestPermissionGate().isAllowed).toBe(true);
2528
+ }
2529
+
2530
+ // Snapshot not landed: nothing honest to say, so no reason.
2531
+ grantNothingYet();
2532
+ expect(getAccessTestPermissionGate()).toEqual({ isAllowed: false });
2533
+ });
2534
+
2535
+ test("a refusal talks about running the test, not changing settings", () => {
2536
+ const message: string = getAccessTestPermissionMessage();
2537
+ expect(message).toMatch(/Running the access test needs permission/);
2538
+ expect(message).toMatch(/Nothing .* was changed/);
2539
+ expect(message).not.toMatch(/permission to change/);
2540
+ });
2541
+ });
2542
+
2543
+ /*
2544
+ * The page's mode copy against the canonical description on
2545
+ * KubernetesAiRemediationMode (Common/Types/Kubernetes/
2546
+ * KubernetesClusterAiAccess.ts). The shared phrases are checked in the
2547
+ * canonical comment too, so rewording it flags this copy for review.
2548
+ */
2549
+ describe("the mode copy follows the canonical description", () => {
2550
+ const CANONICAL_SOURCE: string = path.resolve(
2551
+ __dirname,
2552
+ "../../../Types/Kubernetes/KubernetesClusterAiAccess.ts",
2553
+ );
2554
+ const COMMENT_LINE_BREAK_REGEX: RegExp = /\s*\n\s*\*?\s*/g;
2555
+
2556
+ function getCanonicalModeComment(): string {
2557
+ const source: string = fs.readFileSync(CANONICAL_SOURCE, "utf8");
2558
+ const enumStart: number = source.indexOf(
2559
+ "export enum KubernetesAiRemediationMode",
2560
+ );
2561
+ const commentStart: number = source.lastIndexOf("/*", enumStart);
2562
+ expect(enumStart).toBeGreaterThan(-1);
2563
+ expect(commentStart).toBeGreaterThan(-1);
2564
+ return source
2565
+ .slice(commentStart, enumStart)
2566
+ .replace(COMMENT_LINE_BREAK_REGEX, " ");
2567
+ }
2568
+
2569
+ // Clauses of the canonical "In EVERY mode" paragraph the page repeats verbatim.
2570
+ const EVERY_MODE_CLAUSES: Array<string> = [
2571
+ "never changes its own namespace or anything outside the namespaces its chart may write",
2572
+ "the hourly per-cluster circuit breaker trips or another unattended round already holds the cluster",
2573
+ ];
2574
+
2575
+ /*
2576
+ * The node clause. The canonical comment names a node drain and a node
2577
+ * taint ("a node drain and a node taint always need a human"); the page
2578
+ * adds a patch of a node, which the policy holds to the same rule since
2579
+ * round four (a NoExecute taint written with `kubectl patch node` is the
2580
+ * same change as `kubectl taint`). Matched as a shape, so the canonical
2581
+ * comment may name the node patch too without flagging this copy.
2582
+ */
2583
+ const NODE_CLAUSE_SHAPE_REGEX: RegExp =
2584
+ /a node drain(,| and) a node taint[^;]{0,120} always need a human/;
2585
+ const PAGE_NODE_CLAUSE: string =
2586
+ "a node drain, a node taint and a patch of a node always need a human";
2587
+
2588
+ test("the canonical comment still says what the page repeats", () => {
2589
+ const canonical: string = getCanonicalModeComment();
2590
+ for (const clause of [
2591
+ ...EVERY_MODE_CLAUSES,
2592
+ "In EVERY mode, Bypass approval included",
2593
+ "set image, drain, taint, scale to zero",
2594
+ "except for what always asks",
2595
+ ]) {
2596
+ expect({ clause, inCanonical: canonical.includes(clause) }).toEqual({
2597
+ clause,
2598
+ inCanonical: true,
2599
+ });
2600
+ }
2601
+ expect(canonical).toMatch(NODE_CLAUSE_SHAPE_REGEX);
2602
+ });
2603
+
2604
+ // Negative control: the shape does not accept a clause that drops the taint.
2605
+ test("the node clause shape needs both the drain and the taint", () => {
2606
+ expect(PAGE_NODE_CLAUSE).toMatch(NODE_CLAUSE_SHAPE_REGEX);
2607
+ expect("a node drain and a node taint always need a human").toMatch(
2608
+ NODE_CLAUSE_SHAPE_REGEX,
2609
+ );
2610
+ expect("a node drain always needs a human").not.toMatch(
2611
+ NODE_CLAUSE_SHAPE_REGEX,
2612
+ );
2613
+ expect(
2614
+ "a node drain and a patch of a node always need a human",
2615
+ ).not.toMatch(NODE_CLAUSE_SHAPE_REGEX);
2616
+ });
2617
+
2618
+ test("every mode's protections name the drain, the taint, the Runner's scope and both proposal cases", () => {
2619
+ const sentence: string = getEveryModeProtectionsSentence();
2620
+ for (const clause of EVERY_MODE_CLAUSES) {
2621
+ expect({ clause, said: sentence.includes(clause) }).toEqual({
2622
+ clause,
2623
+ said: true,
2624
+ });
2625
+ }
2626
+ /*
2627
+ * Replaces the pin on "a node drain and a node taint always need a
2628
+ * human": round four adds a patch of a node, which a human must
2629
+ * approve in every mode too.
2630
+ */
2631
+ expect(sentence).toContain(PAGE_NODE_CLAUSE);
2632
+ expect(sentence).toMatch(NODE_CLAUSE_SHAPE_REGEX);
2633
+ for (const namespace of PROTECTED_KUBERNETES_NAMESPACES) {
2634
+ expect(sentence).toContain(namespace);
2635
+ }
2636
+ // The earlier copy: only the breaker, and "a taint" without "node".
2637
+ expect(sentence).not.toContain(
2638
+ "the hourly circuit breaker turns an unattended run into a proposal",
2639
+ );
2640
+ });
2641
+
2642
+ test("the mode field names the taint among the riskier changes and Bypass's exceptions", () => {
2643
+ const description: string = getRemediationModeFieldDescription();
2644
+ expect(description).toContain(
2645
+ "A riskier change (patch, set image, drain, taint, scale to zero,",
2646
+ );
2647
+ expect(description).toContain(
2648
+ "follow-up rounds included, except for what always asks. In every mode, Bypass approval included:",
2649
+ );
2650
+ expect(description).toContain(getEveryModeProtectionsSentence());
2651
+ });
2652
+
2653
+ test("the Bypass approval label names every exception, another unattended round included", () => {
2654
+ const label: string =
2655
+ REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.BypassApproval];
2656
+ for (const exception of [
2657
+ "protected namespaces",
2658
+ // Round four: a patch of a node joins the drain and the taint.
2659
+ "node drains, taints and patches",
2660
+ "circuit breaker",
2661
+ "another unattended round",
2662
+ ]) {
2663
+ expect({ exception, named: label.includes(exception) }).toEqual({
2664
+ exception,
2665
+ named: true,
2666
+ });
2667
+ }
2668
+ expect(label).not.toMatch(/\bonly\b/);
2669
+ });
2670
+
2671
+ test("the Bypass approval confirmation says AI does not ask, then what still asks", () => {
2672
+ const confirmation: KubernetesAiAccessConfirmation | null =
2673
+ getKubernetesAiAccessConfirmation({
2674
+ saved: makeSaved({
2675
+ aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
2676
+ }),
2677
+ changes: {
2678
+ aiRemediationMode: KubernetesAiRemediationMode.BypassApproval,
2679
+ },
2680
+ });
2681
+ expect(confirmation!.description).toContain(
2682
+ "With Bypass approval OneUptime AI does not ask",
2683
+ );
2684
+ expect(confirmation!.description).not.toContain("without asking anyone");
2685
+ expect(confirmation!.description).toContain(
2686
+ getEveryModeProtectionsSentence(),
2687
+ );
2688
+ });
2689
+ });
2690
+
2691
+ /*
2692
+ * Which Runner rows are kubernetes-agent Runners: the server's one rule
2693
+ * (RunnerService.isKubernetesAgentRunnerRow) — the name marker, compared
2694
+ * case-insensitively, OR an agent posture. The page read the name alone,
2695
+ * so a row whose posture says it is an agent (a legacy rename) was
2696
+ * offered like a host Runner and given credentials the server refuses.
2697
+ * KubernetesClusterAiPageServerParity.test.ts runs the same table against
2698
+ * the real RunnerService.
2699
+ */
2700
+ describe("a kubernetes-agent Runner, by the server's rule", () => {
2701
+ // An agent posture without the name marker: in a cluster, naming it.
2702
+ const postureOnly: Runner = makeRunner(
2703
+ "r-renamed",
2704
+ "prod-eu-kubectl",
2705
+ true,
2706
+ "prod-eu",
2707
+ );
2708
+ const postureOnlyHere: Runner = makeRunner(
2709
+ "r-renamed-here",
2710
+ "east-kubectl",
2711
+ true,
2712
+ "prod-east",
2713
+ );
2714
+ const upperCaseName: Runner = makeRunner(
2715
+ "r-upper",
2716
+ "KUBERNETES-AGENT/prod-eu",
2717
+ true,
2718
+ );
2719
+ // In a pod, but naming no cluster: an ordinary Runner, not an agent.
2720
+ const podNoCluster: Runner = Object.assign(new Runner(), {
2721
+ _id: "r-pod",
2722
+ name: "pod-runner",
2723
+ canRunAiCommands: true,
2724
+ hostInfo: { kubernetes: { inCluster: true, allowWrites: true } },
2725
+ });
2726
+ const hostRunner: Runner = makeRunner("r-host", "bash-runner");
2727
+
2728
+ test("the rule reads the name in any case, or the posture", () => {
2729
+ expect(isKubernetesAgentRunnerRow(postureOnly)).toBe(true);
2730
+ expect(isKubernetesAgentRunnerRow(upperCaseName)).toBe(true);
2731
+ expect(
2732
+ isKubernetesAgentRunnerRow({ name: " Kubernetes-Agent/prod-eu " }),
2733
+ ).toBe(true);
2734
+ // Negative controls: an ordinary Runner, in a pod or not.
2735
+ expect(isKubernetesAgentRunnerRow(podNoCluster)).toBe(false);
2736
+ expect(isKubernetesAgentRunnerRow(hostRunner)).toBe(false);
2737
+ expect(isKubernetesAgentRunnerRow({ name: "kubernetes-agent" })).toBe(
2738
+ false,
2739
+ );
2740
+ expect(isKubernetesAgentRunnerRow(null)).toBe(false);
2741
+ expect(isKubernetesAgentRunnerRow({})).toBe(false);
2742
+ });
2743
+
2744
+ test("the directory marks a posture-only or upper-case agent row", () => {
2745
+ const directory: Record<string, KubernetesAiRunnerDirectoryEntry> =
2746
+ buildKubernetesAiRunnerDirectory([
2747
+ postureOnly,
2748
+ upperCaseName,
2749
+ podNoCluster,
2750
+ hostRunner,
2751
+ ]);
2752
+ expect(directory["r-renamed"]!.isAgent).toBe(true);
2753
+ expect(directory["r-upper"]!.isAgent).toBe(true);
2754
+ expect(directory["r-pod"]!.isAgent).toBe(false);
2755
+ expect(directory["r-host"]!.isAgent).toBe(false);
2756
+ });
2757
+
2758
+ test("the Runner picker never offers another cluster's agent, whatever it is named", () => {
2759
+ const options: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
2760
+ runners: [postureOnly, upperCaseName, podNoCluster, hostRunner],
2761
+ clusterIdentifier: "prod-east",
2762
+ boundRunnerId: null,
2763
+ boundRunnerName: null,
2764
+ });
2765
+ // Before: the posture-only row was offered like a host Runner.
2766
+ expect(optionValues(options)).toEqual(["r-pod", "r-host"]);
2767
+ });
2768
+
2769
+ test("the Runner picker lists this cluster's agent first even without the name marker", () => {
2770
+ expect(isThisClustersAgentRunner(postureOnlyHere, "prod-east")).toBe(true);
2771
+ expect(isThisClustersAgentRunner(postureOnly, "prod-east")).toBe(false);
2772
+ expect(isThisClustersAgentRunner(podNoCluster, "prod-east")).toBe(false);
2773
+
2774
+ const options: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
2775
+ runners: [hostRunner, postureOnlyHere],
2776
+ clusterIdentifier: "prod-east",
2777
+ boundRunnerId: null,
2778
+ boundRunnerName: null,
2779
+ });
2780
+ expect(optionValues(options)).toEqual(["r-renamed-here", "r-host"]);
2781
+ expect(options[0]!.label).toBe(
2782
+ "east-kubectl (in-cluster Runner for this cluster)",
2783
+ );
2784
+ });
2785
+
2786
+ test("a bound agent of another cluster stays listed, labelled why it cannot serve", () => {
2787
+ const options: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
2788
+ runners: [postureOnly, hostRunner],
2789
+ clusterIdentifier: "prod-east",
2790
+ boundRunnerId: "r-renamed",
2791
+ boundRunnerName: "prod-eu-kubectl",
2792
+ });
2793
+ expect(optionValues(options)).toEqual(["r-renamed", "r-host"]);
2794
+ expect(options[0]!.label).toMatch(
2795
+ /currently bound — runs inside another cluster/,
2796
+ );
2797
+ });
2798
+
2799
+ test("the credential picker offers nothing for an agent row the name does not mark", () => {
2800
+ const credentials: Array<RunbookCredential> = [
2801
+ makeCredential(CREDENTIAL_ID, "prod token", ["r-renamed-here"]),
2802
+ ];
2803
+ const runner: { id: string; name: string; isAgent: boolean } = {
2804
+ id: "r-renamed-here",
2805
+ name: "east-kubectl",
2806
+ isAgent: true,
2807
+ };
2808
+
2809
+ expect(
2810
+ buildKubernetesAiCredentialOptions({
2811
+ credentials,
2812
+ boundCredentialId: null,
2813
+ boundCredentialName: null,
2814
+ runner,
2815
+ }),
2816
+ ).toEqual([]);
2817
+ expect(getKubernetesAiCredentialFieldDescription(runner)).toMatch(
2818
+ /^No credential can be chosen: "east-kubectl" is an in-cluster Runner/,
2819
+ );
2820
+ expect(
2821
+ getKubernetesAiCredentialAssignmentError({
2822
+ runner,
2823
+ credentialId: CREDENTIAL_ID,
2824
+ credentialName: "prod token",
2825
+ credentialRunnerIds: ["r-renamed-here"],
2826
+ }),
2827
+ ).toMatch(/is an in-cluster Runner/);
2828
+
2829
+ // Negative control: the same row read as an ordinary Runner.
2830
+ const ordinary: { id: string; name: string; isAgent: boolean } = {
2831
+ ...runner,
2832
+ isAgent: false,
2833
+ };
2834
+ expect(
2835
+ optionValues(
2836
+ buildKubernetesAiCredentialOptions({
2837
+ credentials,
2838
+ boundCredentialId: null,
2839
+ boundCredentialName: null,
2840
+ runner: ordinary,
2841
+ }),
2842
+ ),
2843
+ ).toEqual([CREDENTIAL_ID]);
2844
+ expect(
2845
+ getKubernetesAiCredentialAssignmentError({
2846
+ runner: ordinary,
2847
+ credentialId: CREDENTIAL_ID,
2848
+ credentialName: "prod token",
2849
+ credentialRunnerIds: ["r-renamed-here"],
2850
+ }),
2851
+ ).toBeNull();
2852
+ });
2853
+
2854
+ test("an upper-case agent name is an agent even when the row was not read", () => {
2855
+ expect(
2856
+ getKubernetesAiCredentialFieldDescription({
2857
+ id: "r-upper",
2858
+ name: "KUBERNETES-AGENT/prod-eu",
2859
+ }),
2860
+ ).toMatch(/^No credential can be chosen/);
2861
+ });
2862
+
2863
+ test("saving a credential with an agent row the name does not mark is refused", () => {
2864
+ const runners: Record<string, KubernetesAiRunnerDirectoryEntry> =
2865
+ buildKubernetesAiRunnerDirectory([postureOnlyHere]);
2866
+ const credentials: Record<string, KubernetesAiCredentialDirectoryEntry> =
2867
+ buildKubernetesAiCredentialDirectory([
2868
+ makeCredential(CREDENTIAL_ID, "prod token", ["r-renamed-here"]),
2869
+ ]);
2870
+
2871
+ expect(
2872
+ getKubernetesAiAccessBindingError({
2873
+ saved: makeSaved({
2874
+ aiAccessRunnerId: null,
2875
+ aiAccessCredentialId: null,
2876
+ }),
2877
+ changes: {
2878
+ aiAccessRunnerId: "r-renamed-here",
2879
+ aiAccessCredentialId: CREDENTIAL_ID,
2880
+ },
2881
+ runners,
2882
+ credentials,
2883
+ }),
2884
+ ).toMatch(/"east-kubectl" is an in-cluster Runner/);
2885
+ });
2886
+ });