@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.
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/KubernetesAiAgent.ts +364 -0
- package/Models/DatabaseModels/KubernetesCluster.ts +3 -3
- package/Models/DatabaseModels/Project.ts +55 -5
- package/Models/DatabaseModels/ProxmoxResource.ts +49 -1
- package/Models/DatabaseModels/RunnerJob.ts +75 -0
- package/Models/DatabaseModels/TeamComplianceSetting.ts +143 -2
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +17 -0
- package/Server/API/AIChatAPI.ts +6 -3
- package/Server/API/AIInvestigationAPI.ts +17 -10
- package/Server/API/AutoRemediationAPI.ts +207 -25
- package/Server/API/KubernetesClusterAiAccessAPI.ts +623 -21
- package/Server/API/MicrosoftTeamsAPI.ts +93 -14
- package/Server/API/ProxmoxResourceAPI.ts +244 -14
- package/Server/Infrastructure/GlobalCache.ts +32 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.ts +99 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.ts +137 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.ts +42 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Middleware/TelemetryIngest.ts +41 -18
- package/Server/Services/AIService.ts +70 -1
- package/Server/Services/AlertService.ts +352 -11
- package/Server/Services/AlertSeverityService.ts +65 -15
- package/Server/Services/AutoRemediationRuleService.ts +1 -1
- package/Server/Services/IncidentService.ts +280 -104
- package/Server/Services/IncidentSeverityService.ts +66 -15
- package/Server/Services/KubernetesAiAgentJobService.ts +185 -0
- package/Server/Services/KubernetesAiAgentService.ts +1634 -0
- package/Server/Services/KubernetesClusterAiAccessService.ts +1118 -451
- package/Server/Services/KubernetesClusterService.ts +129 -21
- package/Server/Services/ProjectService.ts +32 -0
- package/Server/Services/ProxmoxResourceService.ts +322 -5
- package/Server/Services/RunnerJobService.ts +241 -104
- package/Server/Services/RunnerService.ts +1 -1
- package/Server/Services/ScheduledMaintenanceService.ts +737 -1
- package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +414 -56
- package/Server/Services/TeamComplianceSettingService.ts +1125 -9
- package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +67 -9
- package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +39 -10
- package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +14 -3
- package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +37 -15
- package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +67 -24
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +48 -5
- package/Server/Utils/AI/SRE/IncidentPostmortemRunner.ts +40 -8
- package/Server/Utils/AI/SRE/InvestigationEligibility.ts +13 -2
- package/Server/Utils/AI/SRE/InvestigationGrader.ts +1 -1
- package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +6 -1
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +133 -7
- package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +58 -4
- package/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.ts +447 -0
- package/Server/Utils/Telemetry/ProxmoxNativePush.ts +903 -0
- package/Server/Utils/Telemetry/TelemetryEntity.ts +87 -27
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +684 -47
- package/Tests/App/AdminDashboard/RowActionPlacementOverrides.test.tsx +278 -0
- package/Tests/App/Dashboard/AIChatChartWidgetZoom.test.tsx +738 -0
- package/Tests/App/Dashboard/AIFeaturesSettingsPointers.test.tsx +147 -0
- package/Tests/App/Dashboard/AIInsightsKubernetesClusterPage.test.tsx +1523 -0
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +463 -3
- package/Tests/App/Dashboard/AffectedResourcesPickerDropdownPlacement.test.tsx +958 -0
- package/Tests/App/Dashboard/AffectedResourcesPickerFocusLeave.test.tsx +595 -0
- package/Tests/App/Dashboard/AskAiAvailability.test.ts +20 -3
- package/Tests/App/Dashboard/AskAiDisabled.test.tsx +8 -3
- package/Tests/App/Dashboard/CephRateChartReload.test.tsx +1456 -0
- package/Tests/App/Dashboard/CephTimeRangeZoom.test.tsx +1229 -0
- package/Tests/App/Dashboard/ChartCardTimeRangeZoom.test.tsx +151 -0
- package/Tests/App/Dashboard/ChartCardZoomAffordances.test.tsx +257 -0
- package/Tests/App/Dashboard/ChartRefetchFrame.test.tsx +141 -0
- package/Tests/App/Dashboard/ChartZoomHarness.tsx +249 -0
- package/Tests/App/Dashboard/ChartZoomStandIn.tsx +131 -0
- package/Tests/App/Dashboard/CloudServerlessSlowLoads.test.tsx +628 -0
- package/Tests/App/Dashboard/CloudServerlessTimeRangeZoom.test.tsx +607 -0
- package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +91 -19
- package/Tests/App/Dashboard/CompanionMetricsTabZoom.test.tsx +632 -0
- package/Tests/App/Dashboard/ContainerChartZoomStandIn.tsx +195 -0
- package/Tests/App/Dashboard/ContainerHostOverviewSlowLoads.test.tsx +542 -0
- package/Tests/App/Dashboard/ContainerHostOverviewTimeRangeZoom.test.tsx +848 -0
- package/Tests/App/Dashboard/ContainerMetricsTimeRangeZoom.test.tsx +561 -0
- package/Tests/App/Dashboard/DashboardBoardWideZoomShell.test.tsx +1147 -0
- package/Tests/App/Dashboard/DashboardEditPermissions.test.tsx +3 -2
- package/Tests/App/Dashboard/DashboardHistogramWidgetDragCursor.test.tsx +399 -0
- package/Tests/App/Dashboard/DashboardHistogramWidgetZoom.test.tsx +985 -0
- package/Tests/App/Dashboard/DashboardHistogramWidgetZoomHint.test.tsx +803 -0
- package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +1 -0
- package/Tests/App/Dashboard/DashboardSettingsPreviewZoom.test.tsx +679 -0
- package/Tests/App/Dashboard/DashboardSloAndDataSourceChartZoom.test.tsx +881 -0
- package/Tests/App/Dashboard/DashboardTraceStatusWidgets.test.tsx +878 -0
- package/Tests/App/Dashboard/DatabaseMetricChartModalTimeRangeZoom.test.tsx +442 -0
- package/Tests/App/Dashboard/DatabaseOverviewTimeRangeZoom.test.tsx +831 -0
- package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +43 -0
- package/Tests/App/Dashboard/DockerSwarmInsightsTimeRangeZoom.test.tsx +524 -0
- package/Tests/App/Dashboard/DockerSwarmRateChartRefresh.test.tsx +259 -0
- package/Tests/App/Dashboard/EdgeDetailPanelHistory.test.tsx +763 -0
- package/Tests/App/Dashboard/EdgeDetailPanelTimeRangeZoom.test.tsx +547 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsCases.tsx +565 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsKathmandu.test.tsx +73 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsKolkata.test.tsx +74 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsNewYork.test.tsx +72 -0
- package/Tests/App/Dashboard/EdgeHistoryBucketsUtc.test.tsx +72 -0
- package/Tests/App/Dashboard/EmbeddedMetricCardRefresh.test.tsx +310 -0
- package/Tests/App/Dashboard/EmbeddedMetricCardTimeRangeZoom.test.tsx +536 -0
- package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +3 -1
- package/Tests/App/Dashboard/EscalationRuleRowActions.test.tsx +660 -0
- package/Tests/App/Dashboard/EventNotes.test.tsx +2 -2
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +587 -13
- package/Tests/App/Dashboard/EventOverviewSnapshotExplorerZoom.test.tsx +1139 -0
- package/Tests/App/Dashboard/EventOverviewSnapshotZoomAcrossTabs.test.tsx +1057 -0
- package/Tests/App/Dashboard/EventOverviewTelemetrySnapshotZoom.test.tsx +757 -0
- package/Tests/App/Dashboard/ExceptionOccurrenceTrendZoom.test.tsx +675 -0
- package/Tests/App/Dashboard/ExceptionOccurrencesZoomSurvivesViewSwitch.test.tsx +405 -0
- package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +67 -3
- package/Tests/App/Dashboard/ExceptionTablesSpanStatus.test.tsx +515 -0
- package/Tests/App/Dashboard/ExplorerOwnZoomRegression.test.tsx +739 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisCases.tsx +461 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisKathmandu.test.tsx +25 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisKolkata.test.tsx +25 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisNewYork.test.tsx +25 -0
- package/Tests/App/Dashboard/FlowBandwidthAxisUtc.test.tsx +24 -0
- package/Tests/App/Dashboard/FlowTopTalkersZoomRefetch.test.tsx +722 -0
- package/Tests/App/Dashboard/FlowViewportGuard.test.tsx +2526 -0
- package/Tests/App/Dashboard/HostPagesSlowLoads.test.tsx +872 -0
- package/Tests/App/Dashboard/HostPagesTimeRangeZoom.test.tsx +1208 -0
- package/Tests/App/Dashboard/IncidentRootCauseTimeRangeZoom.test.tsx +358 -0
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +100 -9
- package/Tests/App/Dashboard/InfrastructureMetricsTabTimeRangeZoom.test.tsx +485 -0
- package/Tests/App/Dashboard/InvestigationDrawerCompanionZoom.test.tsx +655 -0
- package/Tests/App/Dashboard/InvestigationDrawerZoom.test.tsx +830 -0
- package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +338 -2
- package/Tests/App/Dashboard/InvoicesPayInvoice.test.tsx +454 -16
- package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +799 -819
- package/Tests/App/Dashboard/KubernetesClusterAiAgentOverviewCard.test.tsx +367 -0
- package/Tests/App/Dashboard/KubernetesClusterAiAgentStatus.test.ts +1202 -0
- package/Tests/App/Dashboard/KubernetesClusterAiDocumentationMarkdown.test.ts +106 -0
- package/Tests/App/Dashboard/KubernetesClusterAiNavigation.test.tsx +354 -0
- package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +1884 -2054
- package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +478 -179
- package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +50 -0
- package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +2 -1
- package/Tests/App/Dashboard/KubernetesClusterOverviewZoom.test.tsx +1217 -0
- package/Tests/App/Dashboard/KubernetesClusterPagesZoom.test.tsx +731 -0
- package/Tests/App/Dashboard/KubernetesCostsLoadRecovery.test.tsx +869 -0
- package/Tests/App/Dashboard/KubernetesCostsZoom.test.tsx +1214 -0
- package/Tests/App/Dashboard/KubernetesCostsZoomExactHours.test.tsx +692 -0
- package/Tests/App/Dashboard/KubernetesNetworkThroughputReload.test.tsx +902 -0
- package/Tests/App/Dashboard/KubernetesNetworkZoomBucketEdges.test.tsx +571 -0
- package/Tests/App/Dashboard/KubernetesOverviewZoomBucketEdges.test.tsx +697 -0
- package/Tests/App/Dashboard/KubernetesResourceMetricsTabZoom.test.tsx +633 -0
- package/Tests/App/Dashboard/LogsExplorerTimeRangeZoom.test.tsx +470 -0
- package/Tests/App/Dashboard/LogsInsightsTimeRangeZoom.test.tsx +948 -0
- package/Tests/App/Dashboard/LogsViewerPinnedWindowZoom.test.tsx +486 -0
- package/Tests/App/Dashboard/MetricChartsBucketInspectorDoubleClick.test.tsx +1001 -0
- package/Tests/App/Dashboard/MetricExplorerInvestigationAutoRefresh.test.tsx +514 -0
- package/Tests/App/Dashboard/MetricExplorerSavedViewZoomRoundTrip.test.tsx +581 -0
- package/Tests/App/Dashboard/MetricExplorerTimeRangeZoom.test.tsx +1162 -0
- package/Tests/App/Dashboard/MetricMonitorPreviewZoomLabel.test.ts +72 -0
- package/Tests/App/Dashboard/MetricRowSparklineTimeRangeZoom.test.tsx +204 -0
- package/Tests/App/Dashboard/MetricViewFirstResultLands.test.tsx +408 -0
- package/Tests/App/Dashboard/MetricViewOwnZoomResetPlacement.test.tsx +952 -0
- package/Tests/App/Dashboard/MetricViewTimeRange.test.ts +139 -0
- package/Tests/App/Dashboard/MetricViewTimeRangeZoom.test.tsx +556 -0
- package/Tests/App/Dashboard/MonitorMetricsChartWrappersZoom.test.tsx +552 -0
- package/Tests/App/Dashboard/MonitorMetricsTabsTimeRangeZoom.test.tsx +740 -0
- package/Tests/App/Dashboard/MonitorOverviewChartsTimeRangeZoom.test.tsx +735 -0
- package/Tests/App/Dashboard/MonitorStepPreviewsTimeRangeZoom.test.tsx +835 -0
- package/Tests/App/Dashboard/NetworkDeviceTimeRangeZoom.test.tsx +1115 -0
- package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +186 -17
- package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +155 -46
- package/Tests/App/Dashboard/PasskeySettings.test.tsx +87 -2
- package/Tests/App/Dashboard/ProjectAISettingsPages.test.tsx +884 -0
- package/Tests/App/Dashboard/ProxmoxNodeRemove.test.tsx +621 -0
- package/Tests/App/Dashboard/ProxmoxOverviewSlowLoads.test.tsx +584 -0
- package/Tests/App/Dashboard/ProxmoxPagesTimeRangeZoom.test.tsx +823 -0
- package/Tests/App/Dashboard/ProxmoxRateChartReload.test.tsx +1253 -0
- package/Tests/App/Dashboard/PublicDashboardBoardWideZoom.test.tsx +605 -0
- package/Tests/App/Dashboard/RealRowsLineChart.tsx +250 -0
- package/Tests/App/Dashboard/RechartsHostsPlotCursor.test.tsx +513 -0
- package/Tests/App/Dashboard/RowActionPlacementOverrides.test.tsx +865 -0
- package/Tests/App/Dashboard/RumOverviewTimeRangeZoom.test.tsx +877 -0
- package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +20 -4
- package/Tests/App/Dashboard/RunnerSupersededByAiAgentNote.test.tsx +296 -0
- package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +146 -36
- package/Tests/App/Dashboard/ServiceDatabaseSlowLoads.test.tsx +1166 -0
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +1243 -104
- package/Tests/App/Dashboard/ServiceOverviewTimeRangeZoom.test.tsx +792 -0
- package/Tests/App/Dashboard/SloBudgetBurnDownZoom.test.tsx +771 -0
- package/Tests/App/Dashboard/SloBudgetBurnDownZoomAffordances.test.tsx +380 -0
- package/Tests/App/Dashboard/SloEventMetricsBarChartZoom.test.tsx +369 -0
- package/Tests/App/Dashboard/SloHistoryChartsTimeRangeZoom.test.tsx +694 -0
- package/Tests/App/Dashboard/SloHistoryChartsZoomAffordances.test.tsx +497 -0
- package/Tests/App/Dashboard/SloMetricsTimeRangeZoom.test.tsx +541 -0
- package/Tests/App/Dashboard/SlowLoadHarness.ts +123 -0
- package/Tests/App/Dashboard/SnapshotExplorerPrimaryZoom.test.tsx +1194 -0
- package/Tests/App/Dashboard/SnapshotViewerPinnedWindow.test.tsx +620 -0
- package/Tests/App/Dashboard/TelemetrySnapshotPanelTimeRangeZoom.test.tsx +765 -0
- package/Tests/App/Dashboard/TelemetrySnapshotZoom.test.tsx +516 -0
- package/Tests/App/Dashboard/TelemetrySnapshotZoomAcrossTabs.test.tsx +827 -0
- package/Tests/App/Dashboard/TimeRangeZoomPageHarness.tsx +463 -0
- package/Tests/App/Dashboard/TraceChartEditorStatusSplit.test.tsx +543 -0
- package/Tests/App/Dashboard/TraceStatusPresentation.test.tsx +967 -0
- package/Tests/App/Dashboard/TraceTimelineMinimapZoom.test.tsx +315 -0
- package/Tests/App/Dashboard/TracesAnalyticsStatusSplit.test.tsx +778 -0
- package/Tests/App/Dashboard/TracesAnalyticsViewZoom.test.tsx +349 -0
- package/Tests/App/Dashboard/TracesExplorerTimeRangeZoom.test.tsx +551 -0
- package/Tests/App/Dashboard/TracesStatusChartAndFacet.test.tsx +687 -0
- package/Tests/App/Dashboard/TracesViewerStaleResponses.test.tsx +458 -0
- package/Tests/App/Dashboard/UptimeStripsExcludedFromZoom.test.tsx +406 -0
- package/Tests/App/Dashboard/VMwareOverviewSlowLoads.test.tsx +535 -0
- package/Tests/App/Dashboard/VMwarePagesTimeRangeZoom.test.tsx +606 -0
- package/Tests/App/Dashboard/ValueWidgetSparklineStaleHover.test.tsx +1292 -0
- package/Tests/App/Dashboard/ValueWidgetSparklineZoom.test.tsx +765 -0
- package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +98 -0
- package/Tests/Models/DatabaseModels/KubernetesAiAgentModel.test.ts +295 -0
- package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +347 -0
- package/Tests/Server/API/AIChatRouteAuthorization.test.ts +58 -0
- package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +1 -1
- package/Tests/Server/API/AIInvestigationAPI.test.ts +123 -45
- package/Tests/Server/API/AutoRemediationApproveClusterRound.test.ts +1329 -0
- package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +25 -2
- package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +2 -0
- package/Tests/Server/API/KubernetesClusterAiAccessAPIAgent.test.ts +903 -0
- package/Tests/Server/API/MicrosoftTeamsChatsRefreshRoute.test.ts +667 -0
- package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +294 -6
- package/Tests/Server/API/ProxmoxResourceAPIRemoveNode.test.ts +1932 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +195 -0
- package/Tests/Server/Infrastructure/Postgres/AddKubernetesAiAgentAndAiDefaultsMigration.test.ts +623 -0
- package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +87 -10
- package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleScopeMigration.test.ts +622 -0
- package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +61 -4
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +98 -2
- package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +105 -2
- package/Tests/Server/Services/AIServiceBalanceBlocker.test.ts +282 -0
- package/Tests/Server/Services/AlertMonitorClearWritesNull.test.ts +409 -0
- package/Tests/Server/Services/AlertMonitorEditFeed.test.ts +474 -0
- package/Tests/Server/Services/AlertMonitorEditGuard.test.ts +610 -0
- package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +2 -0
- package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +2 -1
- package/Tests/Server/Services/IncidentAffectedMonitorsEdit.test.ts +1360 -0
- package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +3 -1
- package/Tests/Server/Services/IncidentSeverityDeletePausesComplianceRules.test.ts +488 -0
- package/Tests/Server/Services/KubernetesAiAgentJobService.test.ts +329 -0
- package/Tests/Server/Services/KubernetesAiAgentServiceHelpers.test.ts +618 -0
- package/Tests/Server/Services/KubernetesAiAgentServiceLifecycle.test.ts +1444 -0
- package/Tests/Server/Services/KubernetesAiAgentServiceRegister.test.ts +1666 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +140 -111
- package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +267 -105
- package/Tests/Server/Services/KubernetesClusterAiAccessLegacySuperseded.test.ts +440 -0
- package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +567 -411
- package/Tests/Server/Services/KubernetesClusterAiAccessTarget.test.ts +1168 -0
- package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +410 -12
- package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +55 -0
- package/Tests/Server/Services/ProjectServiceNewProjectAiDefaults.test.ts +288 -0
- package/Tests/Server/Services/ProxmoxResourcePostgres.test.ts +5633 -0
- package/Tests/Server/Services/ProxmoxResourceService.test.ts +6458 -5
- package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +48 -15
- package/Tests/Server/Services/RunnerJobEnqueueKubectlAiAgent.test.ts +620 -0
- package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +170 -3
- package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +72 -0
- package/Tests/Server/Services/RunnerServiceAgentName.test.ts +6 -4
- package/Tests/Server/Services/ScheduledMaintenanceAttachmentEdit.test.ts +2113 -0
- package/Tests/Server/Services/ScheduledMaintenanceMonitorRelease.test.ts +972 -0
- package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +2237 -0
- package/Tests/Server/Services/TeamComplianceSettingService.test.ts +3988 -138
- package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +107 -1
- package/Tests/Server/Utils/AI/IncidentPostmortemRunner.test.ts +351 -0
- package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +2 -0
- package/Tests/Server/Utils/AI/InvestigationEligibility.test.ts +310 -0
- package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +70 -0
- package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +58 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +125 -1
- package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +252 -3
- package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +2 -0
- package/Tests/Server/Utils/AI/RemediationExecutionRunnerProjectGates.test.ts +240 -0
- package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +2 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +2 -0
- package/Tests/Server/Utils/Telemetry/HostAssetAttributesFromOtlp.test.ts +68 -0
- package/Tests/Server/Utils/Telemetry/InventoryMirrorPostgres.test.ts +370 -0
- package/Tests/Server/Utils/Telemetry/NetworkDeviceInventoryAttributes.test.ts +522 -0
- package/Tests/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.test.ts +155 -0
- package/Tests/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.test.ts +8631 -0
- package/Tests/Server/Utils/Telemetry/ProxmoxNativePush.test.ts +2852 -0
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +367 -1
- package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +45 -3
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +25 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNames.test.ts +3119 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNamesPostgres.test.ts +1721 -0
- package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +581 -113
- package/Tests/Types/Kubernetes/KubernetesAiAgentAccessContract.test.ts +510 -0
- package/Tests/Types/Team/ComplianceRule.test.ts +1029 -0
- package/Tests/Types/Team/TeamComplianceStatus.test.ts +176 -0
- package/Tests/UI/Components/ActionButton/RowActions.test.tsx +641 -0
- package/Tests/UI/Components/ActionButton/RowActionsAccessibility.test.tsx +414 -0
- package/Tests/UI/Components/ActionButton/SplitActionButtons.test.ts +565 -0
- package/Tests/UI/Components/Charts/BarChartDragToSelect.test.tsx +386 -0
- package/Tests/UI/Components/Charts/ChartDoubleClickResetWithoutDblclick.test.tsx +1025 -0
- package/Tests/UI/Components/Charts/ChartGroupPageZoomHint.test.tsx +196 -0
- package/Tests/UI/Components/Charts/ChartPlaceholdersTakeDoubleClick.test.tsx +1861 -0
- package/Tests/UI/Components/Charts/ChartPlotClicksWhileResetArmed.test.tsx +382 -0
- package/Tests/UI/Components/Charts/ChartRangeSelection.test.tsx +704 -0
- package/Tests/UI/Components/Charts/ChartRangeSelectionDoubleClickReset.test.tsx +1481 -0
- package/Tests/UI/Components/Charts/ChartRangeSelectionRealRecharts.test.tsx +918 -0
- package/Tests/UI/Components/Charts/ChartRowBucketStart.test.ts +350 -0
- package/Tests/UI/Components/Charts/DoubleClickReset.test.tsx +996 -0
- package/Tests/UI/Components/Charts/HistogramDoubleClickResetRealRecharts.test.tsx +965 -0
- package/Tests/UI/Components/Charts/HistogramDriftThenRefetch.test.tsx +277 -0
- package/Tests/UI/Components/Charts/HistogramRangeSelectionDataChange.test.tsx +1757 -0
- package/Tests/UI/Components/Charts/HistogramRangeSelectionRealRecharts.test.tsx +560 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/ChartPageTimeRangeZoom.test.tsx +513 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.test.tsx +845 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.test.tsx +119 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHintTailwind.test.tsx +274 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.test.ts +433 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.test.tsx +863 -0
- package/Tests/UI/Components/Charts/TimeRangeZoom/ZoomDocsPromises.test.tsx +1147 -0
- package/Tests/UI/Components/DisabledButtonTooltip.test.tsx +14 -3
- package/Tests/UI/Components/EntityDropdownEscape.test.tsx +454 -0
- package/Tests/UI/Components/EntityDropdownFocusLeave.test.tsx +420 -0
- package/Tests/UI/Components/Forms/FormFieldDropdownDisabled.test.tsx +233 -0
- package/Tests/UI/Components/Input.test.tsx +80 -1
- package/Tests/UI/Components/LogsAnalyticsViewZoom.test.tsx +947 -0
- package/Tests/UI/Components/LogsHistogramDragTooltip.test.tsx +17 -5
- package/Tests/UI/Components/LogsViewerAnalyticsZoom.test.tsx +501 -0
- package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +1 -1
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +6 -4
- package/Tests/UI/Components/ModelTable/BaseModelTablePermissionGating.test.tsx +60 -8
- package/Tests/UI/Components/ModelTable/BaseModelTableRowActions.test.tsx +476 -0
- package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +7 -5
- package/Tests/UI/Components/MoreMenuPortal.test.tsx +623 -0
- package/Tests/UI/Components/StatusPage/ResourceGridActions.test.tsx +538 -0
- package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +14 -4
- package/Tests/UI/Components/TableCellWrapping.test.tsx +3 -3
- package/Tests/UI/Components/TableRowActions.test.tsx +472 -0
- package/Tests/UI/Components/TelemetryHistogramDragTooltip.test.tsx +1 -1
- package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +1 -1
- package/Tests/UI/Components/TelemetryViewer/TelemetryViewerZoomScope.test.tsx +470 -0
- package/Tests/UI/Components/TelemetryViewer/UseViewerTimeRangeZoom.test.tsx +571 -0
- package/Tests/UI/Components/TelemetryViewer/ViewerDoubleClickResetUnderDataChange.test.tsx +928 -0
- package/Tests/UI/Components/TelemetryViewer/ViewerFollowsEnclosingZoom.test.tsx +1569 -0
- package/Tests/UI/Components/Toggle.test.tsx +66 -0
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardRefreshNames.test.tsx +1023 -0
- package/Tests/UI/Rum/ChunkLoader.test.ts +201 -0
- package/Tests/UI/Rum/FidelityNotices.test.ts +54 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +72 -0
- package/Tests/UI/Rum/ReplayFrameCapture.test.ts +1009 -9
- package/Tests/UI/Rum/ReplayRecordedAssets.test.ts +1069 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +913 -2
- package/Tests/UI/Rum/SessionReplayTable.test.tsx +443 -8
- package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +439 -9
- package/Tests/UI/Rum/UserFlowTablesRowActions.test.tsx +501 -0
- package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +47 -19
- package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +2 -0
- package/Tests/Utils/AiRemediation/KubernetesAiAgentPolicyCopyParity.test.ts +396 -0
- package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +104 -1
- package/Types/AI/AIRunType.ts +9 -4
- package/Types/AI/InvestigationNotStartedReason.ts +6 -0
- package/Types/AutoRemediation/AiRemediationCommandPlan.ts +14 -16
- package/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.ts +28 -0
- package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +4 -2
- package/Types/Kubernetes/KubernetesClusterAiAccess.ts +263 -32
- package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +20 -16
- package/Types/Kubernetes/KubernetesClusterAiInsights.ts +81 -0
- package/Types/Monitor/ProxmoxAlertTemplates.ts +4 -4
- package/Types/Team/ComplianceNotificationChannel.ts +22 -0
- package/Types/Team/ComplianceRule.ts +434 -0
- package/Types/Team/ComplianceRuleType.ts +21 -0
- package/Types/Team/TeamComplianceStatus.ts +81 -0
- package/Types/Telemetry/TelemetryIngestSurface.ts +28 -0
- package/UI/Components/ActionButton/ActionButtonSchema.ts +21 -0
- package/UI/Components/ActionButton/RowActions.tsx +238 -0
- package/UI/Components/ActionButton/SplitActionButtons.ts +173 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +32 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +40 -3
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +37 -11
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +80 -243
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +122 -31
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +95 -244
- package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +214 -0
- package/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.ts +32 -0
- package/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.ts +599 -0
- package/UI/Components/Charts/Line/LineChart.tsx +32 -2
- package/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.tsx +70 -0
- package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.tsx +226 -0
- package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.tsx +73 -0
- package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.ts +163 -0
- package/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.ts +215 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +20 -8
- package/UI/Components/Charts/Utils/XAxis.ts +75 -0
- package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +493 -59
- package/UI/Components/EntityDropdown/EntityDropdown.tsx +107 -11
- package/UI/Components/Forms/Fields/FormField.tsx +10 -0
- package/UI/Components/Input/Input.tsx +90 -57
- package/UI/Components/List/ListRow.tsx +8 -74
- package/UI/Components/LogsViewer/LogsViewer.tsx +40 -11
- package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +22 -6
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +209 -6
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +127 -84
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +3 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +12 -1
- package/UI/Components/MoreMenu/MoreMenu.tsx +365 -30
- package/UI/Components/MoreMenu/MoreMenuItem.tsx +70 -17
- package/UI/Components/OrderedStatesList/Item.tsx +7 -72
- package/UI/Components/StatusPage/ResourceGrid.tsx +126 -77
- package/UI/Components/Table/TableBody.tsx +8 -2
- package/UI/Components/Table/TableRow.tsx +20 -140
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +41 -10
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +47 -9
- package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +25 -6
- package/UI/Components/TelemetryViewer/types.ts +2 -0
- package/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.ts +448 -0
- package/UI/Components/Toggle/Toggle.tsx +26 -0
- package/UI/Components/Tooltip/Tooltip.tsx +8 -1
- package/UI/Styles/Theme.css +14 -2
- package/Utils/AiRemediation/CommandPolicy.ts +1 -1
- package/Utils/AiRemediation/KubectlPolicy.ts +1 -1
- package/Utils/Dashboard/DashboardTimeRangeZoom.ts +46 -2
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js +385 -0
- package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js.map +1 -0
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +3 -3
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +56 -5
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ProxmoxResource.js +51 -1
- package/build/dist/Models/DatabaseModels/ProxmoxResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RunnerJob.js +74 -0
- package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js +134 -3
- package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +6 -1
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +17 -10
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/API/AutoRemediationAPI.js +144 -24
- package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
- package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +436 -22
- package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +69 -13
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/ProxmoxResourceAPI.js +187 -14
- package/build/dist/Server/API/ProxmoxResourceAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/GlobalCache.js +27 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js +50 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js +84 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/TelemetryIngest.js +34 -19
- package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
- package/build/dist/Server/Services/AIService.js +59 -1
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +248 -13
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/AlertSeverityService.js +51 -15
- package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleService.js +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +199 -72
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentSeverityService.js +51 -15
- package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesAiAgentJobService.js +158 -0
- package/build/dist/Server/Services/KubernetesAiAgentJobService.js.map +1 -0
- package/build/dist/Server/Services/KubernetesAiAgentService.js +1145 -0
- package/build/dist/Server/Services/KubernetesAiAgentService.js.map +1 -0
- package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +842 -371
- package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +86 -17
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +22 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxResourceService.js +258 -3
- package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
- package/build/dist/Server/Services/RunnerJobService.js +178 -92
- package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
- package/build/dist/Server/Services/RunnerService.js +1 -1
- package/build/dist/Server/Services/RunnerService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +535 -3
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +316 -46
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/TeamComplianceSettingService.js +833 -7
- package/build/dist/Server/Services/TeamComplianceSettingService.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +52 -9
- package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +13 -9
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -1
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +7 -3
- package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +29 -15
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +56 -20
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +39 -5
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js +30 -3
- package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js +13 -2
- package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js +1 -1
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +4 -2
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +111 -6
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +40 -4
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js +296 -0
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js +699 -0
- package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +77 -19
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +505 -42
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/AI/AIRunType.js +9 -4
- package/build/dist/Types/AI/AIRunType.js.map +1 -1
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +10 -16
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js +28 -0
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js.map +1 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +72 -16
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +20 -16
- package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js +17 -0
- package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js.map +1 -0
- package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +4 -4
- package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
- package/build/dist/Types/Team/ComplianceNotificationChannel.js +23 -0
- package/build/dist/Types/Team/ComplianceNotificationChannel.js.map +1 -0
- package/build/dist/Types/Team/ComplianceRule.js +304 -0
- package/build/dist/Types/Team/ComplianceRule.js.map +1 -0
- package/build/dist/Types/Team/ComplianceRuleType.js +21 -0
- package/build/dist/Types/Team/ComplianceRuleType.js.map +1 -1
- package/build/dist/Types/Team/TeamComplianceStatus.js +2 -0
- package/build/dist/Types/Team/TeamComplianceStatus.js.map +1 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +25 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
- package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js +20 -1
- package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js.map +1 -1
- package/build/dist/UI/Components/ActionButton/RowActions.js +113 -0
- package/build/dist/UI/Components/ActionButton/RowActions.js.map +1 -0
- package/build/dist/UI/Components/ActionButton/SplitActionButtons.js +71 -0
- package/build/dist/UI/Components/ActionButton/SplitActionButtons.js.map +1 -0
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +16 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +16 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +27 -5
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +61 -194
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +84 -26
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +75 -196
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +133 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js +26 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js.map +1 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js +371 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +16 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js +35 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js +107 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js +41 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js +117 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js.map +1 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js +121 -0
- package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js.map +1 -0
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -7
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/XAxis.js +70 -0
- package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +305 -38
- package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -1
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +89 -14
- package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -2
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +69 -54
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/List/ListRow.js +4 -39
- package/build/dist/UI/Components/List/ListRow.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +31 -8
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +10 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +139 -21
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +30 -8
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +10 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js +253 -14
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js +43 -13
- package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js.map +1 -1
- package/build/dist/UI/Components/OrderedStatesList/Item.js +2 -38
- package/build/dist/UI/Components/OrderedStatesList/Item.js.map +1 -1
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js +53 -15
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
- package/build/dist/UI/Components/Table/TableBody.js +6 -2
- package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +12 -75
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +32 -7
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +27 -9
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +11 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js +272 -0
- package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js.map +1 -0
- package/build/dist/UI/Components/Toggle/Toggle.js +14 -1
- package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
- package/build/dist/Utils/AiRemediation/CommandPolicy.js +1 -1
- package/build/dist/Utils/AiRemediation/CommandPolicy.js.map +1 -1
- package/build/dist/Utils/AiRemediation/KubectlPolicy.js +1 -1
- package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -1
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +27 -1
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -1
- 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
|
+
});
|