@oneuptime/common 14.0.8 → 14.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (976) hide show
  1. package/Models/DatabaseModels/AutoRemediationSuggestion.ts +60 -0
  2. package/Models/DatabaseModels/CephCluster.ts +213 -0
  3. package/Models/DatabaseModels/DatabaseServer.ts +213 -0
  4. package/Models/DatabaseModels/DockerHost.ts +213 -0
  5. package/Models/DatabaseModels/DockerSwarmCluster.ts +213 -0
  6. package/Models/DatabaseModels/GlobalConfig.ts +1 -1
  7. package/Models/DatabaseModels/Host.ts +213 -0
  8. package/Models/DatabaseModels/Index.ts +4 -0
  9. package/Models/DatabaseModels/KubernetesAiAgent.ts +364 -0
  10. package/Models/DatabaseModels/KubernetesCluster.ts +3 -3
  11. package/Models/DatabaseModels/PodmanHost.ts +213 -0
  12. package/Models/DatabaseModels/Project.ts +55 -5
  13. package/Models/DatabaseModels/ProxmoxCluster.ts +213 -0
  14. package/Models/DatabaseModels/ProxmoxResource.ts +49 -1
  15. package/Models/DatabaseModels/ResourceAiAgent.ts +419 -0
  16. package/Models/DatabaseModels/RunnerJob.ts +219 -0
  17. package/Models/DatabaseModels/TeamComplianceSetting.ts +143 -2
  18. package/Models/DatabaseModels/VMwareVCenter.ts +213 -0
  19. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +17 -0
  20. package/Server/API/AIChatAPI.ts +6 -3
  21. package/Server/API/AIInvestigationAPI.ts +17 -10
  22. package/Server/API/AutoRemediationAPI.ts +598 -25
  23. package/Server/API/KubernetesClusterAiAccessAPI.ts +623 -21
  24. package/Server/API/MicrosoftTeamsAPI.ts +93 -14
  25. package/Server/API/ProxmoxResourceAPI.ts +244 -14
  26. package/Server/API/ResourceAiAccessAPI.ts +1669 -0
  27. package/Server/Infrastructure/GlobalCache.ts +32 -0
  28. package/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.ts +99 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.ts +137 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.ts +42 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.ts +423 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  33. package/Server/Infrastructure/Semaphore.ts +22 -0
  34. package/Server/Middleware/TelemetryIngest.ts +51 -18
  35. package/Server/Services/AIService.ts +70 -1
  36. package/Server/Services/AlertService.ts +352 -11
  37. package/Server/Services/AlertSeverityService.ts +65 -15
  38. package/Server/Services/AnalyticsDatabaseService.ts +45 -1
  39. package/Server/Services/AutoRemediationRuleEngineService.ts +940 -112
  40. package/Server/Services/AutoRemediationRuleService.ts +1 -1
  41. package/Server/Services/CephClusterService.ts +109 -1
  42. package/Server/Services/DatabaseServerService.ts +116 -1
  43. package/Server/Services/DockerHostService.ts +109 -1
  44. package/Server/Services/DockerSwarmClusterService.ts +113 -1
  45. package/Server/Services/HostService.ts +126 -4
  46. package/Server/Services/IncidentService.ts +280 -104
  47. package/Server/Services/IncidentSeverityService.ts +66 -15
  48. package/Server/Services/KubernetesAiAgentJobService.ts +185 -0
  49. package/Server/Services/KubernetesAiAgentService.ts +1634 -0
  50. package/Server/Services/KubernetesClusterAiAccessService.ts +1118 -451
  51. package/Server/Services/KubernetesClusterService.ts +129 -21
  52. package/Server/Services/MetricRecordingRuleService.ts +47 -0
  53. package/Server/Services/PodmanHostService.ts +109 -1
  54. package/Server/Services/ProjectService.ts +32 -0
  55. package/Server/Services/ProxmoxClusterService.ts +110 -1
  56. package/Server/Services/ProxmoxResourceService.ts +322 -5
  57. package/Server/Services/ResourceAiAccessService.ts +1439 -0
  58. package/Server/Services/ResourceAiAgentJobService.ts +202 -0
  59. package/Server/Services/ResourceAiAgentService.ts +2432 -0
  60. package/Server/Services/RunnerJobService.ts +852 -108
  61. package/Server/Services/RunnerService.ts +1 -1
  62. package/Server/Services/ScheduledMaintenanceService.ts +737 -1
  63. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +414 -56
  64. package/Server/Services/TeamComplianceSettingService.ts +1125 -9
  65. package/Server/Services/TelemetryUsageBillingService.ts +28 -0
  66. package/Server/Services/TraceRecordingRuleService.ts +33 -0
  67. package/Server/Services/VMwareVCenterService.ts +110 -1
  68. package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +67 -9
  69. package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +39 -10
  70. package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +14 -3
  71. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +1475 -51
  72. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +1000 -43
  73. package/Server/Utils/AI/Remediation/RemediationPlanRunner.ts +25 -1
  74. package/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.ts +577 -0
  75. package/Server/Utils/AI/ResourceAccess/ResourceAccessContext.ts +271 -0
  76. package/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.ts +45 -0
  77. package/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.ts +964 -0
  78. package/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.ts +333 -0
  79. package/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.ts +873 -0
  80. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +120 -7
  81. package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +72 -5
  82. package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +72 -5
  83. package/Server/Utils/AI/SRE/IncidentPostmortemRunner.ts +40 -8
  84. package/Server/Utils/AI/SRE/InvestigationEligibility.ts +13 -2
  85. package/Server/Utils/AI/SRE/InvestigationGrader.ts +1 -1
  86. package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +402 -14
  87. package/Server/Utils/AutoRemediation/RemediationVerifier.ts +35 -2
  88. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +9 -1
  89. package/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.ts +920 -0
  90. package/Server/Utils/SessionReplay/SessionReplayUsage.ts +84 -6
  91. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +133 -7
  92. package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +58 -4
  93. package/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.ts +447 -0
  94. package/Server/Utils/Telemetry/ProxmoxNativePush.ts +903 -0
  95. package/Server/Utils/Telemetry/TelemetryEntity.ts +87 -27
  96. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +35 -15
  97. package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +33 -15
  98. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +21 -1
  99. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +298 -224
  100. package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +35 -15
  101. package/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.ts +389 -175
  102. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +1073 -213
  103. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.ts +163 -0
  104. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.ts +396 -0
  105. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.ts +418 -0
  106. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.ts +152 -0
  107. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.ts +251 -0
  108. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.ts +214 -0
  109. package/Tests/App/AdminDashboard/RowActionPlacementOverrides.test.tsx +278 -0
  110. package/Tests/App/Dashboard/AIChatChartWidgetZoom.test.tsx +738 -0
  111. package/Tests/App/Dashboard/AIFeaturesSettingsPointers.test.tsx +147 -0
  112. package/Tests/App/Dashboard/AIInsightsKubernetesClusterPage.test.tsx +1523 -0
  113. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +463 -3
  114. package/Tests/App/Dashboard/AffectedResourcesPickerDropdownPlacement.test.tsx +958 -0
  115. package/Tests/App/Dashboard/AffectedResourcesPickerFocusLeave.test.tsx +595 -0
  116. package/Tests/App/Dashboard/AskAiAvailability.test.ts +20 -3
  117. package/Tests/App/Dashboard/AskAiDisabled.test.tsx +8 -3
  118. package/Tests/App/Dashboard/CephRateChartReload.test.tsx +1456 -0
  119. package/Tests/App/Dashboard/CephTimeRangeZoom.test.tsx +1229 -0
  120. package/Tests/App/Dashboard/ChartCardTimeRangeZoom.test.tsx +151 -0
  121. package/Tests/App/Dashboard/ChartCardZoomAffordances.test.tsx +257 -0
  122. package/Tests/App/Dashboard/ChartRefetchFrame.test.tsx +141 -0
  123. package/Tests/App/Dashboard/ChartZoomHarness.tsx +249 -0
  124. package/Tests/App/Dashboard/ChartZoomStandIn.tsx +131 -0
  125. package/Tests/App/Dashboard/CloudServerlessSlowLoads.test.tsx +628 -0
  126. package/Tests/App/Dashboard/CloudServerlessTimeRangeZoom.test.tsx +607 -0
  127. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +184 -19
  128. package/Tests/App/Dashboard/CompanionMetricsTabZoom.test.tsx +632 -0
  129. package/Tests/App/Dashboard/ContainerChartZoomStandIn.tsx +195 -0
  130. package/Tests/App/Dashboard/ContainerHostOverviewSlowLoads.test.tsx +542 -0
  131. package/Tests/App/Dashboard/ContainerHostOverviewTimeRangeZoom.test.tsx +848 -0
  132. package/Tests/App/Dashboard/ContainerMetricsTimeRangeZoom.test.tsx +561 -0
  133. package/Tests/App/Dashboard/DashboardBoardWideZoomShell.test.tsx +1147 -0
  134. package/Tests/App/Dashboard/DashboardEditPermissions.test.tsx +3 -2
  135. package/Tests/App/Dashboard/DashboardHistogramWidgetDragCursor.test.tsx +399 -0
  136. package/Tests/App/Dashboard/DashboardHistogramWidgetZoom.test.tsx +985 -0
  137. package/Tests/App/Dashboard/DashboardHistogramWidgetZoomHint.test.tsx +803 -0
  138. package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +1 -0
  139. package/Tests/App/Dashboard/DashboardSettingsPreviewZoom.test.tsx +679 -0
  140. package/Tests/App/Dashboard/DashboardSloAndDataSourceChartZoom.test.tsx +881 -0
  141. package/Tests/App/Dashboard/DashboardTraceStatusWidgets.test.tsx +878 -0
  142. package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +18 -6
  143. package/Tests/App/Dashboard/DatabaseMetricChartModalTimeRangeZoom.test.tsx +442 -0
  144. package/Tests/App/Dashboard/DatabaseOverviewTimeRangeZoom.test.tsx +831 -0
  145. package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +43 -0
  146. package/Tests/App/Dashboard/DockerSwarmInsightsTimeRangeZoom.test.tsx +524 -0
  147. package/Tests/App/Dashboard/DockerSwarmRateChartRefresh.test.tsx +259 -0
  148. package/Tests/App/Dashboard/EdgeDetailPanelHistory.test.tsx +763 -0
  149. package/Tests/App/Dashboard/EdgeDetailPanelTimeRangeZoom.test.tsx +547 -0
  150. package/Tests/App/Dashboard/EdgeHistoryBucketsCases.tsx +565 -0
  151. package/Tests/App/Dashboard/EdgeHistoryBucketsKathmandu.test.tsx +73 -0
  152. package/Tests/App/Dashboard/EdgeHistoryBucketsKolkata.test.tsx +74 -0
  153. package/Tests/App/Dashboard/EdgeHistoryBucketsNewYork.test.tsx +72 -0
  154. package/Tests/App/Dashboard/EdgeHistoryBucketsUtc.test.tsx +72 -0
  155. package/Tests/App/Dashboard/EmbeddedMetricCardRefresh.test.tsx +310 -0
  156. package/Tests/App/Dashboard/EmbeddedMetricCardTimeRangeZoom.test.tsx +536 -0
  157. package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +3 -1
  158. package/Tests/App/Dashboard/EscalationRuleRowActions.test.tsx +660 -0
  159. package/Tests/App/Dashboard/EventNotes.test.tsx +2 -2
  160. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +587 -13
  161. package/Tests/App/Dashboard/EventOverviewSnapshotExplorerZoom.test.tsx +1139 -0
  162. package/Tests/App/Dashboard/EventOverviewSnapshotZoomAcrossTabs.test.tsx +1057 -0
  163. package/Tests/App/Dashboard/EventOverviewTelemetrySnapshotZoom.test.tsx +757 -0
  164. package/Tests/App/Dashboard/ExceptionOccurrenceTrendZoom.test.tsx +675 -0
  165. package/Tests/App/Dashboard/ExceptionOccurrencesZoomSurvivesViewSwitch.test.tsx +405 -0
  166. package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +67 -3
  167. package/Tests/App/Dashboard/ExceptionTablesSpanStatus.test.tsx +515 -0
  168. package/Tests/App/Dashboard/ExplorerOwnZoomRegression.test.tsx +739 -0
  169. package/Tests/App/Dashboard/FlowBandwidthAxisCases.tsx +461 -0
  170. package/Tests/App/Dashboard/FlowBandwidthAxisKathmandu.test.tsx +25 -0
  171. package/Tests/App/Dashboard/FlowBandwidthAxisKolkata.test.tsx +25 -0
  172. package/Tests/App/Dashboard/FlowBandwidthAxisNewYork.test.tsx +25 -0
  173. package/Tests/App/Dashboard/FlowBandwidthAxisUtc.test.tsx +24 -0
  174. package/Tests/App/Dashboard/FlowTopTalkersZoomRefetch.test.tsx +722 -0
  175. package/Tests/App/Dashboard/FlowViewportGuard.test.tsx +2526 -0
  176. package/Tests/App/Dashboard/HostPagesSlowLoads.test.tsx +872 -0
  177. package/Tests/App/Dashboard/HostPagesTimeRangeZoom.test.tsx +1208 -0
  178. package/Tests/App/Dashboard/IncidentRootCauseTimeRangeZoom.test.tsx +358 -0
  179. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +100 -9
  180. package/Tests/App/Dashboard/InfrastructureMetricsTabTimeRangeZoom.test.tsx +485 -0
  181. package/Tests/App/Dashboard/InvestigationDrawerCompanionZoom.test.tsx +655 -0
  182. package/Tests/App/Dashboard/InvestigationDrawerZoom.test.tsx +830 -0
  183. package/Tests/App/Dashboard/InvestigationInfrastructureTools.test.tsx +506 -0
  184. package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +338 -2
  185. package/Tests/App/Dashboard/InvoicesPayInvoice.test.tsx +454 -16
  186. package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +799 -819
  187. package/Tests/App/Dashboard/KubernetesClusterAiAgentOverviewCard.test.tsx +367 -0
  188. package/Tests/App/Dashboard/KubernetesClusterAiAgentStatus.test.ts +1202 -0
  189. package/Tests/App/Dashboard/KubernetesClusterAiDocumentationMarkdown.test.ts +106 -0
  190. package/Tests/App/Dashboard/KubernetesClusterAiNavigation.test.tsx +354 -0
  191. package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +1884 -2054
  192. package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +478 -179
  193. package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +50 -0
  194. package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +2 -1
  195. package/Tests/App/Dashboard/KubernetesClusterOverviewZoom.test.tsx +1217 -0
  196. package/Tests/App/Dashboard/KubernetesClusterPagesZoom.test.tsx +731 -0
  197. package/Tests/App/Dashboard/KubernetesCostsLoadRecovery.test.tsx +869 -0
  198. package/Tests/App/Dashboard/KubernetesCostsZoom.test.tsx +1214 -0
  199. package/Tests/App/Dashboard/KubernetesCostsZoomExactHours.test.tsx +692 -0
  200. package/Tests/App/Dashboard/KubernetesNetworkThroughputReload.test.tsx +902 -0
  201. package/Tests/App/Dashboard/KubernetesNetworkZoomBucketEdges.test.tsx +571 -0
  202. package/Tests/App/Dashboard/KubernetesOverviewZoomBucketEdges.test.tsx +697 -0
  203. package/Tests/App/Dashboard/KubernetesResourceMetricsTabZoom.test.tsx +633 -0
  204. package/Tests/App/Dashboard/LogsExplorerTimeRangeZoom.test.tsx +470 -0
  205. package/Tests/App/Dashboard/LogsInsightsTimeRangeZoom.test.tsx +948 -0
  206. package/Tests/App/Dashboard/LogsViewerPinnedWindowZoom.test.tsx +486 -0
  207. package/Tests/App/Dashboard/MetricChartsBucketInspectorDoubleClick.test.tsx +1001 -0
  208. package/Tests/App/Dashboard/MetricExplorerInvestigationAutoRefresh.test.tsx +514 -0
  209. package/Tests/App/Dashboard/MetricExplorerSavedViewZoomRoundTrip.test.tsx +581 -0
  210. package/Tests/App/Dashboard/MetricExplorerTimeRangeZoom.test.tsx +1162 -0
  211. package/Tests/App/Dashboard/MetricMonitorPreviewZoomLabel.test.ts +72 -0
  212. package/Tests/App/Dashboard/MetricRowSparklineTimeRangeZoom.test.tsx +204 -0
  213. package/Tests/App/Dashboard/MetricViewFirstResultLands.test.tsx +408 -0
  214. package/Tests/App/Dashboard/MetricViewOwnZoomResetPlacement.test.tsx +952 -0
  215. package/Tests/App/Dashboard/MetricViewTimeRange.test.ts +139 -0
  216. package/Tests/App/Dashboard/MetricViewTimeRangeZoom.test.tsx +556 -0
  217. package/Tests/App/Dashboard/MonitorMetricsChartWrappersZoom.test.tsx +552 -0
  218. package/Tests/App/Dashboard/MonitorMetricsTabsTimeRangeZoom.test.tsx +740 -0
  219. package/Tests/App/Dashboard/MonitorOverviewChartsTimeRangeZoom.test.tsx +735 -0
  220. package/Tests/App/Dashboard/MonitorStepPreviewsTimeRangeZoom.test.tsx +835 -0
  221. package/Tests/App/Dashboard/NetworkDeviceTimeRangeZoom.test.tsx +1115 -0
  222. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +186 -17
  223. package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +155 -46
  224. package/Tests/App/Dashboard/PasskeySettings.test.tsx +87 -2
  225. package/Tests/App/Dashboard/ProjectAISettingsPages.test.tsx +884 -0
  226. package/Tests/App/Dashboard/ProxmoxNodeRemove.test.tsx +621 -0
  227. package/Tests/App/Dashboard/ProxmoxOverviewSlowLoads.test.tsx +584 -0
  228. package/Tests/App/Dashboard/ProxmoxPagesTimeRangeZoom.test.tsx +823 -0
  229. package/Tests/App/Dashboard/ProxmoxRateChartReload.test.tsx +1253 -0
  230. package/Tests/App/Dashboard/PublicDashboardBoardWideZoom.test.tsx +605 -0
  231. package/Tests/App/Dashboard/RealRowsLineChart.tsx +250 -0
  232. package/Tests/App/Dashboard/RechartsHostsPlotCursor.test.tsx +513 -0
  233. package/Tests/App/Dashboard/RemediationSuggestionCardDescription.test.tsx +209 -0
  234. package/Tests/App/Dashboard/RemediationSuggestionCardResource.test.tsx +317 -0
  235. package/Tests/App/Dashboard/ResourceAiAccessSettingsUtil.test.ts +921 -0
  236. package/Tests/App/Dashboard/ResourceAiAgentInstall.test.ts +860 -0
  237. package/Tests/App/Dashboard/ResourceAiAgentPage.test.tsx +1721 -0
  238. package/Tests/App/Dashboard/ResourceAiAgentStatus.test.ts +1002 -0
  239. package/Tests/App/Dashboard/ResourceAiInsightsPage.test.tsx +931 -0
  240. package/Tests/App/Dashboard/ResourceAiNavigation.test.tsx +575 -0
  241. package/Tests/App/Dashboard/RowActionPlacementOverrides.test.tsx +865 -0
  242. package/Tests/App/Dashboard/RumOverviewTimeRangeZoom.test.tsx +877 -0
  243. package/Tests/App/Dashboard/RunbookStepTypeMaps.test.ts +38 -7
  244. package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +20 -4
  245. package/Tests/App/Dashboard/RunnerSupersededByAiAgentNote.test.tsx +296 -0
  246. package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +146 -36
  247. package/Tests/App/Dashboard/ServiceDatabaseSlowLoads.test.tsx +1166 -0
  248. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +1243 -104
  249. package/Tests/App/Dashboard/ServiceOverviewTimeRangeZoom.test.tsx +792 -0
  250. package/Tests/App/Dashboard/SloBudgetBurnDownZoom.test.tsx +771 -0
  251. package/Tests/App/Dashboard/SloBudgetBurnDownZoomAffordances.test.tsx +380 -0
  252. package/Tests/App/Dashboard/SloEventMetricsBarChartZoom.test.tsx +369 -0
  253. package/Tests/App/Dashboard/SloHistoryChartsTimeRangeZoom.test.tsx +694 -0
  254. package/Tests/App/Dashboard/SloHistoryChartsZoomAffordances.test.tsx +497 -0
  255. package/Tests/App/Dashboard/SloMetricsTimeRangeZoom.test.tsx +541 -0
  256. package/Tests/App/Dashboard/SlowLoadHarness.ts +123 -0
  257. package/Tests/App/Dashboard/SnapshotExplorerPrimaryZoom.test.tsx +1194 -0
  258. package/Tests/App/Dashboard/SnapshotViewerPinnedWindow.test.tsx +620 -0
  259. package/Tests/App/Dashboard/TelemetrySnapshotPanelTimeRangeZoom.test.tsx +765 -0
  260. package/Tests/App/Dashboard/TelemetrySnapshotZoom.test.tsx +516 -0
  261. package/Tests/App/Dashboard/TelemetrySnapshotZoomAcrossTabs.test.tsx +827 -0
  262. package/Tests/App/Dashboard/TimeRangeZoomPageHarness.tsx +463 -0
  263. package/Tests/App/Dashboard/TraceChartEditorStatusSplit.test.tsx +543 -0
  264. package/Tests/App/Dashboard/TraceStatusPresentation.test.tsx +967 -0
  265. package/Tests/App/Dashboard/TraceTimelineMinimapZoom.test.tsx +315 -0
  266. package/Tests/App/Dashboard/TracesAnalyticsStatusSplit.test.tsx +778 -0
  267. package/Tests/App/Dashboard/TracesAnalyticsViewZoom.test.tsx +349 -0
  268. package/Tests/App/Dashboard/TracesExplorerTimeRangeZoom.test.tsx +551 -0
  269. package/Tests/App/Dashboard/TracesStatusChartAndFacet.test.tsx +687 -0
  270. package/Tests/App/Dashboard/TracesViewerStaleResponses.test.tsx +458 -0
  271. package/Tests/App/Dashboard/UptimeStripsExcludedFromZoom.test.tsx +406 -0
  272. package/Tests/App/Dashboard/VMwareOverviewSlowLoads.test.tsx +535 -0
  273. package/Tests/App/Dashboard/VMwarePagesTimeRangeZoom.test.tsx +606 -0
  274. package/Tests/App/Dashboard/ValueWidgetSparklineStaleHover.test.tsx +1292 -0
  275. package/Tests/App/Dashboard/ValueWidgetSparklineZoom.test.tsx +765 -0
  276. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +98 -0
  277. package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +33 -1
  278. package/Tests/Models/DatabaseModels/KubernetesAiAgentModel.test.ts +295 -0
  279. package/Tests/Models/DatabaseModels/ResourceAiAccessColumns.test.ts +738 -0
  280. package/Tests/Models/DatabaseModels/ResourceAiAgentModel.test.ts +842 -0
  281. package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +347 -0
  282. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +58 -0
  283. package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +1 -1
  284. package/Tests/Server/API/AIInvestigationAPI.test.ts +123 -45
  285. package/Tests/Server/API/AutoRemediationApproveClusterRound.test.ts +1329 -0
  286. package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +25 -2
  287. package/Tests/Server/API/AutoRemediationApproveResourceRound.test.ts +835 -0
  288. package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +2 -0
  289. package/Tests/Server/API/KubernetesClusterAiAccessAPIAgent.test.ts +903 -0
  290. package/Tests/Server/API/MicrosoftTeamsChatsRefreshRoute.test.ts +667 -0
  291. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +294 -6
  292. package/Tests/Server/API/ProxmoxResourceAPIRemoveNode.test.ts +1932 -0
  293. package/Tests/Server/API/ResourceAiAccessAPI.test.ts +2730 -0
  294. package/Tests/Server/Infrastructure/GlobalCache.test.ts +195 -0
  295. package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +60 -0
  296. package/Tests/Server/Infrastructure/Postgres/AddKubernetesAiAgentAndAiDefaultsMigration.test.ts +623 -0
  297. package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +87 -10
  298. package/Tests/Server/Infrastructure/Postgres/AddResourceAiAgentsMigration.test.ts +659 -0
  299. package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleScopeMigration.test.ts +622 -0
  300. package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +31 -0
  301. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +119 -4
  302. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +196 -2
  303. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +123 -2
  304. package/Tests/Server/Services/AIServiceBalanceBlocker.test.ts +282 -0
  305. package/Tests/Server/Services/AlertMonitorClearWritesNull.test.ts +409 -0
  306. package/Tests/Server/Services/AlertMonitorEditFeed.test.ts +474 -0
  307. package/Tests/Server/Services/AlertMonitorEditGuard.test.ts +610 -0
  308. package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +2 -0
  309. package/Tests/Server/Services/AutoRemediationResourceRuleEngine.test.ts +1279 -0
  310. package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +2 -1
  311. package/Tests/Server/Services/DatabaseServerService.test.ts +55 -0
  312. package/Tests/Server/Services/GroupTelemetryUsageExcludeNames.test.ts +299 -0
  313. package/Tests/Server/Services/HostServiceFindOrCreateMemo.test.ts +44 -0
  314. package/Tests/Server/Services/IncidentAffectedMonitorsEdit.test.ts +1360 -0
  315. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +3 -1
  316. package/Tests/Server/Services/IncidentSeverityDeletePausesComplianceRules.test.ts +488 -0
  317. package/Tests/Server/Services/KubernetesAiAgentJobService.test.ts +329 -0
  318. package/Tests/Server/Services/KubernetesAiAgentServiceHelpers.test.ts +618 -0
  319. package/Tests/Server/Services/KubernetesAiAgentServiceLifecycle.test.ts +1444 -0
  320. package/Tests/Server/Services/KubernetesAiAgentServiceRegister.test.ts +1666 -0
  321. package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +140 -111
  322. package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +267 -105
  323. package/Tests/Server/Services/KubernetesClusterAiAccessLegacySuperseded.test.ts +440 -0
  324. package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +567 -411
  325. package/Tests/Server/Services/KubernetesClusterAiAccessTarget.test.ts +1168 -0
  326. package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +410 -12
  327. package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +55 -0
  328. package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +45 -6
  329. package/Tests/Server/Services/ProjectServiceNewProjectAiDefaults.test.ts +288 -0
  330. package/Tests/Server/Services/ProxmoxResourcePostgres.test.ts +5633 -0
  331. package/Tests/Server/Services/ProxmoxResourceService.test.ts +6458 -5
  332. package/Tests/Server/Services/RecordingRuleReservedMetricName.test.ts +171 -0
  333. package/Tests/Server/Services/ResourceAiAccessChangeAuthorization.test.ts +256 -0
  334. package/Tests/Server/Services/ResourceAiAccessService.test.ts +1373 -0
  335. package/Tests/Server/Services/ResourceAiAgentJobService.test.ts +375 -0
  336. package/Tests/Server/Services/ResourceAiAgentServiceHelpers.test.ts +1114 -0
  337. package/Tests/Server/Services/ResourceAiAgentServiceLifecycle.test.ts +1050 -0
  338. package/Tests/Server/Services/ResourceAiAgentServiceRegister.test.ts +2251 -0
  339. package/Tests/Server/Services/ResourceAiSettingsCreate.test.ts +373 -0
  340. package/Tests/Server/Services/ResourceAiSettingsPermission.test.ts +1042 -0
  341. package/Tests/Server/Services/ResourceServiceDeleteCleansUpAi.test.ts +319 -0
  342. package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +153 -15
  343. package/Tests/Server/Services/RunnerJobEnqueueKubectlAiAgent.test.ts +620 -0
  344. package/Tests/Server/Services/RunnerJobEnqueueResourceCommand.test.ts +986 -0
  345. package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +170 -3
  346. package/Tests/Server/Services/RunnerJobResourceCommandLane.test.ts +211 -0
  347. package/Tests/Server/Services/RunnerJobResourceCommandTimeoutAndRedaction.test.ts +352 -0
  348. package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +72 -0
  349. package/Tests/Server/Services/RunnerServiceAgentName.test.ts +6 -4
  350. package/Tests/Server/Services/ScheduledMaintenanceAttachmentEdit.test.ts +2113 -0
  351. package/Tests/Server/Services/ScheduledMaintenanceMonitorRelease.test.ts +972 -0
  352. package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +2237 -0
  353. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +3988 -138
  354. package/Tests/Server/Services/TelemetryUsageBillingSloExclusion.test.ts +218 -0
  355. package/Tests/Server/TestingUtils/Services/FakeRunnerJobCount.ts +145 -0
  356. package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +107 -1
  357. package/Tests/Server/Utils/AI/IncidentPostmortemRunner.test.ts +351 -0
  358. package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +2 -0
  359. package/Tests/Server/Utils/AI/InvestigationEligibility.test.ts +310 -0
  360. package/Tests/Server/Utils/AI/InvestigationInfrastructureAccessWiring.test.ts +453 -0
  361. package/Tests/Server/Utils/AI/InvestigationInfrastructureReport.test.ts +721 -0
  362. package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +70 -0
  363. package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +58 -0
  364. package/Tests/Server/Utils/AI/RemediationCommandTools.test.ts +94 -1
  365. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +2 -0
  366. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +2 -0
  367. package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +2 -0
  368. package/Tests/Server/Utils/AI/RemediationCommandToolsResource.test.ts +1530 -0
  369. package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +125 -1
  370. package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +252 -3
  371. package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +2 -0
  372. package/Tests/Server/Utils/AI/RemediationExecutionRunnerProjectGates.test.ts +240 -0
  373. package/Tests/Server/Utils/AI/RemediationExecutionRunnerResourceMode.test.ts +1249 -0
  374. package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +2 -0
  375. package/Tests/Server/Utils/AI/RemediationPlanRunner.test.ts +117 -0
  376. package/Tests/Server/Utils/AI/RemediationResourceCopyParity.test.ts +463 -0
  377. package/Tests/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.test.ts +722 -0
  378. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAccessContext.test.ts +266 -0
  379. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.test.ts +1220 -0
  380. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.test.ts +569 -0
  381. package/Tests/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.test.ts +830 -0
  382. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +2 -0
  383. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorResource.test.ts +717 -0
  384. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +2 -0
  385. package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +2 -0
  386. package/Tests/Server/Utils/AutoRemediation/RemediationVerifierResourceFollowUp.test.ts +338 -0
  387. package/Tests/Server/Utils/Monitor/Criteria/SessionReplayBudgetTemplateCriteria.test.ts +422 -0
  388. package/Tests/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.test.ts +1647 -0
  389. package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +143 -1
  390. package/Tests/Server/Utils/Telemetry/HostAssetAttributesFromOtlp.test.ts +68 -0
  391. package/Tests/Server/Utils/Telemetry/InventoryMirrorPostgres.test.ts +370 -0
  392. package/Tests/Server/Utils/Telemetry/NetworkDeviceInventoryAttributes.test.ts +522 -0
  393. package/Tests/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.test.ts +155 -0
  394. package/Tests/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.test.ts +8631 -0
  395. package/Tests/Server/Utils/Telemetry/ProxmoxNativePush.test.ts +2852 -0
  396. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +367 -1
  397. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +75 -3
  398. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsAccountNotLinked.test.ts +1093 -0
  399. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.test.ts +1049 -0
  400. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.test.ts +1676 -0
  401. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCards.test.ts +2884 -0
  402. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.test.ts +2490 -0
  403. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmit.test.ts +2293 -0
  404. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmitServerTimezone.test.ts +386 -0
  405. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.test.ts +1477 -0
  406. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.test.ts +1967 -0
  407. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsStripHtmlTags.test.ts +92 -0
  408. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsSubmittedFormRemoval.test.ts +1819 -0
  409. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.test.ts +1033 -0
  410. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezoneServerZone.test.ts +610 -0
  411. package/Tests/Server/Utils/Workspace/MicrosoftTeamsBotMessageHandling.test.ts +2610 -0
  412. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +25 -0
  413. package/Tests/Server/Utils/Workspace/MicrosoftTeamsCreateCommandsEndToEnd.test.ts +4672 -0
  414. package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNames.test.ts +3119 -0
  415. package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNamesPostgres.test.ts +1721 -0
  416. package/Tests/Server/Utils/Workspace/WorkspaceCreateProjectReferences.test.ts +190 -65
  417. package/Tests/Types/AutoRemediation/AiRemediationCommandPlan.test.ts +10 -4
  418. package/Tests/Types/AutoRemediation/AiRemediationCommandPlanResourceCommand.test.ts +305 -0
  419. package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +581 -113
  420. package/Tests/Types/Kubernetes/KubernetesAiAgentAccessContract.test.ts +510 -0
  421. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +277 -13
  422. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +113 -0
  423. package/Tests/Types/Monitor/RumAlertTemplates.test.ts +773 -2
  424. package/Tests/Types/ResourceAiAgent/AiResourceType.test.ts +419 -0
  425. package/Tests/Types/ResourceAiAgent/ResourceAiAccess.test.ts +517 -0
  426. package/Tests/Types/Runbook/RunbookStepType.test.ts +147 -8
  427. package/Tests/Types/Team/ComplianceRule.test.ts +1029 -0
  428. package/Tests/Types/Team/TeamComplianceStatus.test.ts +176 -0
  429. package/Tests/UI/Components/ActionButton/RowActions.test.tsx +641 -0
  430. package/Tests/UI/Components/ActionButton/RowActionsAccessibility.test.tsx +414 -0
  431. package/Tests/UI/Components/ActionButton/SplitActionButtons.test.ts +565 -0
  432. package/Tests/UI/Components/Charts/BarChartDragToSelect.test.tsx +386 -0
  433. package/Tests/UI/Components/Charts/ChartDoubleClickResetWithoutDblclick.test.tsx +1025 -0
  434. package/Tests/UI/Components/Charts/ChartGroupPageZoomHint.test.tsx +196 -0
  435. package/Tests/UI/Components/Charts/ChartPlaceholdersTakeDoubleClick.test.tsx +1861 -0
  436. package/Tests/UI/Components/Charts/ChartPlotClicksWhileResetArmed.test.tsx +382 -0
  437. package/Tests/UI/Components/Charts/ChartRangeSelection.test.tsx +704 -0
  438. package/Tests/UI/Components/Charts/ChartRangeSelectionDoubleClickReset.test.tsx +1481 -0
  439. package/Tests/UI/Components/Charts/ChartRangeSelectionRealRecharts.test.tsx +918 -0
  440. package/Tests/UI/Components/Charts/ChartRowBucketStart.test.ts +350 -0
  441. package/Tests/UI/Components/Charts/DoubleClickReset.test.tsx +996 -0
  442. package/Tests/UI/Components/Charts/HistogramDoubleClickResetRealRecharts.test.tsx +965 -0
  443. package/Tests/UI/Components/Charts/HistogramDriftThenRefetch.test.tsx +277 -0
  444. package/Tests/UI/Components/Charts/HistogramRangeSelectionDataChange.test.tsx +1757 -0
  445. package/Tests/UI/Components/Charts/HistogramRangeSelectionRealRecharts.test.tsx +560 -0
  446. package/Tests/UI/Components/Charts/TimeRangeZoom/ChartPageTimeRangeZoom.test.tsx +513 -0
  447. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.test.tsx +845 -0
  448. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.test.tsx +119 -0
  449. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHintTailwind.test.tsx +274 -0
  450. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.test.ts +433 -0
  451. package/Tests/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.test.tsx +863 -0
  452. package/Tests/UI/Components/Charts/TimeRangeZoom/ZoomDocsPromises.test.tsx +1147 -0
  453. package/Tests/UI/Components/DisabledButtonTooltip.test.tsx +14 -3
  454. package/Tests/UI/Components/EntityDropdownEscape.test.tsx +454 -0
  455. package/Tests/UI/Components/EntityDropdownFocusLeave.test.tsx +420 -0
  456. package/Tests/UI/Components/Forms/FormFieldDropdownDisabled.test.tsx +233 -0
  457. package/Tests/UI/Components/Input.test.tsx +80 -1
  458. package/Tests/UI/Components/LogsAnalyticsViewZoom.test.tsx +947 -0
  459. package/Tests/UI/Components/LogsHistogramDragTooltip.test.tsx +17 -5
  460. package/Tests/UI/Components/LogsViewerAnalyticsZoom.test.tsx +501 -0
  461. package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +1 -1
  462. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +6 -4
  463. package/Tests/UI/Components/ModelTable/BaseModelTablePermissionGating.test.tsx +60 -8
  464. package/Tests/UI/Components/ModelTable/BaseModelTableRowActions.test.tsx +476 -0
  465. package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +7 -5
  466. package/Tests/UI/Components/MoreMenuPortal.test.tsx +623 -0
  467. package/Tests/UI/Components/StatusPage/ResourceGridActions.test.tsx +538 -0
  468. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +14 -4
  469. package/Tests/UI/Components/TableCellWrapping.test.tsx +3 -3
  470. package/Tests/UI/Components/TableRowActions.test.tsx +472 -0
  471. package/Tests/UI/Components/TelemetryHistogramDragTooltip.test.tsx +1 -1
  472. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +1 -1
  473. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerZoomScope.test.tsx +470 -0
  474. package/Tests/UI/Components/TelemetryViewer/UseViewerTimeRangeZoom.test.tsx +571 -0
  475. package/Tests/UI/Components/TelemetryViewer/ViewerDoubleClickResetUnderDataChange.test.tsx +928 -0
  476. package/Tests/UI/Components/TelemetryViewer/ViewerFollowsEnclosingZoom.test.tsx +1569 -0
  477. package/Tests/UI/Components/Toggle.test.tsx +66 -0
  478. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardRefreshNames.test.tsx +1023 -0
  479. package/Tests/UI/Rum/ChunkLoader.test.ts +201 -0
  480. package/Tests/UI/Rum/FidelityNotices.test.ts +54 -0
  481. package/Tests/UI/Rum/RecordingHealthDashboard.test.tsx +769 -1
  482. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +72 -0
  483. package/Tests/UI/Rum/ReplayFrameCapture.test.ts +1009 -9
  484. package/Tests/UI/Rum/ReplayRecordedAssets.test.ts +1069 -0
  485. package/Tests/UI/Rum/ReplayStage.test.tsx +913 -2
  486. package/Tests/UI/Rum/SessionReplayTable.test.tsx +443 -8
  487. package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +439 -9
  488. package/Tests/UI/Rum/UserFlowTablesRowActions.test.tsx +501 -0
  489. package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +47 -19
  490. package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +2 -0
  491. package/Tests/Utils/AiRemediation/KubernetesAiAgentPolicyCopyParity.test.ts +396 -0
  492. package/Tests/Utils/AiRemediation/Resource/CephCommandPolicy.test.ts +1653 -0
  493. package/Tests/Utils/AiRemediation/Resource/CephOutputRedaction.test.ts +204 -0
  494. package/Tests/Utils/AiRemediation/Resource/DatabaseCommandPolicy.test.ts +1077 -0
  495. package/Tests/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.test.ts +558 -0
  496. package/Tests/Utils/AiRemediation/Resource/DatabaseQueryRedactor.test.ts +834 -0
  497. package/Tests/Utils/AiRemediation/Resource/DockerCliGrammar.test.ts +747 -0
  498. package/Tests/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.test.ts +1259 -0
  499. package/Tests/Utils/AiRemediation/Resource/DockerOutputRedaction.test.ts +386 -0
  500. package/Tests/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.test.ts +782 -0
  501. package/Tests/Utils/AiRemediation/Resource/GovcCommandPolicy.test.ts +2259 -0
  502. package/Tests/Utils/AiRemediation/Resource/HostCommandPolicy.test.ts +2019 -0
  503. package/Tests/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.test.ts +2497 -0
  504. package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicy.test.ts +1305 -0
  505. package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.test.ts +585 -0
  506. package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactor.test.ts +334 -0
  507. package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactorCopyParity.test.ts +98 -0
  508. package/Tests/Utils/AiRemediation/Resource/ResourcePolicyImportClosure.test.ts +391 -0
  509. package/Tests/Utils/AiRemediation/ResourceAiAgentPolicyCopyParity.test.ts +497 -0
  510. package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +104 -1
  511. package/Tests/Utils/SessionReplay/SessionReplayBudgetMetricType.test.ts +383 -0
  512. package/Types/AI/AIRunType.ts +9 -4
  513. package/Types/AI/InvestigationNotStartedReason.ts +6 -0
  514. package/Types/AI/ResourceAiAccessApi.ts +140 -0
  515. package/Types/AI/ResourceAiAccessPermissions.ts +126 -0
  516. package/Types/AutoRemediation/AiRemediationCommandPlan.ts +197 -18
  517. package/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.ts +28 -0
  518. package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +4 -2
  519. package/Types/Kubernetes/KubernetesClusterAiAccess.ts +263 -32
  520. package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +20 -16
  521. package/Types/Kubernetes/KubernetesClusterAiInsights.ts +81 -0
  522. package/Types/Monitor/ProxmoxAlertTemplates.ts +4 -4
  523. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +55 -22
  524. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +35 -3
  525. package/Types/Monitor/RumAlertTemplates.ts +351 -4
  526. package/Types/ResourceAiAgent/AiResourceType.ts +310 -0
  527. package/Types/ResourceAiAgent/ResourceAiAccess.ts +574 -0
  528. package/Types/Rum/SessionReplayBudgetMetricType.ts +47 -0
  529. package/Types/Runbook/RunbookStepType.ts +29 -0
  530. package/Types/Team/ComplianceNotificationChannel.ts +22 -0
  531. package/Types/Team/ComplianceRule.ts +434 -0
  532. package/Types/Team/ComplianceRuleType.ts +21 -0
  533. package/Types/Team/TeamComplianceStatus.ts +81 -0
  534. package/Types/Telemetry/TelemetryIngestSurface.ts +40 -0
  535. package/UI/Components/ActionButton/ActionButtonSchema.ts +21 -0
  536. package/UI/Components/ActionButton/RowActions.tsx +238 -0
  537. package/UI/Components/ActionButton/SplitActionButtons.ts +173 -0
  538. package/UI/Components/Charts/Area/AreaChart.tsx +32 -2
  539. package/UI/Components/Charts/Bar/BarChart.tsx +40 -3
  540. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +37 -11
  541. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +80 -243
  542. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +122 -31
  543. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +95 -244
  544. package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +214 -0
  545. package/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.ts +32 -0
  546. package/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.ts +599 -0
  547. package/UI/Components/Charts/Line/LineChart.tsx +32 -2
  548. package/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.tsx +70 -0
  549. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.tsx +226 -0
  550. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.tsx +73 -0
  551. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.ts +163 -0
  552. package/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.ts +215 -0
  553. package/UI/Components/Charts/Utils/DataPoint.ts +20 -8
  554. package/UI/Components/Charts/Utils/XAxis.ts +75 -0
  555. package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +493 -59
  556. package/UI/Components/EntityDropdown/EntityDropdown.tsx +107 -11
  557. package/UI/Components/Forms/Fields/FormField.tsx +10 -0
  558. package/UI/Components/Input/Input.tsx +90 -57
  559. package/UI/Components/List/ListRow.tsx +8 -74
  560. package/UI/Components/LogsViewer/LogsViewer.tsx +40 -11
  561. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +22 -6
  562. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +209 -6
  563. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +127 -84
  564. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +3 -0
  565. package/UI/Components/ModelTable/BaseModelTable.tsx +12 -1
  566. package/UI/Components/MoreMenu/MoreMenu.tsx +365 -30
  567. package/UI/Components/MoreMenu/MoreMenuItem.tsx +70 -17
  568. package/UI/Components/OrderedStatesList/Item.tsx +7 -72
  569. package/UI/Components/StatusPage/ResourceGrid.tsx +126 -77
  570. package/UI/Components/Table/TableBody.tsx +8 -2
  571. package/UI/Components/Table/TableRow.tsx +20 -140
  572. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +41 -10
  573. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +47 -9
  574. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +25 -6
  575. package/UI/Components/TelemetryViewer/types.ts +2 -0
  576. package/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.ts +448 -0
  577. package/UI/Components/Toggle/Toggle.tsx +26 -0
  578. package/UI/Components/Tooltip/Tooltip.tsx +8 -1
  579. package/UI/Styles/Theme.css +14 -2
  580. package/Utils/AI/InvestigationReport.ts +121 -15
  581. package/Utils/AiRemediation/CommandPolicy.ts +1 -1
  582. package/Utils/AiRemediation/KubectlPolicy.ts +1 -1
  583. package/Utils/AiRemediation/Resource/CephCommandPolicy.ts +1936 -0
  584. package/Utils/AiRemediation/Resource/DatabaseCommandPolicy.ts +166 -0
  585. package/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.ts +1532 -0
  586. package/Utils/AiRemediation/Resource/DatabaseQueryRedactor.ts +1394 -0
  587. package/Utils/AiRemediation/Resource/DockerCliGrammar.ts +1839 -0
  588. package/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.ts +490 -0
  589. package/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.ts +687 -0
  590. package/Utils/AiRemediation/Resource/GovcCommandPolicy.ts +2096 -0
  591. package/Utils/AiRemediation/Resource/HostCommandPolicy.ts +2982 -0
  592. package/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.ts +1679 -0
  593. package/Utils/AiRemediation/Resource/ResourceCommandPolicy.ts +851 -0
  594. package/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.ts +593 -0
  595. package/Utils/AiRemediation/Resource/ResourceOutputRedactor.ts +2812 -0
  596. package/Utils/Dashboard/DashboardTimeRangeZoom.ts +46 -2
  597. package/Utils/SessionReplay/SessionReplayBudgetMetricType.ts +184 -0
  598. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js +62 -0
  599. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js.map +1 -1
  600. package/build/dist/Models/DatabaseModels/CephCluster.js +219 -0
  601. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  602. package/build/dist/Models/DatabaseModels/DatabaseServer.js +219 -0
  603. package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -1
  604. package/build/dist/Models/DatabaseModels/DockerHost.js +219 -0
  605. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  606. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js +219 -0
  607. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js.map +1 -1
  608. package/build/dist/Models/DatabaseModels/GlobalConfig.js +1 -1
  609. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  610. package/build/dist/Models/DatabaseModels/Host.js +219 -0
  611. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  612. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  613. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  614. package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js +385 -0
  615. package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js.map +1 -0
  616. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +3 -3
  617. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  618. package/build/dist/Models/DatabaseModels/PodmanHost.js +219 -0
  619. package/build/dist/Models/DatabaseModels/PodmanHost.js.map +1 -1
  620. package/build/dist/Models/DatabaseModels/Project.js +56 -5
  621. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  622. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +219 -0
  623. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  624. package/build/dist/Models/DatabaseModels/ProxmoxResource.js +51 -1
  625. package/build/dist/Models/DatabaseModels/ProxmoxResource.js.map +1 -1
  626. package/build/dist/Models/DatabaseModels/ResourceAiAgent.js +439 -0
  627. package/build/dist/Models/DatabaseModels/ResourceAiAgent.js.map +1 -0
  628. package/build/dist/Models/DatabaseModels/RunnerJob.js +219 -0
  629. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  630. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js +134 -3
  631. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js.map +1 -1
  632. package/build/dist/Models/DatabaseModels/VMwareVCenter.js +219 -0
  633. package/build/dist/Models/DatabaseModels/VMwareVCenter.js.map +1 -1
  634. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  635. package/build/dist/Server/API/AIChatAPI.js +6 -1
  636. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  637. package/build/dist/Server/API/AIInvestigationAPI.js +17 -10
  638. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  639. package/build/dist/Server/API/AutoRemediationAPI.js +374 -24
  640. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  641. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +436 -22
  642. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -1
  643. package/build/dist/Server/API/MicrosoftTeamsAPI.js +69 -13
  644. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  645. package/build/dist/Server/API/ProxmoxResourceAPI.js +187 -14
  646. package/build/dist/Server/API/ProxmoxResourceAPI.js.map +1 -1
  647. package/build/dist/Server/API/ResourceAiAccessAPI.js +1119 -0
  648. package/build/dist/Server/API/ResourceAiAccessAPI.js.map +1 -0
  649. package/build/dist/Server/Infrastructure/GlobalCache.js +27 -0
  650. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  651. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js +50 -0
  652. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js.map +1 -0
  653. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js +84 -0
  654. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js.map +1 -0
  655. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js +31 -0
  656. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js.map +1 -0
  657. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js +168 -0
  658. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js.map +1 -0
  659. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  660. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  661. package/build/dist/Server/Infrastructure/Semaphore.js +6 -0
  662. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  663. package/build/dist/Server/Middleware/TelemetryIngest.js +41 -19
  664. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  665. package/build/dist/Server/Services/AIService.js +59 -1
  666. package/build/dist/Server/Services/AIService.js.map +1 -1
  667. package/build/dist/Server/Services/AlertService.js +248 -13
  668. package/build/dist/Server/Services/AlertService.js.map +1 -1
  669. package/build/dist/Server/Services/AlertSeverityService.js +51 -15
  670. package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
  671. package/build/dist/Server/Services/AnalyticsDatabaseService.js +26 -1
  672. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  673. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +601 -3
  674. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
  675. package/build/dist/Server/Services/AutoRemediationRuleService.js +1 -1
  676. package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
  677. package/build/dist/Server/Services/CephClusterService.js +104 -0
  678. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  679. package/build/dist/Server/Services/DatabaseServerService.js +99 -0
  680. package/build/dist/Server/Services/DatabaseServerService.js.map +1 -1
  681. package/build/dist/Server/Services/DockerHostService.js +104 -0
  682. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  683. package/build/dist/Server/Services/DockerSwarmClusterService.js +104 -0
  684. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  685. package/build/dist/Server/Services/HostService.js +114 -2
  686. package/build/dist/Server/Services/HostService.js.map +1 -1
  687. package/build/dist/Server/Services/IncidentService.js +199 -72
  688. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  689. package/build/dist/Server/Services/IncidentSeverityService.js +51 -15
  690. package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
  691. package/build/dist/Server/Services/KubernetesAiAgentJobService.js +158 -0
  692. package/build/dist/Server/Services/KubernetesAiAgentJobService.js.map +1 -0
  693. package/build/dist/Server/Services/KubernetesAiAgentService.js +1145 -0
  694. package/build/dist/Server/Services/KubernetesAiAgentService.js.map +1 -0
  695. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +842 -371
  696. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -1
  697. package/build/dist/Server/Services/KubernetesClusterService.js +86 -17
  698. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  699. package/build/dist/Server/Services/MetricRecordingRuleService.js +46 -0
  700. package/build/dist/Server/Services/MetricRecordingRuleService.js.map +1 -1
  701. package/build/dist/Server/Services/PodmanHostService.js +104 -0
  702. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  703. package/build/dist/Server/Services/ProjectService.js +22 -0
  704. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  705. package/build/dist/Server/Services/ProxmoxClusterService.js +104 -0
  706. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  707. package/build/dist/Server/Services/ProxmoxResourceService.js +258 -3
  708. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  709. package/build/dist/Server/Services/ResourceAiAccessService.js +1028 -0
  710. package/build/dist/Server/Services/ResourceAiAccessService.js.map +1 -0
  711. package/build/dist/Server/Services/ResourceAiAgentJobService.js +173 -0
  712. package/build/dist/Server/Services/ResourceAiAgentJobService.js.map +1 -0
  713. package/build/dist/Server/Services/ResourceAiAgentService.js +1687 -0
  714. package/build/dist/Server/Services/ResourceAiAgentService.js.map +1 -0
  715. package/build/dist/Server/Services/RunnerJobService.js +522 -97
  716. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  717. package/build/dist/Server/Services/RunnerService.js +1 -1
  718. package/build/dist/Server/Services/RunnerService.js.map +1 -1
  719. package/build/dist/Server/Services/ScheduledMaintenanceService.js +535 -3
  720. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  721. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +316 -46
  722. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  723. package/build/dist/Server/Services/TeamComplianceSettingService.js +833 -7
  724. package/build/dist/Server/Services/TeamComplianceSettingService.js.map +1 -1
  725. package/build/dist/Server/Services/TelemetryUsageBillingService.js +26 -0
  726. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  727. package/build/dist/Server/Services/TraceRecordingRuleService.js +37 -0
  728. package/build/dist/Server/Services/TraceRecordingRuleService.js.map +1 -1
  729. package/build/dist/Server/Services/VMwareVCenterService.js +104 -0
  730. package/build/dist/Server/Services/VMwareVCenterService.js.map +1 -1
  731. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +52 -9
  732. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -1
  733. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +13 -9
  734. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -1
  735. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +7 -3
  736. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -1
  737. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +1056 -43
  738. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  739. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +683 -42
  740. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  741. package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js +21 -1
  742. package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js.map +1 -1
  743. package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js +330 -0
  744. package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js.map +1 -0
  745. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js +160 -0
  746. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js.map +1 -0
  747. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js +33 -0
  748. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js.map +1 -0
  749. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js +640 -0
  750. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js.map +1 -0
  751. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js +226 -0
  752. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js.map +1 -0
  753. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js +600 -0
  754. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js.map +1 -0
  755. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +89 -9
  756. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  757. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +55 -4
  758. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
  759. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +55 -4
  760. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
  761. package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js +30 -3
  762. package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js.map +1 -1
  763. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js +13 -2
  764. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js.map +1 -1
  765. package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js +1 -1
  766. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +294 -15
  767. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
  768. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js +29 -1
  769. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js.map +1 -1
  770. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +9 -1
  771. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  772. package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js +627 -0
  773. package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js.map +1 -0
  774. package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js +60 -5
  775. package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js.map +1 -1
  776. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +111 -6
  777. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  778. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +40 -4
  779. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
  780. package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js +296 -0
  781. package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js.map +1 -0
  782. package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js +699 -0
  783. package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js.map +1 -0
  784. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +77 -19
  785. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  786. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +19 -15
  787. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  788. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +19 -15
  789. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -1
  790. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +17 -1
  791. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  792. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +183 -209
  793. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  794. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +19 -15
  795. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
  796. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js +233 -167
  797. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js.map +1 -1
  798. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +768 -152
  799. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  800. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js +129 -0
  801. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js.map +1 -0
  802. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js +266 -0
  803. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js.map +1 -0
  804. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js +258 -0
  805. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js.map +1 -0
  806. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js +104 -0
  807. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js.map +1 -0
  808. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js +183 -0
  809. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js.map +1 -0
  810. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js +123 -0
  811. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js.map +1 -0
  812. package/build/dist/Types/AI/AIRunType.js +9 -4
  813. package/build/dist/Types/AI/AIRunType.js.map +1 -1
  814. package/build/dist/Types/AI/ResourceAiAccessApi.js +30 -0
  815. package/build/dist/Types/AI/ResourceAiAccessApi.js.map +1 -0
  816. package/build/dist/Types/AI/ResourceAiAccessPermissions.js +98 -0
  817. package/build/dist/Types/AI/ResourceAiAccessPermissions.js.map +1 -0
  818. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +126 -45
  819. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
  820. package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js +28 -0
  821. package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js.map +1 -0
  822. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +72 -16
  823. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -1
  824. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +20 -16
  825. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -1
  826. package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js +17 -0
  827. package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js.map +1 -0
  828. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +4 -4
  829. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
  830. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +47 -21
  831. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  832. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +5 -0
  833. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  834. package/build/dist/Types/Monitor/RumAlertTemplates.js +199 -3
  835. package/build/dist/Types/Monitor/RumAlertTemplates.js.map +1 -1
  836. package/build/dist/Types/ResourceAiAgent/AiResourceType.js +242 -0
  837. package/build/dist/Types/ResourceAiAgent/AiResourceType.js.map +1 -0
  838. package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js +275 -0
  839. package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js.map +1 -0
  840. package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js +45 -0
  841. package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js.map +1 -0
  842. package/build/dist/Types/Runbook/RunbookStepType.js +27 -0
  843. package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
  844. package/build/dist/Types/Team/ComplianceNotificationChannel.js +23 -0
  845. package/build/dist/Types/Team/ComplianceNotificationChannel.js.map +1 -0
  846. package/build/dist/Types/Team/ComplianceRule.js +304 -0
  847. package/build/dist/Types/Team/ComplianceRule.js.map +1 -0
  848. package/build/dist/Types/Team/ComplianceRuleType.js +21 -0
  849. package/build/dist/Types/Team/ComplianceRuleType.js.map +1 -1
  850. package/build/dist/Types/Team/TeamComplianceStatus.js +2 -0
  851. package/build/dist/Types/Team/TeamComplianceStatus.js.map +1 -0
  852. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +36 -0
  853. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
  854. package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js +20 -1
  855. package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js.map +1 -1
  856. package/build/dist/UI/Components/ActionButton/RowActions.js +113 -0
  857. package/build/dist/UI/Components/ActionButton/RowActions.js.map +1 -0
  858. package/build/dist/UI/Components/ActionButton/SplitActionButtons.js +71 -0
  859. package/build/dist/UI/Components/ActionButton/SplitActionButtons.js.map +1 -0
  860. package/build/dist/UI/Components/Charts/Area/AreaChart.js +16 -1
  861. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  862. package/build/dist/UI/Components/Charts/Bar/BarChart.js +16 -1
  863. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  864. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +27 -5
  865. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  866. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +61 -194
  867. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  868. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +84 -26
  869. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  870. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +75 -196
  871. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  872. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +133 -0
  873. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -1
  874. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js +26 -0
  875. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js.map +1 -0
  876. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js +371 -0
  877. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js.map +1 -0
  878. package/build/dist/UI/Components/Charts/Line/LineChart.js +16 -1
  879. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  880. package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js +35 -0
  881. package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js.map +1 -0
  882. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js +107 -0
  883. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js.map +1 -0
  884. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js +41 -0
  885. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js.map +1 -0
  886. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js +117 -0
  887. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js.map +1 -0
  888. package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js +121 -0
  889. package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js.map +1 -0
  890. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -7
  891. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  892. package/build/dist/UI/Components/Charts/Utils/XAxis.js +70 -0
  893. package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
  894. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +305 -38
  895. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -1
  896. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +89 -14
  897. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  898. package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -2
  899. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  900. package/build/dist/UI/Components/Input/Input.js +69 -54
  901. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  902. package/build/dist/UI/Components/List/ListRow.js +4 -39
  903. package/build/dist/UI/Components/List/ListRow.js.map +1 -1
  904. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +31 -8
  905. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  906. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +10 -1
  907. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
  908. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +139 -21
  909. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  910. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +30 -8
  911. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  912. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
  913. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  914. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +10 -0
  915. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  916. package/build/dist/UI/Components/MoreMenu/MoreMenu.js +253 -14
  917. package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
  918. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js +43 -13
  919. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js.map +1 -1
  920. package/build/dist/UI/Components/OrderedStatesList/Item.js +2 -38
  921. package/build/dist/UI/Components/OrderedStatesList/Item.js.map +1 -1
  922. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +53 -15
  923. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  924. package/build/dist/UI/Components/Table/TableBody.js +6 -2
  925. package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
  926. package/build/dist/UI/Components/Table/TableRow.js +12 -75
  927. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  928. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +32 -7
  929. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  930. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +27 -9
  931. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  932. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +11 -1
  933. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
  934. package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js +272 -0
  935. package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js.map +1 -0
  936. package/build/dist/UI/Components/Toggle/Toggle.js +14 -1
  937. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  938. package/build/dist/UI/Components/Tooltip/Tooltip.js +1 -1
  939. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  940. package/build/dist/Utils/AI/InvestigationReport.js +71 -11
  941. package/build/dist/Utils/AI/InvestigationReport.js.map +1 -1
  942. package/build/dist/Utils/AiRemediation/CommandPolicy.js +1 -1
  943. package/build/dist/Utils/AiRemediation/CommandPolicy.js.map +1 -1
  944. package/build/dist/Utils/AiRemediation/KubectlPolicy.js +1 -1
  945. package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -1
  946. package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js +1462 -0
  947. package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js.map +1 -0
  948. package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js +122 -0
  949. package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js.map +1 -0
  950. package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js +1139 -0
  951. package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js.map +1 -0
  952. package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js +1060 -0
  953. package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js.map +1 -0
  954. package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js +1301 -0
  955. package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js.map +1 -0
  956. package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js +370 -0
  957. package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js.map +1 -0
  958. package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js +498 -0
  959. package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js.map +1 -0
  960. package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js +1565 -0
  961. package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js.map +1 -0
  962. package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js +2206 -0
  963. package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js.map +1 -0
  964. package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js +1129 -0
  965. package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js.map +1 -0
  966. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js +565 -0
  967. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js.map +1 -0
  968. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js +408 -0
  969. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js.map +1 -0
  970. package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js +1910 -0
  971. package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js.map +1 -0
  972. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +27 -1
  973. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -1
  974. package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js +160 -0
  975. package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js.map +1 -0
  976. package/package.json +1 -1
@@ -0,0 +1,2610 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ jest,
7
+ test,
8
+ } from "@jest/globals";
9
+
10
+ /*
11
+ * Issue #4111: in Microsoft Teams, "create incident" and "create maintenance"
12
+ * answered "Sorry, I encountered an error processing your request. Please try
13
+ * again later.", and every such bubble showed up TWICE. The form was too large
14
+ * for Teams (413 MessageSizeTooBig), and handleBotMessageActivity replied and
15
+ * then RETHREW: the CloudAdapter (it has no onTurnError) answered Teams HTTP
16
+ * 500, and Teams delivered the same activity again, which failed and replied
17
+ * a second time. Any failure doubled that way, not only the 413.
18
+ *
19
+ * This file pins the message-level half of the fix, with a fake TurnContext:
20
+ *
21
+ * - handleBotMessageActivity never rejects. Whatever fails, the user gets
22
+ * exactly one reply, and it names the activity id, which the error log line
23
+ * names too, so an administrator can find what went wrong. That line names
24
+ * the command ("create incident", "ask", or "a 43-character question"),
25
+ * never the text, which can carry what does not belong in an error log; the
26
+ * error itself follows it, with its stack, unless it is a Bot Framework
27
+ * HTTP error, whose status and code the line already gives.
28
+ * - "create incident <title>" and "create maintenance <title>" go to
29
+ * MicrosoftTeamsCreateCommands with the title as the user typed it, and the
30
+ * help lists them that way; help, the "show ..." commands and "ask" are
31
+ * routed as before.
32
+ * - A redelivered activity (same conversation, same id) is dropped, also when
33
+ * Teams retries a slow first delivery that is still being handled: the id
34
+ * is claimed before the work, not after it. The claim is kept in this
35
+ * process's memory as well as in Redis, so a redelivery is dropped even if
36
+ * Redis came back since the first delivery, which it never heard of. A
37
+ * message the bot does not answer (no @mention in a channel or group chat)
38
+ * is ignored WITHOUT claiming its id.
39
+ * - "show ..." replies are kept within the Teams text budget, and name ten
40
+ * affected monitors per item, then how many more.
41
+ * - formatAffectedMonitorNames, getUnexpectedErrorMessage,
42
+ * recoverFromFailedTurn (the last line of defence processBotActivity puts
43
+ * around every turn) and the catch of handleBotInvokeActivity: one reply
44
+ * that says why, sent best-effort, never a throw; and the failure logged
45
+ * the same way as above, OneUptime's exceptions by their class.
46
+ *
47
+ * The real botbuilder stack (413s, HTTP statuses, Teams' redelivery) runs in
48
+ * MicrosoftTeamsCreateCommandsEndToEnd.test.ts. Here everything that touches
49
+ * the database is stubbed. The bot drops an activity id it has already
50
+ * handled for ten minutes, in this process's memory (Redis is stubbed out as
51
+ * not connected, see beforeEach), so every test sends activities with ids of
52
+ * its own.
53
+ */
54
+
55
+ jest.mock("../../../../Server/EnvironmentConfig", () => {
56
+ return {
57
+ ...(jest.requireActual("../../../../Server/EnvironmentConfig") as Record<
58
+ string,
59
+ unknown
60
+ >),
61
+ MicrosoftTeamsAppClientId: "11111111-2222-3333-4444-555555555555",
62
+ MicrosoftTeamsAppClientSecret: "test-secret",
63
+ MicrosoftTeamsAppTenantId: "test-tenant",
64
+ };
65
+ });
66
+
67
+ /*
68
+ * Same botbuilder module factory as MicrosoftTeamsTeamInstalls.test.ts: the
69
+ * repo-wide manual mock (Tests/__mocks__/botbuilder.js) does not expose
70
+ * TeamsInfo or MessageFactory.attachment, which MicrosoftTeams.ts uses.
71
+ */
72
+ jest.mock("botbuilder", () => {
73
+ return {
74
+ CloudAdapter: class CloudAdapter {},
75
+ ConfigurationBotFrameworkAuthentication: class ConfigurationBotFrameworkAuthentication {},
76
+ TeamsActivityHandler: class TeamsActivityHandler {},
77
+ TurnContext: class TurnContext {},
78
+ ActivityHandler: class ActivityHandler {},
79
+ MessageFactory: {
80
+ text: jest.fn(),
81
+ attachment: jest.fn((attachment: unknown) => {
82
+ return { type: "message", attachments: [attachment] };
83
+ }),
84
+ },
85
+ CardFactory: { heroCard: jest.fn() },
86
+ TeamsInfo: {
87
+ getMembers: jest.fn(),
88
+ getPagedMembers: jest.fn(),
89
+ },
90
+ };
91
+ });
92
+
93
+ import type { SpyInstance } from "jest-mock";
94
+ import MicrosoftTeamsUtil, {
95
+ MicrosoftTeamsTenantResolution,
96
+ } from "../../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams";
97
+ import MicrosoftTeamsActivityDeduplicator from "../../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator";
98
+ import MicrosoftTeamsCreateCommands from "../../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands";
99
+ import MicrosoftTeamsMessageSize, {
100
+ MICROSOFT_TEAMS_TEXT_MESSAGE_BUDGET_IN_BYTES,
101
+ } from "../../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize";
102
+ import MicrosoftTeamsAuthAction, {
103
+ MicrosoftTeamsAccountNotLinkedException,
104
+ } from "../../../../Server/Utils/Workspace/MicrosoftTeams/Actions/Auth";
105
+ import MicrosoftTeamsIncidentActions from "../../../../Server/Utils/Workspace/MicrosoftTeams/Actions/Incident";
106
+ import { MicrosoftTeamsIncidentActionType } from "../../../../Server/Utils/Workspace/MicrosoftTeams/Actions/ActionTypes";
107
+ import WorkspaceActionAuthorization from "../../../../Server/Utils/Workspace/WorkspaceActionAuthorization";
108
+ import DatabaseConfig from "../../../../Server/DatabaseConfig";
109
+ import GlobalCache from "../../../../Server/Infrastructure/GlobalCache";
110
+ import IncidentService from "../../../../Server/Services/IncidentService";
111
+ import IncidentStateService from "../../../../Server/Services/IncidentStateService";
112
+ import ScheduledMaintenanceService from "../../../../Server/Services/ScheduledMaintenanceService";
113
+ import WorkspaceProjectAuthTokenService from "../../../../Server/Services/WorkspaceProjectAuthTokenService";
114
+ import logger, { LogAttributes } from "../../../../Server/Utils/Logger";
115
+ import Incident from "../../../../Models/DatabaseModels/Incident";
116
+ import IncidentState from "../../../../Models/DatabaseModels/IncidentState";
117
+ import Monitor from "../../../../Models/DatabaseModels/Monitor";
118
+ import ScheduledMaintenance from "../../../../Models/DatabaseModels/ScheduledMaintenance";
119
+ import WorkspaceProjectAuthToken from "../../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
120
+ import URL from "../../../../Types/API/URL";
121
+ import DatabaseCommonInteractionProps from "../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
122
+ import BadDataException from "../../../../Types/Exception/BadDataException";
123
+ import DatabaseNotConnectedException from "../../../../Types/Exception/DatabaseNotConnectedException";
124
+ import NotAuthorizedException from "../../../../Types/Exception/NotAuthorizedException";
125
+ import { JSONObject } from "../../../../Types/JSON";
126
+ import ObjectID from "../../../../Types/ObjectID";
127
+ import type { Activity, ResourceResponse, TurnContext } from "botbuilder";
128
+
129
+ // ---- Fixtures ----
130
+
131
+ const BOT_ID: string = "28:0b8e3f5a-7c1d-4e2f-9a3b-5c6d7e8f9a0b";
132
+ const TENANT_ID: string = "8f3a2b1c-4111-4d5e-9f60-7a8b9c0d1e2f";
133
+ const USER_AAD_OBJECT_ID: string = "6f1c2b3a-4d5e-4f60-8a7b-9c0d1e2f3a4b";
134
+ const SERVICE_URL: string = "https://smba.trafficmanager.net/amer/";
135
+ const PERSONAL_CHAT_ID: string = "a:1pQ3dFxkGqKZ8Yv0cWm7bT2nR5sL9hJ4uE6oA";
136
+ const OTHER_PERSONAL_CHAT_ID: string = "a:1zZ9yY8xX7wW6vV5uU4tT3sS2rR1qQ0pP";
137
+ const GROUP_CHAT_ID: string = "19:4b1d7c9e0f2a4b6c8d0e2f4a6b8c0d2e@thread.v2";
138
+ const TEAM_ID: string = "19:team-engineering@thread.tacv2";
139
+ const CHANNEL_ID: string = "19:general-engineering@thread.tacv2";
140
+ const CHANNEL_THREAD_ID: string = `${CHANNEL_ID};messageid=1727712000000`;
141
+ const DASHBOARD_URL: string = "https://oneuptime.example.com/dashboard";
142
+
143
+ const PROJECT_ID: ObjectID = ObjectID.generate();
144
+ const ONEUPTIME_USER_ID: ObjectID = ObjectID.generate();
145
+ const INCIDENT_ID: ObjectID = ObjectID.generate();
146
+
147
+ // How the @mention of the bot appears in the text Teams delivers.
148
+ const BOT_MENTION_TEXT: string = "<at>OneUptime</at>";
149
+
150
+ // Another member of the chat, whom a message can @mention instead of the bot.
151
+ const OTHER_MEMBER_ID: string = "29:1Bq7-bob-oncall-teams-user";
152
+ const OTHER_MEMBER_MENTION_TEXT: string = "<at>Bob Oncall</at>";
153
+
154
+ // The note fitTextToBudget appends to a reply it had to shorten.
155
+ const SHORTENED_NOTE: string =
156
+ "…\n\n_This reply was shortened to fit in Microsoft Teams. Open OneUptime to see everything._";
157
+
158
+ const UNEXPECTED_ERROR_TEXT: string =
159
+ "Sorry, something went wrong in OneUptime while handling that message. Please try again in a minute.";
160
+
161
+ // What master answered to every failure, and then rethrew.
162
+ const MASTER_ERROR_TEXT: string =
163
+ "Sorry, I encountered an error processing your request. Please try again later.";
164
+
165
+ const ACTION_FAILED_TEXT: string =
166
+ "Sorry, that action failed. Please try again later.";
167
+
168
+ // ---- The private statics this file stubs or reads ----
169
+
170
+ type ShowReplyBuilder = (projectId: ObjectID) => Promise<string>;
171
+
172
+ type ShowReplyBuilderName =
173
+ | "getActiveIncidentsMessage"
174
+ | "getScheduledMaintenanceMessage"
175
+ | "getOngoingMaintenanceMessage"
176
+ | "getActiveAlertsMessage";
177
+
178
+ const SHOW_REPLY_BUILDER_NAMES: Array<ShowReplyBuilderName> = [
179
+ "getActiveIncidentsMessage",
180
+ "getScheduledMaintenanceMessage",
181
+ "getOngoingMaintenanceMessage",
182
+ "getActiveAlertsMessage",
183
+ ];
184
+
185
+ // What each stubbed "show ..." builder answers, unless a test says otherwise.
186
+ const CANNED_SHOW_REPLIES: Record<ShowReplyBuilderName, string> = {
187
+ getActiveIncidentsMessage: "✅ No active incidents.",
188
+ getScheduledMaintenanceMessage: "✅ No scheduled maintenance events.",
189
+ getOngoingMaintenanceMessage: "✅ No ongoing maintenance events.",
190
+ getActiveAlertsMessage: "✅ No active alerts.",
191
+ };
192
+
193
+ /*
194
+ * Private statics of MicrosoftTeamsUtil, reached through a cast the way
195
+ * neighbouring suites reach captureChatFromBotActivity.
196
+ */
197
+ interface MicrosoftTeamsUtilInternals {
198
+ captureChatFromBotActivity: (data: {
199
+ activity: JSONObject;
200
+ turnContext: TurnContext;
201
+ onlyIfMissingOrStale?: boolean | undefined;
202
+ }) => Promise<void>;
203
+ answerObservabilityQuestion: (data: {
204
+ activity: JSONObject;
205
+ turnContext: TurnContext;
206
+ projectId: ObjectID;
207
+ question: string;
208
+ }) => Promise<void>;
209
+ getHelpMessage: () => string;
210
+ getActiveIncidentsMessage: ShowReplyBuilder;
211
+ getScheduledMaintenanceMessage: ShowReplyBuilder;
212
+ getOngoingMaintenanceMessage: ShowReplyBuilder;
213
+ getActiveAlertsMessage: ShowReplyBuilder;
214
+ }
215
+
216
+ const internals: MicrosoftTeamsUtilInternals =
217
+ MicrosoftTeamsUtil as unknown as MicrosoftTeamsUtilInternals;
218
+
219
+ // The answer to "help", as the bot builds it.
220
+ const HELP_TEXT: string = internals.getHelpMessage();
221
+
222
+ type CreateCommandInput = Parameters<
223
+ typeof MicrosoftTeamsCreateCommands.handleCreateIncidentCommand
224
+ >[0];
225
+
226
+ type InvokeHandlerInput = Parameters<
227
+ typeof MicrosoftTeamsUtil.handleBotInvokeActivity
228
+ >[0];
229
+
230
+ interface Stubs {
231
+ resolveTenant: SpyInstance<
232
+ typeof MicrosoftTeamsUtil.resolveProjectByTenantId
233
+ >;
234
+ // A pass-through spy: the real in-process dedupe runs.
235
+ claim: SpyInstance<typeof MicrosoftTeamsActivityDeduplicator.claim>;
236
+ // The dedupe's Redis claim: refused (not connected) unless a test says so.
237
+ redisClaim: SpyInstance<typeof GlobalCache.setStringIfNotExists>;
238
+ captureChat: SpyInstance<
239
+ MicrosoftTeamsUtilInternals["captureChatFromBotActivity"]
240
+ >;
241
+ captureTeam: SpyInstance<
242
+ typeof MicrosoftTeamsUtil.captureTeamFromBotActivity
243
+ >;
244
+ createIncident: SpyInstance<
245
+ typeof MicrosoftTeamsCreateCommands.handleCreateIncidentCommand
246
+ >;
247
+ createMaintenance: SpyInstance<
248
+ typeof MicrosoftTeamsCreateCommands.handleCreateScheduledMaintenanceCommand
249
+ >;
250
+ askAssistant: SpyInstance<
251
+ MicrosoftTeamsUtilInternals["answerObservabilityQuestion"]
252
+ >;
253
+ showReplies: Record<ShowReplyBuilderName, SpyInstance<ShowReplyBuilder>>;
254
+ }
255
+
256
+ let stubs: Stubs;
257
+
258
+ function tenantResolvedToProject(): MicrosoftTeamsTenantResolution {
259
+ return {
260
+ projectAuth: {
261
+ projectId: PROJECT_ID,
262
+ } as unknown as WorkspaceProjectAuthToken,
263
+ isAmbiguous: false,
264
+ candidateProjectIds: [PROJECT_ID],
265
+ };
266
+ }
267
+
268
+ beforeEach(() => {
269
+ /*
270
+ * The dedupe claims an activity in Redis first. Nothing connects Redis in
271
+ * unit tests, so every claim falls back to this process's memory; the stub
272
+ * makes that explicit, so a reachable Redis (whose keys outlive the run,
273
+ * while the activity ids here are the same on every run) can never turn a
274
+ * first delivery into a "repeat".
275
+ */
276
+ stubs = {
277
+ resolveTenant: jest
278
+ .spyOn(MicrosoftTeamsUtil, "resolveProjectByTenantId")
279
+ .mockResolvedValue(tenantResolvedToProject()),
280
+ claim: jest.spyOn(MicrosoftTeamsActivityDeduplicator, "claim"),
281
+ redisClaim: jest
282
+ .spyOn(GlobalCache, "setStringIfNotExists")
283
+ .mockRejectedValue(
284
+ new DatabaseNotConnectedException("Cache is not connected"),
285
+ ),
286
+ captureChat: jest
287
+ .spyOn(internals, "captureChatFromBotActivity")
288
+ .mockResolvedValue(undefined),
289
+ captureTeam: jest
290
+ .spyOn(MicrosoftTeamsUtil, "captureTeamFromBotActivity")
291
+ .mockResolvedValue(undefined),
292
+ createIncident: jest
293
+ .spyOn(MicrosoftTeamsCreateCommands, "handleCreateIncidentCommand")
294
+ .mockResolvedValue(undefined),
295
+ createMaintenance: jest
296
+ .spyOn(
297
+ MicrosoftTeamsCreateCommands,
298
+ "handleCreateScheduledMaintenanceCommand",
299
+ )
300
+ .mockResolvedValue(undefined),
301
+ askAssistant: jest
302
+ .spyOn(internals, "answerObservabilityQuestion")
303
+ .mockResolvedValue(undefined),
304
+ showReplies: {
305
+ getActiveIncidentsMessage: jest
306
+ .spyOn(internals, "getActiveIncidentsMessage")
307
+ .mockResolvedValue(CANNED_SHOW_REPLIES.getActiveIncidentsMessage),
308
+ getScheduledMaintenanceMessage: jest
309
+ .spyOn(internals, "getScheduledMaintenanceMessage")
310
+ .mockResolvedValue(CANNED_SHOW_REPLIES.getScheduledMaintenanceMessage),
311
+ getOngoingMaintenanceMessage: jest
312
+ .spyOn(internals, "getOngoingMaintenanceMessage")
313
+ .mockResolvedValue(CANNED_SHOW_REPLIES.getOngoingMaintenanceMessage),
314
+ getActiveAlertsMessage: jest
315
+ .spyOn(internals, "getActiveAlertsMessage")
316
+ .mockResolvedValue(CANNED_SHOW_REPLIES.getActiveAlertsMessage),
317
+ },
318
+ };
319
+ });
320
+
321
+ afterEach(() => {
322
+ jest.restoreAllMocks();
323
+ });
324
+
325
+ // No "show ..." builder, create command or AI question ran.
326
+ function expectNoCommandRan(): void {
327
+ for (const builderName of SHOW_REPLY_BUILDER_NAMES) {
328
+ expect(stubs.showReplies[builderName]).not.toHaveBeenCalled();
329
+ }
330
+ expect(stubs.createIncident).not.toHaveBeenCalled();
331
+ expect(stubs.createMaintenance).not.toHaveBeenCalled();
332
+ expect(stubs.askAssistant).not.toHaveBeenCalled();
333
+ }
334
+
335
+ // ---- A fake TurnContext that records what the bot sends ----
336
+
337
+ type Reply = string | Partial<Activity>;
338
+
339
+ // accept: Teams takes every reply. refuse-first: the first reply is refused.
340
+ type SendBehaviour = "accept" | "refuse-first" | "refuse-all";
341
+
342
+ interface FakeTurn {
343
+ turnContext: TurnContext;
344
+ // Every reply the bot tried to send, in order, whether Teams took it or not.
345
+ attempted: Array<Reply>;
346
+ // The replies Teams took: what the user sees in the chat.
347
+ delivered: Array<Reply>;
348
+ }
349
+
350
+ interface FakeTurnContextFields {
351
+ activity: JSONObject | undefined;
352
+ responded: boolean;
353
+ sendActivity: (reply: Reply) => Promise<ResourceResponse>;
354
+ }
355
+
356
+ // The RestError the Bot Connector throws when Teams refuses a reply.
357
+ function connectorError(data: {
358
+ statusCode: number;
359
+ code: string;
360
+ message: string;
361
+ }): Error {
362
+ return Object.assign(new Error(data.message), {
363
+ name: "RestError",
364
+ statusCode: data.statusCode,
365
+ code: data.code,
366
+ details: { error: { code: data.code, message: data.message } },
367
+ response: { status: data.statusCode },
368
+ });
369
+ }
370
+
371
+ function messageTooLarge(): Error {
372
+ return connectorError({
373
+ statusCode: 413,
374
+ code: "MessageSizeTooBig",
375
+ message: "Message size too large.",
376
+ });
377
+ }
378
+
379
+ function connectorUnavailable(): Error {
380
+ return connectorError({
381
+ statusCode: 503,
382
+ code: "ServiceUnavailable",
383
+ message: "The service is temporarily unavailable.",
384
+ });
385
+ }
386
+
387
+ function buildTurn(data: {
388
+ activity: JSONObject | undefined;
389
+ responded?: boolean | undefined;
390
+ send?: SendBehaviour | undefined;
391
+ sendError?: Error | undefined;
392
+ }): FakeTurn {
393
+ const attempted: Array<Reply> = [];
394
+ const delivered: Array<Reply> = [];
395
+ const behaviour: SendBehaviour = data.send || "accept";
396
+
397
+ const fields: FakeTurnContextFields = {
398
+ activity: data.activity,
399
+ responded: data.responded || false,
400
+ sendActivity: async (reply: Reply): Promise<ResourceResponse> => {
401
+ attempted.push(reply);
402
+
403
+ if (
404
+ behaviour === "refuse-all" ||
405
+ (behaviour === "refuse-first" && attempted.length === 1)
406
+ ) {
407
+ throw data.sendError || messageTooLarge();
408
+ }
409
+
410
+ delivered.push(reply);
411
+ fields.responded = true;
412
+ return { id: `bot-reply-${delivered.length}` };
413
+ },
414
+ };
415
+
416
+ return {
417
+ turnContext: fields as unknown as TurnContext,
418
+ attempted: attempted,
419
+ delivered: delivered,
420
+ };
421
+ }
422
+
423
+ function textOf(reply: Reply | undefined): string {
424
+ if (typeof reply === "string") {
425
+ return reply;
426
+ }
427
+
428
+ return reply?.text || "";
429
+ }
430
+
431
+ // One inbound message, handled the way processBotActivity hands it over.
432
+ async function deliver(
433
+ activity: JSONObject,
434
+ options?: {
435
+ send?: SendBehaviour | undefined;
436
+ sendError?: Error | undefined;
437
+ },
438
+ ): Promise<FakeTurn> {
439
+ const turn: FakeTurn = buildTurn({
440
+ activity: activity,
441
+ send: options?.send,
442
+ sendError: options?.sendError,
443
+ });
444
+
445
+ await MicrosoftTeamsUtil.handleBotMessageActivity({
446
+ activity: activity,
447
+ turnContext: turn.turnContext,
448
+ });
449
+
450
+ return turn;
451
+ }
452
+
453
+ // A promise a test opens by hand, to hold a handler at a chosen point.
454
+ interface Gate {
455
+ opened: Promise<void>;
456
+ open: () => void;
457
+ }
458
+
459
+ function createGate(): Gate {
460
+ let resolveGate: () => void = (): void => {
461
+ return undefined;
462
+ };
463
+ const opened: Promise<void> = new Promise<void>((resolve: () => void) => {
464
+ resolveGate = resolve;
465
+ });
466
+
467
+ return {
468
+ opened: opened,
469
+ open: (): void => {
470
+ resolveGate();
471
+ },
472
+ };
473
+ }
474
+
475
+ // ---- Teams activities ----
476
+
477
+ let lastActivityNumber: number = 0;
478
+
479
+ // Teams activity ids look like "1727712345678"; every call returns a new one.
480
+ function nextActivityId(): string {
481
+ lastActivityNumber += 1;
482
+ return `17277124${String(lastActivityNumber).padStart(5, "0")}`;
483
+ }
484
+
485
+ function idOf(activity: JSONObject): string {
486
+ return activity["id"] as string;
487
+ }
488
+
489
+ function clientInfoEntity(): JSONObject {
490
+ return {
491
+ type: "clientInfo",
492
+ locale: "en-US",
493
+ country: "US",
494
+ platform: "Web",
495
+ timezone: "America/New_York",
496
+ };
497
+ }
498
+
499
+ function botMentionEntity(): JSONObject {
500
+ return {
501
+ type: "mention",
502
+ mentioned: { id: BOT_ID, name: "OneUptime" },
503
+ text: BOT_MENTION_TEXT,
504
+ };
505
+ }
506
+
507
+ function otherMemberMentionEntity(): JSONObject {
508
+ return {
509
+ type: "mention",
510
+ mentioned: { id: OTHER_MEMBER_ID, name: "Bob Oncall" },
511
+ text: OTHER_MEMBER_MENTION_TEXT,
512
+ };
513
+ }
514
+
515
+ // Whom a message in a group chat or channel @mentions.
516
+ type Mention = "the bot" | "someone else" | "nobody";
517
+
518
+ // The text and entities Teams delivers for a message with that @mention.
519
+ function withMention(
520
+ text: string,
521
+ mention: Mention,
522
+ ): { text: string; entities: Array<JSONObject> } {
523
+ if (mention === "the bot") {
524
+ return {
525
+ text: `${BOT_MENTION_TEXT} ${text}`,
526
+ entities: [botMentionEntity(), clientInfoEntity()],
527
+ };
528
+ }
529
+
530
+ if (mention === "someone else") {
531
+ return {
532
+ text: `${OTHER_MEMBER_MENTION_TEXT} ${text}`,
533
+ entities: [otherMemberMentionEntity(), clientInfoEntity()],
534
+ };
535
+ }
536
+
537
+ return { text: text, entities: [clientInfoEntity()] };
538
+ }
539
+
540
+ // A message typed to the bot in a personal (1:1) chat, as Teams delivers it.
541
+ function personalMessage(text: string, overrides?: JSONObject): JSONObject {
542
+ return {
543
+ type: "message",
544
+ id: nextActivityId(),
545
+ text: text,
546
+ from: {
547
+ id: "29:1Hk8-teams-user",
548
+ name: "Alice Admin",
549
+ aadObjectId: USER_AAD_OBJECT_ID,
550
+ },
551
+ recipient: { id: BOT_ID, name: "OneUptime" },
552
+ conversation: {
553
+ conversationType: "personal",
554
+ id: PERSONAL_CHAT_ID,
555
+ tenantId: TENANT_ID,
556
+ },
557
+ channelData: { tenant: { id: TENANT_ID } },
558
+ entities: [clientInfoEntity()],
559
+ serviceUrl: SERVICE_URL,
560
+ ...(overrides || {}),
561
+ };
562
+ }
563
+
564
+ // A message in a group chat, which the bot only answers when @mentioned.
565
+ function groupChatMessage(data: {
566
+ text: string;
567
+ mention: Mention;
568
+ id?: string | undefined;
569
+ }): JSONObject {
570
+ const message: { text: string; entities: Array<JSONObject> } = withMention(
571
+ data.text,
572
+ data.mention,
573
+ );
574
+
575
+ return personalMessage(message.text, {
576
+ ...(data.id ? { id: data.id } : {}),
577
+ conversation: {
578
+ conversationType: "groupChat",
579
+ id: GROUP_CHAT_ID,
580
+ tenantId: TENANT_ID,
581
+ isGroup: true,
582
+ },
583
+ entities: message.entities,
584
+ });
585
+ }
586
+
587
+ // "@OneUptime <text>" in a group chat: the mention is in the text and entities.
588
+ function groupChatMention(text: string): JSONObject {
589
+ return groupChatMessage({ text: text, mention: "the bot" });
590
+ }
591
+
592
+ // A message in a team channel, which the bot only answers when @mentioned.
593
+ function channelMessage(data: {
594
+ text: string;
595
+ mention: Mention;
596
+ id: string;
597
+ value?: JSONObject | undefined;
598
+ }): JSONObject {
599
+ const message: { text: string; entities: Array<JSONObject> } = withMention(
600
+ data.text,
601
+ data.mention,
602
+ );
603
+
604
+ const activity: JSONObject = personalMessage(message.text, {
605
+ id: data.id,
606
+ conversation: {
607
+ conversationType: "channel",
608
+ id: CHANNEL_THREAD_ID,
609
+ tenantId: TENANT_ID,
610
+ isGroup: true,
611
+ },
612
+ channelData: {
613
+ tenant: { id: TENANT_ID },
614
+ team: { id: TEAM_ID, name: "Engineering" },
615
+ channel: { id: CHANNEL_ID },
616
+ },
617
+ entities: message.entities,
618
+ });
619
+
620
+ if (data.value) {
621
+ activity["value"] = data.value;
622
+ }
623
+
624
+ return activity;
625
+ }
626
+
627
+ // An Adaptive Card Action.Submit, which Teams delivers as a message with a value.
628
+ function cardSubmit(value: JSONObject): JSONObject {
629
+ const activity: JSONObject = personalMessage("", { value: value });
630
+ delete activity["text"];
631
+ return activity;
632
+ }
633
+
634
+ // An Adaptive Card action delivered as an invoke activity.
635
+ function invokeActivity(value: JSONObject): JSONObject {
636
+ return {
637
+ type: "invoke",
638
+ name: "adaptiveCard/action",
639
+ id: nextActivityId(),
640
+ from: {
641
+ id: "29:1Hk8-teams-user",
642
+ name: "Alice Admin",
643
+ aadObjectId: USER_AAD_OBJECT_ID,
644
+ },
645
+ recipient: { id: BOT_ID, name: "OneUptime" },
646
+ conversation: {
647
+ conversationType: "personal",
648
+ id: PERSONAL_CHAT_ID,
649
+ tenantId: TENANT_ID,
650
+ },
651
+ channelData: { tenant: { id: TENANT_ID } },
652
+ serviceUrl: SERVICE_URL,
653
+ value: value,
654
+ };
655
+ }
656
+
657
+ function acknowledgeIncidentValue(): JSONObject {
658
+ return {
659
+ action: MicrosoftTeamsIncidentActionType.AckIncident,
660
+ actionValue: INCIDENT_ID.toString(),
661
+ };
662
+ }
663
+
664
+ function silenceErrorLog(): SpyInstance<typeof logger.error> {
665
+ return jest.spyOn(logger, "error").mockImplementation(() => {
666
+ return undefined;
667
+ });
668
+ }
669
+
670
+ function loggedErrorLines(
671
+ errorLog: SpyInstance<typeof logger.error>,
672
+ ): Array<string> {
673
+ return errorLog.mock.calls.map((call: Parameters<typeof logger.error>) => {
674
+ return String(call[0]);
675
+ });
676
+ }
677
+
678
+ /*
679
+ * Every logger.error call, with exactly the arguments it was given. Copied
680
+ * into arrays made here: jest records calls in arrays of its own realm,
681
+ * which toStrictEqual would tell apart from the literals they are compared
682
+ * with.
683
+ */
684
+ function errorLogCalls(
685
+ errorLog: SpyInstance<typeof logger.error>,
686
+ ): Array<Array<unknown>> {
687
+ return Array.from(
688
+ errorLog.mock.calls,
689
+ (call: Parameters<typeof logger.error>): Array<unknown> => {
690
+ return [...call];
691
+ },
692
+ );
693
+ }
694
+
695
+ /*
696
+ * What MicrosoftTeamsReplies.logFailure logs for an error without an HTTP
697
+ * status, and nothing else: what failed and why, then the error as it was
698
+ * thrown, which keeps its stack and its class.
699
+ */
700
+ function expectFailureLogged(
701
+ errorLog: SpyInstance<typeof logger.error>,
702
+ expected: {
703
+ line: string;
704
+ error: unknown;
705
+ attributes: LogAttributes | undefined;
706
+ },
707
+ ): void {
708
+ expect(errorLogCalls(errorLog)).toStrictEqual([
709
+ [expected.line, expected.attributes],
710
+ [expected.error, expected.attributes],
711
+ ]);
712
+ expect(errorLog.mock.calls[1]?.[0]).toBe(expected.error);
713
+ }
714
+
715
+ // Everything logger.error was given, as text, to look for what must not be there.
716
+ function errorLogText(errorLog: SpyInstance<typeof logger.error>): string {
717
+ return errorLog.mock.calls
718
+ .map((call: Parameters<typeof logger.error>): string => {
719
+ return call
720
+ .map((argument: unknown): string => {
721
+ if (argument instanceof Error) {
722
+ return `${String(argument)}\n${argument.stack || ""}`;
723
+ }
724
+
725
+ return typeof argument === "string"
726
+ ? argument
727
+ : String(JSON.stringify(argument));
728
+ })
729
+ .join("\n");
730
+ })
731
+ .join("\n");
732
+ }
733
+
734
+ // ---- Monitors ----
735
+
736
+ function monitorsNamed(names: Array<string | undefined>): Array<Monitor> {
737
+ return names.map((name: string | undefined) => {
738
+ const monitor: Monitor = new Monitor();
739
+
740
+ if (name !== undefined) {
741
+ monitor.name = name;
742
+ }
743
+
744
+ return monitor;
745
+ });
746
+ }
747
+
748
+ function numberedNames(count: number): Array<string> {
749
+ return Array.from({ length: count }, (_value: unknown, index: number) => {
750
+ return `Monitor ${String(index + 1).padStart(4, "0")}`;
751
+ });
752
+ }
753
+
754
+ describe("handleBotMessageActivity never rejects: one reply, never a rethrow (#4111)", () => {
755
+ test("tenant resolution throws: exactly one reply, the unexpected-error text naming the activity id, which the error log names too", async () => {
756
+ const outage: Error = new Error("simulated database outage");
757
+ stubs.resolveTenant.mockRejectedValue(outage);
758
+ const errorLog: SpyInstance<typeof logger.error> = silenceErrorLog();
759
+ const activity: JSONObject = personalMessage(
760
+ "create incident Database Is Down",
761
+ );
762
+ const turn: FakeTurn = buildTurn({ activity: activity });
763
+
764
+ await expect(
765
+ MicrosoftTeamsUtil.handleBotMessageActivity({
766
+ activity: activity,
767
+ turnContext: turn.turnContext,
768
+ }),
769
+ ).resolves.toBeUndefined();
770
+
771
+ expect(turn.attempted).toEqual([
772
+ MicrosoftTeamsUtil.getUnexpectedErrorMessage(activity),
773
+ ]);
774
+ expect(turn.delivered).toEqual(turn.attempted);
775
+ expect(textOf(turn.delivered[0])).toContain(
776
+ `reference ${idOf(activity)} in the server logs`,
777
+ );
778
+ expect(textOf(turn.delivered[0])).not.toContain(MASTER_ERROR_TEXT);
779
+ expect(stubs.createIncident).not.toHaveBeenCalled();
780
+
781
+ // The reference in the reply is what an administrator searches the log for.
782
+ expect(
783
+ loggedErrorLines(errorLog).filter((line: string) => {
784
+ return (
785
+ line.includes(idOf(activity)) &&
786
+ line.includes("simulated database outage")
787
+ );
788
+ }),
789
+ ).toHaveLength(1);
790
+
791
+ // Named by its command, not by the title typed after it.
792
+ expectFailureLogged(errorLog, {
793
+ line: `Microsoft Teams message ${idOf(activity)} ("create incident") failed: Error: simulated database outage`,
794
+ error: outage,
795
+ // The project was not known yet.
796
+ attributes: { projectId: undefined },
797
+ });
798
+ expect(errorLogText(errorLog)).not.toContain("Database Is Down");
799
+ });
800
+
801
+ test("the error reply cannot be sent either: still resolves, after exactly one attempt", async () => {
802
+ const outage: Error = new Error("simulated database outage");
803
+ stubs.resolveTenant.mockRejectedValue(outage);
804
+ const errorLog: SpyInstance<typeof logger.error> = silenceErrorLog();
805
+ const activity: JSONObject = personalMessage("show active incidents");
806
+ const turn: FakeTurn = buildTurn({
807
+ activity: activity,
808
+ send: "refuse-all",
809
+ sendError: connectorUnavailable(),
810
+ });
811
+
812
+ await expect(
813
+ MicrosoftTeamsUtil.handleBotMessageActivity({
814
+ activity: activity,
815
+ turnContext: turn.turnContext,
816
+ }),
817
+ ).resolves.toBeUndefined();
818
+
819
+ expect(turn.attempted).toEqual([
820
+ MicrosoftTeamsUtil.getUnexpectedErrorMessage(activity),
821
+ ]);
822
+ expect(turn.delivered).toEqual([]);
823
+ expect(errorLogCalls(errorLog)).toStrictEqual([
824
+ [
825
+ `Microsoft Teams message ${idOf(activity)} ("show active incidents") failed: Error: simulated database outage`,
826
+ { projectId: undefined },
827
+ ],
828
+ [outage, { projectId: undefined }],
829
+ // The refused reply: logged, never thrown, never tried again.
830
+ [
831
+ "Could not send a Microsoft Teams reply: RestError 503 ServiceUnavailable: The service is temporarily unavailable.",
832
+ ],
833
+ ]);
834
+ });
835
+
836
+ test("Teams refuses the answer itself (413): the user gets the one error reply instead, and nothing is thrown", async () => {
837
+ const errorLog: SpyInstance<typeof logger.error> = silenceErrorLog();
838
+ const activity: JSONObject = personalMessage("help");
839
+ const turn: FakeTurn = buildTurn({
840
+ activity: activity,
841
+ send: "refuse-first",
842
+ });
843
+
844
+ await expect(
845
+ MicrosoftTeamsUtil.handleBotMessageActivity({
846
+ activity: activity,
847
+ turnContext: turn.turnContext,
848
+ }),
849
+ ).resolves.toBeUndefined();
850
+
851
+ expect(turn.attempted).toEqual([
852
+ HELP_TEXT,
853
+ MicrosoftTeamsUtil.getUnexpectedErrorMessage(activity),
854
+ ]);
855
+ expect(turn.delivered).toEqual([
856
+ MicrosoftTeamsUtil.getUnexpectedErrorMessage(activity),
857
+ ]);
858
+ // A Bot Framework error is one line: its status and code say it all.
859
+ expect(errorLogCalls(errorLog)).toStrictEqual([
860
+ [
861
+ `Microsoft Teams message ${idOf(activity)} ("help") failed: RestError 413 MessageSizeTooBig: Message size too large.`,
862
+ { projectId: PROJECT_ID.toString() },
863
+ ],
864
+ ]);
865
+ });
866
+
867
+ test("a 'show' command whose reply builder throws unexpectedly: one reply", async () => {
868
+ const errorLog: SpyInstance<typeof logger.error> = silenceErrorLog();
869
+ const bug: TypeError = new TypeError(
870
+ "Cannot read properties of undefined (reading 'name')",
871
+ );
872
+ stubs.showReplies.getScheduledMaintenanceMessage.mockRejectedValue(bug);
873
+ const activity: JSONObject = personalMessage("show scheduled maintenance");
874
+
875
+ const turn: FakeTurn = await deliver(activity);
876
+
877
+ expect(
878
+ stubs.showReplies.getScheduledMaintenanceMessage,
879
+ ).toHaveBeenCalledWith(PROJECT_ID);
880
+ expect(turn.attempted).toEqual([
881
+ MicrosoftTeamsUtil.getUnexpectedErrorMessage(activity),
882
+ ]);
883
+ expectFailureLogged(errorLog, {
884
+ line: `Microsoft Teams message ${idOf(activity)} ("show scheduled maintenance") failed: TypeError: Cannot read properties of undefined (reading 'name')`,
885
+ error: bug,
886
+ attributes: { projectId: PROJECT_ID.toString() },
887
+ });
888
+ });
889
+
890
+ test("a create command that throws (a 413 escaping the form): one reply, no rethrow, and Teams' redelivery adds no second bubble", async () => {
891
+ const errorLog: SpyInstance<typeof logger.error> = silenceErrorLog();
892
+ stubs.createIncident.mockRejectedValue(messageTooLarge());
893
+ const activity: JSONObject = personalMessage(
894
+ "create incident Database Is Down",
895
+ );
896
+ const first: FakeTurn = buildTurn({ activity: activity });
897
+
898
+ await expect(
899
+ MicrosoftTeamsUtil.handleBotMessageActivity({
900
+ activity: activity,
901
+ turnContext: first.turnContext,
902
+ }),
903
+ ).resolves.toBeUndefined();
904
+ const redelivery: FakeTurn = await deliver(activity);
905
+
906
+ expect(first.delivered).toEqual([
907
+ MicrosoftTeamsUtil.getUnexpectedErrorMessage(activity),
908
+ ]);
909
+ expect(redelivery.attempted).toEqual([]);
910
+ expect(stubs.createIncident).toHaveBeenCalledTimes(1);
911
+ // Logged once, for the first delivery only.
912
+ expect(errorLogCalls(errorLog)).toStrictEqual([
913
+ [
914
+ `Microsoft Teams message ${idOf(activity)} ("create incident") failed: RestError 413 MessageSizeTooBig: Message size too large.`,
915
+ { projectId: PROJECT_ID.toString() },
916
+ ],
917
+ ]);
918
+ });
919
+
920
+ test("an activity without a tenant: one reply saying the organization could not be identified", async () => {
921
+ silenceErrorLog();
922
+ const activity: JSONObject = personalMessage("create incident", {
923
+ channelData: {},
924
+ });
925
+
926
+ const turn: FakeTurn = await deliver(activity);
927
+
928
+ expect(turn.attempted).toEqual([
929
+ "Sorry, I couldn't identify your organization. Please try again later.",
930
+ ]);
931
+ expect(stubs.resolveTenant).not.toHaveBeenCalled();
932
+ expect(stubs.createIncident).not.toHaveBeenCalled();
933
+ });
934
+
935
+ test.each([
936
+ [
937
+ "no project is connected to the tenant",
938
+ {
939
+ projectAuth: null,
940
+ isAmbiguous: false,
941
+ candidateProjectIds: [],
942
+ },
943
+ "couldn't find your project configuration",
944
+ ],
945
+ [
946
+ "more than one project is connected to the tenant",
947
+ {
948
+ projectAuth: null,
949
+ isAmbiguous: true,
950
+ candidateProjectIds: [PROJECT_ID, ObjectID.generate()],
951
+ },
952
+ "connected to more than one OneUptime project",
953
+ ],
954
+ ])(
955
+ "%s: one reply that says so, and no form",
956
+ async (
957
+ _situation: string,
958
+ resolution: MicrosoftTeamsTenantResolution,
959
+ saying: string,
960
+ ) => {
961
+ stubs.resolveTenant.mockResolvedValue(resolution);
962
+ const activity: JSONObject = personalMessage(
963
+ "create maintenance Patch Tuesday",
964
+ );
965
+
966
+ const turn: FakeTurn = await deliver(activity);
967
+
968
+ expect(turn.attempted).toEqual([
969
+ MicrosoftTeamsUtil.getTenantResolutionFailureMessage(resolution),
970
+ ]);
971
+ expect(textOf(turn.attempted[0])).toContain(saying);
972
+ expectNoCommandRan();
973
+ },
974
+ );
975
+
976
+ test.each([
977
+ ["a personal chat (the chat backfill)", "chat"],
978
+ ["a team channel (the team backfill)", "team"],
979
+ ])(
980
+ "%s whose backfill cannot read the database: the answer still goes out, once",
981
+ async (_where: string, backfill: string) => {
982
+ // The real backfills run; their first read fails.
983
+ stubs.captureChat.mockRestore();
984
+ stubs.captureTeam.mockRestore();
985
+ jest
986
+ .spyOn(WorkspaceProjectAuthTokenService, "findBy")
987
+ .mockRejectedValue(new Error("simulated database outage"));
988
+ const errorLog: SpyInstance<typeof logger.error> = silenceErrorLog();
989
+ const activity: JSONObject =
990
+ backfill === "chat"
991
+ ? personalMessage("help")
992
+ : channelMessage({
993
+ text: "help",
994
+ mention: "the bot",
995
+ id: nextActivityId(),
996
+ });
997
+
998
+ const turn: FakeTurn = await deliver(activity);
999
+
1000
+ expect(turn.attempted).toEqual([HELP_TEXT]);
1001
+ expect(loggedErrorLines(errorLog)).toContain(
1002
+ `Error capturing Microsoft Teams ${backfill} from bot activity:`,
1003
+ );
1004
+ },
1005
+ );
1006
+ });
1007
+
1008
+ describe("a failed message is logged by the command it is, never by its text", () => {
1009
+ /*
1010
+ * The error log goes to whoever runs OneUptime, and what is typed to the
1011
+ * bot can carry anything: a customer's name, an address, a secret pasted
1012
+ * by mistake. The line names the command instead, or how long a question
1013
+ * was; the text itself is only in the debug log.
1014
+ */
1015
+ interface FailedMessageCase {
1016
+ name: string;
1017
+ activity: () => JSONObject;
1018
+ // How the error log line names the message.
1019
+ label: string;
1020
+ // Makes what the message runs fail with `failure`.
1021
+ fail: (failure: Error) => void;
1022
+ // Something the user typed that must not reach the error log, if any.
1023
+ typed: string | null;
1024
+ }
1025
+
1026
+ const QUESTION: string = "why did alice@example.com get paged at 3am?";
1027
+
1028
+ const FAILED_MESSAGES: ReadonlyArray<FailedMessageCase> = [
1029
+ {
1030
+ name: "'create incident <title>'",
1031
+ activity: (): JSONObject => {
1032
+ return personalMessage(
1033
+ "create incident Payroll export leaks alice@example.com",
1034
+ );
1035
+ },
1036
+ label: '"create incident"',
1037
+ fail: (failure: Error): void => {
1038
+ stubs.createIncident.mockRejectedValue(failure);
1039
+ },
1040
+ typed: "alice@example.com",
1041
+ },
1042
+ {
1043
+ name: "'Create Maintenance <title>' in a group chat",
1044
+ activity: (): JSONObject => {
1045
+ return groupChatMention(
1046
+ "Create Maintenance Rotate the key alice@example.com leaked",
1047
+ );
1048
+ },
1049
+ label: '"create maintenance"',
1050
+ fail: (failure: Error): void => {
1051
+ stubs.createMaintenance.mockRejectedValue(failure);
1052
+ },
1053
+ typed: "alice@example.com",
1054
+ },
1055
+ {
1056
+ name: "'Show Active Incidents'",
1057
+ activity: (): JSONObject => {
1058
+ return personalMessage("Show Active Incidents");
1059
+ },
1060
+ label: '"show active incidents"',
1061
+ fail: (failure: Error): void => {
1062
+ stubs.showReplies.getActiveIncidentsMessage.mockRejectedValue(failure);
1063
+ },
1064
+ typed: null,
1065
+ },
1066
+ {
1067
+ // A short form is logged as the command it runs.
1068
+ name: "'ongoing maintenance'",
1069
+ activity: (): JSONObject => {
1070
+ return personalMessage("ongoing maintenance");
1071
+ },
1072
+ label: '"show ongoing maintenance"',
1073
+ fail: (failure: Error): void => {
1074
+ stubs.showReplies.getOngoingMaintenanceMessage.mockRejectedValue(
1075
+ failure,
1076
+ );
1077
+ },
1078
+ typed: null,
1079
+ },
1080
+ {
1081
+ /*
1082
+ * Starts with a command's words but is a question: the router sends
1083
+ * it to the assistant, so the log must not call it "help".
1084
+ */
1085
+ name: "'help me find slow queries' (a question, not the help command)",
1086
+ activity: (): JSONObject => {
1087
+ return personalMessage("help me find slow queries");
1088
+ },
1089
+ label: "a 25-character question",
1090
+ fail: (failure: Error): void => {
1091
+ stubs.askAssistant.mockRejectedValue(failure);
1092
+ },
1093
+ typed: "slow queries",
1094
+ },
1095
+ {
1096
+ name: "'show active incidents in eu-west-1' (a question, not the show command)",
1097
+ activity: (): JSONObject => {
1098
+ return personalMessage("show active incidents in eu-west-1");
1099
+ },
1100
+ label: "a 34-character question",
1101
+ fail: (failure: Error): void => {
1102
+ stubs.askAssistant.mockRejectedValue(failure);
1103
+ },
1104
+ typed: "eu-west-1",
1105
+ },
1106
+ {
1107
+ name: "'ask <question>'",
1108
+ activity: (): JSONObject => {
1109
+ return personalMessage(`ask ${QUESTION}`);
1110
+ },
1111
+ label: '"ask"',
1112
+ fail: (failure: Error): void => {
1113
+ stubs.askAssistant.mockRejectedValue(failure);
1114
+ },
1115
+ typed: "alice@example.com",
1116
+ },
1117
+ {
1118
+ name: "a question in plain words",
1119
+ activity: (): JSONObject => {
1120
+ return personalMessage(QUESTION);
1121
+ },
1122
+ label: "a 43-character question",
1123
+ fail: (failure: Error): void => {
1124
+ stubs.askAssistant.mockRejectedValue(failure);
1125
+ },
1126
+ typed: "alice@example.com",
1127
+ },
1128
+ {
1129
+ name: "a question in plain words to '@OneUptime' in a channel (the mention is not counted)",
1130
+ activity: (): JSONObject => {
1131
+ return channelMessage({
1132
+ text: QUESTION,
1133
+ mention: "the bot",
1134
+ id: nextActivityId(),
1135
+ });
1136
+ },
1137
+ label: "a 43-character question",
1138
+ fail: (failure: Error): void => {
1139
+ stubs.askAssistant.mockRejectedValue(failure);
1140
+ },
1141
+ typed: "alice@example.com",
1142
+ },
1143
+ ];
1144
+
1145
+ test.each(FAILED_MESSAGES)(
1146
+ "$name: the one reply, and a log line that names the command and the error",
1147
+ async (failedMessage: FailedMessageCase) => {
1148
+ const errorLog: SpyInstance<typeof logger.error> = silenceErrorLog();
1149
+ const debugLog: SpyInstance<typeof logger.debug> = jest
1150
+ .spyOn(logger, "debug")
1151
+ .mockImplementation(() => {
1152
+ return undefined;
1153
+ });
1154
+ const failure: Error = new Error("simulated command failure");
1155
+ failedMessage.fail(failure);
1156
+ const activity: JSONObject = failedMessage.activity();
1157
+
1158
+ const turn: FakeTurn = await deliver(activity);
1159
+
1160
+ expect(turn.attempted).toEqual([
1161
+ MicrosoftTeamsUtil.getUnexpectedErrorMessage(activity),
1162
+ ]);
1163
+ expectFailureLogged(errorLog, {
1164
+ line: `Microsoft Teams message ${idOf(activity)} (${failedMessage.label}) failed: Error: simulated command failure`,
1165
+ error: failure,
1166
+ attributes: { projectId: PROJECT_ID.toString() },
1167
+ });
1168
+
1169
+ if (failedMessage.typed) {
1170
+ expect(errorLogText(errorLog)).not.toContain(failedMessage.typed);
1171
+ // Whoever turns the debug log on to look into it finds the text there.
1172
+ expect(debugLog).toHaveBeenCalledWith(
1173
+ `Message text: ${activity["text"] as string}`,
1174
+ );
1175
+ }
1176
+ },
1177
+ );
1178
+ });
1179
+
1180
+ describe("'create incident' and 'create maintenance' go to MicrosoftTeamsCreateCommands with the title as typed", () => {
1181
+ test("'create incident Database Is Down' in a personal chat", async () => {
1182
+ const activity: JSONObject = personalMessage(
1183
+ "create incident Database Is Down",
1184
+ );
1185
+
1186
+ const turn: FakeTurn = await deliver(activity);
1187
+
1188
+ expect(stubs.resolveTenant).toHaveBeenCalledWith({ tenantId: TENANT_ID });
1189
+ expect(stubs.createIncident).toHaveBeenCalledTimes(1);
1190
+ expect(stubs.createIncident).toHaveBeenCalledWith({
1191
+ turnContext: turn.turnContext,
1192
+ activity: activity,
1193
+ projectId: PROJECT_ID,
1194
+ initialTitle: "Database Is Down",
1195
+ });
1196
+ expect(stubs.createMaintenance).not.toHaveBeenCalled();
1197
+ expect(stubs.askAssistant).not.toHaveBeenCalled();
1198
+ // The command answers for itself; the message handler adds nothing.
1199
+ expect(turn.attempted).toEqual([]);
1200
+ });
1201
+
1202
+ test("'@OneUptime create maintenance Patch Tuesday' in a group chat", async () => {
1203
+ const activity: JSONObject = groupChatMention(
1204
+ "create maintenance Patch Tuesday",
1205
+ );
1206
+
1207
+ const turn: FakeTurn = await deliver(activity);
1208
+
1209
+ expect(stubs.captureChat).toHaveBeenCalledTimes(1);
1210
+ expect(stubs.createMaintenance).toHaveBeenCalledTimes(1);
1211
+ expect(stubs.createMaintenance).toHaveBeenCalledWith({
1212
+ turnContext: turn.turnContext,
1213
+ activity: activity,
1214
+ projectId: PROJECT_ID,
1215
+ initialTitle: "Patch Tuesday",
1216
+ });
1217
+ expect(stubs.createIncident).not.toHaveBeenCalled();
1218
+ expect(turn.attempted).toEqual([]);
1219
+ });
1220
+
1221
+ test("'@OneUptime create incident Checkout is slow' in a team channel", async () => {
1222
+ const activity: JSONObject = channelMessage({
1223
+ text: "create incident Checkout is slow",
1224
+ mention: "the bot",
1225
+ id: nextActivityId(),
1226
+ });
1227
+
1228
+ const turn: FakeTurn = await deliver(activity);
1229
+
1230
+ expect(stubs.captureTeam).toHaveBeenCalledTimes(1);
1231
+ expect(stubs.createIncident).toHaveBeenCalledTimes(1);
1232
+ expect(stubs.createIncident).toHaveBeenCalledWith({
1233
+ turnContext: turn.turnContext,
1234
+ activity: activity,
1235
+ projectId: PROJECT_ID,
1236
+ initialTitle: "Checkout is slow",
1237
+ });
1238
+ expect(stubs.createMaintenance).not.toHaveBeenCalled();
1239
+ expect(turn.attempted).toEqual([]);
1240
+ });
1241
+
1242
+ test("the @mention may sit anywhere in the text: 'create incident @OneUptime Database Is Down' in a group chat", async () => {
1243
+ const activity: JSONObject = groupChatMessage({
1244
+ text: "",
1245
+ mention: "the bot",
1246
+ });
1247
+ activity["text"] = `create incident ${BOT_MENTION_TEXT} Database Is Down`;
1248
+
1249
+ await deliver(activity);
1250
+
1251
+ expect(stubs.createIncident).toHaveBeenCalledTimes(1);
1252
+ expect(stubs.createIncident).toHaveBeenCalledWith(
1253
+ expect.objectContaining({
1254
+ projectId: PROJECT_ID,
1255
+ initialTitle: "Database Is Down",
1256
+ }),
1257
+ );
1258
+ });
1259
+
1260
+ test.each([
1261
+ ["CREATE INCIDENT Database Is Down", "Database Is Down"],
1262
+ ["Create Incident API p99 > 2s on EU-West", "API p99 > 2s on EU-West"],
1263
+ [" create incident Disk full on DB-01 ", "Disk full on DB-01"],
1264
+ ])(
1265
+ "the command is matched in any case; the title keeps the user's casing: %j",
1266
+ async (text: string, expectedTitle: string) => {
1267
+ await deliver(personalMessage(text));
1268
+
1269
+ expect(stubs.createIncident).toHaveBeenCalledTimes(1);
1270
+ expect(stubs.createIncident).toHaveBeenCalledWith(
1271
+ expect.objectContaining({ initialTitle: expectedTitle }),
1272
+ );
1273
+ expect(stubs.createMaintenance).not.toHaveBeenCalled();
1274
+ },
1275
+ );
1276
+
1277
+ test("'Create Maintenance Q3 Kernel Upgrade (Frankfurt)' keeps the title's casing and punctuation", async () => {
1278
+ await deliver(
1279
+ groupChatMention("Create Maintenance Q3 Kernel Upgrade (Frankfurt)"),
1280
+ );
1281
+
1282
+ expect(stubs.createMaintenance).toHaveBeenCalledTimes(1);
1283
+ expect(stubs.createMaintenance).toHaveBeenCalledWith(
1284
+ expect.objectContaining({
1285
+ initialTitle: "Q3 Kernel Upgrade (Frankfurt)",
1286
+ }),
1287
+ );
1288
+ expect(stubs.createIncident).not.toHaveBeenCalled();
1289
+ });
1290
+
1291
+ test("bare 'create incident' and bare 'create maintenance' open the form with no title", async () => {
1292
+ await deliver(personalMessage("create incident"));
1293
+ await deliver(groupChatMention("create maintenance"));
1294
+
1295
+ expect(stubs.createIncident).toHaveBeenCalledTimes(1);
1296
+ expect(stubs.createIncident).toHaveBeenCalledWith(
1297
+ expect.objectContaining({ projectId: PROJECT_ID, initialTitle: "" }),
1298
+ );
1299
+ expect(stubs.createMaintenance).toHaveBeenCalledTimes(1);
1300
+ expect(stubs.createMaintenance).toHaveBeenCalledWith(
1301
+ expect.objectContaining({ projectId: PROJECT_ID, initialTitle: "" }),
1302
+ );
1303
+ });
1304
+
1305
+ test.each([
1306
+ "creative incident",
1307
+ "create incidents",
1308
+ "create incident-report for yesterday",
1309
+ "create maintenance-window for db",
1310
+ "please create incident Database Is Down",
1311
+ ])(
1312
+ "%j is not a create command: it goes to the AI assistant as typed",
1313
+ async (text: string) => {
1314
+ const activity: JSONObject = personalMessage(text);
1315
+
1316
+ const turn: FakeTurn = await deliver(activity);
1317
+
1318
+ expect(stubs.createIncident).not.toHaveBeenCalled();
1319
+ expect(stubs.createMaintenance).not.toHaveBeenCalled();
1320
+ expect(stubs.askAssistant).toHaveBeenCalledTimes(1);
1321
+ expect(stubs.askAssistant).toHaveBeenCalledWith({
1322
+ activity: activity,
1323
+ turnContext: turn.turnContext,
1324
+ projectId: PROJECT_ID,
1325
+ question: text,
1326
+ });
1327
+ },
1328
+ );
1329
+ });
1330
+
1331
+ describe("help, 'show ...' and 'ask' are routed as before", () => {
1332
+ const HELP_REQUESTS: Array<[string, () => JSONObject]> = [
1333
+ [
1334
+ "'help'",
1335
+ (): JSONObject => {
1336
+ return personalMessage("help");
1337
+ },
1338
+ ],
1339
+ [
1340
+ "'HELP', in capitals",
1341
+ (): JSONObject => {
1342
+ return personalMessage("HELP");
1343
+ },
1344
+ ],
1345
+ [
1346
+ "'?'",
1347
+ (): JSONObject => {
1348
+ return personalMessage("?");
1349
+ },
1350
+ ],
1351
+ [
1352
+ "an @mention with no text",
1353
+ (): JSONObject => {
1354
+ return groupChatMention("");
1355
+ },
1356
+ ],
1357
+ ];
1358
+
1359
+ test.each(HELP_REQUESTS)(
1360
+ "%s: the help text, once, and nothing else runs",
1361
+ async (_request: string, buildActivity: () => JSONObject) => {
1362
+ const turn: FakeTurn = await deliver(buildActivity());
1363
+
1364
+ expect(turn.attempted).toEqual([HELP_TEXT]);
1365
+ expect(HELP_TEXT).toContain("Available Commands");
1366
+ expectNoCommandRan();
1367
+ },
1368
+ );
1369
+
1370
+ test("the help says both create commands take a title, as the router reads them", () => {
1371
+ expect(HELP_TEXT).toContain(
1372
+ "\n- **create incident [title]** - Create a new incident\n",
1373
+ );
1374
+ expect(HELP_TEXT).toContain(
1375
+ "\n- **create maintenance [title]** - Create a new scheduled maintenance event\n",
1376
+ );
1377
+ });
1378
+
1379
+ const SHOW_ROUTES: Array<[string, ShowReplyBuilderName]> = [
1380
+ ["show active incidents", "getActiveIncidentsMessage"],
1381
+ ["active incidents", "getActiveIncidentsMessage"],
1382
+ ["show scheduled maintenance", "getScheduledMaintenanceMessage"],
1383
+ ["scheduled maintenance", "getScheduledMaintenanceMessage"],
1384
+ ["show ongoing maintenance", "getOngoingMaintenanceMessage"],
1385
+ ["ongoing maintenance", "getOngoingMaintenanceMessage"],
1386
+ ["Show Active Alerts", "getActiveAlertsMessage"],
1387
+ ["active alerts", "getActiveAlertsMessage"],
1388
+ ];
1389
+
1390
+ test.each(SHOW_ROUTES)(
1391
+ "%j: answered once by %s for the resolved project",
1392
+ async (text: string, builderName: ShowReplyBuilderName) => {
1393
+ const turn: FakeTurn = await deliver(personalMessage(text));
1394
+
1395
+ expect(stubs.showReplies[builderName]).toHaveBeenCalledTimes(1);
1396
+ expect(stubs.showReplies[builderName]).toHaveBeenCalledWith(PROJECT_ID);
1397
+ for (const otherName of SHOW_REPLY_BUILDER_NAMES) {
1398
+ if (otherName !== builderName) {
1399
+ expect(stubs.showReplies[otherName]).not.toHaveBeenCalled();
1400
+ }
1401
+ }
1402
+ expect(stubs.askAssistant).not.toHaveBeenCalled();
1403
+ expect(turn.attempted).toEqual([CANNED_SHOW_REPLIES[builderName]]);
1404
+ },
1405
+ );
1406
+
1407
+ test.each([
1408
+ [
1409
+ "ask Which monitors are down right now?",
1410
+ "Which monitors are down right now?",
1411
+ ],
1412
+ ["ASK why did checkout latency spike?", "why did checkout latency spike?"],
1413
+ [
1414
+ "ask create incident Database Is Down",
1415
+ "create incident Database Is Down",
1416
+ ],
1417
+ ])(
1418
+ "%j: the AI assistant gets the question after 'ask', as typed",
1419
+ async (text: string, question: string) => {
1420
+ const activity: JSONObject = personalMessage(text);
1421
+
1422
+ const turn: FakeTurn = await deliver(activity);
1423
+
1424
+ expect(stubs.askAssistant).toHaveBeenCalledTimes(1);
1425
+ expect(stubs.askAssistant).toHaveBeenCalledWith({
1426
+ activity: activity,
1427
+ turnContext: turn.turnContext,
1428
+ projectId: PROJECT_ID,
1429
+ question: question,
1430
+ });
1431
+ expect(stubs.createIncident).not.toHaveBeenCalled();
1432
+ // The assistant answers for itself; the message handler adds nothing.
1433
+ expect(turn.attempted).toEqual([]);
1434
+ },
1435
+ );
1436
+
1437
+ test("a bare 'ask': the help text, and the assistant is not asked anything", async () => {
1438
+ const turn: FakeTurn = await deliver(personalMessage("ask"));
1439
+
1440
+ expect(turn.attempted).toEqual([HELP_TEXT]);
1441
+ expectNoCommandRan();
1442
+ });
1443
+ });
1444
+
1445
+ describe("a redelivered activity is handled once (#4111's second bubble)", () => {
1446
+ test("the same activity delivered twice: the second delivery sends nothing and does not run the command again", async () => {
1447
+ const activity: JSONObject = personalMessage(
1448
+ "create incident Database Is Down",
1449
+ );
1450
+
1451
+ await deliver(activity);
1452
+ const redelivery: FakeTurn = await deliver(activity);
1453
+
1454
+ expect(stubs.createIncident).toHaveBeenCalledTimes(1);
1455
+ expect(redelivery.attempted).toEqual([]);
1456
+ // Dropped before anything else runs: no second tenant lookup either.
1457
+ expect(stubs.resolveTenant).toHaveBeenCalledTimes(1);
1458
+ expect(stubs.claim).toHaveBeenCalledTimes(2);
1459
+ await expect(stubs.claim.mock.results[0]?.value).resolves.toBe(true);
1460
+ await expect(stubs.claim.mock.results[1]?.value).resolves.toBe(false);
1461
+ });
1462
+
1463
+ /*
1464
+ * A claim made while Redis was down never reached Redis. If Redis is back
1465
+ * by the time Teams redelivers, Redis alone would take the redelivery for a
1466
+ * first delivery; this process remembers the claim, and that decides.
1467
+ */
1468
+ test("Redis comes back between a delivery and its redelivery: the redelivery is still dropped, and Redis is told of the claim", async () => {
1469
+ const activity: JSONObject = personalMessage(
1470
+ "create incident Database Is Down",
1471
+ );
1472
+
1473
+ await deliver(activity);
1474
+ // Back, and it never heard of the first delivery.
1475
+ stubs.redisClaim.mockResolvedValue(true);
1476
+ const redelivery: FakeTurn = await deliver(activity);
1477
+
1478
+ expect(redelivery.attempted).toEqual([]);
1479
+ expect(stubs.createIncident).toHaveBeenCalledTimes(1);
1480
+ await expect(stubs.claim.mock.results[1]?.value).resolves.toBe(false);
1481
+ // Asked all the same, so that another App instance drops it too.
1482
+ expect(stubs.redisClaim).toHaveBeenCalledTimes(2);
1483
+ expect(stubs.redisClaim).toHaveBeenLastCalledWith(
1484
+ "microsoft-teams-inbound-activity",
1485
+ MicrosoftTeamsActivityDeduplicator.getClaimKey(activity),
1486
+ "1",
1487
+ { expiresInSeconds: 600 },
1488
+ );
1489
+ });
1490
+
1491
+ test("an activity another App instance claimed first (Redis answers that it is taken): nothing is sent, and nothing runs", async () => {
1492
+ stubs.redisClaim.mockResolvedValue(false);
1493
+ const activity: JSONObject = personalMessage(
1494
+ "create incident Database Is Down",
1495
+ );
1496
+
1497
+ const turn: FakeTurn = await deliver(activity);
1498
+
1499
+ expect(turn.attempted).toEqual([]);
1500
+ expect(stubs.resolveTenant).not.toHaveBeenCalled();
1501
+ expectNoCommandRan();
1502
+ });
1503
+
1504
+ test("the same 'help' delivered twice is answered once", async () => {
1505
+ const activity: JSONObject = personalMessage("help");
1506
+
1507
+ const first: FakeTurn = await deliver(activity);
1508
+ const redelivery: FakeTurn = await deliver(activity);
1509
+
1510
+ expect(first.delivered).toEqual([HELP_TEXT]);
1511
+ expect(redelivery.attempted).toEqual([]);
1512
+ });
1513
+
1514
+ /*
1515
+ * Teams delivers an activity again when the bot has not answered it within
1516
+ * 15 seconds, so the retry arrives while the first delivery is still being
1517
+ * handled. Only claiming the id BEFORE the work (not remembering it once
1518
+ * handled) drops that retry.
1519
+ */
1520
+ test("Teams retries a slow 'create incident' that is still being handled: the retry sends nothing, and the form is sent once", async () => {
1521
+ const formReply: string = "📝 Create New Incident (form)";
1522
+ const release: Gate = createGate();
1523
+ const commandEntered: Gate = createGate();
1524
+ stubs.createIncident
1525
+ .mockImplementation(async (data: CreateCommandInput): Promise<void> => {
1526
+ await data.turnContext.sendActivity(formReply);
1527
+ })
1528
+ .mockImplementationOnce(
1529
+ async (data: CreateCommandInput): Promise<void> => {
1530
+ commandEntered.open();
1531
+ await release.opened;
1532
+ await data.turnContext.sendActivity(formReply);
1533
+ },
1534
+ );
1535
+ const activity: JSONObject = personalMessage(
1536
+ "create incident Database Is Down",
1537
+ );
1538
+ const first: FakeTurn = buildTurn({ activity: activity });
1539
+ let firstSettled: boolean = false;
1540
+ const firstDelivery: Promise<void> =
1541
+ MicrosoftTeamsUtil.handleBotMessageActivity({
1542
+ activity: activity,
1543
+ turnContext: first.turnContext,
1544
+ }).then(() => {
1545
+ firstSettled = true;
1546
+ });
1547
+
1548
+ let retry: FakeTurn | undefined = undefined;
1549
+ let firstSettledWhenRetryWasDone: boolean = true;
1550
+ try {
1551
+ // Also settles if the first delivery ends without reaching the command.
1552
+ await Promise.race([commandEntered.opened, firstDelivery]);
1553
+ retry = await deliver(activity);
1554
+ firstSettledWhenRetryWasDone = firstSettled;
1555
+ } finally {
1556
+ release.open();
1557
+ await firstDelivery;
1558
+ }
1559
+
1560
+ expect(firstSettledWhenRetryWasDone).toBe(false);
1561
+ expect(retry?.attempted).toEqual([]);
1562
+ expect(first.delivered).toEqual([formReply]);
1563
+ expect(stubs.createIncident).toHaveBeenCalledTimes(1);
1564
+ expect(stubs.resolveTenant).toHaveBeenCalledTimes(1);
1565
+ });
1566
+
1567
+ test("Teams retries a slow form submit that is still being handled: the submit runs once, so one incident", async () => {
1568
+ const confirmation: string = "✅ Incident created successfully!";
1569
+ const release: Gate = createGate();
1570
+ const submitEntered: Gate = createGate();
1571
+ const invokeHandler: SpyInstance<
1572
+ typeof MicrosoftTeamsUtil.handleBotInvokeActivity
1573
+ > = jest
1574
+ .spyOn(MicrosoftTeamsUtil, "handleBotInvokeActivity")
1575
+ .mockImplementation(async (data: InvokeHandlerInput): Promise<void> => {
1576
+ await data.turnContext.sendActivity(confirmation);
1577
+ })
1578
+ .mockImplementationOnce(
1579
+ async (data: InvokeHandlerInput): Promise<void> => {
1580
+ submitEntered.open();
1581
+ await release.opened;
1582
+ await data.turnContext.sendActivity(confirmation);
1583
+ },
1584
+ );
1585
+ const activity: JSONObject = cardSubmit({
1586
+ action: MicrosoftTeamsIncidentActionType.SubmitNewIncident,
1587
+ incidentTitle: "Database Is Down",
1588
+ });
1589
+ const first: FakeTurn = buildTurn({ activity: activity });
1590
+ const firstDelivery: Promise<void> =
1591
+ MicrosoftTeamsUtil.handleBotMessageActivity({
1592
+ activity: activity,
1593
+ turnContext: first.turnContext,
1594
+ });
1595
+
1596
+ let retry: FakeTurn | undefined = undefined;
1597
+ try {
1598
+ // Also settles if the first delivery ends without reaching the submit.
1599
+ await Promise.race([submitEntered.opened, firstDelivery]);
1600
+ retry = await deliver(activity);
1601
+ } finally {
1602
+ release.open();
1603
+ await firstDelivery;
1604
+ }
1605
+
1606
+ expect(retry?.attempted).toEqual([]);
1607
+ expect(first.delivered).toEqual([confirmation]);
1608
+ expect(invokeHandler).toHaveBeenCalledTimes(1);
1609
+ });
1610
+
1611
+ test("the same activity id in another conversation is another activity: both are handled", async () => {
1612
+ const activity: JSONObject = personalMessage(
1613
+ "create incident Database Is Down",
1614
+ );
1615
+ const sameIdElsewhere: JSONObject = personalMessage(
1616
+ "create incident Database Is Down",
1617
+ {
1618
+ id: idOf(activity),
1619
+ conversation: {
1620
+ conversationType: "personal",
1621
+ id: OTHER_PERSONAL_CHAT_ID,
1622
+ tenantId: TENANT_ID,
1623
+ },
1624
+ },
1625
+ );
1626
+
1627
+ await deliver(activity);
1628
+ await deliver(sameIdElsewhere);
1629
+
1630
+ expect(stubs.createIncident).toHaveBeenCalledTimes(2);
1631
+ expect(stubs.createIncident.mock.calls[0]?.[0].activity).toBe(activity);
1632
+ expect(stubs.createIncident.mock.calls[1]?.[0].activity).toBe(
1633
+ sameIdElsewhere,
1634
+ );
1635
+ });
1636
+
1637
+ test("an activity without an id is handled every time", async () => {
1638
+ const activity: JSONObject = personalMessage("help");
1639
+ delete activity["id"];
1640
+
1641
+ const turns: Array<FakeTurn> = [
1642
+ await deliver(activity),
1643
+ await deliver(activity),
1644
+ await deliver(activity),
1645
+ ];
1646
+
1647
+ expect(
1648
+ turns.map((turn: FakeTurn) => {
1649
+ return turn.delivered;
1650
+ }),
1651
+ ).toEqual([[HELP_TEXT], [HELP_TEXT], [HELP_TEXT]]);
1652
+ });
1653
+
1654
+ test("a card submit (value.action) delivered twice runs handleBotInvokeActivity once", async () => {
1655
+ const invokeHandler: SpyInstance<
1656
+ typeof MicrosoftTeamsUtil.handleBotInvokeActivity
1657
+ > = jest
1658
+ .spyOn(MicrosoftTeamsUtil, "handleBotInvokeActivity")
1659
+ .mockResolvedValue(undefined);
1660
+ const activity: JSONObject = cardSubmit({
1661
+ action: MicrosoftTeamsIncidentActionType.SubmitNewIncident,
1662
+ incidentTitle: "Database Is Down",
1663
+ });
1664
+
1665
+ const first: FakeTurn = await deliver(activity);
1666
+ const redelivery: FakeTurn = await deliver(activity);
1667
+
1668
+ expect(invokeHandler).toHaveBeenCalledTimes(1);
1669
+ expect(invokeHandler).toHaveBeenCalledWith({
1670
+ activity: activity,
1671
+ turnContext: first.turnContext,
1672
+ });
1673
+ expect(redelivery.attempted).toEqual([]);
1674
+ // A submit is not a command: the text path never runs.
1675
+ expect(stubs.resolveTenant).not.toHaveBeenCalled();
1676
+ expectNoCommandRan();
1677
+ });
1678
+
1679
+ test("a card submit whose action is under value.data, in a channel without an @mention, is handled and claimed", async () => {
1680
+ const invokeHandler: SpyInstance<
1681
+ typeof MicrosoftTeamsUtil.handleBotInvokeActivity
1682
+ > = jest
1683
+ .spyOn(MicrosoftTeamsUtil, "handleBotInvokeActivity")
1684
+ .mockResolvedValue(undefined);
1685
+ const activity: JSONObject = channelMessage({
1686
+ text: "",
1687
+ mention: "nobody",
1688
+ id: nextActivityId(),
1689
+ value: { data: acknowledgeIncidentValue() },
1690
+ });
1691
+
1692
+ await deliver(activity);
1693
+ await deliver(activity);
1694
+
1695
+ expect(stubs.claim).toHaveBeenCalledTimes(2);
1696
+ expect(invokeHandler).toHaveBeenCalledTimes(1);
1697
+ });
1698
+
1699
+ const UNANSWERED_MESSAGES: Array<
1700
+ [
1701
+ string,
1702
+ "chat" | "team",
1703
+ (id: string) => JSONObject,
1704
+ (id: string) => JSONObject,
1705
+ ]
1706
+ > = [
1707
+ [
1708
+ "a channel message that does not @mention the bot",
1709
+ "team",
1710
+ (id: string): JSONObject => {
1711
+ return channelMessage({ text: "help", mention: "nobody", id: id });
1712
+ },
1713
+ (id: string): JSONObject => {
1714
+ return channelMessage({ text: "help", mention: "the bot", id: id });
1715
+ },
1716
+ ],
1717
+ [
1718
+ "a channel message that @mentions someone else",
1719
+ "team",
1720
+ (id: string): JSONObject => {
1721
+ return channelMessage({
1722
+ text: "help",
1723
+ mention: "someone else",
1724
+ id: id,
1725
+ });
1726
+ },
1727
+ (id: string): JSONObject => {
1728
+ return channelMessage({ text: "help", mention: "the bot", id: id });
1729
+ },
1730
+ ],
1731
+ [
1732
+ "a group chat message that does not @mention the bot",
1733
+ "chat",
1734
+ (id: string): JSONObject => {
1735
+ return groupChatMessage({ text: "help", mention: "nobody", id: id });
1736
+ },
1737
+ (id: string): JSONObject => {
1738
+ return groupChatMessage({ text: "help", mention: "the bot", id: id });
1739
+ },
1740
+ ],
1741
+ ];
1742
+
1743
+ test.each(UNANSWERED_MESSAGES)(
1744
+ "%s is ignored without claiming its id: a delivery with that id that @mentions the bot is still answered",
1745
+ async (
1746
+ _message: string,
1747
+ backfill: "chat" | "team",
1748
+ buildIgnored: (id: string) => JSONObject,
1749
+ buildMentioned: (id: string) => JSONObject,
1750
+ ) => {
1751
+ const activityId: string = nextActivityId();
1752
+
1753
+ const ignored: FakeTurn = await deliver(buildIgnored(activityId));
1754
+
1755
+ expect(ignored.attempted).toEqual([]);
1756
+ // The install backfill runs for every message, answered or not.
1757
+ expect(
1758
+ backfill === "team" ? stubs.captureTeam : stubs.captureChat,
1759
+ ).toHaveBeenCalledTimes(1);
1760
+ expect(stubs.claim).not.toHaveBeenCalled();
1761
+ expect(stubs.resolveTenant).not.toHaveBeenCalled();
1762
+
1763
+ const mentioned: JSONObject = buildMentioned(activityId);
1764
+ const answered: FakeTurn = await deliver(mentioned);
1765
+
1766
+ expect(stubs.claim).toHaveBeenCalledTimes(1);
1767
+ expect(stubs.claim).toHaveBeenCalledWith(mentioned);
1768
+ expect(answered.delivered).toEqual([HELP_TEXT]);
1769
+ },
1770
+ );
1771
+
1772
+ test.each([
1773
+ ["the bot itself", { id: BOT_ID, name: "OneUptime" }],
1774
+ [
1775
+ "another bot",
1776
+ { id: "28:9f8e7d6c-build-bot", name: "Build Bot", role: "bot" },
1777
+ ],
1778
+ ])(
1779
+ "a message from %s is ignored before anything is claimed",
1780
+ async (_sender: string, from: JSONObject) => {
1781
+ const activity: JSONObject = personalMessage("help", { from: from });
1782
+
1783
+ const turn: FakeTurn = await deliver(activity);
1784
+
1785
+ expect(turn.attempted).toEqual([]);
1786
+ expect(stubs.claim).not.toHaveBeenCalled();
1787
+ expect(stubs.captureChat).not.toHaveBeenCalled();
1788
+ expect(stubs.resolveTenant).not.toHaveBeenCalled();
1789
+ },
1790
+ );
1791
+ });
1792
+
1793
+ describe("'show ...' replies are kept within what Teams accepts", () => {
1794
+ /*
1795
+ * A reply the size of a big project's maintenance list: every line ends in
1796
+ * a character that is not white space, so a cut at a line break is visible
1797
+ * as the character right after the kept text.
1798
+ */
1799
+ function buildHugeShowReply(): string {
1800
+ const blocks: Array<string> = [];
1801
+
1802
+ for (let index: number = 1; index <= 400; index++) {
1803
+ blocks.push(
1804
+ `🛠️ **[Scheduled Maintenance SM-${index}: Patch window ${index}](${DASHBOARD_URL}/${PROJECT_ID.toString()}/scheduled-maintenance-events/${index})**\n` +
1805
+ `• **Status:** Scheduled\n` +
1806
+ `• **Affected Services:** Checkout API ${index}, Payments ${index}, Search ${index} and 250 more\n`,
1807
+ );
1808
+ }
1809
+
1810
+ return `**Scheduled Maintenance Events:**\n\n${blocks.join("\n")}`;
1811
+ }
1812
+
1813
+ const SHOW_COMMANDS: Array<[string, ShowReplyBuilderName]> = [
1814
+ ["show active incidents", "getActiveIncidentsMessage"],
1815
+ ["show scheduled maintenance", "getScheduledMaintenanceMessage"],
1816
+ ["show ongoing maintenance", "getOngoingMaintenanceMessage"],
1817
+ ["show active alerts", "getActiveAlertsMessage"],
1818
+ ];
1819
+
1820
+ test.each(SHOW_COMMANDS)(
1821
+ "%j: a reply over the budget is cut at a line break, fits in 40 KiB of UTF-16 and ends with the shortened note",
1822
+ async (command: string, builderName: ShowReplyBuilderName) => {
1823
+ const hugeReply: string = buildHugeShowReply();
1824
+ expect(hugeReply.length * 2).toBeGreaterThan(
1825
+ MICROSOFT_TEAMS_TEXT_MESSAGE_BUDGET_IN_BYTES * 2,
1826
+ );
1827
+ stubs.showReplies[builderName].mockResolvedValue(hugeReply);
1828
+ const fitText: SpyInstance<
1829
+ typeof MicrosoftTeamsMessageSize.fitTextToBudget
1830
+ > = jest.spyOn(MicrosoftTeamsMessageSize, "fitTextToBudget");
1831
+
1832
+ const turn: FakeTurn = await deliver(personalMessage(command));
1833
+
1834
+ expect(stubs.showReplies[builderName]).toHaveBeenCalledWith(PROJECT_ID);
1835
+ expect(fitText).toHaveBeenCalledWith({ text: hugeReply });
1836
+ expect(turn.delivered).toHaveLength(1);
1837
+
1838
+ const sent: string = textOf(turn.delivered[0]);
1839
+ expect(sent.length * 2).toBeLessThanOrEqual(
1840
+ MICROSOFT_TEAMS_TEXT_MESSAGE_BUDGET_IN_BYTES,
1841
+ );
1842
+ // Not cut to a stub: all but a line and the note is kept.
1843
+ expect(sent.length * 2).toBeGreaterThan(
1844
+ MICROSOFT_TEAMS_TEXT_MESSAGE_BUDGET_IN_BYTES - 1024,
1845
+ );
1846
+ expect(sent.endsWith(SHORTENED_NOTE)).toBe(true);
1847
+
1848
+ const kept: string = sent.substring(
1849
+ 0,
1850
+ sent.length - SHORTENED_NOTE.length,
1851
+ );
1852
+ expect(hugeReply.startsWith(kept)).toBe(true);
1853
+ expect(hugeReply.charAt(kept.length)).toBe("\n");
1854
+ },
1855
+ );
1856
+
1857
+ test("a reply within the budget is sent exactly as it was built", async () => {
1858
+ const reply: string =
1859
+ "**Active Alerts:**\n\n🔴 **[Alert #12: CPU above 90%](https://oneuptime.example.com/alerts/12)**\n• **Status:** Created\n";
1860
+ stubs.showReplies.getActiveAlertsMessage.mockResolvedValue(reply);
1861
+
1862
+ const turn: FakeTurn = await deliver(personalMessage("show active alerts"));
1863
+
1864
+ expect(turn.delivered).toEqual([reply]);
1865
+ });
1866
+ });
1867
+
1868
+ /*
1869
+ * The real "show ..." builders, with only their database reads stubbed: an
1870
+ * incident or maintenance event can cover hundreds of monitors, and master
1871
+ * listed every name, one of the ways a reply grew past what Teams accepts.
1872
+ */
1873
+ describe("'show ...' replies name ten affected monitors, then how many more", () => {
1874
+ const MONITOR_NAMES: Array<string> = numberedNames(1500);
1875
+
1876
+ const AFFECTED_SERVICES_LINE: string = `• **Affected Services:** ${MONITOR_NAMES.slice(
1877
+ 0,
1878
+ 10,
1879
+ ).join(", ")} and 1490 more\n`;
1880
+
1881
+ // The project each stubbed findBy was asked about.
1882
+ let queriedProjectIds: Array<string> = [];
1883
+
1884
+ beforeEach(() => {
1885
+ queriedProjectIds = [];
1886
+ });
1887
+
1888
+ function recordQueriedProject(findBy: unknown): void {
1889
+ queriedProjectIds.push(
1890
+ String(
1891
+ (findBy as { query?: { projectId?: unknown } | undefined }).query
1892
+ ?.projectId,
1893
+ ),
1894
+ );
1895
+ }
1896
+
1897
+ function stubOneActiveIncident(): void {
1898
+ const incidentId: ObjectID = ObjectID.generate();
1899
+
1900
+ jest
1901
+ .spyOn(IncidentStateService, "getUnresolvedIncidentStates")
1902
+ .mockResolvedValue([
1903
+ {
1904
+ id: ObjectID.generate(),
1905
+ name: "Identified",
1906
+ } as unknown as IncidentState,
1907
+ ]);
1908
+ jest
1909
+ .spyOn(IncidentService, "findBy")
1910
+ .mockImplementation((findBy: unknown) => {
1911
+ recordQueriedProject(findBy);
1912
+ return Promise.resolve([
1913
+ {
1914
+ id: incidentId,
1915
+ incidentNumber: 42,
1916
+ title: "Checkout is down",
1917
+ currentIncidentState: { name: "Identified" },
1918
+ incidentSeverity: { name: "Critical" },
1919
+ declaredAt: new Date("2026-09-29T08:00:00.000Z"),
1920
+ monitors: monitorsNamed(MONITOR_NAMES),
1921
+ } as unknown as Incident,
1922
+ ]);
1923
+ });
1924
+ jest
1925
+ .spyOn(IncidentService, "getIncidentLinkInDashboard")
1926
+ .mockResolvedValue(
1927
+ URL.fromString(
1928
+ `${DASHBOARD_URL}/${PROJECT_ID.toString()}/incidents/${incidentId.toString()}`,
1929
+ ),
1930
+ );
1931
+ }
1932
+
1933
+ function stubOneMaintenanceEvent(): void {
1934
+ const eventId: ObjectID = ObjectID.generate();
1935
+
1936
+ jest
1937
+ .spyOn(ScheduledMaintenanceService, "findBy")
1938
+ .mockImplementation((findBy: unknown) => {
1939
+ recordQueriedProject(findBy);
1940
+ return Promise.resolve([
1941
+ {
1942
+ id: eventId,
1943
+ scheduledMaintenanceNumber: 7,
1944
+ title: "Database failover drill",
1945
+ startsAt: new Date("2026-10-01T14:00:00.000Z"),
1946
+ endsAt: new Date("2026-10-01T16:00:00.000Z"),
1947
+ currentScheduledMaintenanceState: { name: "Scheduled" },
1948
+ monitors: monitorsNamed(MONITOR_NAMES),
1949
+ } as unknown as ScheduledMaintenance,
1950
+ ]);
1951
+ });
1952
+ jest
1953
+ .spyOn(
1954
+ ScheduledMaintenanceService,
1955
+ "getScheduledMaintenanceLinkInDashboard",
1956
+ )
1957
+ .mockResolvedValue(
1958
+ URL.fromString(
1959
+ `${DASHBOARD_URL}/${PROJECT_ID.toString()}/scheduled-maintenance-events/${eventId.toString()}`,
1960
+ ),
1961
+ );
1962
+ }
1963
+
1964
+ const COMMANDS: Array<[string, ShowReplyBuilderName, () => void]> = [
1965
+ [
1966
+ "show active incidents",
1967
+ "getActiveIncidentsMessage",
1968
+ stubOneActiveIncident,
1969
+ ],
1970
+ [
1971
+ "show scheduled maintenance",
1972
+ "getScheduledMaintenanceMessage",
1973
+ stubOneMaintenanceEvent,
1974
+ ],
1975
+ [
1976
+ "show ongoing maintenance",
1977
+ "getOngoingMaintenanceMessage",
1978
+ stubOneMaintenanceEvent,
1979
+ ],
1980
+ ];
1981
+
1982
+ test.each(COMMANDS)(
1983
+ "%j, one item with 1500 monitors: the first ten names and 'and 1490 more', unshortened",
1984
+ async (
1985
+ command: string,
1986
+ builderName: ShowReplyBuilderName,
1987
+ stubReads: () => void,
1988
+ ) => {
1989
+ stubs.showReplies[builderName].mockRestore();
1990
+ stubReads();
1991
+
1992
+ const turn: FakeTurn = await deliver(personalMessage(command));
1993
+
1994
+ // Read for the project the tenant resolved to, and no other.
1995
+ expect(queriedProjectIds).toEqual([PROJECT_ID.toString()]);
1996
+ expect(turn.delivered).toHaveLength(1);
1997
+ const reply: string = textOf(turn.delivered[0]);
1998
+ expect(reply).toContain(AFFECTED_SERVICES_LINE);
1999
+ expect(reply).not.toContain("Monitor 0011");
2000
+ expect(reply.endsWith(SHORTENED_NOTE)).toBe(false);
2001
+ },
2002
+ );
2003
+ });
2004
+
2005
+ describe("MicrosoftTeamsUtil.formatAffectedMonitorNames", () => {
2006
+ test("no monitors: an empty string", () => {
2007
+ expect(MicrosoftTeamsUtil.formatAffectedMonitorNames([])).toBe("");
2008
+ });
2009
+
2010
+ test("one monitor: its name", () => {
2011
+ expect(
2012
+ MicrosoftTeamsUtil.formatAffectedMonitorNames(
2013
+ monitorsNamed(["Checkout API"]),
2014
+ ),
2015
+ ).toBe("Checkout API");
2016
+ });
2017
+
2018
+ test("ten monitors: all ten names, nothing about more", () => {
2019
+ const names: Array<string> = numberedNames(10);
2020
+
2021
+ expect(
2022
+ MicrosoftTeamsUtil.formatAffectedMonitorNames(monitorsNamed(names)),
2023
+ ).toBe(names.join(", "));
2024
+ });
2025
+
2026
+ test("eleven monitors: the first ten, then 'and 1 more'", () => {
2027
+ const names: Array<string> = numberedNames(11);
2028
+
2029
+ expect(
2030
+ MicrosoftTeamsUtil.formatAffectedMonitorNames(monitorsNamed(names)),
2031
+ ).toBe(`${names.slice(0, 10).join(", ")} and 1 more`);
2032
+ });
2033
+
2034
+ test("1500 monitors: the first ten, then 'and 1490 more', in one short line", () => {
2035
+ const names: Array<string> = numberedNames(1500);
2036
+
2037
+ const line: string = MicrosoftTeamsUtil.formatAffectedMonitorNames(
2038
+ monitorsNamed(names),
2039
+ );
2040
+
2041
+ expect(line).toBe(`${names.slice(0, 10).join(", ")} and 1490 more`);
2042
+ expect(line).not.toContain("Monitor 0011");
2043
+ expect(line.length).toBeLessThan(200);
2044
+ });
2045
+
2046
+ test("monitors without a name are skipped, and are not counted as more", () => {
2047
+ expect(
2048
+ MicrosoftTeamsUtil.formatAffectedMonitorNames(
2049
+ monitorsNamed(["", "Checkout API", undefined, "Payments", ""]),
2050
+ ),
2051
+ ).toBe("Checkout API, Payments");
2052
+
2053
+ const tenNamed: Array<string> = numberedNames(10);
2054
+ expect(
2055
+ MicrosoftTeamsUtil.formatAffectedMonitorNames(
2056
+ monitorsNamed([...tenNamed, "", undefined, "", undefined, ""]),
2057
+ ),
2058
+ ).toBe(tenNamed.join(", "));
2059
+
2060
+ const twelveNamed: Array<string> = numberedNames(12);
2061
+ expect(
2062
+ MicrosoftTeamsUtil.formatAffectedMonitorNames(
2063
+ monitorsNamed([
2064
+ "",
2065
+ ...twelveNamed.slice(0, 5),
2066
+ undefined,
2067
+ ...twelveNamed.slice(5),
2068
+ "",
2069
+ ]),
2070
+ ),
2071
+ ).toBe(`${twelveNamed.slice(0, 10).join(", ")} and 2 more`);
2072
+ });
2073
+ });
2074
+
2075
+ describe("MicrosoftTeamsUtil.getUnexpectedErrorMessage", () => {
2076
+ test("with an activity id: names it as the reference to look for in the server logs", () => {
2077
+ expect(
2078
+ MicrosoftTeamsUtil.getUnexpectedErrorMessage({ id: "1727712345678" }),
2079
+ ).toBe(
2080
+ `${UNEXPECTED_ERROR_TEXT} If it keeps happening, ask your OneUptime administrator to look for reference 1727712345678 in the server logs.`,
2081
+ );
2082
+ });
2083
+
2084
+ test("without an activity id, or with an empty one: no reference sentence", () => {
2085
+ expect(MicrosoftTeamsUtil.getUnexpectedErrorMessage({})).toBe(
2086
+ UNEXPECTED_ERROR_TEXT,
2087
+ );
2088
+ expect(MicrosoftTeamsUtil.getUnexpectedErrorMessage({ id: "" })).toBe(
2089
+ UNEXPECTED_ERROR_TEXT,
2090
+ );
2091
+ });
2092
+ });
2093
+
2094
+ describe("MicrosoftTeamsUtil.recoverFromFailedTurn: the turn ends normally, with at most one reply", () => {
2095
+ test("a message that got no reply yet: one generic reply naming the activity id, and the error is logged with it", async () => {
2096
+ const errorLog: SpyInstance<typeof logger.error> = silenceErrorLog();
2097
+ const activity: JSONObject = personalMessage("create incident");
2098
+ const turn: FakeTurn = buildTurn({ activity: activity });
2099
+ const bug: Error = new Error("simulated handler bug");
2100
+
2101
+ await expect(
2102
+ MicrosoftTeamsUtil.recoverFromFailedTurn({
2103
+ turnContext: turn.turnContext,
2104
+ error: bug,
2105
+ }),
2106
+ ).resolves.toBeUndefined();
2107
+
2108
+ expect(turn.delivered).toEqual([
2109
+ MicrosoftTeamsUtil.getUnexpectedErrorMessage(activity),
2110
+ ]);
2111
+ // The line carries the reference the reply gives; the error, its stack.
2112
+ expectFailureLogged(errorLog, {
2113
+ line: `Microsoft Teams message activity ${idOf(activity)} failed: Error: simulated handler bug`,
2114
+ error: bug,
2115
+ attributes: undefined,
2116
+ });
2117
+ });
2118
+
2119
+ test("a message that was already answered: nothing more is sent, and the failure is still logged", async () => {
2120
+ const errorLog: SpyInstance<typeof logger.error> = silenceErrorLog();
2121
+ const activity: JSONObject = personalMessage("create incident");
2122
+ const turn: FakeTurn = buildTurn({
2123
+ activity: activity,
2124
+ responded: true,
2125
+ });
2126
+ const bug: Error = new Error("simulated failure after the reply");
2127
+
2128
+ await MicrosoftTeamsUtil.recoverFromFailedTurn({
2129
+ turnContext: turn.turnContext,
2130
+ error: bug,
2131
+ });
2132
+
2133
+ expect(turn.attempted).toEqual([]);
2134
+ expectFailureLogged(errorLog, {
2135
+ line: `Microsoft Teams message activity ${idOf(activity)} failed: Error: simulated failure after the reply`,
2136
+ error: bug,
2137
+ attributes: undefined,
2138
+ });
2139
+ });
2140
+
2141
+ test("a Bot Framework HTTP error: one log line with its status and code, not the error object, which carries the whole request", async () => {
2142
+ const errorLog: SpyInstance<typeof logger.error> = silenceErrorLog();
2143
+ const activity: JSONObject = personalMessage("create incident");
2144
+ const turn: FakeTurn = buildTurn({ activity: activity });
2145
+
2146
+ await MicrosoftTeamsUtil.recoverFromFailedTurn({
2147
+ turnContext: turn.turnContext,
2148
+ error: connectorUnavailable(),
2149
+ });
2150
+
2151
+ expect(turn.delivered).toEqual([
2152
+ MicrosoftTeamsUtil.getUnexpectedErrorMessage(activity),
2153
+ ]);
2154
+ expect(errorLogCalls(errorLog)).toStrictEqual([
2155
+ [
2156
+ `Microsoft Teams message activity ${idOf(activity)} failed: RestError 503 ServiceUnavailable: The service is temporarily unavailable.`,
2157
+ undefined,
2158
+ ],
2159
+ ]);
2160
+ });
2161
+
2162
+ test("an invoke: the invoke response Teams waits for, status 200, and no message", async () => {
2163
+ const errorLog: SpyInstance<typeof logger.error> = silenceErrorLog();
2164
+ const activity: JSONObject = invokeActivity(acknowledgeIncidentValue());
2165
+ const turn: FakeTurn = buildTurn({ activity: activity });
2166
+ const failure: Error = new Error("simulated invoke failure");
2167
+
2168
+ await MicrosoftTeamsUtil.recoverFromFailedTurn({
2169
+ turnContext: turn.turnContext,
2170
+ error: failure,
2171
+ });
2172
+
2173
+ expect(turn.attempted).toEqual([
2174
+ { type: "invokeResponse", value: { status: 200 } },
2175
+ ]);
2176
+ expectFailureLogged(errorLog, {
2177
+ line: `Microsoft Teams invoke activity ${idOf(activity)} failed: Error: simulated invoke failure`,
2178
+ error: failure,
2179
+ attributes: undefined,
2180
+ });
2181
+ });
2182
+
2183
+ /*
2184
+ * turnContext.responded turns true with any reply, but only an
2185
+ * invokeResponse answers the invoke itself: without one, CloudAdapter
2186
+ * answers Teams HTTP 501.
2187
+ */
2188
+ test("an invoke whose handler already posted a message still gets the invoke response", async () => {
2189
+ silenceErrorLog();
2190
+ const turn: FakeTurn = buildTurn({
2191
+ activity: invokeActivity(acknowledgeIncidentValue()),
2192
+ responded: true,
2193
+ });
2194
+
2195
+ await MicrosoftTeamsUtil.recoverFromFailedTurn({
2196
+ turnContext: turn.turnContext,
2197
+ error: new Error("simulated failure after the reply"),
2198
+ });
2199
+
2200
+ expect(turn.attempted).toEqual([
2201
+ { type: "invokeResponse", value: { status: 200 } },
2202
+ ]);
2203
+ });
2204
+
2205
+ test.each(["conversationUpdate", "installationUpdate", "messageReaction"])(
2206
+ "activity type %j: logged only, nothing is sent",
2207
+ async (activityType: string) => {
2208
+ const errorLog: SpyInstance<typeof logger.error> = silenceErrorLog();
2209
+ const activity: JSONObject = personalMessage("", { type: activityType });
2210
+ const turn: FakeTurn = buildTurn({ activity: activity });
2211
+ const failure: Error = new Error("simulated capture failure");
2212
+
2213
+ await MicrosoftTeamsUtil.recoverFromFailedTurn({
2214
+ turnContext: turn.turnContext,
2215
+ error: failure,
2216
+ });
2217
+
2218
+ expect(turn.attempted).toEqual([]);
2219
+ expectFailureLogged(errorLog, {
2220
+ line: `Microsoft Teams ${activityType} activity ${idOf(activity)} failed: Error: simulated capture failure`,
2221
+ error: failure,
2222
+ attributes: undefined,
2223
+ });
2224
+ },
2225
+ );
2226
+
2227
+ test("a turn with no activity at all: logged only, nothing is sent, nothing is thrown", async () => {
2228
+ silenceErrorLog();
2229
+ const turn: FakeTurn = buildTurn({ activity: undefined });
2230
+
2231
+ await expect(
2232
+ MicrosoftTeamsUtil.recoverFromFailedTurn({
2233
+ turnContext: turn.turnContext,
2234
+ error: "a thrown string",
2235
+ }),
2236
+ ).resolves.toBeUndefined();
2237
+
2238
+ expect(turn.attempted).toEqual([]);
2239
+ });
2240
+
2241
+ test("the generic reply cannot be sent: nothing is thrown", async () => {
2242
+ silenceErrorLog();
2243
+ const turn: FakeTurn = buildTurn({
2244
+ activity: personalMessage("create incident"),
2245
+ send: "refuse-all",
2246
+ sendError: connectorUnavailable(),
2247
+ });
2248
+
2249
+ await expect(
2250
+ MicrosoftTeamsUtil.recoverFromFailedTurn({
2251
+ turnContext: turn.turnContext,
2252
+ error: new Error("simulated handler bug"),
2253
+ }),
2254
+ ).resolves.toBeUndefined();
2255
+
2256
+ expect(turn.attempted).toHaveLength(1);
2257
+ expect(turn.delivered).toEqual([]);
2258
+ });
2259
+
2260
+ test("the invoke response cannot be sent: nothing is thrown", async () => {
2261
+ silenceErrorLog();
2262
+ const turn: FakeTurn = buildTurn({
2263
+ activity: invokeActivity(acknowledgeIncidentValue()),
2264
+ send: "refuse-all",
2265
+ sendError: connectorUnavailable(),
2266
+ });
2267
+
2268
+ await expect(
2269
+ MicrosoftTeamsUtil.recoverFromFailedTurn({
2270
+ turnContext: turn.turnContext,
2271
+ error: new Error("simulated invoke failure"),
2272
+ }),
2273
+ ).resolves.toBeUndefined();
2274
+
2275
+ expect(turn.attempted).toHaveLength(1);
2276
+ });
2277
+ });
2278
+
2279
+ describe("handleBotInvokeActivity: a failed action gets one reply that says why, and never throws", () => {
2280
+ const SETTINGS_LINK_TEXT: string = `[OneUptime → User Settings → Microsoft Teams](${DASHBOARD_URL}/${PROJECT_ID.toString()}/user-settings/microsoft-teams-integration)`;
2281
+
2282
+ function stubLinkedMember(): void {
2283
+ jest
2284
+ .spyOn(MicrosoftTeamsAuthAction, "getOneUptimeUserIdFromTeamsUserId")
2285
+ .mockResolvedValue(ONEUPTIME_USER_ID);
2286
+ jest
2287
+ .spyOn(WorkspaceActionAuthorization, "getProjectMemberProps")
2288
+ .mockResolvedValue({
2289
+ userId: ONEUPTIME_USER_ID,
2290
+ tenantId: PROJECT_ID,
2291
+ } as DatabaseCommonInteractionProps);
2292
+ }
2293
+
2294
+ // The sender's account is linked, but it is not a member of the project.
2295
+ function stubLinkedNonMember(): void {
2296
+ jest
2297
+ .spyOn(MicrosoftTeamsAuthAction, "getOneUptimeUserIdFromTeamsUserId")
2298
+ .mockResolvedValue(ONEUPTIME_USER_ID);
2299
+ jest
2300
+ .spyOn(WorkspaceActionAuthorization, "getProjectMemberProps")
2301
+ .mockRejectedValue(
2302
+ new NotAuthorizedException(
2303
+ WorkspaceActionAuthorization.NOT_A_PROJECT_MEMBER_MESSAGE,
2304
+ ),
2305
+ );
2306
+ }
2307
+
2308
+ function stubDashboardUrl(): void {
2309
+ jest.spyOn(DatabaseConfig, "getDashboardUrl").mockImplementation(() => {
2310
+ return Promise.resolve(URL.fromString(DASHBOARD_URL));
2311
+ });
2312
+ }
2313
+
2314
+ function stubIncidentAction(): SpyInstance<
2315
+ typeof MicrosoftTeamsIncidentActions.handleBotIncidentAction
2316
+ > {
2317
+ return jest.spyOn(MicrosoftTeamsIncidentActions, "handleBotIncidentAction");
2318
+ }
2319
+
2320
+ async function invoke(
2321
+ activity: JSONObject,
2322
+ send?: SendBehaviour | undefined,
2323
+ ): Promise<FakeTurn> {
2324
+ const turn: FakeTurn = buildTurn({ activity: activity, send: send });
2325
+
2326
+ await MicrosoftTeamsUtil.handleBotInvokeActivity({
2327
+ activity: activity,
2328
+ turnContext: turn.turnContext,
2329
+ });
2330
+
2331
+ return turn;
2332
+ }
2333
+
2334
+ test("a Teams account nobody connected: guidance with the link to User Settings, not an error", async () => {
2335
+ jest
2336
+ .spyOn(MicrosoftTeamsAuthAction, "getOneUptimeUserIdFromTeamsUserId")
2337
+ .mockRejectedValue(
2338
+ new MicrosoftTeamsAccountNotLinkedException(
2339
+ "No OneUptime user linked to this Microsoft Teams user. Please authenticate with Microsoft Teams.",
2340
+ ),
2341
+ );
2342
+ stubDashboardUrl();
2343
+ const action: SpyInstance<
2344
+ typeof MicrosoftTeamsIncidentActions.handleBotIncidentAction
2345
+ > = stubIncidentAction().mockResolvedValue(undefined);
2346
+ const errorLog: SpyInstance<typeof logger.error> = silenceErrorLog();
2347
+
2348
+ const turn: FakeTurn = await invoke(
2349
+ invokeActivity(acknowledgeIncidentValue()),
2350
+ );
2351
+
2352
+ expect(turn.attempted).toEqual([
2353
+ `To use OneUptime actions in Microsoft Teams, first connect your Microsoft Teams account to OneUptime in ${SETTINGS_LINK_TEXT}, then try again.`,
2354
+ ]);
2355
+ expect(action).not.toHaveBeenCalled();
2356
+ // Expected, not a server fault: nothing for an operator to chase.
2357
+ expect(errorLog).not.toHaveBeenCalled();
2358
+ });
2359
+
2360
+ test("a Teams account nobody connected, dashboard URL unknown: the same guidance without a link", async () => {
2361
+ jest
2362
+ .spyOn(MicrosoftTeamsAuthAction, "getOneUptimeUserIdFromTeamsUserId")
2363
+ .mockRejectedValue(
2364
+ new MicrosoftTeamsAccountNotLinkedException("No linked user."),
2365
+ );
2366
+ jest
2367
+ .spyOn(DatabaseConfig, "getDashboardUrl")
2368
+ .mockRejectedValue(new Error("GlobalConfig is not reachable"));
2369
+
2370
+ const turn: FakeTurn = await invoke(
2371
+ invokeActivity(acknowledgeIncidentValue()),
2372
+ );
2373
+
2374
+ expect(turn.attempted).toEqual([
2375
+ "To use OneUptime actions in Microsoft Teams, first connect your Microsoft Teams account to OneUptime in OneUptime under User Settings → Microsoft Teams, then try again.",
2376
+ ]);
2377
+ });
2378
+
2379
+ test("a sender who is not a member of the project: the refusal's own message, not an error", async () => {
2380
+ stubLinkedNonMember();
2381
+ const action: SpyInstance<
2382
+ typeof MicrosoftTeamsIncidentActions.handleBotIncidentAction
2383
+ > = stubIncidentAction().mockResolvedValue(undefined);
2384
+ const errorLog: SpyInstance<typeof logger.error> = silenceErrorLog();
2385
+
2386
+ const turn: FakeTurn = await invoke(
2387
+ invokeActivity(acknowledgeIncidentValue()),
2388
+ );
2389
+
2390
+ expect(turn.attempted).toEqual([
2391
+ WorkspaceActionAuthorization.NOT_A_PROJECT_MEMBER_MESSAGE,
2392
+ ]);
2393
+ expect(action).not.toHaveBeenCalled();
2394
+ expect(errorLog).not.toHaveBeenCalled();
2395
+ });
2396
+
2397
+ test("the refusal cannot be sent: handleBotInvokeActivity still resolves, after one attempt", async () => {
2398
+ stubLinkedNonMember();
2399
+ silenceErrorLog();
2400
+ const activity: JSONObject = invokeActivity(acknowledgeIncidentValue());
2401
+ const turn: FakeTurn = buildTurn({
2402
+ activity: activity,
2403
+ send: "refuse-all",
2404
+ sendError: connectorUnavailable(),
2405
+ });
2406
+
2407
+ await expect(
2408
+ MicrosoftTeamsUtil.handleBotInvokeActivity({
2409
+ activity: activity,
2410
+ turnContext: turn.turnContext,
2411
+ }),
2412
+ ).resolves.toBeUndefined();
2413
+
2414
+ expect(turn.attempted).toEqual([
2415
+ WorkspaceActionAuthorization.NOT_A_PROJECT_MEMBER_MESSAGE,
2416
+ ]);
2417
+ expect(turn.delivered).toEqual([]);
2418
+ });
2419
+
2420
+ test("an action refused by a permission check inside it: the refusal's own message", async () => {
2421
+ stubLinkedMember();
2422
+ const errorLog: SpyInstance<typeof logger.error> = silenceErrorLog();
2423
+ stubIncidentAction().mockRejectedValue(
2424
+ new NotAuthorizedException(
2425
+ "You do not have permission to acknowledge this incident.",
2426
+ ),
2427
+ );
2428
+
2429
+ const turn: FakeTurn = await invoke(
2430
+ invokeActivity(acknowledgeIncidentValue()),
2431
+ );
2432
+
2433
+ expect(turn.attempted).toEqual([
2434
+ "You do not have permission to acknowledge this incident.",
2435
+ ]);
2436
+ expect(errorLog).not.toHaveBeenCalled();
2437
+ });
2438
+
2439
+ // What handleBotInvokeActivity's log lines carry, for the Acknowledge button.
2440
+ const ACKNOWLEDGE_LOG_ATTRIBUTES: LogAttributes = {
2441
+ actionType: MicrosoftTeamsIncidentActionType.AckIncident,
2442
+ projectId: PROJECT_ID.toString(),
2443
+ };
2444
+
2445
+ test("an action that throws BadDataException: 'Sorry, that action failed: <reason>'", async () => {
2446
+ stubLinkedMember();
2447
+ const errorLog: SpyInstance<typeof logger.error> = silenceErrorLog();
2448
+ const refusal: BadDataException = new BadDataException(
2449
+ "Incident not found.",
2450
+ );
2451
+ const action: SpyInstance<
2452
+ typeof MicrosoftTeamsIncidentActions.handleBotIncidentAction
2453
+ > = stubIncidentAction().mockRejectedValue(refusal);
2454
+
2455
+ const turn: FakeTurn = await invoke(
2456
+ invokeActivity(acknowledgeIncidentValue()),
2457
+ );
2458
+
2459
+ // Named by its class: OneUptime's exceptions all keep Error's name.
2460
+ expectFailureLogged(errorLog, {
2461
+ line: "Error handling bot invoke activity: BadDataException: Incident not found.",
2462
+ error: refusal,
2463
+ attributes: ACKNOWLEDGE_LOG_ATTRIBUTES,
2464
+ });
2465
+
2466
+ expect(action).toHaveBeenCalledTimes(1);
2467
+ expect(action).toHaveBeenCalledWith(
2468
+ expect.objectContaining({
2469
+ actionType: MicrosoftTeamsIncidentActionType.AckIncident,
2470
+ actionValue: INCIDENT_ID.toString(),
2471
+ projectId: PROJECT_ID,
2472
+ oneUptimeUserId: ONEUPTIME_USER_ID,
2473
+ }),
2474
+ );
2475
+ expect(turn.attempted).toEqual([
2476
+ "Sorry, that action failed: Incident not found.",
2477
+ ]);
2478
+ });
2479
+
2480
+ test("an action that throws anything else: the generic line, and the internal error stays in the log", async () => {
2481
+ stubLinkedMember();
2482
+ const errorLog: SpyInstance<typeof logger.error> = silenceErrorLog();
2483
+ const failure: Error = new Error("connect ECONNREFUSED 10.0.0.5:5432");
2484
+ stubIncidentAction().mockRejectedValue(failure);
2485
+
2486
+ const turn: FakeTurn = await invoke(
2487
+ invokeActivity(acknowledgeIncidentValue()),
2488
+ );
2489
+
2490
+ expect(turn.attempted).toEqual([ACTION_FAILED_TEXT]);
2491
+ expect(
2492
+ loggedErrorLines(errorLog).some((line: string) => {
2493
+ return line.includes("connect ECONNREFUSED 10.0.0.5:5432");
2494
+ }),
2495
+ ).toBe(true);
2496
+ expectFailureLogged(errorLog, {
2497
+ line: "Error handling bot invoke activity: Error: connect ECONNREFUSED 10.0.0.5:5432",
2498
+ error: failure,
2499
+ attributes: ACKNOWLEDGE_LOG_ATTRIBUTES,
2500
+ });
2501
+ });
2502
+
2503
+ test("an action whose own reply Teams refused (RestError 413): the generic line, not Teams' text, and the log keeps the status and code", async () => {
2504
+ stubLinkedMember();
2505
+ const errorLog: SpyInstance<typeof logger.error> = silenceErrorLog();
2506
+ stubIncidentAction().mockRejectedValue(messageTooLarge());
2507
+
2508
+ const turn: FakeTurn = await invoke(
2509
+ invokeActivity(acknowledgeIncidentValue()),
2510
+ );
2511
+
2512
+ expect(turn.attempted).toEqual([ACTION_FAILED_TEXT]);
2513
+ // One line: the error object would carry the whole request and response.
2514
+ expect(errorLogCalls(errorLog)).toStrictEqual([
2515
+ [
2516
+ "Error handling bot invoke activity: RestError 413 MessageSizeTooBig: Message size too large.",
2517
+ ACKNOWLEDGE_LOG_ATTRIBUTES,
2518
+ ],
2519
+ ]);
2520
+ });
2521
+
2522
+ test.each([
2523
+ ["a BadDataException with no message", new BadDataException("")],
2524
+ ["a thrown string", "simulated string failure"],
2525
+ ["a thrown plain object", { status: "broken" }],
2526
+ ])(
2527
+ "an action that fails with %s: the generic line",
2528
+ async (_failure: string, thrown: unknown) => {
2529
+ stubLinkedMember();
2530
+ silenceErrorLog();
2531
+ stubIncidentAction().mockRejectedValue(thrown);
2532
+
2533
+ const turn: FakeTurn = await invoke(
2534
+ invokeActivity(acknowledgeIncidentValue()),
2535
+ );
2536
+
2537
+ expect(turn.attempted).toEqual([ACTION_FAILED_TEXT]);
2538
+ },
2539
+ );
2540
+
2541
+ test("the failure reply cannot be sent: handleBotInvokeActivity still resolves, after one attempt", async () => {
2542
+ stubLinkedMember();
2543
+ silenceErrorLog();
2544
+ stubIncidentAction().mockRejectedValue(
2545
+ new BadDataException("Incident not found."),
2546
+ );
2547
+ const activity: JSONObject = invokeActivity(acknowledgeIncidentValue());
2548
+ const turn: FakeTurn = buildTurn({
2549
+ activity: activity,
2550
+ send: "refuse-all",
2551
+ sendError: connectorUnavailable(),
2552
+ });
2553
+
2554
+ await expect(
2555
+ MicrosoftTeamsUtil.handleBotInvokeActivity({
2556
+ activity: activity,
2557
+ turnContext: turn.turnContext,
2558
+ }),
2559
+ ).resolves.toBeUndefined();
2560
+
2561
+ expect(turn.attempted).toEqual([
2562
+ "Sorry, that action failed: Incident not found.",
2563
+ ]);
2564
+ expect(turn.delivered).toEqual([]);
2565
+ });
2566
+
2567
+ test("the not-linked guidance cannot be sent: still resolves", async () => {
2568
+ jest
2569
+ .spyOn(MicrosoftTeamsAuthAction, "getOneUptimeUserIdFromTeamsUserId")
2570
+ .mockRejectedValue(
2571
+ new MicrosoftTeamsAccountNotLinkedException("No linked user."),
2572
+ );
2573
+ stubDashboardUrl();
2574
+ silenceErrorLog();
2575
+ const activity: JSONObject = invokeActivity(acknowledgeIncidentValue());
2576
+ const turn: FakeTurn = buildTurn({
2577
+ activity: activity,
2578
+ send: "refuse-all",
2579
+ });
2580
+
2581
+ await expect(
2582
+ MicrosoftTeamsUtil.handleBotInvokeActivity({
2583
+ activity: activity,
2584
+ turnContext: turn.turnContext,
2585
+ }),
2586
+ ).resolves.toBeUndefined();
2587
+
2588
+ expect(turn.attempted).toHaveLength(1);
2589
+ });
2590
+
2591
+ test("through handleBotMessageActivity (an Action.Submit): one 'that action failed' reply, and the redelivered submit adds nothing", async () => {
2592
+ stubLinkedMember();
2593
+ silenceErrorLog();
2594
+ const action: SpyInstance<
2595
+ typeof MicrosoftTeamsIncidentActions.handleBotIncidentAction
2596
+ > = stubIncidentAction().mockRejectedValue(
2597
+ new BadDataException("Incident not found."),
2598
+ );
2599
+ const activity: JSONObject = cardSubmit(acknowledgeIncidentValue());
2600
+
2601
+ const first: FakeTurn = await deliver(activity);
2602
+ const redelivery: FakeTurn = await deliver(activity);
2603
+
2604
+ expect(first.attempted).toEqual([
2605
+ "Sorry, that action failed: Incident not found.",
2606
+ ]);
2607
+ expect(redelivery.attempted).toEqual([]);
2608
+ expect(action).toHaveBeenCalledTimes(1);
2609
+ });
2610
+ });