@oneuptime/common 14.0.8 → 14.0.9

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 (639) hide show
  1. package/Models/DatabaseModels/Index.ts +2 -0
  2. package/Models/DatabaseModels/KubernetesAiAgent.ts +364 -0
  3. package/Models/DatabaseModels/KubernetesCluster.ts +3 -3
  4. package/Models/DatabaseModels/Project.ts +55 -5
  5. package/Models/DatabaseModels/ProxmoxResource.ts +49 -1
  6. package/Models/DatabaseModels/RunnerJob.ts +75 -0
  7. package/Models/DatabaseModels/TeamComplianceSetting.ts +143 -2
  8. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +17 -0
  9. package/Server/API/AIChatAPI.ts +6 -3
  10. package/Server/API/AIInvestigationAPI.ts +17 -10
  11. package/Server/API/AutoRemediationAPI.ts +207 -25
  12. package/Server/API/KubernetesClusterAiAccessAPI.ts +623 -21
  13. package/Server/API/MicrosoftTeamsAPI.ts +93 -14
  14. package/Server/API/ProxmoxResourceAPI.ts +244 -14
  15. package/Server/Infrastructure/GlobalCache.ts +32 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.ts +99 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.ts +137 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.ts +42 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  20. package/Server/Middleware/TelemetryIngest.ts +41 -18
  21. package/Server/Services/AIService.ts +70 -1
  22. package/Server/Services/AlertService.ts +352 -11
  23. package/Server/Services/AlertSeverityService.ts +65 -15
  24. package/Server/Services/AutoRemediationRuleService.ts +1 -1
  25. package/Server/Services/IncidentService.ts +280 -104
  26. package/Server/Services/IncidentSeverityService.ts +66 -15
  27. package/Server/Services/KubernetesAiAgentJobService.ts +185 -0
  28. package/Server/Services/KubernetesAiAgentService.ts +1634 -0
  29. package/Server/Services/KubernetesClusterAiAccessService.ts +1118 -451
  30. package/Server/Services/KubernetesClusterService.ts +129 -21
  31. package/Server/Services/ProjectService.ts +32 -0
  32. package/Server/Services/ProxmoxResourceService.ts +322 -5
  33. package/Server/Services/RunnerJobService.ts +241 -104
  34. package/Server/Services/RunnerService.ts +1 -1
  35. package/Server/Services/ScheduledMaintenanceService.ts +737 -1
  36. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +414 -56
  37. package/Server/Services/TeamComplianceSettingService.ts +1125 -9
  38. package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +67 -9
  39. package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +39 -10
  40. package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +14 -3
  41. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +37 -15
  42. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +67 -24
  43. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +48 -5
  44. package/Server/Utils/AI/SRE/IncidentPostmortemRunner.ts +40 -8
  45. package/Server/Utils/AI/SRE/InvestigationEligibility.ts +13 -2
  46. package/Server/Utils/AI/SRE/InvestigationGrader.ts +1 -1
  47. package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +6 -1
  48. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +133 -7
  49. package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +58 -4
  50. package/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.ts +447 -0
  51. package/Server/Utils/Telemetry/ProxmoxNativePush.ts +903 -0
  52. package/Server/Utils/Telemetry/TelemetryEntity.ts +87 -27
  53. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +684 -47
  54. package/Tests/App/AdminDashboard/RowActionPlacementOverrides.test.tsx +278 -0
  55. package/Tests/App/Dashboard/AIChatChartWidgetZoom.test.tsx +738 -0
  56. package/Tests/App/Dashboard/AIFeaturesSettingsPointers.test.tsx +147 -0
  57. package/Tests/App/Dashboard/AIInsightsKubernetesClusterPage.test.tsx +1523 -0
  58. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +463 -3
  59. package/Tests/App/Dashboard/AffectedResourcesPickerDropdownPlacement.test.tsx +958 -0
  60. package/Tests/App/Dashboard/AffectedResourcesPickerFocusLeave.test.tsx +595 -0
  61. package/Tests/App/Dashboard/AskAiAvailability.test.ts +20 -3
  62. package/Tests/App/Dashboard/AskAiDisabled.test.tsx +8 -3
  63. package/Tests/App/Dashboard/CephRateChartReload.test.tsx +1456 -0
  64. package/Tests/App/Dashboard/CephTimeRangeZoom.test.tsx +1229 -0
  65. package/Tests/App/Dashboard/ChartCardTimeRangeZoom.test.tsx +151 -0
  66. package/Tests/App/Dashboard/ChartCardZoomAffordances.test.tsx +257 -0
  67. package/Tests/App/Dashboard/ChartRefetchFrame.test.tsx +141 -0
  68. package/Tests/App/Dashboard/ChartZoomHarness.tsx +249 -0
  69. package/Tests/App/Dashboard/ChartZoomStandIn.tsx +131 -0
  70. package/Tests/App/Dashboard/CloudServerlessSlowLoads.test.tsx +628 -0
  71. package/Tests/App/Dashboard/CloudServerlessTimeRangeZoom.test.tsx +607 -0
  72. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +91 -19
  73. package/Tests/App/Dashboard/CompanionMetricsTabZoom.test.tsx +632 -0
  74. package/Tests/App/Dashboard/ContainerChartZoomStandIn.tsx +195 -0
  75. package/Tests/App/Dashboard/ContainerHostOverviewSlowLoads.test.tsx +542 -0
  76. package/Tests/App/Dashboard/ContainerHostOverviewTimeRangeZoom.test.tsx +848 -0
  77. package/Tests/App/Dashboard/ContainerMetricsTimeRangeZoom.test.tsx +561 -0
  78. package/Tests/App/Dashboard/DashboardBoardWideZoomShell.test.tsx +1147 -0
  79. package/Tests/App/Dashboard/DashboardEditPermissions.test.tsx +3 -2
  80. package/Tests/App/Dashboard/DashboardHistogramWidgetDragCursor.test.tsx +399 -0
  81. package/Tests/App/Dashboard/DashboardHistogramWidgetZoom.test.tsx +985 -0
  82. package/Tests/App/Dashboard/DashboardHistogramWidgetZoomHint.test.tsx +803 -0
  83. package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +1 -0
  84. package/Tests/App/Dashboard/DashboardSettingsPreviewZoom.test.tsx +679 -0
  85. package/Tests/App/Dashboard/DashboardSloAndDataSourceChartZoom.test.tsx +881 -0
  86. package/Tests/App/Dashboard/DashboardTraceStatusWidgets.test.tsx +878 -0
  87. package/Tests/App/Dashboard/DatabaseMetricChartModalTimeRangeZoom.test.tsx +442 -0
  88. package/Tests/App/Dashboard/DatabaseOverviewTimeRangeZoom.test.tsx +831 -0
  89. package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +43 -0
  90. package/Tests/App/Dashboard/DockerSwarmInsightsTimeRangeZoom.test.tsx +524 -0
  91. package/Tests/App/Dashboard/DockerSwarmRateChartRefresh.test.tsx +259 -0
  92. package/Tests/App/Dashboard/EdgeDetailPanelHistory.test.tsx +763 -0
  93. package/Tests/App/Dashboard/EdgeDetailPanelTimeRangeZoom.test.tsx +547 -0
  94. package/Tests/App/Dashboard/EdgeHistoryBucketsCases.tsx +565 -0
  95. package/Tests/App/Dashboard/EdgeHistoryBucketsKathmandu.test.tsx +73 -0
  96. package/Tests/App/Dashboard/EdgeHistoryBucketsKolkata.test.tsx +74 -0
  97. package/Tests/App/Dashboard/EdgeHistoryBucketsNewYork.test.tsx +72 -0
  98. package/Tests/App/Dashboard/EdgeHistoryBucketsUtc.test.tsx +72 -0
  99. package/Tests/App/Dashboard/EmbeddedMetricCardRefresh.test.tsx +310 -0
  100. package/Tests/App/Dashboard/EmbeddedMetricCardTimeRangeZoom.test.tsx +536 -0
  101. package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +3 -1
  102. package/Tests/App/Dashboard/EscalationRuleRowActions.test.tsx +660 -0
  103. package/Tests/App/Dashboard/EventNotes.test.tsx +2 -2
  104. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +587 -13
  105. package/Tests/App/Dashboard/EventOverviewSnapshotExplorerZoom.test.tsx +1139 -0
  106. package/Tests/App/Dashboard/EventOverviewSnapshotZoomAcrossTabs.test.tsx +1057 -0
  107. package/Tests/App/Dashboard/EventOverviewTelemetrySnapshotZoom.test.tsx +757 -0
  108. package/Tests/App/Dashboard/ExceptionOccurrenceTrendZoom.test.tsx +675 -0
  109. package/Tests/App/Dashboard/ExceptionOccurrencesZoomSurvivesViewSwitch.test.tsx +405 -0
  110. package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +67 -3
  111. package/Tests/App/Dashboard/ExceptionTablesSpanStatus.test.tsx +515 -0
  112. package/Tests/App/Dashboard/ExplorerOwnZoomRegression.test.tsx +739 -0
  113. package/Tests/App/Dashboard/FlowBandwidthAxisCases.tsx +461 -0
  114. package/Tests/App/Dashboard/FlowBandwidthAxisKathmandu.test.tsx +25 -0
  115. package/Tests/App/Dashboard/FlowBandwidthAxisKolkata.test.tsx +25 -0
  116. package/Tests/App/Dashboard/FlowBandwidthAxisNewYork.test.tsx +25 -0
  117. package/Tests/App/Dashboard/FlowBandwidthAxisUtc.test.tsx +24 -0
  118. package/Tests/App/Dashboard/FlowTopTalkersZoomRefetch.test.tsx +722 -0
  119. package/Tests/App/Dashboard/FlowViewportGuard.test.tsx +2526 -0
  120. package/Tests/App/Dashboard/HostPagesSlowLoads.test.tsx +872 -0
  121. package/Tests/App/Dashboard/HostPagesTimeRangeZoom.test.tsx +1208 -0
  122. package/Tests/App/Dashboard/IncidentRootCauseTimeRangeZoom.test.tsx +358 -0
  123. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +100 -9
  124. package/Tests/App/Dashboard/InfrastructureMetricsTabTimeRangeZoom.test.tsx +485 -0
  125. package/Tests/App/Dashboard/InvestigationDrawerCompanionZoom.test.tsx +655 -0
  126. package/Tests/App/Dashboard/InvestigationDrawerZoom.test.tsx +830 -0
  127. package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +338 -2
  128. package/Tests/App/Dashboard/InvoicesPayInvoice.test.tsx +454 -16
  129. package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +799 -819
  130. package/Tests/App/Dashboard/KubernetesClusterAiAgentOverviewCard.test.tsx +367 -0
  131. package/Tests/App/Dashboard/KubernetesClusterAiAgentStatus.test.ts +1202 -0
  132. package/Tests/App/Dashboard/KubernetesClusterAiDocumentationMarkdown.test.ts +106 -0
  133. package/Tests/App/Dashboard/KubernetesClusterAiNavigation.test.tsx +354 -0
  134. package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +1884 -2054
  135. package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +478 -179
  136. package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +50 -0
  137. package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +2 -1
  138. package/Tests/App/Dashboard/KubernetesClusterOverviewZoom.test.tsx +1217 -0
  139. package/Tests/App/Dashboard/KubernetesClusterPagesZoom.test.tsx +731 -0
  140. package/Tests/App/Dashboard/KubernetesCostsLoadRecovery.test.tsx +869 -0
  141. package/Tests/App/Dashboard/KubernetesCostsZoom.test.tsx +1214 -0
  142. package/Tests/App/Dashboard/KubernetesCostsZoomExactHours.test.tsx +692 -0
  143. package/Tests/App/Dashboard/KubernetesNetworkThroughputReload.test.tsx +902 -0
  144. package/Tests/App/Dashboard/KubernetesNetworkZoomBucketEdges.test.tsx +571 -0
  145. package/Tests/App/Dashboard/KubernetesOverviewZoomBucketEdges.test.tsx +697 -0
  146. package/Tests/App/Dashboard/KubernetesResourceMetricsTabZoom.test.tsx +633 -0
  147. package/Tests/App/Dashboard/LogsExplorerTimeRangeZoom.test.tsx +470 -0
  148. package/Tests/App/Dashboard/LogsInsightsTimeRangeZoom.test.tsx +948 -0
  149. package/Tests/App/Dashboard/LogsViewerPinnedWindowZoom.test.tsx +486 -0
  150. package/Tests/App/Dashboard/MetricChartsBucketInspectorDoubleClick.test.tsx +1001 -0
  151. package/Tests/App/Dashboard/MetricExplorerInvestigationAutoRefresh.test.tsx +514 -0
  152. package/Tests/App/Dashboard/MetricExplorerSavedViewZoomRoundTrip.test.tsx +581 -0
  153. package/Tests/App/Dashboard/MetricExplorerTimeRangeZoom.test.tsx +1162 -0
  154. package/Tests/App/Dashboard/MetricMonitorPreviewZoomLabel.test.ts +72 -0
  155. package/Tests/App/Dashboard/MetricRowSparklineTimeRangeZoom.test.tsx +204 -0
  156. package/Tests/App/Dashboard/MetricViewFirstResultLands.test.tsx +408 -0
  157. package/Tests/App/Dashboard/MetricViewOwnZoomResetPlacement.test.tsx +952 -0
  158. package/Tests/App/Dashboard/MetricViewTimeRange.test.ts +139 -0
  159. package/Tests/App/Dashboard/MetricViewTimeRangeZoom.test.tsx +556 -0
  160. package/Tests/App/Dashboard/MonitorMetricsChartWrappersZoom.test.tsx +552 -0
  161. package/Tests/App/Dashboard/MonitorMetricsTabsTimeRangeZoom.test.tsx +740 -0
  162. package/Tests/App/Dashboard/MonitorOverviewChartsTimeRangeZoom.test.tsx +735 -0
  163. package/Tests/App/Dashboard/MonitorStepPreviewsTimeRangeZoom.test.tsx +835 -0
  164. package/Tests/App/Dashboard/NetworkDeviceTimeRangeZoom.test.tsx +1115 -0
  165. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +186 -17
  166. package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +155 -46
  167. package/Tests/App/Dashboard/PasskeySettings.test.tsx +87 -2
  168. package/Tests/App/Dashboard/ProjectAISettingsPages.test.tsx +884 -0
  169. package/Tests/App/Dashboard/ProxmoxNodeRemove.test.tsx +621 -0
  170. package/Tests/App/Dashboard/ProxmoxOverviewSlowLoads.test.tsx +584 -0
  171. package/Tests/App/Dashboard/ProxmoxPagesTimeRangeZoom.test.tsx +823 -0
  172. package/Tests/App/Dashboard/ProxmoxRateChartReload.test.tsx +1253 -0
  173. package/Tests/App/Dashboard/PublicDashboardBoardWideZoom.test.tsx +605 -0
  174. package/Tests/App/Dashboard/RealRowsLineChart.tsx +250 -0
  175. package/Tests/App/Dashboard/RechartsHostsPlotCursor.test.tsx +513 -0
  176. package/Tests/App/Dashboard/RowActionPlacementOverrides.test.tsx +865 -0
  177. package/Tests/App/Dashboard/RumOverviewTimeRangeZoom.test.tsx +877 -0
  178. package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +20 -4
  179. package/Tests/App/Dashboard/RunnerSupersededByAiAgentNote.test.tsx +296 -0
  180. package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +146 -36
  181. package/Tests/App/Dashboard/ServiceDatabaseSlowLoads.test.tsx +1166 -0
  182. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +1243 -104
  183. package/Tests/App/Dashboard/ServiceOverviewTimeRangeZoom.test.tsx +792 -0
  184. package/Tests/App/Dashboard/SloBudgetBurnDownZoom.test.tsx +771 -0
  185. package/Tests/App/Dashboard/SloBudgetBurnDownZoomAffordances.test.tsx +380 -0
  186. package/Tests/App/Dashboard/SloEventMetricsBarChartZoom.test.tsx +369 -0
  187. package/Tests/App/Dashboard/SloHistoryChartsTimeRangeZoom.test.tsx +694 -0
  188. package/Tests/App/Dashboard/SloHistoryChartsZoomAffordances.test.tsx +497 -0
  189. package/Tests/App/Dashboard/SloMetricsTimeRangeZoom.test.tsx +541 -0
  190. package/Tests/App/Dashboard/SlowLoadHarness.ts +123 -0
  191. package/Tests/App/Dashboard/SnapshotExplorerPrimaryZoom.test.tsx +1194 -0
  192. package/Tests/App/Dashboard/SnapshotViewerPinnedWindow.test.tsx +620 -0
  193. package/Tests/App/Dashboard/TelemetrySnapshotPanelTimeRangeZoom.test.tsx +765 -0
  194. package/Tests/App/Dashboard/TelemetrySnapshotZoom.test.tsx +516 -0
  195. package/Tests/App/Dashboard/TelemetrySnapshotZoomAcrossTabs.test.tsx +827 -0
  196. package/Tests/App/Dashboard/TimeRangeZoomPageHarness.tsx +463 -0
  197. package/Tests/App/Dashboard/TraceChartEditorStatusSplit.test.tsx +543 -0
  198. package/Tests/App/Dashboard/TraceStatusPresentation.test.tsx +967 -0
  199. package/Tests/App/Dashboard/TraceTimelineMinimapZoom.test.tsx +315 -0
  200. package/Tests/App/Dashboard/TracesAnalyticsStatusSplit.test.tsx +778 -0
  201. package/Tests/App/Dashboard/TracesAnalyticsViewZoom.test.tsx +349 -0
  202. package/Tests/App/Dashboard/TracesExplorerTimeRangeZoom.test.tsx +551 -0
  203. package/Tests/App/Dashboard/TracesStatusChartAndFacet.test.tsx +687 -0
  204. package/Tests/App/Dashboard/TracesViewerStaleResponses.test.tsx +458 -0
  205. package/Tests/App/Dashboard/UptimeStripsExcludedFromZoom.test.tsx +406 -0
  206. package/Tests/App/Dashboard/VMwareOverviewSlowLoads.test.tsx +535 -0
  207. package/Tests/App/Dashboard/VMwarePagesTimeRangeZoom.test.tsx +606 -0
  208. package/Tests/App/Dashboard/ValueWidgetSparklineStaleHover.test.tsx +1292 -0
  209. package/Tests/App/Dashboard/ValueWidgetSparklineZoom.test.tsx +765 -0
  210. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +98 -0
  211. package/Tests/Models/DatabaseModels/KubernetesAiAgentModel.test.ts +295 -0
  212. package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +347 -0
  213. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +58 -0
  214. package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +1 -1
  215. package/Tests/Server/API/AIInvestigationAPI.test.ts +123 -45
  216. package/Tests/Server/API/AutoRemediationApproveClusterRound.test.ts +1329 -0
  217. package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +25 -2
  218. package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +2 -0
  219. package/Tests/Server/API/KubernetesClusterAiAccessAPIAgent.test.ts +903 -0
  220. package/Tests/Server/API/MicrosoftTeamsChatsRefreshRoute.test.ts +667 -0
  221. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +294 -6
  222. package/Tests/Server/API/ProxmoxResourceAPIRemoveNode.test.ts +1932 -0
  223. package/Tests/Server/Infrastructure/GlobalCache.test.ts +195 -0
  224. package/Tests/Server/Infrastructure/Postgres/AddKubernetesAiAgentAndAiDefaultsMigration.test.ts +623 -0
  225. package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +87 -10
  226. package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleScopeMigration.test.ts +622 -0
  227. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +61 -4
  228. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +98 -2
  229. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +105 -2
  230. package/Tests/Server/Services/AIServiceBalanceBlocker.test.ts +282 -0
  231. package/Tests/Server/Services/AlertMonitorClearWritesNull.test.ts +409 -0
  232. package/Tests/Server/Services/AlertMonitorEditFeed.test.ts +474 -0
  233. package/Tests/Server/Services/AlertMonitorEditGuard.test.ts +610 -0
  234. package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +2 -0
  235. package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +2 -1
  236. package/Tests/Server/Services/IncidentAffectedMonitorsEdit.test.ts +1360 -0
  237. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +3 -1
  238. package/Tests/Server/Services/IncidentSeverityDeletePausesComplianceRules.test.ts +488 -0
  239. package/Tests/Server/Services/KubernetesAiAgentJobService.test.ts +329 -0
  240. package/Tests/Server/Services/KubernetesAiAgentServiceHelpers.test.ts +618 -0
  241. package/Tests/Server/Services/KubernetesAiAgentServiceLifecycle.test.ts +1444 -0
  242. package/Tests/Server/Services/KubernetesAiAgentServiceRegister.test.ts +1666 -0
  243. package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +140 -111
  244. package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +267 -105
  245. package/Tests/Server/Services/KubernetesClusterAiAccessLegacySuperseded.test.ts +440 -0
  246. package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +567 -411
  247. package/Tests/Server/Services/KubernetesClusterAiAccessTarget.test.ts +1168 -0
  248. package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +410 -12
  249. package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +55 -0
  250. package/Tests/Server/Services/ProjectServiceNewProjectAiDefaults.test.ts +288 -0
  251. package/Tests/Server/Services/ProxmoxResourcePostgres.test.ts +5633 -0
  252. package/Tests/Server/Services/ProxmoxResourceService.test.ts +6458 -5
  253. package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +48 -15
  254. package/Tests/Server/Services/RunnerJobEnqueueKubectlAiAgent.test.ts +620 -0
  255. package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +170 -3
  256. package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +72 -0
  257. package/Tests/Server/Services/RunnerServiceAgentName.test.ts +6 -4
  258. package/Tests/Server/Services/ScheduledMaintenanceAttachmentEdit.test.ts +2113 -0
  259. package/Tests/Server/Services/ScheduledMaintenanceMonitorRelease.test.ts +972 -0
  260. package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +2237 -0
  261. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +3988 -138
  262. package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +107 -1
  263. package/Tests/Server/Utils/AI/IncidentPostmortemRunner.test.ts +351 -0
  264. package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +2 -0
  265. package/Tests/Server/Utils/AI/InvestigationEligibility.test.ts +310 -0
  266. package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +70 -0
  267. package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +58 -0
  268. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +2 -0
  269. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +2 -0
  270. package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +2 -0
  271. package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +125 -1
  272. package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +252 -3
  273. package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +2 -0
  274. package/Tests/Server/Utils/AI/RemediationExecutionRunnerProjectGates.test.ts +240 -0
  275. package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +2 -0
  276. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +2 -0
  277. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +2 -0
  278. package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +2 -0
  279. package/Tests/Server/Utils/Telemetry/HostAssetAttributesFromOtlp.test.ts +68 -0
  280. package/Tests/Server/Utils/Telemetry/InventoryMirrorPostgres.test.ts +370 -0
  281. package/Tests/Server/Utils/Telemetry/NetworkDeviceInventoryAttributes.test.ts +522 -0
  282. package/Tests/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.test.ts +155 -0
  283. package/Tests/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.test.ts +8631 -0
  284. package/Tests/Server/Utils/Telemetry/ProxmoxNativePush.test.ts +2852 -0
  285. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +367 -1
  286. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +45 -3
  287. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +25 -0
  288. package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNames.test.ts +3119 -0
  289. package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNamesPostgres.test.ts +1721 -0
  290. package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +581 -113
  291. package/Tests/Types/Kubernetes/KubernetesAiAgentAccessContract.test.ts +510 -0
  292. package/Tests/Types/Team/ComplianceRule.test.ts +1029 -0
  293. package/Tests/Types/Team/TeamComplianceStatus.test.ts +176 -0
  294. package/Tests/UI/Components/ActionButton/RowActions.test.tsx +641 -0
  295. package/Tests/UI/Components/ActionButton/RowActionsAccessibility.test.tsx +414 -0
  296. package/Tests/UI/Components/ActionButton/SplitActionButtons.test.ts +565 -0
  297. package/Tests/UI/Components/Charts/BarChartDragToSelect.test.tsx +386 -0
  298. package/Tests/UI/Components/Charts/ChartDoubleClickResetWithoutDblclick.test.tsx +1025 -0
  299. package/Tests/UI/Components/Charts/ChartGroupPageZoomHint.test.tsx +196 -0
  300. package/Tests/UI/Components/Charts/ChartPlaceholdersTakeDoubleClick.test.tsx +1861 -0
  301. package/Tests/UI/Components/Charts/ChartPlotClicksWhileResetArmed.test.tsx +382 -0
  302. package/Tests/UI/Components/Charts/ChartRangeSelection.test.tsx +704 -0
  303. package/Tests/UI/Components/Charts/ChartRangeSelectionDoubleClickReset.test.tsx +1481 -0
  304. package/Tests/UI/Components/Charts/ChartRangeSelectionRealRecharts.test.tsx +918 -0
  305. package/Tests/UI/Components/Charts/ChartRowBucketStart.test.ts +350 -0
  306. package/Tests/UI/Components/Charts/DoubleClickReset.test.tsx +996 -0
  307. package/Tests/UI/Components/Charts/HistogramDoubleClickResetRealRecharts.test.tsx +965 -0
  308. package/Tests/UI/Components/Charts/HistogramDriftThenRefetch.test.tsx +277 -0
  309. package/Tests/UI/Components/Charts/HistogramRangeSelectionDataChange.test.tsx +1757 -0
  310. package/Tests/UI/Components/Charts/HistogramRangeSelectionRealRecharts.test.tsx +560 -0
  311. package/Tests/UI/Components/Charts/TimeRangeZoom/ChartPageTimeRangeZoom.test.tsx +513 -0
  312. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.test.tsx +845 -0
  313. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.test.tsx +119 -0
  314. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHintTailwind.test.tsx +274 -0
  315. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.test.ts +433 -0
  316. package/Tests/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.test.tsx +863 -0
  317. package/Tests/UI/Components/Charts/TimeRangeZoom/ZoomDocsPromises.test.tsx +1147 -0
  318. package/Tests/UI/Components/DisabledButtonTooltip.test.tsx +14 -3
  319. package/Tests/UI/Components/EntityDropdownEscape.test.tsx +454 -0
  320. package/Tests/UI/Components/EntityDropdownFocusLeave.test.tsx +420 -0
  321. package/Tests/UI/Components/Forms/FormFieldDropdownDisabled.test.tsx +233 -0
  322. package/Tests/UI/Components/Input.test.tsx +80 -1
  323. package/Tests/UI/Components/LogsAnalyticsViewZoom.test.tsx +947 -0
  324. package/Tests/UI/Components/LogsHistogramDragTooltip.test.tsx +17 -5
  325. package/Tests/UI/Components/LogsViewerAnalyticsZoom.test.tsx +501 -0
  326. package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +1 -1
  327. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +6 -4
  328. package/Tests/UI/Components/ModelTable/BaseModelTablePermissionGating.test.tsx +60 -8
  329. package/Tests/UI/Components/ModelTable/BaseModelTableRowActions.test.tsx +476 -0
  330. package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +7 -5
  331. package/Tests/UI/Components/MoreMenuPortal.test.tsx +623 -0
  332. package/Tests/UI/Components/StatusPage/ResourceGridActions.test.tsx +538 -0
  333. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +14 -4
  334. package/Tests/UI/Components/TableCellWrapping.test.tsx +3 -3
  335. package/Tests/UI/Components/TableRowActions.test.tsx +472 -0
  336. package/Tests/UI/Components/TelemetryHistogramDragTooltip.test.tsx +1 -1
  337. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +1 -1
  338. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerZoomScope.test.tsx +470 -0
  339. package/Tests/UI/Components/TelemetryViewer/UseViewerTimeRangeZoom.test.tsx +571 -0
  340. package/Tests/UI/Components/TelemetryViewer/ViewerDoubleClickResetUnderDataChange.test.tsx +928 -0
  341. package/Tests/UI/Components/TelemetryViewer/ViewerFollowsEnclosingZoom.test.tsx +1569 -0
  342. package/Tests/UI/Components/Toggle.test.tsx +66 -0
  343. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardRefreshNames.test.tsx +1023 -0
  344. package/Tests/UI/Rum/ChunkLoader.test.ts +201 -0
  345. package/Tests/UI/Rum/FidelityNotices.test.ts +54 -0
  346. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +72 -0
  347. package/Tests/UI/Rum/ReplayFrameCapture.test.ts +1009 -9
  348. package/Tests/UI/Rum/ReplayRecordedAssets.test.ts +1069 -0
  349. package/Tests/UI/Rum/ReplayStage.test.tsx +913 -2
  350. package/Tests/UI/Rum/SessionReplayTable.test.tsx +443 -8
  351. package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +439 -9
  352. package/Tests/UI/Rum/UserFlowTablesRowActions.test.tsx +501 -0
  353. package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +47 -19
  354. package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +2 -0
  355. package/Tests/Utils/AiRemediation/KubernetesAiAgentPolicyCopyParity.test.ts +396 -0
  356. package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +104 -1
  357. package/Types/AI/AIRunType.ts +9 -4
  358. package/Types/AI/InvestigationNotStartedReason.ts +6 -0
  359. package/Types/AutoRemediation/AiRemediationCommandPlan.ts +14 -16
  360. package/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.ts +28 -0
  361. package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +4 -2
  362. package/Types/Kubernetes/KubernetesClusterAiAccess.ts +263 -32
  363. package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +20 -16
  364. package/Types/Kubernetes/KubernetesClusterAiInsights.ts +81 -0
  365. package/Types/Monitor/ProxmoxAlertTemplates.ts +4 -4
  366. package/Types/Team/ComplianceNotificationChannel.ts +22 -0
  367. package/Types/Team/ComplianceRule.ts +434 -0
  368. package/Types/Team/ComplianceRuleType.ts +21 -0
  369. package/Types/Team/TeamComplianceStatus.ts +81 -0
  370. package/Types/Telemetry/TelemetryIngestSurface.ts +28 -0
  371. package/UI/Components/ActionButton/ActionButtonSchema.ts +21 -0
  372. package/UI/Components/ActionButton/RowActions.tsx +238 -0
  373. package/UI/Components/ActionButton/SplitActionButtons.ts +173 -0
  374. package/UI/Components/Charts/Area/AreaChart.tsx +32 -2
  375. package/UI/Components/Charts/Bar/BarChart.tsx +40 -3
  376. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +37 -11
  377. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +80 -243
  378. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +122 -31
  379. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +95 -244
  380. package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +214 -0
  381. package/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.ts +32 -0
  382. package/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.ts +599 -0
  383. package/UI/Components/Charts/Line/LineChart.tsx +32 -2
  384. package/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.tsx +70 -0
  385. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.tsx +226 -0
  386. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.tsx +73 -0
  387. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.ts +163 -0
  388. package/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.ts +215 -0
  389. package/UI/Components/Charts/Utils/DataPoint.ts +20 -8
  390. package/UI/Components/Charts/Utils/XAxis.ts +75 -0
  391. package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +493 -59
  392. package/UI/Components/EntityDropdown/EntityDropdown.tsx +107 -11
  393. package/UI/Components/Forms/Fields/FormField.tsx +10 -0
  394. package/UI/Components/Input/Input.tsx +90 -57
  395. package/UI/Components/List/ListRow.tsx +8 -74
  396. package/UI/Components/LogsViewer/LogsViewer.tsx +40 -11
  397. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +22 -6
  398. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +209 -6
  399. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +127 -84
  400. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +3 -0
  401. package/UI/Components/ModelTable/BaseModelTable.tsx +12 -1
  402. package/UI/Components/MoreMenu/MoreMenu.tsx +365 -30
  403. package/UI/Components/MoreMenu/MoreMenuItem.tsx +70 -17
  404. package/UI/Components/OrderedStatesList/Item.tsx +7 -72
  405. package/UI/Components/StatusPage/ResourceGrid.tsx +126 -77
  406. package/UI/Components/Table/TableBody.tsx +8 -2
  407. package/UI/Components/Table/TableRow.tsx +20 -140
  408. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +41 -10
  409. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +47 -9
  410. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +25 -6
  411. package/UI/Components/TelemetryViewer/types.ts +2 -0
  412. package/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.ts +448 -0
  413. package/UI/Components/Toggle/Toggle.tsx +26 -0
  414. package/UI/Components/Tooltip/Tooltip.tsx +8 -1
  415. package/UI/Styles/Theme.css +14 -2
  416. package/Utils/AiRemediation/CommandPolicy.ts +1 -1
  417. package/Utils/AiRemediation/KubectlPolicy.ts +1 -1
  418. package/Utils/Dashboard/DashboardTimeRangeZoom.ts +46 -2
  419. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  420. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  421. package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js +385 -0
  422. package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js.map +1 -0
  423. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +3 -3
  424. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  425. package/build/dist/Models/DatabaseModels/Project.js +56 -5
  426. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  427. package/build/dist/Models/DatabaseModels/ProxmoxResource.js +51 -1
  428. package/build/dist/Models/DatabaseModels/ProxmoxResource.js.map +1 -1
  429. package/build/dist/Models/DatabaseModels/RunnerJob.js +74 -0
  430. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  431. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js +134 -3
  432. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js.map +1 -1
  433. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  434. package/build/dist/Server/API/AIChatAPI.js +6 -1
  435. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  436. package/build/dist/Server/API/AIInvestigationAPI.js +17 -10
  437. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  438. package/build/dist/Server/API/AutoRemediationAPI.js +144 -24
  439. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  440. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +436 -22
  441. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -1
  442. package/build/dist/Server/API/MicrosoftTeamsAPI.js +69 -13
  443. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  444. package/build/dist/Server/API/ProxmoxResourceAPI.js +187 -14
  445. package/build/dist/Server/API/ProxmoxResourceAPI.js.map +1 -1
  446. package/build/dist/Server/Infrastructure/GlobalCache.js +27 -0
  447. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  448. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js +50 -0
  449. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js.map +1 -0
  450. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js +84 -0
  451. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js.map +1 -0
  452. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js +31 -0
  453. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js.map +1 -0
  454. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  455. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  456. package/build/dist/Server/Middleware/TelemetryIngest.js +34 -19
  457. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  458. package/build/dist/Server/Services/AIService.js +59 -1
  459. package/build/dist/Server/Services/AIService.js.map +1 -1
  460. package/build/dist/Server/Services/AlertService.js +248 -13
  461. package/build/dist/Server/Services/AlertService.js.map +1 -1
  462. package/build/dist/Server/Services/AlertSeverityService.js +51 -15
  463. package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
  464. package/build/dist/Server/Services/AutoRemediationRuleService.js +1 -1
  465. package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
  466. package/build/dist/Server/Services/IncidentService.js +199 -72
  467. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  468. package/build/dist/Server/Services/IncidentSeverityService.js +51 -15
  469. package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
  470. package/build/dist/Server/Services/KubernetesAiAgentJobService.js +158 -0
  471. package/build/dist/Server/Services/KubernetesAiAgentJobService.js.map +1 -0
  472. package/build/dist/Server/Services/KubernetesAiAgentService.js +1145 -0
  473. package/build/dist/Server/Services/KubernetesAiAgentService.js.map +1 -0
  474. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +842 -371
  475. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -1
  476. package/build/dist/Server/Services/KubernetesClusterService.js +86 -17
  477. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  478. package/build/dist/Server/Services/ProjectService.js +22 -0
  479. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  480. package/build/dist/Server/Services/ProxmoxResourceService.js +258 -3
  481. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  482. package/build/dist/Server/Services/RunnerJobService.js +178 -92
  483. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  484. package/build/dist/Server/Services/RunnerService.js +1 -1
  485. package/build/dist/Server/Services/RunnerService.js.map +1 -1
  486. package/build/dist/Server/Services/ScheduledMaintenanceService.js +535 -3
  487. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  488. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +316 -46
  489. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  490. package/build/dist/Server/Services/TeamComplianceSettingService.js +833 -7
  491. package/build/dist/Server/Services/TeamComplianceSettingService.js.map +1 -1
  492. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +52 -9
  493. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -1
  494. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +13 -9
  495. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -1
  496. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +7 -3
  497. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -1
  498. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +29 -15
  499. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  500. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +56 -20
  501. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  502. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +39 -5
  503. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  504. package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js +30 -3
  505. package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js.map +1 -1
  506. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js +13 -2
  507. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js.map +1 -1
  508. package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js +1 -1
  509. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +4 -2
  510. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
  511. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +111 -6
  512. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  513. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +40 -4
  514. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
  515. package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js +296 -0
  516. package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js.map +1 -0
  517. package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js +699 -0
  518. package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js.map +1 -0
  519. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +77 -19
  520. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  521. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +505 -42
  522. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  523. package/build/dist/Types/AI/AIRunType.js +9 -4
  524. package/build/dist/Types/AI/AIRunType.js.map +1 -1
  525. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +10 -16
  526. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
  527. package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js +28 -0
  528. package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js.map +1 -0
  529. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +72 -16
  530. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -1
  531. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +20 -16
  532. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -1
  533. package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js +17 -0
  534. package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js.map +1 -0
  535. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +4 -4
  536. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
  537. package/build/dist/Types/Team/ComplianceNotificationChannel.js +23 -0
  538. package/build/dist/Types/Team/ComplianceNotificationChannel.js.map +1 -0
  539. package/build/dist/Types/Team/ComplianceRule.js +304 -0
  540. package/build/dist/Types/Team/ComplianceRule.js.map +1 -0
  541. package/build/dist/Types/Team/ComplianceRuleType.js +21 -0
  542. package/build/dist/Types/Team/ComplianceRuleType.js.map +1 -1
  543. package/build/dist/Types/Team/TeamComplianceStatus.js +2 -0
  544. package/build/dist/Types/Team/TeamComplianceStatus.js.map +1 -0
  545. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +25 -0
  546. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
  547. package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js +20 -1
  548. package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js.map +1 -1
  549. package/build/dist/UI/Components/ActionButton/RowActions.js +113 -0
  550. package/build/dist/UI/Components/ActionButton/RowActions.js.map +1 -0
  551. package/build/dist/UI/Components/ActionButton/SplitActionButtons.js +71 -0
  552. package/build/dist/UI/Components/ActionButton/SplitActionButtons.js.map +1 -0
  553. package/build/dist/UI/Components/Charts/Area/AreaChart.js +16 -1
  554. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  555. package/build/dist/UI/Components/Charts/Bar/BarChart.js +16 -1
  556. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  557. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +27 -5
  558. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  559. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +61 -194
  560. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  561. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +84 -26
  562. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  563. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +75 -196
  564. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  565. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +133 -0
  566. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -1
  567. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js +26 -0
  568. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js.map +1 -0
  569. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js +371 -0
  570. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js.map +1 -0
  571. package/build/dist/UI/Components/Charts/Line/LineChart.js +16 -1
  572. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  573. package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js +35 -0
  574. package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js.map +1 -0
  575. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js +107 -0
  576. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js.map +1 -0
  577. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js +41 -0
  578. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js.map +1 -0
  579. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js +117 -0
  580. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js.map +1 -0
  581. package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js +121 -0
  582. package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js.map +1 -0
  583. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -7
  584. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  585. package/build/dist/UI/Components/Charts/Utils/XAxis.js +70 -0
  586. package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
  587. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +305 -38
  588. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -1
  589. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +89 -14
  590. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  591. package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -2
  592. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  593. package/build/dist/UI/Components/Input/Input.js +69 -54
  594. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  595. package/build/dist/UI/Components/List/ListRow.js +4 -39
  596. package/build/dist/UI/Components/List/ListRow.js.map +1 -1
  597. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +31 -8
  598. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  599. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +10 -1
  600. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
  601. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +139 -21
  602. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  603. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +30 -8
  604. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  605. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
  606. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  607. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +10 -0
  608. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  609. package/build/dist/UI/Components/MoreMenu/MoreMenu.js +253 -14
  610. package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
  611. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js +43 -13
  612. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js.map +1 -1
  613. package/build/dist/UI/Components/OrderedStatesList/Item.js +2 -38
  614. package/build/dist/UI/Components/OrderedStatesList/Item.js.map +1 -1
  615. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +53 -15
  616. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  617. package/build/dist/UI/Components/Table/TableBody.js +6 -2
  618. package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
  619. package/build/dist/UI/Components/Table/TableRow.js +12 -75
  620. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  621. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +32 -7
  622. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  623. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +27 -9
  624. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  625. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +11 -1
  626. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
  627. package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js +272 -0
  628. package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js.map +1 -0
  629. package/build/dist/UI/Components/Toggle/Toggle.js +14 -1
  630. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  631. package/build/dist/UI/Components/Tooltip/Tooltip.js +1 -1
  632. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  633. package/build/dist/Utils/AiRemediation/CommandPolicy.js +1 -1
  634. package/build/dist/Utils/AiRemediation/CommandPolicy.js.map +1 -1
  635. package/build/dist/Utils/AiRemediation/KubectlPolicy.js +1 -1
  636. package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -1
  637. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +27 -1
  638. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -1
  639. package/package.json +1 -1
@@ -0,0 +1,1360 @@
1
+ import Incident from "../../../Models/DatabaseModels/Incident";
2
+ import { IncidentFeedEventType } from "../../../Models/DatabaseModels/IncidentFeed";
3
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
4
+ import MonitorStatus from "../../../Models/DatabaseModels/MonitorStatus";
5
+ import CustomFieldMappingService from "../../../Server/Services/CustomFieldMappingService";
6
+ import IncidentFeedService from "../../../Server/Services/IncidentFeedService";
7
+ import IncidentService from "../../../Server/Services/IncidentService";
8
+ import MonitorService from "../../../Server/Services/MonitorService";
9
+ import MonitorStatusService from "../../../Server/Services/MonitorStatusService";
10
+ import { OnUpdate } from "../../../Server/Types/Database/Hooks";
11
+ import UpdateBy from "../../../Server/Types/Database/UpdateBy";
12
+ import URL from "../../../Types/API/URL";
13
+ import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
14
+ import Dictionary from "../../../Types/Dictionary";
15
+ import ObjectID from "../../../Types/ObjectID";
16
+ import getJestMockFunction, { MockFunction } from "../../MockType";
17
+ import {
18
+ afterEach,
19
+ beforeEach,
20
+ describe,
21
+ expect,
22
+ jest,
23
+ test,
24
+ } from "@jest/globals";
25
+
26
+ /*
27
+ * Editing an incident's Affected Resources sends its monitor list, and the
28
+ * status it puts its monitors in, back with every save. What that does to
29
+ * the monitors themselves:
30
+ *
31
+ * - a resolved incident touches no monitor. Resolving it restored its
32
+ * monitors, and nothing would ever undo a status (or, for a manual
33
+ * incident, the switch that stops probing) put on them afterwards;
34
+ * - an open incident puts the monitors added in its status, all of its
35
+ * monitors only when the edit changes that status, and restores the
36
+ * monitors taken off it;
37
+ * - a monitor taken off is restored whenever the incident was open before
38
+ * the update, even when the same update resolves it: resolving restores
39
+ * only the monitors the incident still holds;
40
+ * - an update that sends only the status (API, CLI, workflow) changes no
41
+ * membership, and must not throw;
42
+ * - the feed says "Monitor Status Changed" only when it did.
43
+ *
44
+ * Both hooks run for real, in order: onBeforeUpdate reads the stored
45
+ * incidents through a stub of findBy and hands its carry-forward to
46
+ * onUpdateSuccess. What the database holds after the write is served by a
47
+ * stub of findOneById, and whether the incident is resolved by a stub of
48
+ * isIncidentResolved (the stored state before the write, the state after it
49
+ * from then on). The monitor side effects are spies.
50
+ */
51
+
52
+ const projectId: ObjectID = new ObjectID(
53
+ "6e1f0b2a-7c3d-4e5f-8a9b-0c1d2e3f4a01",
54
+ );
55
+ const userId: ObjectID = new ObjectID("6e1f0b2a-7c3d-4e5f-8a9b-0c1d2e3f4a02");
56
+
57
+ const INCIDENT_ID: string = "c1000000-0000-4000-8000-000000000001";
58
+ const SECOND_INCIDENT_ID: string = "c1000000-0000-4000-8000-000000000002";
59
+
60
+ const MONITOR_A: string = "d2000000-0000-4000-8000-00000000000a";
61
+ const MONITOR_B: string = "d2000000-0000-4000-8000-00000000000b";
62
+ const MONITOR_C: string = "d2000000-0000-4000-8000-00000000000c";
63
+
64
+ const OFFLINE: string = "e3000000-0000-4000-8000-0000000000f1";
65
+ const DEGRADED: string = "e3000000-0000-4000-8000-0000000000f2";
66
+
67
+ const RESOLVED_STATE: string = "f4000000-0000-4000-8000-0000000000aa";
68
+ // A state ordered after Resolved, so an incident in it is resolved too.
69
+ const CLOSED_STATE: string = "f4000000-0000-4000-8000-0000000000ab";
70
+ const ACKNOWLEDGED_STATE: string = "f4000000-0000-4000-8000-0000000000a2";
71
+
72
+ const HOST_ID: string = "a5000000-0000-4000-8000-000000000001";
73
+
74
+ const MONITOR_NAMES: Dictionary<string> = {
75
+ [MONITOR_A]: "checkout-web",
76
+ [MONITOR_B]: "payments-api",
77
+ [MONITOR_C]: "search-api",
78
+ };
79
+
80
+ const STATUS_NAMES: Dictionary<string> = {
81
+ [OFFLINE]: "Offline",
82
+ [DEGRADED]: "Degraded",
83
+ };
84
+
85
+ // An update that matches several incidents can write to up to this many.
86
+ const LIMIT_FOR_TEST: number = 10;
87
+
88
+ const PROPS: DatabaseCommonInteractionProps = {
89
+ tenantId: projectId,
90
+ userId: userId,
91
+ };
92
+
93
+ type OnBeforeUpdate = (
94
+ updateBy: UpdateBy<Incident>,
95
+ ) => Promise<OnUpdate<Incident>>;
96
+ type OnUpdateSuccess = (
97
+ onUpdate: OnUpdate<Incident>,
98
+ updatedItemIds: Array<ObjectID>,
99
+ ) => Promise<OnUpdate<Incident>>;
100
+
101
+ interface MonitorCarryForward {
102
+ monitorsRemoved: Array<Monitor>;
103
+ monitorsAdded: Array<Monitor>;
104
+ isResolvedBeforeUpdate?: boolean | undefined;
105
+ oldChangeMonitorStatusIdTo: ObjectID | undefined;
106
+ newMonitorChangeStatusIdTo: ObjectID | undefined;
107
+ isChangeMonitorStatusToCleared?: boolean | undefined;
108
+ }
109
+
110
+ // An incident as the database holds it before the update.
111
+ interface StoredIncident {
112
+ id: string;
113
+ incidentNumber: number;
114
+ monitorIds: Array<string>;
115
+ changeMonitorStatusToId: string | undefined;
116
+ // Resolved before the update, and after it unless the update moves it.
117
+ isResolved: boolean;
118
+ isCreatedAutomatically: boolean;
119
+ }
120
+
121
+ function storedIncident(
122
+ overrides: Partial<StoredIncident> = {},
123
+ ): StoredIncident {
124
+ return {
125
+ id: INCIDENT_ID,
126
+ incidentNumber: 42,
127
+ monitorIds: [MONITOR_A],
128
+ changeMonitorStatusToId: OFFLINE,
129
+ isResolved: false,
130
+ isCreatedAutomatically: false,
131
+ ...overrides,
132
+ };
133
+ }
134
+
135
+ function monitorStub(id: string): Monitor {
136
+ const monitor: Monitor = new Monitor();
137
+ monitor._id = id;
138
+ return monitor;
139
+ }
140
+
141
+ // The ids QueryHelper.any was given (it builds a Raw IN operator).
142
+ function idsInQueryOperator(operator: unknown): Array<string> {
143
+ const raw: { objectLiteralParameters?: Dictionary<unknown> } = operator as {
144
+ objectLiteralParameters?: Dictionary<unknown>;
145
+ };
146
+
147
+ return (
148
+ Object.values(raw.objectLiteralParameters || {}) as Array<Array<unknown>>
149
+ )
150
+ .flat()
151
+ .map((id: unknown): string => {
152
+ return String(id).toLowerCase();
153
+ });
154
+ }
155
+
156
+ function lowercaseIds(ids: Array<unknown>): Array<string> {
157
+ return ids.map((id: unknown): string => {
158
+ return String(id).toLowerCase();
159
+ });
160
+ }
161
+
162
+ let storedIncidents: Array<StoredIncident> = [];
163
+ // The monitors each incident holds once the update is written.
164
+ let monitorsAfterWrite: Dictionary<Array<string>> = {};
165
+ // Whether each incident is resolved once the update is written.
166
+ let resolvedAfterWrite: Dictionary<boolean> = {};
167
+
168
+ let disableActiveMonitoringSpy: jest.SpyInstance;
169
+ let changeMonitorStatus: MockFunction;
170
+ let markMonitorsActive: MockFunction;
171
+ let disableActiveMonitoring: MockFunction;
172
+ let isIncidentResolved: MockFunction;
173
+ let createFeedItem: MockFunction;
174
+ let monitorUpdateOneById: MockFunction;
175
+
176
+ /*
177
+ * Whether an incident is resolved at this point of the update: as stored
178
+ * until the write, as the write (or a state change after it) left it from
179
+ * then on.
180
+ */
181
+ function isResolvedNow(incidentId: string): boolean {
182
+ if (resolvedAfterWrite[incidentId] !== undefined) {
183
+ return resolvedAfterWrite[incidentId] === true;
184
+ }
185
+
186
+ const stored: StoredIncident | undefined = storedIncidents.find(
187
+ (incident: StoredIncident): boolean => {
188
+ return incident.id === incidentId;
189
+ },
190
+ );
191
+
192
+ return stored?.isResolved === true;
193
+ }
194
+
195
+ /*
196
+ * The update moves the incident to another state: changeIncidentState runs
197
+ * in onUpdateSuccess, and from then on the incident is resolved or not as
198
+ * given. The calls, and the isIncidentResolved reads around them, are
199
+ * recorded in order.
200
+ */
201
+ function stubStateChange(
202
+ isResolvedAfterStateChange: boolean,
203
+ order: Array<string>,
204
+ ): void {
205
+ jest
206
+ .spyOn(IncidentService, "changeIncidentState")
207
+ .mockImplementation(((data: { incidentId: ObjectID }): Promise<void> => {
208
+ order.push("changeIncidentState");
209
+ resolvedAfterWrite[data.incidentId.toString()] =
210
+ isResolvedAfterStateChange;
211
+ return Promise.resolve();
212
+ }) as never);
213
+
214
+ isIncidentResolved.mockImplementation(
215
+ (data: { incidentId: ObjectID }): Promise<boolean> => {
216
+ const isResolved: boolean = isResolvedNow(data.incidentId.toString());
217
+ order.push(`isIncidentResolved:${isResolved}`);
218
+ return Promise.resolve(isResolved);
219
+ },
220
+ );
221
+ }
222
+
223
+ function monitorIdsOf(monitors: Array<Monitor>): Array<string> {
224
+ return monitors.map((monitor: Monitor): string => {
225
+ return monitor._id!.toString().toLowerCase();
226
+ });
227
+ }
228
+
229
+ // The monitors restored, one list per markMonitorsActiveForMonitoring call.
230
+ function monitorsRestored(): Array<Array<string>> {
231
+ return markMonitorsActive.mock.calls.map(
232
+ (args: Array<unknown>): Array<string> => {
233
+ return monitorIdsOf(args[1] as Array<Monitor>);
234
+ },
235
+ );
236
+ }
237
+
238
+ /*
239
+ * The monitors a write leaves on an incident: the list the update sends,
240
+ * or the stored ones when it sends none (null clears the list).
241
+ */
242
+ function monitorIdsWritten(
243
+ data: Dictionary<unknown>,
244
+ incident: StoredIncident,
245
+ ): Array<string> {
246
+ if (data["monitors"] === undefined) {
247
+ return incident.monitorIds;
248
+ }
249
+
250
+ return ((data["monitors"] as Array<unknown> | null) || []).map(
251
+ (entry: unknown): string => {
252
+ if (typeof entry === "string") {
253
+ return entry.toLowerCase();
254
+ }
255
+
256
+ if (entry instanceof ObjectID) {
257
+ return entry.toString().toLowerCase();
258
+ }
259
+
260
+ return String((entry as { _id?: unknown })._id).toLowerCase();
261
+ },
262
+ );
263
+ }
264
+
265
+ function makeUpdateBy(
266
+ data: Dictionary<unknown>,
267
+ query: Dictionary<unknown> = { _id: INCIDENT_ID },
268
+ ): UpdateBy<Incident> {
269
+ return {
270
+ query: query as UpdateBy<Incident>["query"],
271
+ data: data as UpdateBy<Incident>["data"],
272
+ props: PROPS,
273
+ limit: LIMIT_FOR_TEST,
274
+ skip: 0,
275
+ };
276
+ }
277
+
278
+ async function runBeforeUpdate(
279
+ updateBy: UpdateBy<Incident>,
280
+ ): Promise<OnUpdate<Incident>> {
281
+ return await (
282
+ IncidentService as unknown as { onBeforeUpdate: OnBeforeUpdate }
283
+ ).onBeforeUpdate(updateBy);
284
+ }
285
+
286
+ function monitorCarryForwardOf(
287
+ onUpdate: OnUpdate<Incident>,
288
+ incidentId: string = INCIDENT_ID,
289
+ ): MonitorCarryForward {
290
+ return (onUpdate.carryForward as Dictionary<MonitorCarryForward>)[
291
+ incidentId
292
+ ]!;
293
+ }
294
+
295
+ /*
296
+ * One update end to end: onBeforeUpdate, the write (what findOneById and
297
+ * isIncidentResolved answer from then on), and onUpdateSuccess.
298
+ */
299
+ async function runUpdate(
300
+ data: Dictionary<unknown>,
301
+ options: {
302
+ query?: Dictionary<unknown>;
303
+ monitorsAfterWrite?: Dictionary<Array<string>>;
304
+ } = {},
305
+ ): Promise<OnUpdate<Incident>> {
306
+ const updateBy: UpdateBy<Incident> = makeUpdateBy(data, options.query);
307
+
308
+ const onUpdate: OnUpdate<Incident> = await runBeforeUpdate(updateBy);
309
+
310
+ for (const incident of storedIncidents) {
311
+ monitorsAfterWrite[incident.id] =
312
+ options.monitorsAfterWrite?.[incident.id] ||
313
+ monitorIdsWritten(data, incident);
314
+ resolvedAfterWrite[incident.id] = incident.isResolved;
315
+ }
316
+
317
+ await (
318
+ IncidentService as unknown as { onUpdateSuccess: OnUpdateSuccess }
319
+ ).onUpdateSuccess(
320
+ onUpdate,
321
+ storedIncidents.map((incident: StoredIncident): ObjectID => {
322
+ return new ObjectID(incident.id);
323
+ }),
324
+ );
325
+
326
+ return onUpdate;
327
+ }
328
+
329
+ // The feed item written for an incident, or undefined when none was.
330
+ function feedMarkdownFor(incidentId: string = INCIDENT_ID): string | undefined {
331
+ const call: Array<unknown> | undefined = createFeedItem.mock.calls.find(
332
+ (args: Array<unknown>): boolean => {
333
+ return (
334
+ String((args[0] as { incidentId: ObjectID }).incidentId) === incidentId
335
+ );
336
+ },
337
+ );
338
+
339
+ if (!call) {
340
+ return undefined;
341
+ }
342
+
343
+ return (call[0] as { feedInfoInMarkdown: string }).feedInfoInMarkdown;
344
+ }
345
+
346
+ function monitorLink(monitorId: string): string {
347
+ return `https://oneuptime.example/monitors/${monitorId}`;
348
+ }
349
+
350
+ // The status changes made, as [monitor ids, status id] per call.
351
+ function statusChanges(): Array<[Array<string>, string]> {
352
+ return changeMonitorStatus.mock.calls.map(
353
+ (args: Array<unknown>): [Array<string>, string] => {
354
+ return [
355
+ lowercaseIds(args[1] as Array<ObjectID>),
356
+ String(args[2]).toLowerCase(),
357
+ ];
358
+ },
359
+ );
360
+ }
361
+
362
+ beforeEach(() => {
363
+ storedIncidents = [storedIncident()];
364
+ monitorsAfterWrite = {};
365
+ resolvedAfterWrite = {};
366
+
367
+ jest.spyOn(IncidentService, "findBy").mockImplementation((() => {
368
+ return Promise.resolve(
369
+ storedIncidents.map((stored: StoredIncident): Incident => {
370
+ const incident: Incident = new Incident();
371
+ incident._id = stored.id;
372
+ incident.projectId = projectId;
373
+ incident.monitors = stored.monitorIds.map(monitorStub);
374
+ if (stored.changeMonitorStatusToId) {
375
+ incident.changeMonitorStatusToId = new ObjectID(
376
+ stored.changeMonitorStatusToId,
377
+ );
378
+ }
379
+ return incident;
380
+ }),
381
+ );
382
+ }) as never);
383
+
384
+ jest.spyOn(IncidentService, "findOneById").mockImplementation(((findBy: {
385
+ id: ObjectID;
386
+ }): Promise<Incident | null> => {
387
+ const stored: StoredIncident | undefined = storedIncidents.find(
388
+ (incident: StoredIncident): boolean => {
389
+ return incident.id === findBy.id.toString();
390
+ },
391
+ );
392
+
393
+ if (!stored) {
394
+ return Promise.resolve(null);
395
+ }
396
+
397
+ const incident: Incident = new Incident();
398
+ incident._id = stored.id;
399
+ incident.projectId = projectId;
400
+ incident.incidentNumber = stored.incidentNumber;
401
+ incident.incidentNumberWithPrefix = `INC-${stored.incidentNumber}`;
402
+ incident.isCreatedAutomatically = stored.isCreatedAutomatically;
403
+ incident.monitors = (
404
+ monitorsAfterWrite[stored.id] || stored.monitorIds
405
+ ).map(monitorStub);
406
+ return Promise.resolve(incident);
407
+ }) as never);
408
+
409
+ isIncidentResolved = getJestMockFunction();
410
+ isIncidentResolved.mockImplementation(
411
+ (data: { incidentId: ObjectID }): Promise<boolean> => {
412
+ return Promise.resolve(isResolvedNow(data.incidentId.toString()));
413
+ },
414
+ );
415
+ jest
416
+ .spyOn(IncidentService, "isIncidentResolved")
417
+ .mockImplementation(isIncidentResolved as never);
418
+
419
+ markMonitorsActive = getJestMockFunction();
420
+ markMonitorsActive.mockResolvedValue(undefined);
421
+ jest
422
+ .spyOn(IncidentService, "markMonitorsActiveForMonitoring")
423
+ .mockImplementation(markMonitorsActive as never);
424
+
425
+ disableActiveMonitoring = getJestMockFunction();
426
+ disableActiveMonitoring.mockResolvedValue(undefined);
427
+ // Restored by the tests that follow the real one down to the monitors.
428
+ disableActiveMonitoringSpy = jest
429
+ .spyOn(IncidentService, "disableActiveMonitoringIfManualIncident")
430
+ .mockImplementation(
431
+ disableActiveMonitoring as never,
432
+ ) as unknown as jest.SpyInstance;
433
+
434
+ changeMonitorStatus = getJestMockFunction();
435
+ changeMonitorStatus.mockResolvedValue(undefined);
436
+ jest
437
+ .spyOn(MonitorService, "changeMonitorStatus")
438
+ .mockImplementation(changeMonitorStatus as never);
439
+
440
+ monitorUpdateOneById = getJestMockFunction();
441
+ monitorUpdateOneById.mockResolvedValue(undefined);
442
+ jest
443
+ .spyOn(MonitorService, "updateOneById")
444
+ .mockImplementation(monitorUpdateOneById as never);
445
+
446
+ jest.spyOn(MonitorService, "findBy").mockImplementation(((findBy: {
447
+ query: { _id: unknown };
448
+ }): Promise<Array<Monitor>> => {
449
+ return Promise.resolve(
450
+ idsInQueryOperator(findBy.query._id).map((id: string): Monitor => {
451
+ const monitor: Monitor = monitorStub(id);
452
+ monitor.name = MONITOR_NAMES[id] || id;
453
+ return monitor;
454
+ }),
455
+ );
456
+ }) as never);
457
+
458
+ jest.spyOn(MonitorService, "getMonitorLinkInDashboard").mockImplementation(((
459
+ _projectId: ObjectID,
460
+ monitorId: ObjectID,
461
+ ) => {
462
+ return Promise.resolve(URL.fromString(monitorLink(monitorId.toString())));
463
+ }) as never);
464
+
465
+ jest.spyOn(MonitorStatusService, "findOneBy").mockImplementation(((findBy: {
466
+ query: { _id: unknown };
467
+ }): Promise<MonitorStatus | null> => {
468
+ const id: string = String(findBy.query._id).toLowerCase();
469
+
470
+ if (!STATUS_NAMES[id]) {
471
+ return Promise.resolve(null);
472
+ }
473
+
474
+ const status: MonitorStatus = new MonitorStatus();
475
+ status._id = id;
476
+ status.name = STATUS_NAMES[id];
477
+ return Promise.resolve(status);
478
+ }) as never);
479
+
480
+ createFeedItem = getJestMockFunction();
481
+ createFeedItem.mockResolvedValue(undefined);
482
+ jest
483
+ .spyOn(IncidentFeedService, "createIncidentFeedItem")
484
+ .mockImplementation(createFeedItem as never);
485
+
486
+ jest
487
+ .spyOn(IncidentService, "getIncidentLinkInDashboard")
488
+ .mockResolvedValue(URL.fromString("https://oneuptime.example/incidents"));
489
+
490
+ jest
491
+ .spyOn(
492
+ IncidentService as unknown as {
493
+ validateProjectScopedReferences: () => Promise<void>;
494
+ },
495
+ "validateProjectScopedReferences",
496
+ )
497
+ .mockResolvedValue(undefined as never);
498
+ jest
499
+ .spyOn(CustomFieldMappingService, "applyMappingsToUpdate")
500
+ .mockResolvedValue(undefined as never);
501
+ jest
502
+ .spyOn(CustomFieldMappingService, "restampAfterMultiRowUpdate")
503
+ .mockReturnValue(undefined as never);
504
+ });
505
+
506
+ afterEach(() => {
507
+ jest.restoreAllMocks();
508
+ });
509
+
510
+ describe("IncidentService: editing the monitors of a resolved incident", () => {
511
+ beforeEach(() => {
512
+ storedIncidents = [storedIncident({ isResolved: true })];
513
+ });
514
+
515
+ test("adding a monitor puts no monitor back in the incident's status, and the feed still lists it", async () => {
516
+ await runUpdate({
517
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_B }],
518
+ changeMonitorStatusTo: { _id: OFFLINE },
519
+ });
520
+
521
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
522
+ expect(disableActiveMonitoring).not.toHaveBeenCalled();
523
+ expect(markMonitorsActive).not.toHaveBeenCalled();
524
+
525
+ const markdown: string | undefined = feedMarkdownFor();
526
+
527
+ expect(markdown).toContain(
528
+ `**🌎 Monitors Added**:\n- [payments-api](${monitorLink(MONITOR_B)})\n`,
529
+ );
530
+ expect(markdown).not.toContain("checkout-web");
531
+ expect(markdown).not.toContain("Monitor Status Changed");
532
+ });
533
+
534
+ test("a manual incident's added monitor is never switched off active monitoring", async () => {
535
+ /*
536
+ * The switch is only cleared when an incident resolves, so setting it on
537
+ * a resolved incident stops probing the monitor for good.
538
+ */
539
+ disableActiveMonitoringSpy.mockRestore();
540
+
541
+ await runUpdate({
542
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_B }],
543
+ changeMonitorStatusTo: { _id: OFFLINE },
544
+ });
545
+
546
+ expect(monitorUpdateOneById).not.toHaveBeenCalled();
547
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
548
+ });
549
+
550
+ test("removing a monitor does not restore it again", async () => {
551
+ storedIncidents = [
552
+ storedIncident({ isResolved: true, monitorIds: [MONITOR_A, MONITOR_B] }),
553
+ ];
554
+
555
+ await runUpdate({
556
+ monitors: [{ _id: MONITOR_A }],
557
+ changeMonitorStatusTo: { _id: OFFLINE },
558
+ });
559
+
560
+ expect(markMonitorsActive).not.toHaveBeenCalled();
561
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
562
+ expect(feedMarkdownFor()).toContain(
563
+ `**🗑️ Monitors Removed**:\n- [payments-api](${monitorLink(MONITOR_B)})\n`,
564
+ );
565
+ });
566
+
567
+ test("changing the monitor status puts no monitor in it, and the feed records the change", async () => {
568
+ await runUpdate({
569
+ monitors: [{ _id: MONITOR_A }],
570
+ changeMonitorStatusTo: { _id: DEGRADED },
571
+ });
572
+
573
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
574
+ expect(feedMarkdownFor()).toContain(
575
+ "**🔄 Monitor Status Changed**:\n- **From** Offline to Degraded",
576
+ );
577
+ });
578
+
579
+ test("replacing every monitor touches none of them", async () => {
580
+ await runUpdate({
581
+ monitors: [MONITOR_B, MONITOR_C],
582
+ changeMonitorStatusToId: DEGRADED,
583
+ });
584
+
585
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
586
+ expect(markMonitorsActive).not.toHaveBeenCalled();
587
+ expect(disableActiveMonitoring).not.toHaveBeenCalled();
588
+
589
+ const markdown: string | undefined = feedMarkdownFor();
590
+
591
+ expect(markdown).toContain("**🗑️ Monitors Removed**:");
592
+ expect(markdown).toContain("**🌎 Monitors Added**:");
593
+ });
594
+
595
+ test("the incident's state is read after the update is written", async () => {
596
+ /*
597
+ * An edit that resolves the incident and adds a monitor at once: changing
598
+ * the state restores every monitor the incident holds then, the added
599
+ * one included, so the added monitor must not be put in the incident's
600
+ * status after that.
601
+ */
602
+ storedIncidents = [storedIncident({ isResolved: false })];
603
+
604
+ const order: Array<string> = [];
605
+
606
+ jest
607
+ .spyOn(IncidentService, "changeIncidentState")
608
+ .mockImplementation(((data: { incidentId: ObjectID }): Promise<void> => {
609
+ order.push("changeIncidentState");
610
+ resolvedAfterWrite[data.incidentId.toString()] = true;
611
+ return Promise.resolve();
612
+ }) as never);
613
+
614
+ isIncidentResolved.mockImplementation(
615
+ (data: { incidentId: ObjectID }): Promise<boolean> => {
616
+ order.push("isIncidentResolved");
617
+ return Promise.resolve(
618
+ resolvedAfterWrite[data.incidentId.toString()] === true,
619
+ );
620
+ },
621
+ );
622
+
623
+ await runUpdate({
624
+ currentIncidentStateId: new ObjectID(RESOLVED_STATE),
625
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_B }],
626
+ changeMonitorStatusTo: { _id: OFFLINE },
627
+ });
628
+
629
+ expect(order).toEqual(["changeIncidentState", "isIncidentResolved"]);
630
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
631
+ expect(disableActiveMonitoring).not.toHaveBeenCalled();
632
+ });
633
+ });
634
+
635
+ describe("IncidentService: editing the monitors of an open incident", () => {
636
+ test("an added monitor is put in the incident's status, and only it", async () => {
637
+ await runUpdate({
638
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_B }],
639
+ changeMonitorStatusTo: { _id: OFFLINE },
640
+ });
641
+
642
+ expect(changeMonitorStatus).toHaveBeenCalledTimes(1);
643
+
644
+ const args: Array<unknown> = changeMonitorStatus.mock.calls[0]!;
645
+
646
+ expect(String(args[0])).toBe(projectId.toString());
647
+ expect(lowercaseIds(args[1] as Array<ObjectID>)).toEqual([MONITOR_B]);
648
+ expect(String(args[2])).toBe(OFFLINE);
649
+ expect(args[3]).toBe(true);
650
+ expect(args[4]).toBe(
651
+ "Status was changed because Incident INC-42 was updated.",
652
+ );
653
+ expect(args[6]).toBe(PROPS);
654
+
655
+ expect(disableActiveMonitoring).toHaveBeenCalledTimes(1);
656
+ expect(String(disableActiveMonitoring.mock.calls[0]![0])).toBe(INCIDENT_ID);
657
+ expect(markMonitorsActive).not.toHaveBeenCalled();
658
+
659
+ expect(feedMarkdownFor()).toContain(
660
+ `**🌎 Monitors Added**:\n- [payments-api](${monitorLink(MONITOR_B)})\n`,
661
+ );
662
+ });
663
+
664
+ test("a manual incident stops probing its monitors when one is added", async () => {
665
+ disableActiveMonitoringSpy.mockRestore();
666
+
667
+ await runUpdate({
668
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_B }],
669
+ changeMonitorStatusTo: { _id: OFFLINE },
670
+ });
671
+
672
+ const switchedOff: Array<string> = monitorUpdateOneById.mock.calls
673
+ .filter((args: Array<unknown>): boolean => {
674
+ return (
675
+ (args[0] as { data: Dictionary<unknown> }).data[
676
+ "disableActiveMonitoringBecauseOfManualIncident"
677
+ ] === true
678
+ );
679
+ })
680
+ .map((args: Array<unknown>): string => {
681
+ return String((args[0] as { id: ObjectID }).id).toLowerCase();
682
+ });
683
+
684
+ expect(switchedOff.sort()).toEqual([MONITOR_A, MONITOR_B]);
685
+ });
686
+
687
+ test("an automatically created incident leaves probing on when a monitor is added", async () => {
688
+ storedIncidents = [storedIncident({ isCreatedAutomatically: true })];
689
+
690
+ disableActiveMonitoringSpy.mockRestore();
691
+
692
+ await runUpdate({
693
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_B }],
694
+ changeMonitorStatusTo: { _id: OFFLINE },
695
+ });
696
+
697
+ expect(monitorUpdateOneById).not.toHaveBeenCalled();
698
+ expect(statusChanges()).toEqual([[[MONITOR_B], OFFLINE]]);
699
+ });
700
+
701
+ test("a removed monitor is restored, and no monitor is put in the status again", async () => {
702
+ storedIncidents = [storedIncident({ monitorIds: [MONITOR_A, MONITOR_B] })];
703
+
704
+ await runUpdate({
705
+ monitors: [{ _id: MONITOR_A }],
706
+ changeMonitorStatusTo: { _id: OFFLINE },
707
+ });
708
+
709
+ expect(markMonitorsActive).toHaveBeenCalledTimes(1);
710
+ expect(String(markMonitorsActive.mock.calls[0]![0])).toBe(
711
+ projectId.toString(),
712
+ );
713
+ expect(
714
+ lowercaseIds(
715
+ (markMonitorsActive.mock.calls[0]![1] as Array<Monitor>).map(
716
+ (monitor: Monitor): string => {
717
+ return monitor._id!.toString();
718
+ },
719
+ ),
720
+ ),
721
+ ).toEqual([MONITOR_B]);
722
+
723
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
724
+ expect(disableActiveMonitoring).not.toHaveBeenCalled();
725
+ expect(feedMarkdownFor()).toContain(
726
+ `**🗑️ Monitors Removed**:\n- [payments-api](${monitorLink(MONITOR_B)})\n`,
727
+ );
728
+ });
729
+
730
+ test("replacing a monitor restores the old one and puts the new one in the status", async () => {
731
+ await runUpdate({
732
+ monitors: [{ _id: MONITOR_B }],
733
+ changeMonitorStatusTo: { _id: OFFLINE },
734
+ });
735
+
736
+ expect(
737
+ (markMonitorsActive.mock.calls[0]![1] as Array<Monitor>).map(
738
+ (monitor: Monitor): string => {
739
+ return monitor._id!.toString();
740
+ },
741
+ ),
742
+ ).toEqual([MONITOR_A]);
743
+ expect(statusChanges()).toEqual([[[MONITOR_B], OFFLINE]]);
744
+ });
745
+
746
+ test("saving the edit form unchanged puts no monitor in the status again", async () => {
747
+ /*
748
+ * The Affected Resources form sends the monitors and the status back
749
+ * with every save, here with only the hosts edited. A probe may have
750
+ * moved a monitor out of the incident's status since; forcing it back
751
+ * wrote "Status was changed because Incident #N was updated.".
752
+ */
753
+ storedIncidents = [storedIncident({ monitorIds: [MONITOR_A, MONITOR_B] })];
754
+
755
+ await runUpdate({
756
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_B }],
757
+ changeMonitorStatusTo: { _id: OFFLINE },
758
+ hosts: [{ _id: HOST_ID }],
759
+ });
760
+
761
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
762
+ expect(markMonitorsActive).not.toHaveBeenCalled();
763
+ expect(disableActiveMonitoring).not.toHaveBeenCalled();
764
+ // Nothing changed for the monitors, so there is nothing to look up.
765
+ expect(isIncidentResolved).not.toHaveBeenCalled();
766
+ // And nothing for the feed: "From Offline to Offline" is not a change.
767
+ expect(createFeedItem).not.toHaveBeenCalled();
768
+ });
769
+
770
+ test("changing the monitor status puts every monitor in the new one", async () => {
771
+ storedIncidents = [storedIncident({ monitorIds: [MONITOR_A, MONITOR_B] })];
772
+
773
+ await runUpdate({
774
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_B }],
775
+ changeMonitorStatusTo: { _id: DEGRADED },
776
+ });
777
+
778
+ expect(statusChanges()).toEqual([[[MONITOR_A, MONITOR_B], DEGRADED]]);
779
+ expect(markMonitorsActive).not.toHaveBeenCalled();
780
+ expect(disableActiveMonitoring).not.toHaveBeenCalled();
781
+ expect(feedMarkdownFor()).toContain(
782
+ "**🔄 Monitor Status Changed**:\n- **From** Offline to Degraded",
783
+ );
784
+ });
785
+
786
+ test("changing the status and adding a monitor puts all of them in the new status at once", async () => {
787
+ await runUpdate({
788
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_B }],
789
+ changeMonitorStatusTo: { _id: DEGRADED },
790
+ });
791
+
792
+ expect(statusChanges()).toEqual([[[MONITOR_A, MONITOR_B], DEGRADED]]);
793
+ expect(disableActiveMonitoring).toHaveBeenCalledTimes(1);
794
+ });
795
+
796
+ test("giving an incident a status it did not have puts every monitor in it", async () => {
797
+ storedIncidents = [
798
+ storedIncident({
799
+ monitorIds: [MONITOR_A, MONITOR_B],
800
+ changeMonitorStatusToId: undefined,
801
+ }),
802
+ ];
803
+
804
+ await runUpdate({
805
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_B }],
806
+ changeMonitorStatusTo: { _id: OFFLINE },
807
+ });
808
+
809
+ expect(statusChanges()).toEqual([[[MONITOR_A, MONITOR_B], OFFLINE]]);
810
+ });
811
+
812
+ test("an added monitor of an incident without a status keeps its own", async () => {
813
+ storedIncidents = [storedIncident({ changeMonitorStatusToId: undefined })];
814
+
815
+ await runUpdate({
816
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_B }],
817
+ });
818
+
819
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
820
+ // Probing still stops for a manual incident's monitors, as on create.
821
+ expect(disableActiveMonitoring).toHaveBeenCalledTimes(1);
822
+ });
823
+
824
+ test("clearing the status while adding a monitor puts the added monitor in no status", async () => {
825
+ await runUpdate({
826
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_B }],
827
+ changeMonitorStatusTo: null,
828
+ changeMonitorStatusToId: null,
829
+ });
830
+
831
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
832
+ expect(disableActiveMonitoring).toHaveBeenCalledTimes(1);
833
+ });
834
+
835
+ test("the status is put only on monitors the incident holds after the write", async () => {
836
+ /*
837
+ * The monitors are read back once the update is written, so a status
838
+ * is never put on a monitor the write did not leave on the incident.
839
+ */
840
+ await runUpdate(
841
+ {
842
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_B }],
843
+ changeMonitorStatusTo: { _id: OFFLINE },
844
+ },
845
+ { monitorsAfterWrite: { [INCIDENT_ID]: [MONITOR_A] } },
846
+ );
847
+
848
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
849
+ });
850
+ });
851
+
852
+ describe("IncidentService: an update that changes the incident's state and its monitors at once", () => {
853
+ test("resolving the incident and taking a monitor off restores the monitor taken off", async () => {
854
+ /*
855
+ * Resolving restores only the monitors the incident holds after the
856
+ * write (IncidentStateTimelineService), so the one taken off in the same
857
+ * update is restored here or never: it would sit in the incident's
858
+ * status, and a manual incident would keep it from being probed.
859
+ */
860
+ storedIncidents = [storedIncident({ monitorIds: [MONITOR_A, MONITOR_B] })];
861
+
862
+ const order: Array<string> = [];
863
+ stubStateChange(true, order);
864
+
865
+ await runUpdate({
866
+ currentIncidentStateId: new ObjectID(RESOLVED_STATE),
867
+ monitors: [{ _id: MONITOR_A }],
868
+ changeMonitorStatusTo: { _id: OFFLINE },
869
+ });
870
+
871
+ expect(monitorsRestored()).toEqual([[MONITOR_B]]);
872
+ expect(String(markMonitorsActive.mock.calls[0]![0])).toBe(
873
+ projectId.toString(),
874
+ );
875
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
876
+ expect(disableActiveMonitoring).not.toHaveBeenCalled();
877
+ // The state is read before the write, while it is still open.
878
+ expect(order).toEqual(["isIncidentResolved:false", "changeIncidentState"]);
879
+ expect(feedMarkdownFor()).toContain(
880
+ `**🗑️ Monitors Removed**:\n- [payments-api](${monitorLink(MONITOR_B)})\n`,
881
+ );
882
+ });
883
+
884
+ test("resolving the incident and taking every monitor off restores all of them", async () => {
885
+ storedIncidents = [storedIncident({ monitorIds: [MONITOR_A, MONITOR_B] })];
886
+
887
+ stubStateChange(true, []);
888
+
889
+ await runUpdate({
890
+ currentIncidentStateId: new ObjectID(RESOLVED_STATE),
891
+ monitors: [],
892
+ });
893
+
894
+ expect(monitorsRestored()).toEqual([[MONITOR_A, MONITOR_B]]);
895
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
896
+ });
897
+
898
+ test("resolving the incident and replacing a monitor restores the old one and leaves the new one alone", async () => {
899
+ const order: Array<string> = [];
900
+ stubStateChange(true, order);
901
+
902
+ await runUpdate({
903
+ currentIncidentStateId: new ObjectID(RESOLVED_STATE),
904
+ monitors: [{ _id: MONITOR_B }],
905
+ changeMonitorStatusTo: { _id: OFFLINE },
906
+ });
907
+
908
+ expect(monitorsRestored()).toEqual([[MONITOR_A]]);
909
+ // Resolving restored the added monitor with the others it holds.
910
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
911
+ expect(disableActiveMonitoring).not.toHaveBeenCalled();
912
+ expect(order).toEqual([
913
+ "isIncidentResolved:false",
914
+ "changeIncidentState",
915
+ "isIncidentResolved:true",
916
+ ]);
917
+ });
918
+
919
+ test("resolving the incident and changing its status restores the monitor taken off and puts none in the new status", async () => {
920
+ storedIncidents = [storedIncident({ monitorIds: [MONITOR_A, MONITOR_B] })];
921
+
922
+ stubStateChange(true, []);
923
+
924
+ await runUpdate({
925
+ currentIncidentStateId: new ObjectID(RESOLVED_STATE),
926
+ monitors: [{ _id: MONITOR_A }],
927
+ changeMonitorStatusTo: { _id: DEGRADED },
928
+ });
929
+
930
+ expect(monitorsRestored()).toEqual([[MONITOR_B]]);
931
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
932
+ expect(feedMarkdownFor()).toContain("**From** Offline to Degraded");
933
+ });
934
+
935
+ test("acknowledging the incident and taking a monitor off restores it as on any open incident", async () => {
936
+ storedIncidents = [storedIncident({ monitorIds: [MONITOR_A, MONITOR_B] })];
937
+
938
+ stubStateChange(false, []);
939
+
940
+ await runUpdate({
941
+ currentIncidentStateId: new ObjectID(ACKNOWLEDGED_STATE),
942
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }],
943
+ changeMonitorStatusTo: { _id: OFFLINE },
944
+ });
945
+
946
+ expect(monitorsRestored()).toEqual([[MONITOR_B]]);
947
+ expect(statusChanges()).toEqual([[[MONITOR_C], OFFLINE]]);
948
+ expect(disableActiveMonitoring).toHaveBeenCalledTimes(1);
949
+ });
950
+
951
+ test("reopening a resolved incident and taking a monitor off does not restore it again", async () => {
952
+ /*
953
+ * It was restored when the incident resolved. Restoring it again could
954
+ * overwrite a status set on it since.
955
+ */
956
+ storedIncidents = [
957
+ storedIncident({ monitorIds: [MONITOR_A, MONITOR_B], isResolved: true }),
958
+ ];
959
+
960
+ const order: Array<string> = [];
961
+ stubStateChange(false, order);
962
+
963
+ await runUpdate({
964
+ currentIncidentStateId: new ObjectID(ACKNOWLEDGED_STATE),
965
+ monitors: [{ _id: MONITOR_A }],
966
+ changeMonitorStatusTo: { _id: OFFLINE },
967
+ });
968
+
969
+ expect(markMonitorsActive).not.toHaveBeenCalled();
970
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
971
+ expect(order).toEqual(["isIncidentResolved:true", "changeIncidentState"]);
972
+ expect(feedMarkdownFor()).toContain("**🗑️ Monitors Removed**:");
973
+ });
974
+
975
+ test("reopening a resolved incident and adding a monitor puts the added monitor in the incident's status", async () => {
976
+ storedIncidents = [storedIncident({ isResolved: true })];
977
+
978
+ stubStateChange(false, []);
979
+
980
+ await runUpdate({
981
+ currentIncidentStateId: new ObjectID(ACKNOWLEDGED_STATE),
982
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_B }],
983
+ changeMonitorStatusTo: { _id: OFFLINE },
984
+ });
985
+
986
+ expect(statusChanges()).toEqual([[[MONITOR_B], OFFLINE]]);
987
+ expect(disableActiveMonitoring).toHaveBeenCalledTimes(1);
988
+ expect(markMonitorsActive).not.toHaveBeenCalled();
989
+ });
990
+
991
+ test("moving a resolved incident to a later resolved state and taking a monitor off does not restore it", async () => {
992
+ /*
993
+ * The update changes the state, but the incident was resolved before it
994
+ * as well, so its monitors were restored already.
995
+ */
996
+ storedIncidents = [
997
+ storedIncident({ monitorIds: [MONITOR_A, MONITOR_B], isResolved: true }),
998
+ ];
999
+
1000
+ stubStateChange(true, []);
1001
+
1002
+ await runUpdate({
1003
+ currentIncidentStateId: new ObjectID(CLOSED_STATE),
1004
+ monitors: [{ _id: MONITOR_A }],
1005
+ });
1006
+
1007
+ expect(markMonitorsActive).not.toHaveBeenCalled();
1008
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
1009
+ });
1010
+
1011
+ test("each incident of the update restores the monitors taken off it by its own state before the update", async () => {
1012
+ storedIncidents = [
1013
+ storedIncident({ monitorIds: [MONITOR_A, MONITOR_B] }),
1014
+ storedIncident({
1015
+ id: SECOND_INCIDENT_ID,
1016
+ incidentNumber: 43,
1017
+ monitorIds: [MONITOR_B, MONITOR_C],
1018
+ isResolved: true,
1019
+ }),
1020
+ ];
1021
+
1022
+ stubStateChange(true, []);
1023
+
1024
+ await runUpdate(
1025
+ {
1026
+ currentIncidentStateId: new ObjectID(RESOLVED_STATE),
1027
+ monitors: [MONITOR_B],
1028
+ },
1029
+ { query: { projectId: projectId } },
1030
+ );
1031
+
1032
+ // Only the incident that was open restores the monitor taken off it.
1033
+ expect(monitorsRestored()).toEqual([[MONITOR_A]]);
1034
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
1035
+ expect(feedMarkdownFor(INCIDENT_ID)).toContain(
1036
+ `**🗑️ Monitors Removed**:\n- [checkout-web](${monitorLink(MONITOR_A)})\n`,
1037
+ );
1038
+ expect(feedMarkdownFor(SECOND_INCIDENT_ID)).toContain(
1039
+ `**🗑️ Monitors Removed**:\n- [search-api](${monitorLink(MONITOR_C)})\n`,
1040
+ );
1041
+ });
1042
+ });
1043
+
1044
+ describe("IncidentService: an update that sends the monitor status without the monitor list", () => {
1045
+ beforeEach(() => {
1046
+ storedIncidents = [storedIncident({ monitorIds: [MONITOR_A, MONITOR_B] })];
1047
+ });
1048
+
1049
+ test("does not throw, and takes no monitor off the incident", async () => {
1050
+ const onUpdate: OnUpdate<Incident> = await runBeforeUpdate(
1051
+ makeUpdateBy({ changeMonitorStatusToId: new ObjectID(DEGRADED) }),
1052
+ );
1053
+
1054
+ const carryForward: MonitorCarryForward = monitorCarryForwardOf(onUpdate);
1055
+
1056
+ expect(carryForward.monitorsRemoved).toEqual([]);
1057
+ expect(carryForward.monitorsAdded).toEqual([]);
1058
+ expect(String(carryForward.oldChangeMonitorStatusIdTo)).toBe(OFFLINE);
1059
+ expect(String(carryForward.newMonitorChangeStatusIdTo)).toBe(DEGRADED);
1060
+ });
1061
+
1062
+ test("puts every monitor in the new status and restores none", async () => {
1063
+ await runUpdate({ changeMonitorStatusToId: new ObjectID(DEGRADED) });
1064
+
1065
+ expect(statusChanges()).toEqual([[[MONITOR_A, MONITOR_B], DEGRADED]]);
1066
+ expect(markMonitorsActive).not.toHaveBeenCalled();
1067
+ expect(disableActiveMonitoring).not.toHaveBeenCalled();
1068
+ expect(feedMarkdownFor()).toContain("**From** Offline to Degraded");
1069
+ expect(feedMarkdownFor()).not.toContain("Monitors Removed");
1070
+ });
1071
+
1072
+ test.each([
1073
+ ["a bare id string", { changeMonitorStatusToId: DEGRADED }],
1074
+ ["the relation object", { changeMonitorStatusTo: { _id: DEGRADED } }],
1075
+ ["a bare id in the relation", { changeMonitorStatusTo: DEGRADED }],
1076
+ ] as Array<[string, Dictionary<unknown>]>)(
1077
+ "reads the status given as %s",
1078
+ async (_label: string, data: Dictionary<unknown>) => {
1079
+ await runUpdate(data);
1080
+
1081
+ expect(statusChanges()).toEqual([[[MONITOR_A, MONITOR_B], DEGRADED]]);
1082
+ },
1083
+ );
1084
+
1085
+ test("the same status again changes nothing and writes no feed item", async () => {
1086
+ await runUpdate({ changeMonitorStatusToId: new ObjectID(OFFLINE) });
1087
+
1088
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
1089
+ expect(markMonitorsActive).not.toHaveBeenCalled();
1090
+ expect(createFeedItem).not.toHaveBeenCalled();
1091
+ });
1092
+
1093
+ test("on a resolved incident, puts no monitor in the new status", async () => {
1094
+ storedIncidents = [
1095
+ storedIncident({ monitorIds: [MONITOR_A, MONITOR_B], isResolved: true }),
1096
+ ];
1097
+
1098
+ await runUpdate({ changeMonitorStatusToId: new ObjectID(DEGRADED) });
1099
+
1100
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
1101
+ expect(feedMarkdownFor()).toContain("**From** Offline to Degraded");
1102
+ });
1103
+ });
1104
+
1105
+ describe("IncidentService: the feed's monitor status line", () => {
1106
+ test("is not written when the status saved is the one the incident had", async () => {
1107
+ await runUpdate({
1108
+ title: "Checkout is slow",
1109
+ monitors: [{ _id: MONITOR_A }],
1110
+ changeMonitorStatusTo: { _id: OFFLINE },
1111
+ });
1112
+
1113
+ const markdown: string | undefined = feedMarkdownFor();
1114
+
1115
+ // The feed item is written for the title.
1116
+ expect(markdown).toContain("Checkout is slow");
1117
+ expect(markdown).not.toContain("Monitor Status Changed");
1118
+ });
1119
+
1120
+ test("compares the ids regardless of case", async () => {
1121
+ await runUpdate({
1122
+ monitors: [{ _id: MONITOR_A }],
1123
+ changeMonitorStatusToId: OFFLINE.toUpperCase(),
1124
+ });
1125
+
1126
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
1127
+ expect(createFeedItem).not.toHaveBeenCalled();
1128
+ });
1129
+
1130
+ test("is written once when the status changes", async () => {
1131
+ await runUpdate({
1132
+ monitors: [{ _id: MONITOR_A }],
1133
+ changeMonitorStatusTo: { _id: DEGRADED },
1134
+ });
1135
+
1136
+ const markdown: string = feedMarkdownFor()!;
1137
+
1138
+ expect(markdown.match(/Monitor Status Changed/g)).toHaveLength(1);
1139
+ expect(
1140
+ (createFeedItem.mock.calls[0]![0] as { incidentFeedEventType: string })
1141
+ .incidentFeedEventType,
1142
+ ).toBe(IncidentFeedEventType.IncidentUpdated);
1143
+ });
1144
+ });
1145
+
1146
+ describe("IncidentService.onBeforeUpdate: which monitors an update adds and removes", () => {
1147
+ beforeEach(() => {
1148
+ storedIncidents = [storedIncident({ monitorIds: [MONITOR_A, MONITOR_B] })];
1149
+ });
1150
+
1151
+ function addedIds(carryForward: MonitorCarryForward): Array<string> {
1152
+ return carryForward.monitorsAdded.map((monitor: Monitor): string => {
1153
+ return monitor._id!.toString().toLowerCase();
1154
+ });
1155
+ }
1156
+
1157
+ function removedIds(carryForward: MonitorCarryForward): Array<string> {
1158
+ return carryForward.monitorsRemoved.map((monitor: Monitor): string => {
1159
+ return monitor._id!.toString().toLowerCase();
1160
+ });
1161
+ }
1162
+
1163
+ test.each([
1164
+ ["relation objects", [{ _id: MONITOR_A }, { _id: MONITOR_B }]],
1165
+ ["bare id strings (API update)", [MONITOR_A, MONITOR_B]],
1166
+ ["ObjectIDs", [new ObjectID(MONITOR_A), new ObjectID(MONITOR_B)]],
1167
+ ["models", [monitorStub(MONITOR_A), monitorStub(MONITOR_B)]],
1168
+ ["upper-case ids", [MONITOR_A.toUpperCase(), MONITOR_B.toUpperCase()]],
1169
+ ] as Array<[string, Array<unknown>]>)(
1170
+ "re-sending the same monitors as %s changes nothing",
1171
+ async (_label: string, monitors: Array<unknown>) => {
1172
+ const carryForward: MonitorCarryForward = monitorCarryForwardOf(
1173
+ await runBeforeUpdate(makeUpdateBy({ monitors: monitors })),
1174
+ );
1175
+
1176
+ expect(removedIds(carryForward)).toEqual([]);
1177
+ expect(addedIds(carryForward)).toEqual([]);
1178
+ },
1179
+ );
1180
+
1181
+ test("records the monitors added and removed", async () => {
1182
+ const carryForward: MonitorCarryForward = monitorCarryForwardOf(
1183
+ await runBeforeUpdate(
1184
+ makeUpdateBy({ monitors: [MONITOR_B, { _id: MONITOR_C }] }),
1185
+ ),
1186
+ );
1187
+
1188
+ expect(removedIds(carryForward)).toEqual([MONITOR_A]);
1189
+ expect(addedIds(carryForward)).toEqual([MONITOR_C]);
1190
+ });
1191
+
1192
+ test("a monitor sent twice is added once", async () => {
1193
+ const carryForward: MonitorCarryForward = monitorCarryForwardOf(
1194
+ await runBeforeUpdate(
1195
+ makeUpdateBy({
1196
+ monitors: [MONITOR_A, MONITOR_B, MONITOR_C, { _id: MONITOR_C }],
1197
+ }),
1198
+ ),
1199
+ );
1200
+
1201
+ expect(addedIds(carryForward)).toEqual([MONITOR_C]);
1202
+ });
1203
+
1204
+ test.each([
1205
+ ["an empty list", []],
1206
+ ["null", null],
1207
+ ] as Array<[string, Array<unknown> | null]>)(
1208
+ "%s takes every monitor off",
1209
+ async (_label: string, monitors: Array<unknown> | null) => {
1210
+ const carryForward: MonitorCarryForward = monitorCarryForwardOf(
1211
+ await runBeforeUpdate(makeUpdateBy({ monitors: monitors })),
1212
+ );
1213
+
1214
+ expect(removedIds(carryForward)).toEqual([MONITOR_A, MONITOR_B]);
1215
+ expect(addedIds(carryForward)).toEqual([]);
1216
+ },
1217
+ );
1218
+
1219
+ test("records clearing the status", async () => {
1220
+ const carryForward: MonitorCarryForward = monitorCarryForwardOf(
1221
+ await runBeforeUpdate(
1222
+ makeUpdateBy({
1223
+ monitors: [MONITOR_A, MONITOR_B],
1224
+ changeMonitorStatusTo: null,
1225
+ }),
1226
+ ),
1227
+ );
1228
+
1229
+ expect(carryForward.isChangeMonitorStatusToCleared).toBe(true);
1230
+ expect(carryForward.newMonitorChangeStatusIdTo).toBeUndefined();
1231
+ });
1232
+
1233
+ test("a status sent is not a clear, even next to a null relation", async () => {
1234
+ const carryForward: MonitorCarryForward = monitorCarryForwardOf(
1235
+ await runBeforeUpdate(
1236
+ makeUpdateBy({
1237
+ changeMonitorStatusTo: null,
1238
+ changeMonitorStatusToId: DEGRADED,
1239
+ }),
1240
+ ),
1241
+ );
1242
+
1243
+ expect(carryForward.isChangeMonitorStatusToCleared).toBe(false);
1244
+ expect(String(carryForward.newMonitorChangeStatusIdTo)).toBe(DEGRADED);
1245
+ });
1246
+
1247
+ test.each([
1248
+ ["open", false],
1249
+ ["resolved", true],
1250
+ ] as Array<[string, boolean]>)(
1251
+ "records that the incident was %s before an update that takes a monitor off",
1252
+ async (_label: string, isResolved: boolean) => {
1253
+ storedIncidents = [
1254
+ storedIncident({
1255
+ monitorIds: [MONITOR_A, MONITOR_B],
1256
+ isResolved: isResolved,
1257
+ }),
1258
+ ];
1259
+
1260
+ const carryForward: MonitorCarryForward = monitorCarryForwardOf(
1261
+ await runBeforeUpdate(makeUpdateBy({ monitors: [MONITOR_A] })),
1262
+ );
1263
+
1264
+ expect(carryForward.isResolvedBeforeUpdate).toBe(isResolved);
1265
+ expect(isIncidentResolved).toHaveBeenCalledTimes(1);
1266
+ expect(
1267
+ String(
1268
+ (isIncidentResolved.mock.calls[0]![0] as { incidentId: ObjectID })
1269
+ .incidentId,
1270
+ ),
1271
+ ).toBe(INCIDENT_ID);
1272
+ },
1273
+ );
1274
+
1275
+ test.each([
1276
+ ["adds a monitor", { monitors: [MONITOR_A, MONITOR_B, MONITOR_C] }],
1277
+ ["re-sends the same monitors", { monitors: [MONITOR_A, MONITOR_B] }],
1278
+ ["sends only the status", { changeMonitorStatusToId: DEGRADED }],
1279
+ ] as Array<[string, Dictionary<unknown>]>)(
1280
+ "does not read the state before an update that %s",
1281
+ async (_label: string, data: Dictionary<unknown>) => {
1282
+ const carryForward: MonitorCarryForward = monitorCarryForwardOf(
1283
+ await runBeforeUpdate(makeUpdateBy(data)),
1284
+ );
1285
+
1286
+ expect(carryForward.isResolvedBeforeUpdate).toBeUndefined();
1287
+ expect(isIncidentResolved).not.toHaveBeenCalled();
1288
+ },
1289
+ );
1290
+
1291
+ test("an update that touches neither reads no incident", async () => {
1292
+ const onUpdate: OnUpdate<Incident> = await runBeforeUpdate(
1293
+ makeUpdateBy({ title: "Renamed" }),
1294
+ );
1295
+
1296
+ expect(onUpdate.carryForward).toEqual({});
1297
+ expect(IncidentService.findBy).not.toHaveBeenCalled();
1298
+ });
1299
+ });
1300
+
1301
+ describe("IncidentService: an update that matches several incidents", () => {
1302
+ test("each incident puts the monitors added to it in its own status", async () => {
1303
+ storedIncidents = [
1304
+ storedIncident({ monitorIds: [MONITOR_A] }),
1305
+ storedIncident({
1306
+ id: SECOND_INCIDENT_ID,
1307
+ incidentNumber: 43,
1308
+ monitorIds: [MONITOR_B],
1309
+ changeMonitorStatusToId: DEGRADED,
1310
+ }),
1311
+ ];
1312
+
1313
+ await runUpdate(
1314
+ { monitors: [MONITOR_A, MONITOR_B] },
1315
+ { query: { projectId: projectId } },
1316
+ );
1317
+
1318
+ expect(statusChanges()).toEqual([
1319
+ [[MONITOR_B], OFFLINE],
1320
+ [[MONITOR_A], DEGRADED],
1321
+ ]);
1322
+ expect(changeMonitorStatus.mock.calls[1]![4]).toBe(
1323
+ "Status was changed because Incident INC-43 was updated.",
1324
+ );
1325
+ expect(disableActiveMonitoring).toHaveBeenCalledTimes(2);
1326
+ });
1327
+
1328
+ test("a resolved one is left alone while an open one is updated", async () => {
1329
+ storedIncidents = [
1330
+ storedIncident({ monitorIds: [MONITOR_A] }),
1331
+ storedIncident({
1332
+ id: SECOND_INCIDENT_ID,
1333
+ incidentNumber: 43,
1334
+ monitorIds: [MONITOR_C],
1335
+ changeMonitorStatusToId: DEGRADED,
1336
+ isResolved: true,
1337
+ }),
1338
+ ];
1339
+
1340
+ await runUpdate(
1341
+ { monitors: [MONITOR_A, MONITOR_B] },
1342
+ { query: { projectId: projectId } },
1343
+ );
1344
+
1345
+ expect(statusChanges()).toEqual([[[MONITOR_B], OFFLINE]]);
1346
+ expect(
1347
+ disableActiveMonitoring.mock.calls.map((args: Array<unknown>): string => {
1348
+ return String(args[0]);
1349
+ }),
1350
+ ).toEqual([INCIDENT_ID]);
1351
+ // The resolved incident's removed monitor is not restored again.
1352
+ expect(markMonitorsActive).not.toHaveBeenCalled();
1353
+
1354
+ // Both feeds still record the edit.
1355
+ expect(feedMarkdownFor(INCIDENT_ID)).toContain("**🌎 Monitors Added**:");
1356
+ expect(feedMarkdownFor(SECOND_INCIDENT_ID)).toContain(
1357
+ `**🗑️ Monitors Removed**:\n- [search-api](${monitorLink(MONITOR_C)})\n`,
1358
+ );
1359
+ });
1360
+ });