@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,2113 @@
1
+ import DatabaseConfig from "../../../Server/DatabaseConfig";
2
+ import CustomFieldMappingService from "../../../Server/Services/CustomFieldMappingService";
3
+ import MonitorService from "../../../Server/Services/MonitorService";
4
+ import ScheduledMaintenanceFeedService from "../../../Server/Services/ScheduledMaintenanceFeedService";
5
+ import ScheduledMaintenanceService from "../../../Server/Services/ScheduledMaintenanceService";
6
+ import ScheduledMaintenanceStateTimelineService from "../../../Server/Services/ScheduledMaintenanceStateTimelineService";
7
+ import { OnUpdate } from "../../../Server/Types/Database/Hooks";
8
+ import UpdateBy from "../../../Server/Types/Database/UpdateBy";
9
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
10
+ import NetworkSite from "../../../Models/DatabaseModels/NetworkSite";
11
+ import ScheduledMaintenance from "../../../Models/DatabaseModels/ScheduledMaintenance";
12
+ import ScheduledMaintenanceState from "../../../Models/DatabaseModels/ScheduledMaintenanceState";
13
+ import ScheduledMaintenanceStateTimeline from "../../../Models/DatabaseModels/ScheduledMaintenanceStateTimeline";
14
+ import URL from "../../../Types/API/URL";
15
+ import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
16
+ import SortOrder from "../../../Types/BaseDatabase/SortOrder";
17
+ import Dictionary from "../../../Types/Dictionary";
18
+ import { JSONObject } from "../../../Types/JSON";
19
+ import ObjectID from "../../../Types/ObjectID";
20
+ import logger, { LogAttributes } from "../../../Server/Utils/Logger";
21
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
22
+
23
+ /*
24
+ * A scheduled maintenance event puts its monitors into maintenance when it
25
+ * moves to ongoing (probing stops, and the configured status is applied) and
26
+ * restores them when it ends. Both transitions read the monitor list at that
27
+ * moment, so editing the list while the window ran used to leave the edited
28
+ * part behind:
29
+ *
30
+ * - a monitor detached mid-window stayed disabled and in the maintenance
31
+ * status for good - ending or deleting the event no longer reached it,
32
+ * and the flag is not user-editable;
33
+ * - a monitor attached mid-window kept being probed and alerting, in its
34
+ * real status.
35
+ *
36
+ * The edit now does for the changed part of the list what the transitions
37
+ * did for the rest, while the event holds its monitors - ongoing, or in a
38
+ * state of the project's own after ongoing and before it ended - and the
39
+ * feed records what was attached and detached. What changed is what the
40
+ * write stored, not what the payload asked for.
41
+ *
42
+ * The database is stubbed: the event before the write is served by a stub of
43
+ * findBy, after it by findOneById, and its state timeline by the timeline
44
+ * service's findBy. Every monitor carries the maintenance flag unless a test
45
+ * says otherwise (monitorsNotFlagged).
46
+ */
47
+
48
+ const DASHBOARD: string = "https://oneuptime.example/dashboard";
49
+
50
+ const PROJECT_ID: ObjectID = new ObjectID(
51
+ "0193c0de-5a5a-4aaa-8bbb-000000000001",
52
+ );
53
+ const USER_ID: ObjectID = new ObjectID("0193c0de-5a5a-4aaa-8bbb-000000000002");
54
+ const EVENT_ID: string = "0193c0de-5a5a-4aaa-8bbb-0000000000e1";
55
+ const SECOND_EVENT_ID: string = "0193c0de-5a5a-4aaa-8bbb-0000000000e2";
56
+ const MAINTENANCE_STATUS_ID: ObjectID = new ObjectID(
57
+ "0193c0de-5a5a-4aaa-8bbb-0000000000f1",
58
+ );
59
+ const ENDED_STATE_ID: string = "0193c0de-5a5a-4aaa-8bbb-0000000000d3";
60
+
61
+ const MONITOR_A: string = "0193c0de-5a5a-4aaa-8bbb-0000000000a1";
62
+ const MONITOR_B: string = "0193c0de-5a5a-4aaa-8bbb-0000000000a2";
63
+ const MONITOR_C: string = "0193c0de-5a5a-4aaa-8bbb-0000000000a3";
64
+ const MONITOR_D: string = "0193c0de-5a5a-4aaa-8bbb-0000000000a4";
65
+ // Not a monitor of PROJECT_ID, so the project-held name read never finds it.
66
+ const FOREIGN_MONITOR: string = "0193c0de-5a5a-4aaa-8bbb-0000000000a9";
67
+
68
+ const SITE_1: string = "0193c0de-5a5a-4aaa-8bbb-0000000000b1";
69
+ const SITE_2: string = "0193c0de-5a5a-4aaa-8bbb-0000000000b2";
70
+ const SITE_3: string = "0193c0de-5a5a-4aaa-8bbb-0000000000b3";
71
+
72
+ const MONITOR_NAMES: Dictionary<string> = {
73
+ [MONITOR_A]: "checkout-web",
74
+ [MONITOR_B]: "payments-api",
75
+ [MONITOR_C]: "search-api",
76
+ [MONITOR_D]: "auth-api",
77
+ };
78
+
79
+ const REMOVED_HEADER: string = "**🗑️ Monitors Removed**:\n";
80
+ const ADDED_HEADER: string = "**🌎 Monitors Added**:\n";
81
+
82
+ /*
83
+ * "custom" is a state the project added itself, such as "Verifying" between
84
+ * Ongoing and Ended: it is none of the four built-in kinds.
85
+ */
86
+ type StateKind = "scheduled" | "ongoing" | "ended" | "resolved" | "custom";
87
+
88
+ const STATE_IDS: Dictionary<string> = {
89
+ scheduled: "0193c0de-5a5a-4aaa-8bbb-0000000000d1",
90
+ ongoing: "0193c0de-5a5a-4aaa-8bbb-0000000000d2",
91
+ ended: ENDED_STATE_ID,
92
+ resolved: "0193c0de-5a5a-4aaa-8bbb-0000000000d4",
93
+ custom: "0193c0de-5a5a-4aaa-8bbb-0000000000d5",
94
+ };
95
+
96
+ // Mirrors the service's carry-forward entry.
97
+ type AttachmentsBeforeUpdate = {
98
+ projectId: ObjectID | undefined;
99
+ wasOngoingBeforeUpdate: boolean;
100
+ wasHoldingMonitorsBeforeUpdate: boolean;
101
+ monitorIdsBeforeUpdate: Array<ObjectID> | undefined;
102
+ networkSiteIdsBeforeUpdate: Array<ObjectID> | undefined;
103
+ };
104
+
105
+ type OnBeforeUpdateFunction = (
106
+ updateBy: UpdateBy<ScheduledMaintenance>,
107
+ ) => Promise<OnUpdate<ScheduledMaintenance>>;
108
+
109
+ type OnUpdateSuccessFunction = (
110
+ onUpdate: OnUpdate<ScheduledMaintenance>,
111
+ updatedItemIds: Array<ObjectID>,
112
+ ) => Promise<OnUpdate<ScheduledMaintenance>>;
113
+
114
+ function state(kind: StateKind): ScheduledMaintenanceState {
115
+ const scheduledMaintenanceState: ScheduledMaintenanceState =
116
+ new ScheduledMaintenanceState();
117
+ scheduledMaintenanceState._id = STATE_IDS[kind]!;
118
+ scheduledMaintenanceState.isScheduledState = kind === "scheduled";
119
+ scheduledMaintenanceState.isOngoingState = kind === "ongoing";
120
+ scheduledMaintenanceState.isEndedState = kind === "ended";
121
+ scheduledMaintenanceState.isResolvedState = kind === "resolved";
122
+ return scheduledMaintenanceState;
123
+ }
124
+
125
+ function monitor(id: string): Monitor {
126
+ const item: Monitor = new Monitor();
127
+ item._id = id;
128
+ return item;
129
+ }
130
+
131
+ function site(id: string): NetworkSite {
132
+ const item: NetworkSite = new NetworkSite();
133
+ item._id = id;
134
+ return item;
135
+ }
136
+
137
+ function maintenanceEvent(data: {
138
+ id?: string;
139
+ state: StateKind;
140
+ monitors?: Array<string>;
141
+ networkSites?: Array<string>;
142
+ changeMonitorStatusToId?: ObjectID;
143
+ }): ScheduledMaintenance {
144
+ const scheduledMaintenance: ScheduledMaintenance = new ScheduledMaintenance();
145
+ scheduledMaintenance._id = data.id || EVENT_ID;
146
+ scheduledMaintenance.projectId = PROJECT_ID;
147
+ scheduledMaintenance.currentScheduledMaintenanceState = state(data.state);
148
+ scheduledMaintenance.monitors = (data.monitors || []).map(
149
+ (id: string): Monitor => {
150
+ return monitor(id);
151
+ },
152
+ );
153
+ scheduledMaintenance.networkSites = (data.networkSites || []).map(
154
+ (id: string): NetworkSite => {
155
+ return site(id);
156
+ },
157
+ );
158
+
159
+ if (data.changeMonitorStatusToId) {
160
+ scheduledMaintenance.changeMonitorStatusToId = data.changeMonitorStatusToId;
161
+ }
162
+
163
+ return scheduledMaintenance;
164
+ }
165
+
166
+ // The event's state timeline, oldest first, as the timeline read returns it.
167
+ function timelineOf(
168
+ kinds: Array<StateKind>,
169
+ ): Array<ScheduledMaintenanceStateTimeline> {
170
+ return kinds.map((kind: StateKind): ScheduledMaintenanceStateTimeline => {
171
+ const item: ScheduledMaintenanceStateTimeline =
172
+ new ScheduledMaintenanceStateTimeline();
173
+ item.scheduledMaintenanceId = new ObjectID(EVENT_ID);
174
+ item.scheduledMaintenanceState = state(kind);
175
+ return item;
176
+ });
177
+ }
178
+
179
+ // Sorted and lower-cased, so assertions do not depend on read order.
180
+ function idsOf(
181
+ items: Array<ObjectID | Monitor | NetworkSite | string> | undefined,
182
+ ): Array<string> {
183
+ return (items || [])
184
+ .map((item: ObjectID | Monitor | NetworkSite | string): string => {
185
+ if (item instanceof ObjectID || typeof item === "string") {
186
+ return item.toString().toLowerCase();
187
+ }
188
+
189
+ return (item._id || "").toString().toLowerCase();
190
+ })
191
+ .sort();
192
+ }
193
+
194
+ // The ids a QueryHelper.any operator (a Raw IN) was built from.
195
+ function idsInOperator(operator: unknown): Array<string> {
196
+ const parameters: Dictionary<unknown> =
197
+ (operator as { objectLiteralParameters?: Dictionary<unknown> })
198
+ .objectLiteralParameters || {};
199
+
200
+ return (Object.values(parameters) as Array<Array<string>>)
201
+ .flat()
202
+ .map((id: string): string => {
203
+ return id.toLowerCase();
204
+ });
205
+ }
206
+
207
+ function link(monitorId: string): string {
208
+ return `${DASHBOARD}/${PROJECT_ID.toString()}/monitors/${monitorId}`;
209
+ }
210
+
211
+ function updateByFor(
212
+ data: Record<string, unknown>,
213
+ options: {
214
+ query?: Record<string, unknown>;
215
+ props?: DatabaseCommonInteractionProps;
216
+ } = {},
217
+ ): UpdateBy<ScheduledMaintenance> {
218
+ return {
219
+ query: (options.query || {
220
+ _id: EVENT_ID,
221
+ }) as UpdateBy<ScheduledMaintenance>["query"],
222
+ data: data as UpdateBy<ScheduledMaintenance>["data"],
223
+ props: options.props || { tenantId: PROJECT_ID, userId: USER_ID },
224
+ limit: 1,
225
+ skip: 0,
226
+ };
227
+ }
228
+
229
+ async function runBeforeUpdate(
230
+ updateBy: UpdateBy<ScheduledMaintenance>,
231
+ ): Promise<OnUpdate<ScheduledMaintenance>> {
232
+ return await (
233
+ ScheduledMaintenanceService as unknown as {
234
+ onBeforeUpdate: OnBeforeUpdateFunction;
235
+ }
236
+ ).onBeforeUpdate(updateBy);
237
+ }
238
+
239
+ async function runUpdateSuccess(
240
+ onUpdate: OnUpdate<ScheduledMaintenance>,
241
+ updatedItemIds: Array<string>,
242
+ ): Promise<void> {
243
+ await (
244
+ ScheduledMaintenanceService as unknown as {
245
+ onUpdateSuccess: OnUpdateSuccessFunction;
246
+ }
247
+ ).onUpdateSuccess(
248
+ onUpdate,
249
+ updatedItemIds.map((id: string): ObjectID => {
250
+ return new ObjectID(id);
251
+ }),
252
+ );
253
+ }
254
+
255
+ function carriedOf(
256
+ onUpdate: OnUpdate<ScheduledMaintenance>,
257
+ ): Dictionary<AttachmentsBeforeUpdate> | null {
258
+ return onUpdate.carryForward as Dictionary<AttachmentsBeforeUpdate> | null;
259
+ }
260
+
261
+ // What the database holds before the write, and after it.
262
+ let eventsBeforeWrite: Array<ScheduledMaintenance> = [];
263
+ let eventsAfterWrite: Dictionary<ScheduledMaintenance> = {};
264
+ let stateTimeline: Array<ScheduledMaintenanceStateTimeline> = [];
265
+ /*
266
+ * Monitors without disableActiveMonitoringBecauseOfScheduledMaintenanceEvent:
267
+ * no event put them into maintenance, or one has released them already.
268
+ */
269
+ let monitorsNotFlagged: Set<string> = new Set<string>();
270
+
271
+ type MonitorFindBy = {
272
+ query: {
273
+ _id: unknown;
274
+ projectId: ObjectID;
275
+ disableActiveMonitoringBecauseOfScheduledMaintenanceEvent?: boolean;
276
+ };
277
+ select: JSONObject;
278
+ props: JSONObject;
279
+ };
280
+
281
+ let eventFindBy: jest.SpyInstance;
282
+ let eventFindOneById: jest.SpyInstance;
283
+ let timelineFindBy: jest.SpyInstance;
284
+ let enableActiveMonitoring: jest.SpyInstance;
285
+ let recomputeSiteRollups: jest.SpyInstance;
286
+ let monitorUpdateOneById: jest.SpyInstance;
287
+ let changeMonitorStatus: jest.SpyInstance;
288
+ let monitorFindBy: jest.SpyInstance;
289
+ let feedItem: jest.SpyInstance;
290
+
291
+ /*
292
+ * The whole update as DatabaseService runs it: the before hook reads the
293
+ * stored event, the write happens, and the success hook gets the carry
294
+ * forward and the ids of the rows written.
295
+ */
296
+ async function edit(
297
+ data: Record<string, unknown>,
298
+ options: {
299
+ query?: Record<string, unknown>;
300
+ props?: DatabaseCommonInteractionProps;
301
+ updatedItemIds?: Array<string>;
302
+ } = {},
303
+ ): Promise<OnUpdate<ScheduledMaintenance>> {
304
+ const onUpdate: OnUpdate<ScheduledMaintenance> = await runBeforeUpdate(
305
+ updateByFor(data, options),
306
+ );
307
+
308
+ await runUpdateSuccess(onUpdate, options.updatedItemIds || [EVENT_ID]);
309
+
310
+ return onUpdate;
311
+ }
312
+
313
+ // The event as it reads after the write.
314
+ function afterWrite(scheduledMaintenance: ScheduledMaintenance): void {
315
+ eventsAfterWrite[scheduledMaintenance._id!.toString()] = scheduledMaintenance;
316
+ }
317
+
318
+ function feedMarkdown(call: number = 0): string {
319
+ return (feedItem.mock.calls[call]![0] as { feedInfoInMarkdown: string })
320
+ .feedInfoInMarkdown;
321
+ }
322
+
323
+ // The bullets under `header`, or null when there is no such section.
324
+ function sectionLines(markdown: string, header: string): Array<string> | null {
325
+ const start: number = markdown.indexOf(header);
326
+
327
+ if (start === -1) {
328
+ return null;
329
+ }
330
+
331
+ const lines: Array<string> = markdown
332
+ .slice(start + header.length)
333
+ .split("\n");
334
+ const end: number = lines.indexOf("");
335
+
336
+ return end === -1 ? lines : lines.slice(0, end);
337
+ }
338
+
339
+ // The reads that name monitors for the feed, apart from the flag reads.
340
+ function nameReads(): Array<MonitorFindBy> {
341
+ return monitorFindBy.mock.calls
342
+ .map((call: Array<unknown>): MonitorFindBy => {
343
+ return call[0] as MonitorFindBy;
344
+ })
345
+ .filter((findBy: MonitorFindBy): boolean => {
346
+ return Boolean(findBy.select["name"]);
347
+ });
348
+ }
349
+
350
+ // The reads of which detached monitors carry the maintenance flag.
351
+ function flagReads(): Array<MonitorFindBy> {
352
+ return monitorFindBy.mock.calls
353
+ .map((call: Array<unknown>): MonitorFindBy => {
354
+ return call[0] as MonitorFindBy;
355
+ })
356
+ .filter((findBy: MonitorFindBy): boolean => {
357
+ return (
358
+ findBy.query
359
+ .disableActiveMonitoringBecauseOfScheduledMaintenanceEvent === true
360
+ );
361
+ });
362
+ }
363
+
364
+ function restoredMonitorIds(call: number = 0): Array<string> {
365
+ return idsOf(
366
+ (enableActiveMonitoring.mock.calls[call]![0] as ScheduledMaintenance)
367
+ .monitors,
368
+ );
369
+ }
370
+
371
+ function flaggedMonitorIds(): Array<string> {
372
+ return idsOf(
373
+ monitorUpdateOneById.mock.calls.map((call: Array<unknown>): ObjectID => {
374
+ return (call[0] as { id: ObjectID }).id;
375
+ }),
376
+ );
377
+ }
378
+
379
+ function expectNoMonitorSideEffects(): void {
380
+ expect(enableActiveMonitoring).not.toHaveBeenCalled();
381
+ expect(monitorUpdateOneById).not.toHaveBeenCalled();
382
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
383
+ expect(recomputeSiteRollups).not.toHaveBeenCalled();
384
+ }
385
+
386
+ beforeEach(() => {
387
+ eventsBeforeWrite = [];
388
+ eventsAfterWrite = {};
389
+ stateTimeline = [];
390
+ monitorsNotFlagged = new Set<string>();
391
+
392
+ eventFindBy = jest
393
+ .spyOn(ScheduledMaintenanceService, "findBy")
394
+ .mockImplementation((async (): Promise<Array<ScheduledMaintenance>> => {
395
+ return eventsBeforeWrite;
396
+ }) as never);
397
+
398
+ eventFindOneById = jest
399
+ .spyOn(ScheduledMaintenanceService, "findOneById")
400
+ .mockImplementation((async (findOneById: {
401
+ id: ObjectID;
402
+ }): Promise<ScheduledMaintenance | null> => {
403
+ return eventsAfterWrite[findOneById.id.toString()] || null;
404
+ }) as never);
405
+
406
+ timelineFindBy = jest
407
+ .spyOn(ScheduledMaintenanceStateTimelineService, "findBy")
408
+ .mockImplementation((async (): Promise<
409
+ Array<ScheduledMaintenanceStateTimeline>
410
+ > => {
411
+ return stateTimeline;
412
+ }) as never);
413
+
414
+ // The "Resources Affected" read; nothing to list unless a test says so.
415
+ jest
416
+ .spyOn(ScheduledMaintenanceService, "findAllBy")
417
+ .mockResolvedValue([] as never);
418
+
419
+ jest
420
+ .spyOn(
421
+ ScheduledMaintenanceService as unknown as {
422
+ validateProjectScopedReferences: () => Promise<void>;
423
+ },
424
+ "validateProjectScopedReferences",
425
+ )
426
+ .mockResolvedValue(undefined as never);
427
+
428
+ jest
429
+ .spyOn(CustomFieldMappingService, "applyMappingsToUpdate")
430
+ .mockResolvedValue(undefined as never);
431
+
432
+ jest
433
+ .spyOn(CustomFieldMappingService, "restampAfterMultiRowUpdate")
434
+ .mockImplementation((() => {
435
+ return undefined;
436
+ }) as never);
437
+
438
+ enableActiveMonitoring = jest
439
+ .spyOn(
440
+ ScheduledMaintenanceStateTimelineService,
441
+ "enableActiveMonitoringForMonitors",
442
+ )
443
+ .mockResolvedValue(undefined as never);
444
+
445
+ recomputeSiteRollups = jest
446
+ .spyOn(
447
+ ScheduledMaintenanceStateTimelineService,
448
+ "recomputeNetworkSiteRollups",
449
+ )
450
+ .mockResolvedValue(undefined as never);
451
+
452
+ monitorUpdateOneById = jest
453
+ .spyOn(MonitorService, "updateOneById")
454
+ .mockResolvedValue(undefined as never);
455
+
456
+ changeMonitorStatus = jest
457
+ .spyOn(MonitorService, "changeMonitorStatus")
458
+ .mockResolvedValue(undefined as never);
459
+
460
+ /*
461
+ * Names only the project's own monitors, as the project-held read would,
462
+ * and answers the flag read with the flagged ones.
463
+ */
464
+ monitorFindBy = jest
465
+ .spyOn(MonitorService, "findBy")
466
+ .mockImplementation((async (
467
+ findBy: MonitorFindBy,
468
+ ): Promise<Array<Monitor>> => {
469
+ if (findBy.query.projectId?.toString() !== PROJECT_ID.toString()) {
470
+ return [];
471
+ }
472
+
473
+ if (
474
+ findBy.query.disableActiveMonitoringBecauseOfScheduledMaintenanceEvent
475
+ ) {
476
+ return idsInOperator(findBy.query._id)
477
+ .filter((id: string): boolean => {
478
+ return !monitorsNotFlagged.has(id);
479
+ })
480
+ .map((id: string): Monitor => {
481
+ return monitor(id);
482
+ });
483
+ }
484
+
485
+ return idsInOperator(findBy.query._id)
486
+ .filter((id: string): boolean => {
487
+ return Boolean(MONITOR_NAMES[id]);
488
+ })
489
+ .map((id: string): Monitor => {
490
+ const item: Monitor = monitor(id);
491
+ item.name = MONITOR_NAMES[id]!;
492
+ return item;
493
+ });
494
+ }) as never);
495
+
496
+ jest
497
+ .spyOn(DatabaseConfig, "getDashboardUrl")
498
+ .mockImplementation(async (): Promise<URL> => {
499
+ return URL.fromString(DASHBOARD);
500
+ });
501
+
502
+ feedItem = jest
503
+ .spyOn(
504
+ ScheduledMaintenanceFeedService,
505
+ "createScheduledMaintenanceFeedItem",
506
+ )
507
+ .mockResolvedValue(undefined as never);
508
+ });
509
+
510
+ afterEach(() => {
511
+ jest.restoreAllMocks();
512
+ });
513
+
514
+ describe("ScheduledMaintenanceService.onBeforeUpdate: what each event holds before the write", () => {
515
+ test("an update that writes neither list reads nothing and carries nothing", async () => {
516
+ eventsBeforeWrite = [
517
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_B] }),
518
+ ];
519
+
520
+ const onUpdate: OnUpdate<ScheduledMaintenance> = await runBeforeUpdate(
521
+ updateByFor({ title: "Database upgrade" }),
522
+ );
523
+
524
+ expect(eventFindBy).not.toHaveBeenCalled();
525
+ expect(timelineFindBy).not.toHaveBeenCalled();
526
+ expect(carriedOf(onUpdate)).toBeNull();
527
+ });
528
+
529
+ test("each list is read on its own, as root, held to the tenant's project", async () => {
530
+ eventsBeforeWrite = [
531
+ maintenanceEvent({
532
+ state: "ongoing",
533
+ monitors: [MONITOR_A],
534
+ networkSites: [SITE_1],
535
+ }),
536
+ ];
537
+
538
+ await runBeforeUpdate(
539
+ updateByFor({
540
+ monitors: [{ _id: MONITOR_A }],
541
+ networkSites: [{ _id: SITE_1 }],
542
+ }),
543
+ );
544
+
545
+ expect(eventFindBy).toHaveBeenCalledTimes(2);
546
+
547
+ const selectedRelations: Array<Array<string>> = [];
548
+
549
+ for (const call of eventFindBy.mock.calls) {
550
+ const findBy: {
551
+ query: JSONObject;
552
+ select: JSONObject;
553
+ props: JSONObject;
554
+ } = call[0] as {
555
+ query: JSONObject;
556
+ select: JSONObject;
557
+ props: JSONObject;
558
+ };
559
+
560
+ expect(findBy.query["_id"]).toBe(EVENT_ID);
561
+ expect(findBy.query["projectId"]).toBe(PROJECT_ID);
562
+ expect(findBy.props).toEqual({ isRoot: true });
563
+ // Every kind, because a state of the project's own is none of them.
564
+ expect(findBy.select["currentScheduledMaintenanceState"]).toEqual({
565
+ _id: true,
566
+ isScheduledState: true,
567
+ isOngoingState: true,
568
+ isEndedState: true,
569
+ isResolvedState: true,
570
+ });
571
+
572
+ selectedRelations.push(
573
+ ["monitors", "networkSites"].filter((column: string): boolean => {
574
+ return Boolean(findBy.select[column]);
575
+ }),
576
+ );
577
+ }
578
+
579
+ // Never both in one read: that would join one row per combination.
580
+ expect(selectedRelations).toEqual([["monitors"], ["networkSites"]]);
581
+ });
582
+
583
+ test("without a tenant the query is used as the caller wrote it", async () => {
584
+ eventsBeforeWrite = [
585
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A] }),
586
+ ];
587
+
588
+ await runBeforeUpdate(
589
+ updateByFor(
590
+ { monitors: [] },
591
+ { query: { _id: EVENT_ID }, props: { isRoot: true } },
592
+ ),
593
+ );
594
+
595
+ expect(
596
+ (eventFindBy.mock.calls[0]![0] as { query: JSONObject }).query,
597
+ ).toEqual({ _id: EVENT_ID });
598
+ });
599
+
600
+ test("remembers the list the event held, not the ids the payload asks for", async () => {
601
+ eventsBeforeWrite = [
602
+ maintenanceEvent({
603
+ state: "ongoing",
604
+ monitors: [MONITOR_A, MONITOR_B, MONITOR_A.toUpperCase()],
605
+ networkSites: [SITE_1],
606
+ }),
607
+ ];
608
+
609
+ const carried: Dictionary<AttachmentsBeforeUpdate> | null = carriedOf(
610
+ await runBeforeUpdate(
611
+ updateByFor({ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }] }),
612
+ ),
613
+ );
614
+
615
+ const attachments: AttachmentsBeforeUpdate = carried![EVENT_ID]!;
616
+
617
+ // Each once, whatever case it was read back in.
618
+ expect(idsOf(attachments.monitorIdsBeforeUpdate)).toEqual([
619
+ MONITOR_A,
620
+ MONITOR_B,
621
+ ]);
622
+ expect(attachments.wasOngoingBeforeUpdate).toBe(true);
623
+ expect(attachments.wasHoldingMonitorsBeforeUpdate).toBe(true);
624
+ expect(attachments.projectId?.toString()).toBe(PROJECT_ID.toString());
625
+ // The network sites were not written, so they are not remembered.
626
+ expect(attachments.networkSiteIdsBeforeUpdate).toBeUndefined();
627
+ });
628
+
629
+ test.each([
630
+ ["an empty list", []],
631
+ ["null", null],
632
+ ])(
633
+ "%s is a written list, so what the event held is remembered",
634
+ async (_label: string, monitors: Array<unknown> | null) => {
635
+ eventsBeforeWrite = [
636
+ maintenanceEvent({
637
+ state: "ongoing",
638
+ monitors: [MONITOR_A, MONITOR_B],
639
+ }),
640
+ ];
641
+
642
+ const carried: Dictionary<AttachmentsBeforeUpdate> | null = carriedOf(
643
+ await runBeforeUpdate(updateByFor({ monitors: monitors })),
644
+ );
645
+
646
+ expect(idsOf(carried![EVENT_ID]!.monitorIdsBeforeUpdate)).toEqual([
647
+ MONITOR_A,
648
+ MONITOR_B,
649
+ ]);
650
+ },
651
+ );
652
+
653
+ test("a bulk update remembers every event it matches by its own list and state", async () => {
654
+ eventsBeforeWrite = [
655
+ maintenanceEvent({
656
+ id: EVENT_ID,
657
+ state: "ongoing",
658
+ monitors: [MONITOR_A],
659
+ }),
660
+ maintenanceEvent({
661
+ id: SECOND_EVENT_ID,
662
+ state: "scheduled",
663
+ monitors: [MONITOR_A, MONITOR_B],
664
+ }),
665
+ ];
666
+
667
+ const carried: Dictionary<AttachmentsBeforeUpdate> | null = carriedOf(
668
+ await runBeforeUpdate(
669
+ updateByFor(
670
+ { monitors: [{ _id: MONITOR_C }] },
671
+ { query: { projectId: PROJECT_ID } },
672
+ ),
673
+ ),
674
+ );
675
+
676
+ expect(idsOf(carried![EVENT_ID]!.monitorIdsBeforeUpdate)).toEqual([
677
+ MONITOR_A,
678
+ ]);
679
+ expect(carried![EVENT_ID]!.wasHoldingMonitorsBeforeUpdate).toBe(true);
680
+
681
+ expect(idsOf(carried![SECOND_EVENT_ID]!.monitorIdsBeforeUpdate)).toEqual([
682
+ MONITOR_A,
683
+ MONITOR_B,
684
+ ]);
685
+ expect(carried![SECOND_EVENT_ID]!.wasHoldingMonitorsBeforeUpdate).toBe(
686
+ false,
687
+ );
688
+ });
689
+
690
+ test("a network site list is remembered on its own, and says nothing about monitors", async () => {
691
+ eventsBeforeWrite = [
692
+ maintenanceEvent({ state: "custom", networkSites: [SITE_1, SITE_2] }),
693
+ ];
694
+ stateTimeline = timelineOf(["scheduled", "ongoing", "custom"]);
695
+
696
+ const carried: Dictionary<AttachmentsBeforeUpdate> | null = carriedOf(
697
+ await runBeforeUpdate(
698
+ updateByFor({ networkSites: [{ _id: SITE_2 }, { _id: SITE_3 }] }),
699
+ ),
700
+ );
701
+
702
+ expect(idsOf(carried![EVENT_ID]!.networkSiteIdsBeforeUpdate)).toEqual([
703
+ SITE_1,
704
+ SITE_2,
705
+ ]);
706
+ expect(carried![EVENT_ID]!.monitorIdsBeforeUpdate).toBeUndefined();
707
+ expect(carried![EVENT_ID]!.wasOngoingBeforeUpdate).toBe(false);
708
+ // Whether monitors are held only matters when they are written.
709
+ expect(carried![EVENT_ID]!.wasHoldingMonitorsBeforeUpdate).toBe(false);
710
+ expect(timelineFindBy).not.toHaveBeenCalled();
711
+ });
712
+ });
713
+
714
+ describe("ScheduledMaintenanceService.onBeforeUpdate: whether the event holds its monitors", () => {
715
+ test.each([
716
+ ["ongoing", true],
717
+ ["scheduled", false],
718
+ ["ended", false],
719
+ ["resolved", false],
720
+ ] as Array<[StateKind, boolean]>)(
721
+ "a built-in %s state answers for itself (%s), with no timeline read",
722
+ async (kind: StateKind, isHolding: boolean) => {
723
+ eventsBeforeWrite = [
724
+ maintenanceEvent({ state: kind, monitors: [MONITOR_A] }),
725
+ ];
726
+
727
+ const carried: Dictionary<AttachmentsBeforeUpdate> | null = carriedOf(
728
+ await runBeforeUpdate(updateByFor({ monitors: [] })),
729
+ );
730
+
731
+ expect(carried![EVENT_ID]!.wasHoldingMonitorsBeforeUpdate).toBe(
732
+ isHolding,
733
+ );
734
+ expect(carried![EVENT_ID]!.wasOngoingBeforeUpdate).toBe(
735
+ kind === "ongoing",
736
+ );
737
+ expect(timelineFindBy).not.toHaveBeenCalled();
738
+ },
739
+ );
740
+
741
+ test.each([
742
+ [
743
+ "past ongoing and not yet ended",
744
+ true,
745
+ ["scheduled", "ongoing", "custom"],
746
+ ],
747
+ [
748
+ "past ongoing through more than one state of its own",
749
+ true,
750
+ ["scheduled", "ongoing", "custom", "custom"],
751
+ ],
752
+ ["not yet ongoing", false, ["scheduled", "custom"]],
753
+ ["past ended", false, ["scheduled", "ongoing", "ended", "custom"]],
754
+ ["past resolved", false, ["scheduled", "ongoing", "resolved", "custom"]],
755
+ ["with no timeline", false, []],
756
+ ] as Array<[string, boolean, Array<StateKind>]>)(
757
+ "a state of the project's own %s: holding is %s",
758
+ async (_label: string, isHolding: boolean, kinds: Array<StateKind>) => {
759
+ eventsBeforeWrite = [
760
+ maintenanceEvent({ state: "custom", monitors: [MONITOR_A] }),
761
+ ];
762
+ stateTimeline = timelineOf(kinds);
763
+
764
+ const carried: Dictionary<AttachmentsBeforeUpdate> | null = carriedOf(
765
+ await runBeforeUpdate(updateByFor({ monitors: [] })),
766
+ );
767
+
768
+ expect(carried![EVENT_ID]!.wasHoldingMonitorsBeforeUpdate).toBe(
769
+ isHolding,
770
+ );
771
+ // Sites follow only the live state.
772
+ expect(carried![EVENT_ID]!.wasOngoingBeforeUpdate).toBe(false);
773
+ expect(timelineFindBy).toHaveBeenCalledTimes(1);
774
+ },
775
+ );
776
+
777
+ test("the timeline is read as root, held to the event and its project, oldest first", async () => {
778
+ eventsBeforeWrite = [
779
+ maintenanceEvent({ state: "custom", monitors: [MONITOR_A] }),
780
+ ];
781
+ stateTimeline = timelineOf(["scheduled", "ongoing", "custom"]);
782
+
783
+ await runBeforeUpdate(updateByFor({ monitors: [] }));
784
+
785
+ const findBy: {
786
+ query: JSONObject;
787
+ select: JSONObject;
788
+ sort: JSONObject;
789
+ props: JSONObject;
790
+ } = timelineFindBy.mock.calls[0]![0] as {
791
+ query: JSONObject;
792
+ select: JSONObject;
793
+ sort: JSONObject;
794
+ props: JSONObject;
795
+ };
796
+
797
+ expect(
798
+ (findBy.query["scheduledMaintenanceId"] as ObjectID).toString(),
799
+ ).toBe(EVENT_ID);
800
+ expect((findBy.query["projectId"] as ObjectID).toString()).toBe(
801
+ PROJECT_ID.toString(),
802
+ );
803
+ expect(findBy.select["scheduledMaintenanceState"]).toEqual({
804
+ _id: true,
805
+ isScheduledState: true,
806
+ isOngoingState: true,
807
+ isEndedState: true,
808
+ isResolvedState: true,
809
+ });
810
+ expect(findBy.sort).toEqual({ startsAt: SortOrder.Ascending });
811
+ expect(findBy.props).toEqual({ isRoot: true });
812
+ });
813
+ });
814
+
815
+ describe("ScheduledMaintenanceService.onUpdateSuccess: editing the monitors of an ongoing event", () => {
816
+ test("a detached monitor is restored, and only that one", async () => {
817
+ eventsBeforeWrite = [
818
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_B] }),
819
+ ];
820
+ afterWrite(maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A] }));
821
+
822
+ await edit({ monitors: [{ _id: MONITOR_A }] });
823
+
824
+ expect(enableActiveMonitoring).toHaveBeenCalledTimes(1);
825
+
826
+ const restored: ScheduledMaintenance = enableActiveMonitoring.mock
827
+ .calls[0]![0] as ScheduledMaintenance;
828
+
829
+ expect(idsOf(restored.monitors)).toEqual([MONITOR_B]);
830
+ expect(restored.projectId?.toString()).toBe(PROJECT_ID.toString());
831
+
832
+ expect(monitorUpdateOneById).not.toHaveBeenCalled();
833
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
834
+ });
835
+
836
+ test("an attached monitor is put into maintenance with the event's status; the others are left alone", async () => {
837
+ eventsBeforeWrite = [
838
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A] }),
839
+ ];
840
+ afterWrite(
841
+ maintenanceEvent({
842
+ state: "ongoing",
843
+ monitors: [MONITOR_A, MONITOR_C],
844
+ changeMonitorStatusToId: MAINTENANCE_STATUS_ID,
845
+ }),
846
+ );
847
+
848
+ await edit({ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }] });
849
+
850
+ // The flag the ongoing transition writes, on the attached monitor only.
851
+ expect(monitorUpdateOneById).toHaveBeenCalledTimes(1);
852
+ const flagWrite: {
853
+ id: ObjectID;
854
+ data: JSONObject;
855
+ props: JSONObject;
856
+ } = monitorUpdateOneById.mock.calls[0]![0] as {
857
+ id: ObjectID;
858
+ data: JSONObject;
859
+ props: JSONObject;
860
+ };
861
+ expect(flagWrite.id.toString()).toBe(MONITOR_C);
862
+ expect(flagWrite.data).toEqual({
863
+ disableActiveMonitoringBecauseOfScheduledMaintenanceEvent: true,
864
+ });
865
+ expect(flagWrite.props).toEqual({ isRoot: true });
866
+
867
+ // The status the event applies, with the transition's root cause.
868
+ expect(changeMonitorStatus).toHaveBeenCalledTimes(1);
869
+ const statusCall: Array<unknown> = changeMonitorStatus.mock.calls[0]!;
870
+ expect((statusCall[0] as ObjectID).toString()).toBe(PROJECT_ID.toString());
871
+ expect(idsOf(statusCall[1] as Array<ObjectID>)).toEqual([MONITOR_C]);
872
+ expect((statusCall[2] as ObjectID).toString()).toBe(
873
+ MAINTENANCE_STATUS_ID.toString(),
874
+ );
875
+ expect(statusCall[3]).toBe(true);
876
+ expect(statusCall[4]).toBe(
877
+ "Changed because of scheduled maintenance event: " + EVENT_ID,
878
+ );
879
+ expect(statusCall[6]).toEqual({ isRoot: true });
880
+
881
+ expect(enableActiveMonitoring).not.toHaveBeenCalled();
882
+ // An ongoing event needs no timeline to say it holds its monitors.
883
+ expect(timelineFindBy).not.toHaveBeenCalled();
884
+ });
885
+
886
+ test("an attached monitor on an event that changes no status only has probing stopped", async () => {
887
+ eventsBeforeWrite = [
888
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A] }),
889
+ ];
890
+ afterWrite(
891
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_C] }),
892
+ );
893
+
894
+ await edit({ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }] });
895
+
896
+ expect(flaggedMonitorIds()).toEqual([MONITOR_C]);
897
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
898
+ });
899
+
900
+ test("swapping one monitor for another restores the old and holds the new", async () => {
901
+ eventsBeforeWrite = [
902
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_B] }),
903
+ ];
904
+ afterWrite(
905
+ maintenanceEvent({
906
+ state: "ongoing",
907
+ monitors: [MONITOR_A, MONITOR_C],
908
+ changeMonitorStatusToId: MAINTENANCE_STATUS_ID,
909
+ }),
910
+ );
911
+
912
+ await edit({ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }] });
913
+
914
+ expect(restoredMonitorIds()).toEqual([MONITOR_B]);
915
+ expect(
916
+ idsOf(changeMonitorStatus.mock.calls[0]![1] as Array<ObjectID>),
917
+ ).toEqual([MONITOR_C]);
918
+ });
919
+
920
+ test("detaching every monitor restores them all", async () => {
921
+ eventsBeforeWrite = [
922
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_B] }),
923
+ ];
924
+ afterWrite(maintenanceEvent({ state: "ongoing" }));
925
+
926
+ await edit({ monitors: [] });
927
+
928
+ expect(restoredMonitorIds()).toEqual([MONITOR_A, MONITOR_B]);
929
+ });
930
+
931
+ test("saving the list unchanged touches no monitor and writes no feed item", async () => {
932
+ eventsBeforeWrite = [
933
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_B] }),
934
+ ];
935
+ afterWrite(
936
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_B] }),
937
+ );
938
+
939
+ await edit({ monitors: [{ _id: MONITOR_B }, { _id: MONITOR_A }] });
940
+
941
+ expectNoMonitorSideEffects();
942
+ // Read back once to learn that nothing changed.
943
+ expect(eventFindOneById).toHaveBeenCalledTimes(1);
944
+ expect(monitorFindBy).not.toHaveBeenCalled();
945
+ expect(feedItem).not.toHaveBeenCalled();
946
+ });
947
+
948
+ test("the event is read back as root, with its monitors, state and status", async () => {
949
+ eventsBeforeWrite = [
950
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A] }),
951
+ ];
952
+ afterWrite(maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A] }));
953
+
954
+ await edit({ monitors: [{ _id: MONITOR_A }] });
955
+
956
+ const findOneById: {
957
+ id: ObjectID;
958
+ select: JSONObject;
959
+ props: JSONObject;
960
+ } = eventFindOneById.mock.calls[0]![0] as {
961
+ id: ObjectID;
962
+ select: JSONObject;
963
+ props: JSONObject;
964
+ };
965
+
966
+ expect(findOneById.id.toString()).toBe(EVENT_ID);
967
+ expect(findOneById.select).toEqual({
968
+ _id: true,
969
+ changeMonitorStatusToId: true,
970
+ currentScheduledMaintenanceState: {
971
+ _id: true,
972
+ isScheduledState: true,
973
+ isOngoingState: true,
974
+ isEndedState: true,
975
+ isResolvedState: true,
976
+ },
977
+ monitors: {
978
+ _id: true,
979
+ },
980
+ });
981
+ expect(findOneById.props).toEqual({ isRoot: true });
982
+ });
983
+
984
+ test("attached and detached network sites are re-rolled now, and only those", async () => {
985
+ eventsBeforeWrite = [
986
+ maintenanceEvent({ state: "ongoing", networkSites: [SITE_1, SITE_2] }),
987
+ ];
988
+ afterWrite(
989
+ maintenanceEvent({ state: "ongoing", networkSites: [SITE_2, SITE_3] }),
990
+ );
991
+
992
+ await edit({ networkSites: [{ _id: SITE_2 }, { _id: SITE_3 }] });
993
+
994
+ expect(recomputeSiteRollups).toHaveBeenCalledTimes(1);
995
+
996
+ const rerolled: ScheduledMaintenance = recomputeSiteRollups.mock
997
+ .calls[0]![0] as ScheduledMaintenance;
998
+
999
+ expect(idsOf(rerolled.networkSites)).toEqual([SITE_1, SITE_3]);
1000
+ expect(rerolled.projectId?.toString()).toBe(PROJECT_ID.toString());
1001
+
1002
+ // The sites are read back on their own, never joined with the monitors.
1003
+ expect(eventFindOneById).toHaveBeenCalledTimes(1);
1004
+ const select: JSONObject = (
1005
+ eventFindOneById.mock.calls[0]![0] as { select: JSONObject }
1006
+ ).select;
1007
+ expect(select).toEqual({ _id: true, networkSites: { _id: true } });
1008
+
1009
+ // Sites are not monitors.
1010
+ expect(enableActiveMonitoring).not.toHaveBeenCalled();
1011
+ expect(monitorUpdateOneById).not.toHaveBeenCalled();
1012
+ });
1013
+
1014
+ test("an unchanged network site list re-rolls nothing", async () => {
1015
+ eventsBeforeWrite = [
1016
+ maintenanceEvent({ state: "ongoing", networkSites: [SITE_1] }),
1017
+ ];
1018
+ afterWrite(maintenanceEvent({ state: "ongoing", networkSites: [SITE_1] }));
1019
+
1020
+ await edit({ networkSites: [{ _id: SITE_1 }] });
1021
+
1022
+ expect(recomputeSiteRollups).not.toHaveBeenCalled();
1023
+ });
1024
+
1025
+ test("an update the same request ends still restores what it detached, and holds nothing new", async () => {
1026
+ eventsBeforeWrite = [
1027
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_B] }),
1028
+ ];
1029
+ afterWrite(
1030
+ maintenanceEvent({
1031
+ state: "ended",
1032
+ monitors: [MONITOR_A, MONITOR_C],
1033
+ changeMonitorStatusToId: MAINTENANCE_STATUS_ID,
1034
+ }),
1035
+ );
1036
+
1037
+ // The transition itself restores the list attached at that moment.
1038
+ const changeState: jest.SpyInstance = jest
1039
+ .spyOn(ScheduledMaintenanceService, "changeScheduledMaintenanceState")
1040
+ .mockResolvedValue(undefined as never);
1041
+
1042
+ await edit({
1043
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }],
1044
+ currentScheduledMaintenanceStateId: new ObjectID(ENDED_STATE_ID),
1045
+ });
1046
+
1047
+ expect(changeState).toHaveBeenCalledTimes(1);
1048
+
1049
+ // B left the event while it was held; nothing else would reach it.
1050
+ expect(restoredMonitorIds()).toEqual([MONITOR_B]);
1051
+ // C would be held after the window closed, for good.
1052
+ expect(monitorUpdateOneById).not.toHaveBeenCalled();
1053
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
1054
+ });
1055
+
1056
+ test("an update the same request starts restores nothing it never held", async () => {
1057
+ eventsBeforeWrite = [
1058
+ maintenanceEvent({
1059
+ state: "scheduled",
1060
+ monitors: [MONITOR_A, MONITOR_B],
1061
+ }),
1062
+ ];
1063
+ afterWrite(
1064
+ maintenanceEvent({
1065
+ state: "ongoing",
1066
+ monitors: [MONITOR_A, MONITOR_C],
1067
+ changeMonitorStatusToId: MAINTENANCE_STATUS_ID,
1068
+ }),
1069
+ );
1070
+ // The transition ran after the write, so it never reached B.
1071
+ monitorsNotFlagged.add(MONITOR_B);
1072
+
1073
+ // The transition itself holds the list attached at that moment.
1074
+ jest
1075
+ .spyOn(ScheduledMaintenanceService, "changeScheduledMaintenanceState")
1076
+ .mockResolvedValue(undefined as never);
1077
+
1078
+ await edit({
1079
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }],
1080
+ currentScheduledMaintenanceStateId: new ObjectID(STATE_IDS["ongoing"]!),
1081
+ });
1082
+
1083
+ // B was asked about, and is not forced into the operational status.
1084
+ expect(flagReads()).toHaveLength(1);
1085
+ expect(enableActiveMonitoring).not.toHaveBeenCalled();
1086
+
1087
+ // C is held again, which changes nothing the transition did.
1088
+ expect(flaggedMonitorIds()).toEqual([MONITOR_C]);
1089
+ expect(
1090
+ idsOf(changeMonitorStatus.mock.calls[0]![1] as Array<ObjectID>),
1091
+ ).toEqual([MONITOR_C]);
1092
+ });
1093
+
1094
+ test("an event whose row the write skipped is left alone", async () => {
1095
+ eventsBeforeWrite = [
1096
+ maintenanceEvent({
1097
+ id: EVENT_ID,
1098
+ state: "ongoing",
1099
+ monitors: [MONITOR_A],
1100
+ }),
1101
+ maintenanceEvent({
1102
+ id: SECOND_EVENT_ID,
1103
+ state: "ongoing",
1104
+ monitors: [MONITOR_B],
1105
+ }),
1106
+ ];
1107
+ afterWrite(maintenanceEvent({ id: EVENT_ID, state: "ongoing" }));
1108
+ afterWrite(maintenanceEvent({ id: SECOND_EVENT_ID, state: "ongoing" }));
1109
+
1110
+ await edit(
1111
+ { monitors: [] },
1112
+ { query: { projectId: PROJECT_ID }, updatedItemIds: [EVENT_ID] },
1113
+ );
1114
+
1115
+ expect(enableActiveMonitoring).toHaveBeenCalledTimes(1);
1116
+ expect(restoredMonitorIds()).toEqual([MONITOR_A]);
1117
+ expect(eventFindOneById).toHaveBeenCalledTimes(1);
1118
+ expect(feedItem).toHaveBeenCalledTimes(1);
1119
+ });
1120
+
1121
+ test("a bulk update acts on each event by its own state and list", async () => {
1122
+ eventsBeforeWrite = [
1123
+ maintenanceEvent({
1124
+ id: EVENT_ID,
1125
+ state: "ongoing",
1126
+ monitors: [MONITOR_A],
1127
+ }),
1128
+ maintenanceEvent({
1129
+ id: SECOND_EVENT_ID,
1130
+ state: "scheduled",
1131
+ monitors: [MONITOR_B],
1132
+ }),
1133
+ ];
1134
+ afterWrite(
1135
+ maintenanceEvent({
1136
+ id: EVENT_ID,
1137
+ state: "ongoing",
1138
+ monitors: [MONITOR_C],
1139
+ changeMonitorStatusToId: MAINTENANCE_STATUS_ID,
1140
+ }),
1141
+ );
1142
+ afterWrite(
1143
+ maintenanceEvent({
1144
+ id: SECOND_EVENT_ID,
1145
+ state: "scheduled",
1146
+ monitors: [MONITOR_C],
1147
+ changeMonitorStatusToId: MAINTENANCE_STATUS_ID,
1148
+ }),
1149
+ );
1150
+
1151
+ await edit(
1152
+ { monitors: [{ _id: MONITOR_C }] },
1153
+ {
1154
+ query: { projectId: PROJECT_ID },
1155
+ updatedItemIds: [EVENT_ID, SECOND_EVENT_ID],
1156
+ },
1157
+ );
1158
+
1159
+ // Only the ongoing event was holding anything.
1160
+ expect(enableActiveMonitoring).toHaveBeenCalledTimes(1);
1161
+ const restored: ScheduledMaintenance = enableActiveMonitoring.mock
1162
+ .calls[0]![0] as ScheduledMaintenance;
1163
+ expect(restored.id?.toString()).toBe(EVENT_ID);
1164
+ expect(idsOf(restored.monitors)).toEqual([MONITOR_A]);
1165
+
1166
+ expect(monitorUpdateOneById).toHaveBeenCalledTimes(1);
1167
+ expect(changeMonitorStatus).toHaveBeenCalledTimes(1);
1168
+ expect(changeMonitorStatus.mock.calls[0]![4]).toBe(
1169
+ "Changed because of scheduled maintenance event: " + EVENT_ID,
1170
+ );
1171
+
1172
+ // Each event records its own edit.
1173
+ expect(feedItem).toHaveBeenCalledTimes(2);
1174
+ expect(sectionLines(feedMarkdown(0), REMOVED_HEADER)).toEqual([
1175
+ `- [checkout-web](${link(MONITOR_A)})`,
1176
+ ]);
1177
+ expect(sectionLines(feedMarkdown(1), REMOVED_HEADER)).toEqual([
1178
+ `- [payments-api](${link(MONITOR_B)})`,
1179
+ ]);
1180
+ });
1181
+ });
1182
+
1183
+ describe("ScheduledMaintenanceService.onUpdateSuccess: what the write stored, not what the payload asked for", () => {
1184
+ /*
1185
+ * The write keeps a list entry only if it can read an id from it as a
1186
+ * string (DatabaseService.sanitizeCreateOrUpdate); `{ _id: ObjectID }` -
1187
+ * what an API body's serialized ObjectID becomes - is silently dropped,
1188
+ * while the reference checks read it as an id. Stubbed here as the event
1189
+ * the write leaves behind.
1190
+ */
1191
+ test("an entry the write drops is never put into maintenance or named as added", async () => {
1192
+ eventsBeforeWrite = [
1193
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A] }),
1194
+ ];
1195
+ afterWrite(
1196
+ maintenanceEvent({
1197
+ state: "ongoing",
1198
+ monitors: [MONITOR_A],
1199
+ changeMonitorStatusToId: MAINTENANCE_STATUS_ID,
1200
+ }),
1201
+ );
1202
+
1203
+ await edit({
1204
+ monitors: [{ _id: MONITOR_A }, { _id: new ObjectID(MONITOR_C) }],
1205
+ });
1206
+
1207
+ // C was never attached: nothing would ever restore it.
1208
+ expectNoMonitorSideEffects();
1209
+ expect(feedItem).not.toHaveBeenCalled();
1210
+ });
1211
+
1212
+ test("a held monitor the write detaches is restored though the payload seemed to keep it", async () => {
1213
+ eventsBeforeWrite = [
1214
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_B] }),
1215
+ ];
1216
+ afterWrite(maintenanceEvent({ state: "ongoing", monitors: [MONITOR_B] }));
1217
+
1218
+ await edit({
1219
+ monitors: [{ _id: new ObjectID(MONITOR_A) }, { _id: MONITOR_B }],
1220
+ });
1221
+
1222
+ expect(restoredMonitorIds()).toEqual([MONITOR_A]);
1223
+ expect(sectionLines(feedMarkdown(), REMOVED_HEADER)).toEqual([
1224
+ `- [checkout-web](${link(MONITOR_A)})`,
1225
+ ]);
1226
+ expect(feedMarkdown()).not.toContain(ADDED_HEADER);
1227
+ });
1228
+
1229
+ test("a payload that leaves the list in place restores nothing", async () => {
1230
+ eventsBeforeWrite = [
1231
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A] }),
1232
+ ];
1233
+ afterWrite(maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A] }));
1234
+
1235
+ await edit({ monitors: null });
1236
+
1237
+ expectNoMonitorSideEffects();
1238
+ expect(feedItem).not.toHaveBeenCalled();
1239
+ });
1240
+
1241
+ test("an event that is gone before it can be read back is left to its delete hook", async () => {
1242
+ eventsBeforeWrite = [
1243
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_B] }),
1244
+ ];
1245
+
1246
+ await edit({ monitors: [{ _id: MONITOR_C }] });
1247
+
1248
+ expectNoMonitorSideEffects();
1249
+ expect(monitorFindBy).not.toHaveBeenCalled();
1250
+ expect(feedItem).not.toHaveBeenCalled();
1251
+ });
1252
+ });
1253
+
1254
+ describe("ScheduledMaintenanceService.onUpdateSuccess: an event in a state of the project's own", () => {
1255
+ test("past ongoing, a detached monitor is restored", async () => {
1256
+ eventsBeforeWrite = [
1257
+ maintenanceEvent({ state: "custom", monitors: [MONITOR_A, MONITOR_B] }),
1258
+ ];
1259
+ afterWrite(maintenanceEvent({ state: "custom", monitors: [MONITOR_A] }));
1260
+ stateTimeline = timelineOf(["scheduled", "ongoing", "custom"]);
1261
+
1262
+ await edit({ monitors: [{ _id: MONITOR_A }] });
1263
+
1264
+ // Ending the event later only reaches A; B has to be restored now.
1265
+ expect(enableActiveMonitoring).toHaveBeenCalledTimes(1);
1266
+ expect(restoredMonitorIds()).toEqual([MONITOR_B]);
1267
+ expect(monitorUpdateOneById).not.toHaveBeenCalled();
1268
+ });
1269
+
1270
+ test("past ongoing, an attached monitor is held with the event's status", async () => {
1271
+ eventsBeforeWrite = [
1272
+ maintenanceEvent({ state: "custom", monitors: [MONITOR_A] }),
1273
+ ];
1274
+ afterWrite(
1275
+ maintenanceEvent({
1276
+ state: "custom",
1277
+ monitors: [MONITOR_A, MONITOR_C],
1278
+ changeMonitorStatusToId: MAINTENANCE_STATUS_ID,
1279
+ }),
1280
+ );
1281
+ stateTimeline = timelineOf(["scheduled", "ongoing", "custom"]);
1282
+
1283
+ await edit({ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }] });
1284
+
1285
+ expect(flaggedMonitorIds()).toEqual([MONITOR_C]);
1286
+ expect(changeMonitorStatus).toHaveBeenCalledTimes(1);
1287
+ expect(
1288
+ idsOf(changeMonitorStatus.mock.calls[0]![1] as Array<ObjectID>),
1289
+ ).toEqual([MONITOR_C]);
1290
+ // Asked before the write, and again after it.
1291
+ expect(timelineFindBy).toHaveBeenCalledTimes(2);
1292
+ });
1293
+
1294
+ test("moving from ongoing to a state of its own in the same update keeps holding what it attaches", async () => {
1295
+ eventsBeforeWrite = [
1296
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A] }),
1297
+ ];
1298
+ afterWrite(
1299
+ maintenanceEvent({
1300
+ state: "custom",
1301
+ monitors: [MONITOR_A, MONITOR_C],
1302
+ }),
1303
+ );
1304
+ // As the timeline reads once the same update has added the new state.
1305
+ stateTimeline = timelineOf(["scheduled", "ongoing", "custom"]);
1306
+
1307
+ jest
1308
+ .spyOn(ScheduledMaintenanceService, "changeScheduledMaintenanceState")
1309
+ .mockResolvedValue(undefined as never);
1310
+
1311
+ await edit({
1312
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }],
1313
+ currentScheduledMaintenanceStateId: new ObjectID(STATE_IDS["custom"]!),
1314
+ });
1315
+
1316
+ // Entering that state releases nothing, so C joins what is held.
1317
+ expect(flaggedMonitorIds()).toEqual([MONITOR_C]);
1318
+ });
1319
+
1320
+ test.each([
1321
+ ["not yet ongoing", ["scheduled", "custom"]],
1322
+ ["past ended", ["scheduled", "ongoing", "ended", "custom"]],
1323
+ ] as Array<[string, Array<StateKind>]>)(
1324
+ "%s, it touches no monitor",
1325
+ async (_label: string, kinds: Array<StateKind>) => {
1326
+ eventsBeforeWrite = [
1327
+ maintenanceEvent({
1328
+ state: "custom",
1329
+ monitors: [MONITOR_A, MONITOR_B],
1330
+ }),
1331
+ ];
1332
+ afterWrite(
1333
+ maintenanceEvent({
1334
+ state: "custom",
1335
+ monitors: [MONITOR_A, MONITOR_C],
1336
+ changeMonitorStatusToId: MAINTENANCE_STATUS_ID,
1337
+ }),
1338
+ );
1339
+ stateTimeline = timelineOf(kinds);
1340
+
1341
+ await edit({ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }] });
1342
+
1343
+ expectNoMonitorSideEffects();
1344
+ // The edit is still recorded.
1345
+ expect(sectionLines(feedMarkdown(), REMOVED_HEADER)).toEqual([
1346
+ `- [payments-api](${link(MONITOR_B)})`,
1347
+ ]);
1348
+ },
1349
+ );
1350
+
1351
+ test("past ongoing, its network sites are not re-rolled: only a live ongoing event suppresses them", async () => {
1352
+ eventsBeforeWrite = [
1353
+ maintenanceEvent({ state: "custom", networkSites: [SITE_1] }),
1354
+ ];
1355
+ afterWrite(maintenanceEvent({ state: "custom", networkSites: [SITE_2] }));
1356
+ stateTimeline = timelineOf(["scheduled", "ongoing", "custom"]);
1357
+
1358
+ await edit({ networkSites: [{ _id: SITE_2 }] });
1359
+
1360
+ expectNoMonitorSideEffects();
1361
+ // Nothing reads the sites' change, so they are not read back.
1362
+ expect(eventFindOneById).not.toHaveBeenCalled();
1363
+ });
1364
+ });
1365
+
1366
+ describe("ScheduledMaintenanceService.onUpdateSuccess: events that are not holding their monitors", () => {
1367
+ test.each(["scheduled", "ended", "resolved"] as Array<StateKind>)(
1368
+ "an event in the %s state touches no monitor or site when its lists are edited",
1369
+ async (kind: StateKind) => {
1370
+ eventsBeforeWrite = [
1371
+ maintenanceEvent({
1372
+ state: kind,
1373
+ monitors: [MONITOR_A, MONITOR_B],
1374
+ networkSites: [SITE_1],
1375
+ }),
1376
+ ];
1377
+ afterWrite(
1378
+ maintenanceEvent({
1379
+ state: kind,
1380
+ monitors: [MONITOR_A, MONITOR_C],
1381
+ networkSites: [SITE_2],
1382
+ changeMonitorStatusToId: MAINTENANCE_STATUS_ID,
1383
+ }),
1384
+ );
1385
+
1386
+ await edit({
1387
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }],
1388
+ networkSites: [{ _id: SITE_2 }],
1389
+ });
1390
+
1391
+ expectNoMonitorSideEffects();
1392
+ // Only the monitors are read back, for the feed; the sites need nothing.
1393
+ expect(eventFindOneById).toHaveBeenCalledTimes(1);
1394
+ expect(timelineFindBy).not.toHaveBeenCalled();
1395
+
1396
+ // The edit is still recorded.
1397
+ expect(sectionLines(feedMarkdown(), REMOVED_HEADER)).toEqual([
1398
+ `- [payments-api](${link(MONITOR_B)})`,
1399
+ ]);
1400
+ expect(sectionLines(feedMarkdown(), ADDED_HEADER)).toEqual([
1401
+ `- [search-api](${link(MONITOR_C)})`,
1402
+ ]);
1403
+ },
1404
+ );
1405
+
1406
+ test("an update written without the before hook changes nothing about monitors", async () => {
1407
+ afterWrite(maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A] }));
1408
+
1409
+ await runUpdateSuccess(
1410
+ {
1411
+ updateBy: updateByFor({ monitors: [{ _id: MONITOR_A }] }),
1412
+ carryForward: null,
1413
+ },
1414
+ [EVENT_ID],
1415
+ );
1416
+
1417
+ expectNoMonitorSideEffects();
1418
+ expect(eventFindOneById).not.toHaveBeenCalled();
1419
+ expect(monitorFindBy).not.toHaveBeenCalled();
1420
+ });
1421
+ });
1422
+
1423
+ describe("ScheduledMaintenanceService.onUpdateSuccess: the updated feed item", () => {
1424
+ test("names the detached and attached monitors with dashboard links", async () => {
1425
+ eventsBeforeWrite = [
1426
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_B] }),
1427
+ ];
1428
+ afterWrite(
1429
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_C] }),
1430
+ );
1431
+
1432
+ await edit({ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }] });
1433
+
1434
+ expect(feedItem).toHaveBeenCalledTimes(1);
1435
+
1436
+ const markdown: string = feedMarkdown();
1437
+
1438
+ expect(markdown).toContain(
1439
+ `\n\n${REMOVED_HEADER}- [payments-api](${link(MONITOR_B)})\n`,
1440
+ );
1441
+ expect(markdown).toContain(
1442
+ `\n\n${ADDED_HEADER}- [search-api](${link(MONITOR_C)})\n`,
1443
+ );
1444
+ // Removed first, as the incident feed has it.
1445
+ expect(markdown.indexOf(REMOVED_HEADER)).toBeLessThan(
1446
+ markdown.indexOf(ADDED_HEADER),
1447
+ );
1448
+ // A monitor that stayed is not a change.
1449
+ expect(markdown).not.toContain("checkout-web");
1450
+
1451
+ const item: JSONObject = feedItem.mock.calls[0]![0] as JSONObject;
1452
+ expect((item["projectId"] as ObjectID).toString()).toBe(
1453
+ PROJECT_ID.toString(),
1454
+ );
1455
+ expect((item["scheduledMaintenanceId"] as ObjectID).toString()).toBe(
1456
+ EVENT_ID,
1457
+ );
1458
+ expect((item["userId"] as ObjectID).toString()).toBe(USER_ID.toString());
1459
+ });
1460
+
1461
+ test("removing every monitor still records the edit", async () => {
1462
+ eventsBeforeWrite = [
1463
+ maintenanceEvent({
1464
+ state: "scheduled",
1465
+ monitors: [MONITOR_A, MONITOR_B],
1466
+ }),
1467
+ ];
1468
+ afterWrite(maintenanceEvent({ state: "scheduled" }));
1469
+
1470
+ // The dashboard sends every list back, each of them empty here.
1471
+ await edit({ monitors: [], networkSites: [], hosts: [] });
1472
+
1473
+ expect(feedItem).toHaveBeenCalledTimes(1);
1474
+ expect(sectionLines(feedMarkdown(), REMOVED_HEADER)).toEqual([
1475
+ `- [checkout-web](${link(MONITOR_A)})`,
1476
+ `- [payments-api](${link(MONITOR_B)})`,
1477
+ ]);
1478
+ expect(feedMarkdown()).not.toContain(ADDED_HEADER);
1479
+ expect(feedMarkdown()).not.toContain("**Resources Affected**");
1480
+ });
1481
+
1482
+ test("the names are read as root, held to the project", async () => {
1483
+ eventsBeforeWrite = [
1484
+ maintenanceEvent({ state: "scheduled", monitors: [MONITOR_A] }),
1485
+ ];
1486
+ afterWrite(maintenanceEvent({ state: "scheduled" }));
1487
+
1488
+ await edit({ monitors: [] });
1489
+
1490
+ expect(monitorFindBy).toHaveBeenCalledTimes(1);
1491
+
1492
+ const findBy: { query: JSONObject; props: JSONObject } = monitorFindBy.mock
1493
+ .calls[0]![0] as { query: JSONObject; props: JSONObject };
1494
+
1495
+ expect(findBy.query["projectId"]).toBe(PROJECT_ID);
1496
+ expect(idsInOperator(findBy.query["_id"])).toEqual([MONITOR_A]);
1497
+ expect(findBy.props).toEqual({ isRoot: true });
1498
+ });
1499
+
1500
+ test("a monitor the project does not own is never named", async () => {
1501
+ eventsBeforeWrite = [
1502
+ maintenanceEvent({ state: "scheduled", monitors: [FOREIGN_MONITOR] }),
1503
+ ];
1504
+ afterWrite(maintenanceEvent({ state: "scheduled" }));
1505
+
1506
+ await edit({ monitors: [] });
1507
+
1508
+ // Nothing nameable changed, and nothing else was edited.
1509
+ expect(feedItem).not.toHaveBeenCalled();
1510
+ });
1511
+
1512
+ test("the list of what the event now affects is kept beside the change", async () => {
1513
+ eventsBeforeWrite = [
1514
+ maintenanceEvent({ state: "scheduled", monitors: [MONITOR_A] }),
1515
+ ];
1516
+ afterWrite(
1517
+ maintenanceEvent({
1518
+ state: "scheduled",
1519
+ monitors: [MONITOR_A, MONITOR_C],
1520
+ }),
1521
+ );
1522
+
1523
+ // One read per relation, each answered with only the relation selected.
1524
+ jest
1525
+ .spyOn(ScheduledMaintenanceService, "findAllBy")
1526
+ .mockImplementation((async (findAllBy: {
1527
+ select: JSONObject;
1528
+ }): Promise<Array<JSONObject>> => {
1529
+ const row: JSONObject = {
1530
+ _id: EVENT_ID,
1531
+ projectId: PROJECT_ID as unknown as JSONObject,
1532
+ };
1533
+
1534
+ for (const column of Object.keys(findAllBy.select)) {
1535
+ if (column !== "_id" && column !== "projectId") {
1536
+ row[column] = null;
1537
+ }
1538
+ }
1539
+
1540
+ if (findAllBy.select["monitors"]) {
1541
+ row["monitors"] = [MONITOR_A, MONITOR_C].map(
1542
+ (id: string): JSONObject => {
1543
+ return {
1544
+ _id: id,
1545
+ name: MONITOR_NAMES[id]!,
1546
+ projectId: PROJECT_ID as unknown as JSONObject,
1547
+ };
1548
+ },
1549
+ );
1550
+ }
1551
+
1552
+ return [row];
1553
+ }) as never);
1554
+
1555
+ await edit({ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }] });
1556
+
1557
+ const markdown: string = feedMarkdown();
1558
+
1559
+ expect(sectionLines(markdown, "**Resources Affected**:\n\n")).toEqual([
1560
+ `- [checkout-web](${link(MONITOR_A)})`,
1561
+ `- [search-api](${link(MONITOR_C)})`,
1562
+ ]);
1563
+ expect(sectionLines(markdown, ADDED_HEADER)).toEqual([
1564
+ `- [search-api](${link(MONITOR_C)})`,
1565
+ ]);
1566
+ });
1567
+
1568
+ test("a root update without a tenant still acts on monitors but writes no monitor lines", async () => {
1569
+ eventsBeforeWrite = [
1570
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_B] }),
1571
+ ];
1572
+ afterWrite(maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A] }));
1573
+
1574
+ await edit({ monitors: [{ _id: MONITOR_A }] }, { props: { isRoot: true } });
1575
+
1576
+ // The project comes from the event itself.
1577
+ const restored: ScheduledMaintenance = enableActiveMonitoring.mock
1578
+ .calls[0]![0] as ScheduledMaintenance;
1579
+ expect(idsOf(restored.monitors)).toEqual([MONITOR_B]);
1580
+ expect(restored.projectId?.toString()).toBe(PROJECT_ID.toString());
1581
+
1582
+ // The flag is read held to the event's project; no name is read.
1583
+ expect(flagReads()[0]!.query.projectId.toString()).toBe(
1584
+ PROJECT_ID.toString(),
1585
+ );
1586
+ expect(nameReads()).toHaveLength(0);
1587
+ });
1588
+ });
1589
+
1590
+ describe("ScheduledMaintenanceService.onUpdateSuccess: which detached monitors are released", () => {
1591
+ test("only a detached monitor still in maintenance is released", async () => {
1592
+ eventsBeforeWrite = [
1593
+ maintenanceEvent({
1594
+ state: "ongoing",
1595
+ monitors: [MONITOR_A, MONITOR_B, MONITOR_C],
1596
+ }),
1597
+ ];
1598
+ afterWrite(maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A] }));
1599
+ // Released already, by another event ending or a restore of its own.
1600
+ monitorsNotFlagged.add(MONITOR_C);
1601
+
1602
+ await edit({ monitors: [{ _id: MONITOR_A }] });
1603
+
1604
+ expect(enableActiveMonitoring).toHaveBeenCalledTimes(1);
1605
+ expect(restoredMonitorIds()).toEqual([MONITOR_B]);
1606
+
1607
+ // Both are still named in the feed: the edit detached both.
1608
+ expect(sectionLines(feedMarkdown(), REMOVED_HEADER)).toEqual([
1609
+ `- [payments-api](${link(MONITOR_B)})`,
1610
+ `- [search-api](${link(MONITOR_C)})`,
1611
+ ]);
1612
+ });
1613
+
1614
+ test("the flag is read as root, held to the project, for the detached monitors only", async () => {
1615
+ eventsBeforeWrite = [
1616
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_B] }),
1617
+ ];
1618
+ afterWrite(
1619
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_C] }),
1620
+ );
1621
+
1622
+ await edit({ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }] });
1623
+
1624
+ expect(flagReads()).toHaveLength(1);
1625
+
1626
+ const flagRead: MonitorFindBy = flagReads()[0]!;
1627
+
1628
+ expect(idsInOperator(flagRead.query._id)).toEqual([MONITOR_B]);
1629
+ expect(flagRead.query.projectId.toString()).toBe(PROJECT_ID.toString());
1630
+ expect(flagRead.props).toEqual({ isRoot: true });
1631
+ });
1632
+
1633
+ test("with no detached monitor in maintenance, nothing is released", async () => {
1634
+ eventsBeforeWrite = [
1635
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_B] }),
1636
+ ];
1637
+ afterWrite(maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A] }));
1638
+ monitorsNotFlagged.add(MONITOR_B);
1639
+
1640
+ await edit({ monitors: [{ _id: MONITOR_A }] });
1641
+
1642
+ expect(enableActiveMonitoring).not.toHaveBeenCalled();
1643
+ expect(feedItem).toHaveBeenCalledTimes(1);
1644
+ });
1645
+ });
1646
+
1647
+ /*
1648
+ * The ChangeStateToOngoing job, or another request, can move the event
1649
+ * between onBeforeUpdate's read and the write, or just after the write. The
1650
+ * state before the write is then not the one the transition acted on, so
1651
+ * neither list can be decided by it alone.
1652
+ */
1653
+ describe("ScheduledMaintenanceService.onUpdateSuccess: a state change racing the edit", () => {
1654
+ test("started between the read and the write: the detached monitor the start flagged is restored, the attached one held", async () => {
1655
+ eventsBeforeWrite = [
1656
+ maintenanceEvent({
1657
+ state: "scheduled",
1658
+ monitors: [MONITOR_A, MONITOR_B],
1659
+ }),
1660
+ ];
1661
+ // The start read [A, B], so B is flagged and C is not.
1662
+ afterWrite(
1663
+ maintenanceEvent({
1664
+ state: "ongoing",
1665
+ monitors: [MONITOR_A, MONITOR_C],
1666
+ changeMonitorStatusToId: MAINTENANCE_STATUS_ID,
1667
+ }),
1668
+ );
1669
+
1670
+ await edit({ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }] });
1671
+
1672
+ // B would otherwise stay disabled for good.
1673
+ expect(restoredMonitorIds()).toEqual([MONITOR_B]);
1674
+ // C would otherwise be probed and alerting through the window.
1675
+ expect(flaggedMonitorIds()).toEqual([MONITOR_C]);
1676
+ expect(changeMonitorStatus).toHaveBeenCalledTimes(1);
1677
+ expect(
1678
+ idsOf(changeMonitorStatus.mock.calls[0]![1] as Array<ObjectID>),
1679
+ ).toEqual([MONITOR_C]);
1680
+ expect((changeMonitorStatus.mock.calls[0]![2] as ObjectID).toString()).toBe(
1681
+ MAINTENANCE_STATUS_ID.toString(),
1682
+ );
1683
+ });
1684
+
1685
+ test("started just after the write: the detached monitor it never reached is left alone, the attached one held again", async () => {
1686
+ eventsBeforeWrite = [
1687
+ maintenanceEvent({
1688
+ state: "scheduled",
1689
+ monitors: [MONITOR_A, MONITOR_B],
1690
+ }),
1691
+ ];
1692
+ // The start read [A, C]: B was never flagged, C already is.
1693
+ afterWrite(
1694
+ maintenanceEvent({
1695
+ state: "ongoing",
1696
+ monitors: [MONITOR_A, MONITOR_C],
1697
+ changeMonitorStatusToId: MAINTENANCE_STATUS_ID,
1698
+ }),
1699
+ );
1700
+ monitorsNotFlagged.add(MONITOR_B);
1701
+
1702
+ await edit({ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }] });
1703
+
1704
+ // B is not forced into the operational status over its real one.
1705
+ expect(enableActiveMonitoring).not.toHaveBeenCalled();
1706
+ // Idempotent with what the start did.
1707
+ expect(flaggedMonitorIds()).toEqual([MONITOR_C]);
1708
+ expect(changeMonitorStatus).toHaveBeenCalledTimes(1);
1709
+ });
1710
+
1711
+ test("started and moved to a state of the project's own before the write: still held", async () => {
1712
+ eventsBeforeWrite = [
1713
+ maintenanceEvent({
1714
+ state: "scheduled",
1715
+ monitors: [MONITOR_A, MONITOR_B],
1716
+ }),
1717
+ ];
1718
+ afterWrite(
1719
+ maintenanceEvent({
1720
+ state: "custom",
1721
+ monitors: [MONITOR_A, MONITOR_C],
1722
+ }),
1723
+ );
1724
+ stateTimeline = timelineOf(["scheduled", "ongoing", "custom"]);
1725
+
1726
+ await edit({ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }] });
1727
+
1728
+ expect(restoredMonitorIds()).toEqual([MONITOR_B]);
1729
+ expect(flaggedMonitorIds()).toEqual([MONITOR_C]);
1730
+ // Asked once, after the write; the scheduled state answered before it.
1731
+ expect(timelineFindBy).toHaveBeenCalledTimes(1);
1732
+ });
1733
+
1734
+ test("ended by another request just before the write: nothing is released twice, nothing new held", async () => {
1735
+ eventsBeforeWrite = [
1736
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_B] }),
1737
+ ];
1738
+ // The end read [A, B] and released both.
1739
+ afterWrite(
1740
+ maintenanceEvent({
1741
+ state: "ended",
1742
+ monitors: [MONITOR_A, MONITOR_C],
1743
+ changeMonitorStatusToId: MAINTENANCE_STATUS_ID,
1744
+ }),
1745
+ );
1746
+ monitorsNotFlagged.add(MONITOR_A);
1747
+ monitorsNotFlagged.add(MONITOR_B);
1748
+
1749
+ await edit({ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }] });
1750
+
1751
+ expect(flagReads()).toHaveLength(1);
1752
+ expect(enableActiveMonitoring).not.toHaveBeenCalled();
1753
+ expect(monitorUpdateOneById).not.toHaveBeenCalled();
1754
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
1755
+ });
1756
+
1757
+ test("ended by another request just after the write: the detached monitor it no longer reached is released", async () => {
1758
+ eventsBeforeWrite = [
1759
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_B] }),
1760
+ ];
1761
+ // The end read [A, C]; B is still flagged.
1762
+ afterWrite(
1763
+ maintenanceEvent({
1764
+ state: "ended",
1765
+ monitors: [MONITOR_A, MONITOR_C],
1766
+ }),
1767
+ );
1768
+
1769
+ await edit({ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }] });
1770
+
1771
+ expect(restoredMonitorIds()).toEqual([MONITOR_B]);
1772
+ expect(monitorUpdateOneById).not.toHaveBeenCalled();
1773
+ });
1774
+
1775
+ test("an event holding nothing on either side of the write reads no flag and touches nothing", async () => {
1776
+ eventsBeforeWrite = [
1777
+ maintenanceEvent({
1778
+ state: "scheduled",
1779
+ monitors: [MONITOR_A, MONITOR_B],
1780
+ }),
1781
+ ];
1782
+ afterWrite(
1783
+ maintenanceEvent({
1784
+ state: "scheduled",
1785
+ monitors: [MONITOR_A, MONITOR_C],
1786
+ }),
1787
+ );
1788
+
1789
+ await edit({ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }] });
1790
+
1791
+ expect(flagReads()).toHaveLength(0);
1792
+ expectNoMonitorSideEffects();
1793
+ });
1794
+ });
1795
+
1796
+ /*
1797
+ * The write has committed by the time these run. A failure used to escape
1798
+ * onUpdateSuccess: it stranded the monitors after the one that failed, and
1799
+ * skipped the rest of the edit's effects, the feed item and every later
1800
+ * event of a bulk update - and the saved update came back as an error.
1801
+ */
1802
+ describe("ScheduledMaintenanceService.onUpdateSuccess: a failure after the write", () => {
1803
+ let loggerError: jest.SpyInstance;
1804
+
1805
+ beforeEach(() => {
1806
+ loggerError = jest.spyOn(logger, "error").mockImplementation(() => {
1807
+ return undefined;
1808
+ });
1809
+ });
1810
+
1811
+ // Every error was logged against the event and its project.
1812
+ function expectLoggedFor(scheduledMaintenanceId: string): void {
1813
+ const attributes: Array<LogAttributes> = loggerError.mock.calls
1814
+ .map((call: Array<unknown>): LogAttributes => {
1815
+ return (call[1] || {}) as LogAttributes;
1816
+ })
1817
+ .filter((logAttributes: LogAttributes): boolean => {
1818
+ return (
1819
+ logAttributes["scheduledMaintenanceId"] === scheduledMaintenanceId
1820
+ );
1821
+ });
1822
+
1823
+ expect(attributes.length).toBeGreaterThan(0);
1824
+
1825
+ for (const logAttributes of attributes) {
1826
+ expect(logAttributes.projectId).toBe(PROJECT_ID.toString());
1827
+ }
1828
+ }
1829
+
1830
+ test("restoring the detached monitors fails: the attached ones are still held, the sites re-rolled and the feed written", async () => {
1831
+ eventsBeforeWrite = [
1832
+ maintenanceEvent({
1833
+ state: "ongoing",
1834
+ monitors: [MONITOR_A, MONITOR_B],
1835
+ networkSites: [SITE_1],
1836
+ }),
1837
+ ];
1838
+ afterWrite(
1839
+ maintenanceEvent({
1840
+ state: "ongoing",
1841
+ monitors: [MONITOR_A, MONITOR_C],
1842
+ networkSites: [SITE_2],
1843
+ changeMonitorStatusToId: MAINTENANCE_STATUS_ID,
1844
+ }),
1845
+ );
1846
+ enableActiveMonitoring.mockRejectedValue(
1847
+ new Error("Could not acquire the status timeline lock") as never,
1848
+ );
1849
+
1850
+ await expect(
1851
+ edit({
1852
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }],
1853
+ networkSites: [{ _id: SITE_2 }],
1854
+ }),
1855
+ ).resolves.toBeDefined();
1856
+
1857
+ expect(enableActiveMonitoring).toHaveBeenCalledTimes(1);
1858
+ expect(flaggedMonitorIds()).toEqual([MONITOR_C]);
1859
+ expect(changeMonitorStatus).toHaveBeenCalledTimes(1);
1860
+ expect(
1861
+ idsOf(
1862
+ (recomputeSiteRollups.mock.calls[0]![0] as ScheduledMaintenance)
1863
+ .networkSites,
1864
+ ),
1865
+ ).toEqual([SITE_1, SITE_2]);
1866
+
1867
+ expect(feedItem).toHaveBeenCalledTimes(1);
1868
+ expect(sectionLines(feedMarkdown(), REMOVED_HEADER)).toEqual([
1869
+ `- [payments-api](${link(MONITOR_B)})`,
1870
+ ]);
1871
+ expect(sectionLines(feedMarkdown(), ADDED_HEADER)).toEqual([
1872
+ `- [search-api](${link(MONITOR_C)})`,
1873
+ ]);
1874
+
1875
+ expectLoggedFor(EVENT_ID);
1876
+ });
1877
+
1878
+ test("reading which detached monitors are in maintenance fails: the attached ones are still held and the feed written", async () => {
1879
+ eventsBeforeWrite = [
1880
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_B] }),
1881
+ ];
1882
+ afterWrite(
1883
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_C] }),
1884
+ );
1885
+
1886
+ const namesOnly: (findBy: MonitorFindBy) => Promise<Array<Monitor>> =
1887
+ monitorFindBy.getMockImplementation() as (
1888
+ findBy: MonitorFindBy,
1889
+ ) => Promise<Array<Monitor>>;
1890
+
1891
+ monitorFindBy.mockImplementation((async (
1892
+ findBy: MonitorFindBy,
1893
+ ): Promise<Array<Monitor>> => {
1894
+ if (
1895
+ findBy.query.disableActiveMonitoringBecauseOfScheduledMaintenanceEvent
1896
+ ) {
1897
+ throw new Error("connection reset");
1898
+ }
1899
+
1900
+ return await namesOnly(findBy);
1901
+ }) as never);
1902
+
1903
+ await edit({ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }] });
1904
+
1905
+ expect(enableActiveMonitoring).not.toHaveBeenCalled();
1906
+ expect(flaggedMonitorIds()).toEqual([MONITOR_C]);
1907
+ expect(feedItem).toHaveBeenCalledTimes(1);
1908
+ expectLoggedFor(EVENT_ID);
1909
+ });
1910
+
1911
+ test("putting one attached monitor into maintenance fails: the others are still held, and the feed written", async () => {
1912
+ eventsBeforeWrite = [
1913
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A] }),
1914
+ ];
1915
+ afterWrite(
1916
+ maintenanceEvent({
1917
+ state: "ongoing",
1918
+ monitors: [MONITOR_A, MONITOR_C, MONITOR_D],
1919
+ changeMonitorStatusToId: MAINTENANCE_STATUS_ID,
1920
+ }),
1921
+ );
1922
+
1923
+ monitorUpdateOneById.mockImplementation((async (updateOneById: {
1924
+ id: ObjectID;
1925
+ }): Promise<void> => {
1926
+ if (updateOneById.id.toString() === MONITOR_C) {
1927
+ throw new Error("connection reset");
1928
+ }
1929
+ }) as never);
1930
+
1931
+ await edit({
1932
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }, { _id: MONITOR_D }],
1933
+ });
1934
+
1935
+ // Both were tried; D went all the way.
1936
+ expect(flaggedMonitorIds()).toEqual([MONITOR_C, MONITOR_D]);
1937
+ expect(changeMonitorStatus).toHaveBeenCalledTimes(1);
1938
+ expect(
1939
+ idsOf(changeMonitorStatus.mock.calls[0]![1] as Array<ObjectID>),
1940
+ ).toEqual([MONITOR_D]);
1941
+
1942
+ expect(sectionLines(feedMarkdown(), ADDED_HEADER)).toEqual([
1943
+ `- [search-api](${link(MONITOR_C)})`,
1944
+ `- [auth-api](${link(MONITOR_D)})`,
1945
+ ]);
1946
+
1947
+ const monitorAttributes: Array<LogAttributes> = loggerError.mock.calls
1948
+ .map((call: Array<unknown>): LogAttributes => {
1949
+ return (call[1] || {}) as LogAttributes;
1950
+ })
1951
+ .filter((logAttributes: LogAttributes): boolean => {
1952
+ return Boolean(logAttributes["monitorId"]);
1953
+ });
1954
+ expect(monitorAttributes.length).toBeGreaterThan(0);
1955
+ expect(monitorAttributes[0]!["monitorId"]).toBe(MONITOR_C);
1956
+ expectLoggedFor(EVENT_ID);
1957
+ });
1958
+
1959
+ test("the status change of one attached monitor fails: the others still get it", async () => {
1960
+ eventsBeforeWrite = [
1961
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A] }),
1962
+ ];
1963
+ afterWrite(
1964
+ maintenanceEvent({
1965
+ state: "ongoing",
1966
+ monitors: [MONITOR_A, MONITOR_C, MONITOR_D],
1967
+ changeMonitorStatusToId: MAINTENANCE_STATUS_ID,
1968
+ }),
1969
+ );
1970
+ changeMonitorStatus.mockRejectedValueOnce(
1971
+ new Error("Could not acquire the status timeline lock") as never,
1972
+ );
1973
+
1974
+ await edit({
1975
+ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }, { _id: MONITOR_D }],
1976
+ });
1977
+
1978
+ expect(flaggedMonitorIds()).toEqual([MONITOR_C, MONITOR_D]);
1979
+ expect(changeMonitorStatus).toHaveBeenCalledTimes(2);
1980
+ expect(
1981
+ changeMonitorStatus.mock.calls
1982
+ .map((call: Array<unknown>): Array<string> => {
1983
+ return idsOf(call[1] as Array<ObjectID>);
1984
+ })
1985
+ .flat()
1986
+ .sort(),
1987
+ ).toEqual([MONITOR_C, MONITOR_D]);
1988
+ expect(feedItem).toHaveBeenCalledTimes(1);
1989
+ });
1990
+
1991
+ test("one event of a bulk update failing leaves the next one to its edit and its feed item", async () => {
1992
+ eventsBeforeWrite = [
1993
+ maintenanceEvent({
1994
+ id: EVENT_ID,
1995
+ state: "ongoing",
1996
+ monitors: [MONITOR_A],
1997
+ }),
1998
+ maintenanceEvent({
1999
+ id: SECOND_EVENT_ID,
2000
+ state: "ongoing",
2001
+ monitors: [MONITOR_B],
2002
+ }),
2003
+ ];
2004
+ afterWrite(
2005
+ maintenanceEvent({ id: EVENT_ID, state: "ongoing", monitors: [] }),
2006
+ );
2007
+ afterWrite(
2008
+ maintenanceEvent({
2009
+ id: SECOND_EVENT_ID,
2010
+ state: "ongoing",
2011
+ monitors: [],
2012
+ }),
2013
+ );
2014
+
2015
+ enableActiveMonitoring.mockImplementation((async (
2016
+ scheduledMaintenance: ScheduledMaintenance,
2017
+ ): Promise<void> => {
2018
+ if (scheduledMaintenance.id?.toString() === EVENT_ID) {
2019
+ throw new Error("connection reset");
2020
+ }
2021
+ }) as never);
2022
+
2023
+ await edit(
2024
+ { monitors: [] },
2025
+ {
2026
+ query: { projectId: PROJECT_ID },
2027
+ updatedItemIds: [EVENT_ID, SECOND_EVENT_ID],
2028
+ },
2029
+ );
2030
+
2031
+ expect(enableActiveMonitoring).toHaveBeenCalledTimes(2);
2032
+ expect(restoredMonitorIds(1)).toEqual([MONITOR_B]);
2033
+ expect(
2034
+ (
2035
+ enableActiveMonitoring.mock.calls[1]![0] as ScheduledMaintenance
2036
+ ).id?.toString(),
2037
+ ).toBe(SECOND_EVENT_ID);
2038
+
2039
+ // Each event still records its own edit.
2040
+ expect(feedItem).toHaveBeenCalledTimes(2);
2041
+ expect(sectionLines(feedMarkdown(0), REMOVED_HEADER)).toEqual([
2042
+ `- [checkout-web](${link(MONITOR_A)})`,
2043
+ ]);
2044
+ expect(sectionLines(feedMarkdown(1), REMOVED_HEADER)).toEqual([
2045
+ `- [payments-api](${link(MONITOR_B)})`,
2046
+ ]);
2047
+
2048
+ expectLoggedFor(EVENT_ID);
2049
+ });
2050
+
2051
+ test("the event cannot be read back: nothing is acted on, and the rest of the edit is still recorded", async () => {
2052
+ eventsBeforeWrite = [
2053
+ maintenanceEvent({ state: "ongoing", monitors: [MONITOR_A, MONITOR_B] }),
2054
+ ];
2055
+ eventFindOneById.mockRejectedValue(new Error("connection reset") as never);
2056
+
2057
+ await edit({
2058
+ title: "Database upgrade",
2059
+ monitors: [{ _id: MONITOR_A }],
2060
+ });
2061
+
2062
+ expectNoMonitorSideEffects();
2063
+ expect(feedItem).toHaveBeenCalledTimes(1);
2064
+ expect(feedMarkdown()).toContain("Database upgrade");
2065
+ expect(feedMarkdown()).not.toContain(REMOVED_HEADER);
2066
+ expectLoggedFor(EVENT_ID);
2067
+ });
2068
+
2069
+ test("whether the event holds its monitors after the write cannot be read: nothing new is held, the detached ones are still restored", async () => {
2070
+ eventsBeforeWrite = [
2071
+ maintenanceEvent({ state: "custom", monitors: [MONITOR_A, MONITOR_B] }),
2072
+ ];
2073
+ afterWrite(
2074
+ maintenanceEvent({
2075
+ state: "custom",
2076
+ monitors: [MONITOR_A, MONITOR_C],
2077
+ changeMonitorStatusToId: MAINTENANCE_STATUS_ID,
2078
+ }),
2079
+ );
2080
+
2081
+ // Read before the write; the read after it fails.
2082
+ timelineFindBy
2083
+ .mockResolvedValueOnce(
2084
+ timelineOf(["scheduled", "ongoing", "custom"]) as never,
2085
+ )
2086
+ .mockRejectedValueOnce(new Error("connection reset") as never);
2087
+
2088
+ await edit({ monitors: [{ _id: MONITOR_A }, { _id: MONITOR_C }] });
2089
+
2090
+ expect(timelineFindBy).toHaveBeenCalledTimes(2);
2091
+ expect(restoredMonitorIds()).toEqual([MONITOR_B]);
2092
+ // Left probed rather than disabled with nothing to restore it.
2093
+ expect(monitorUpdateOneById).not.toHaveBeenCalled();
2094
+ expect(changeMonitorStatus).not.toHaveBeenCalled();
2095
+ expect(feedItem).toHaveBeenCalledTimes(1);
2096
+ expectLoggedFor(EVENT_ID);
2097
+ });
2098
+
2099
+ test("naming the changed monitors fails: the feed item is still written for the rest", async () => {
2100
+ eventsBeforeWrite = [
2101
+ maintenanceEvent({ state: "scheduled", monitors: [MONITOR_A] }),
2102
+ ];
2103
+ afterWrite(maintenanceEvent({ state: "scheduled" }));
2104
+ monitorFindBy.mockRejectedValue(new Error("connection reset") as never);
2105
+
2106
+ await edit({ title: "Database upgrade", monitors: [] });
2107
+
2108
+ expect(feedItem).toHaveBeenCalledTimes(1);
2109
+ expect(feedMarkdown()).toContain("Database upgrade");
2110
+ expect(feedMarkdown()).not.toContain(REMOVED_HEADER);
2111
+ expectLoggedFor(EVENT_ID);
2112
+ });
2113
+ });