@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,2490 @@
1
+ /** @timezone Asia/Tokyo */
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ jest,
8
+ test,
9
+ } from "@jest/globals";
10
+ import type { TurnContext } from "botbuilder";
11
+ import type { SpyInstance } from "jest-mock";
12
+ import Incident from "../../../../../Models/DatabaseModels/Incident";
13
+ import ScheduledMaintenance from "../../../../../Models/DatabaseModels/ScheduledMaintenance";
14
+ import logger, { LogAttributes } from "../../../../../Server/Utils/Logger";
15
+ import {
16
+ MicrosoftTeamsIncidentActionType,
17
+ MicrosoftTeamsScheduledMaintenanceActionType,
18
+ } from "../../../../../Server/Utils/Workspace/MicrosoftTeams/Actions/ActionTypes";
19
+ import MicrosoftTeamsAuthAction, {
20
+ MicrosoftTeamsAccountNotLinkedException,
21
+ } from "../../../../../Server/Utils/Workspace/MicrosoftTeams/Actions/Auth";
22
+ import MicrosoftTeamsIncidentActions, {
23
+ MicrosoftTeamsNewIncidentFormChoices,
24
+ } from "../../../../../Server/Utils/Workspace/MicrosoftTeams/Actions/Incident";
25
+ import MicrosoftTeamsScheduledMaintenanceActions, {
26
+ MicrosoftTeamsNewScheduledMaintenanceFormChoices,
27
+ } from "../../../../../Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance";
28
+ import MicrosoftTeamsCardChoices, {
29
+ MicrosoftTeamsCardChoice,
30
+ MicrosoftTeamsCardChoiceList,
31
+ } from "../../../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices";
32
+ import MicrosoftTeamsCreateCommands, {
33
+ CreateFormText,
34
+ } from "../../../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands";
35
+ import MicrosoftTeamsMessageSize, {
36
+ MICROSOFT_TEAMS_CARD_SIZE_BUDGETS_IN_BYTES,
37
+ } from "../../../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize";
38
+ import MicrosoftTeamsReplies, {
39
+ MICROSOFT_TEAMS_ADAPTIVE_CARD_CONTENT_TYPE,
40
+ } from "../../../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies";
41
+ import WorkspaceActionAuthorization from "../../../../../Server/Utils/Workspace/WorkspaceActionAuthorization";
42
+ import DatabaseCommonInteractionProps from "../../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
43
+ import ColumnLength from "../../../../../Types/Database/ColumnLength";
44
+ import BadDataException from "../../../../../Types/Exception/BadDataException";
45
+ import DatabaseNotConnectedException from "../../../../../Types/Exception/DatabaseNotConnectedException";
46
+ import NotAuthorizedException from "../../../../../Types/Exception/NotAuthorizedException";
47
+ import { JSONObject } from "../../../../../Types/JSON";
48
+ import ObjectID from "../../../../../Types/ObjectID";
49
+ import Permission, {
50
+ UserPermission,
51
+ UserTenantAccessPermission,
52
+ } from "../../../../../Types/Permission";
53
+
54
+ /*
55
+ * "create incident" and "create maintenance" typed to the Microsoft Teams bot,
56
+ * through MicrosoftTeamsCreateCommands (issue #4111).
57
+ *
58
+ * A customer on 14.0.8 typed either command and got "Sorry, I encountered an
59
+ * error processing your request. Please try again later." - twice. The forms
60
+ * listed every monitor, label and on-call policy of the project, so Teams
61
+ * refused them with 413 MessageSizeTooBig; the handler replied, rethrew, the
62
+ * adapter answered Teams with HTTP 500, and Teams delivered the message again.
63
+ * Each command now answers a message with exactly ONE reply: the form, or the
64
+ * reason it cannot be shown and what to do instead.
65
+ *
66
+ * The TurnContext here is a fake whose sendActivity records every call and can
67
+ * refuse it the way the Bot Connector does, with a RestError that carries the
68
+ * HTTP status and Teams' error code. Pinned:
69
+ *
70
+ * - In a personal chat, where the sender is the only one who can submit the
71
+ * form, the sender is checked before any list is read: no AAD object id, no
72
+ * connected account (with the settings link), not a project member, and -
73
+ * for maintenance only, since only its submit checks it - no permission to
74
+ * create the event (also with the real permission model deciding). A check
75
+ * that itself fails gets a reply of its own.
76
+ * - In a channel or group chat anyone there may submit the form, and the
77
+ * submit checks whoever does, so the form is posted for everyone and nobody
78
+ * is looked up, not even a sender every check would refuse.
79
+ * - Creating an incident asks for no incident-create permission, on purpose
80
+ * (see "create incident" for where that decision lives).
81
+ * - Lists that cannot be read, and an incident form with no severity to pick,
82
+ * get a reply instead of a form; the second links the page where incident
83
+ * severities are added.
84
+ * - The form is exactly the card the production builder makes from the
85
+ * project's lists, the typed title (cut to fit, never through an emoji),
86
+ * the dashboard link and the requester's time zone for that budget, so no
87
+ * list or value can go missing on the way.
88
+ * - A form Teams refuses as too large goes out again smaller, down to no lists
89
+ * at all; a card no different from the one refused is not sent twice; any
90
+ * other refusal is answered at once, naming Teams' status and code.
91
+ * - Nothing escapes the handler, not even when Teams refuses the reply too.
92
+ * A failure is logged call for call, with the project it happened in: a
93
+ * line that says what failed and why (a OneUptime exception named by its
94
+ * class), then the error itself for its stack - except a Teams refusal,
95
+ * whose HTTP status and code the line already names.
96
+ * - The maintenance form names and carries the time zone Teams reports for
97
+ * whoever asked for it - in a channel, the zone everyone who fills it in is
98
+ * told to type the times in - and never the server's (this file runs the
99
+ * server in Asia/Tokyo).
100
+ * - getFormNotAcceptedMessage, branch by branch.
101
+ *
102
+ * Every test counts the sendActivity calls, and the ones that reached the
103
+ * user: one per command, or none when Teams refused that reply as well. A
104
+ * create command never removes a message, so every count also expects no
105
+ * deleteActivity call.
106
+ */
107
+
108
+ const PROJECT_ID: ObjectID = new ObjectID(
109
+ "2b9f6c1e-4d3a-4e8b-9c7d-1a2b3c4d5e6f",
110
+ );
111
+ const USER_ID: ObjectID = new ObjectID("7d4e2a91-3c5b-4f6a-8e9d-0b1c2d3e4f5a");
112
+
113
+ // The sender's Microsoft Entra (AAD) object id, which Teams puts on "from".
114
+ const AAD_OBJECT_ID: string = "f3a9c2d1-6b4e-4a7f-9c8d-2e1f0a9b8c7d";
115
+
116
+ const DASHBOARD_URL: string = "https://oneuptime.test/dashboard";
117
+
118
+ function dashboardLink(route: string): string {
119
+ return `${DASHBOARD_URL}/${PROJECT_ID.toString()}${route}`;
120
+ }
121
+
122
+ const TEAMS_SETTINGS_LINK: string = dashboardLink(
123
+ "/user-settings/microsoft-teams-integration",
124
+ );
125
+ const CREATE_INCIDENT_LINK: string = dashboardLink("/incidents/create");
126
+ const CREATE_MAINTENANCE_LINK: string = dashboardLink(
127
+ "/scheduled-maintenance-events/create",
128
+ );
129
+ // Where incident severities are added: Incidents → Settings → Incident Severity.
130
+ const SEVERITY_SETTINGS_LINK: string = dashboardLink(
131
+ "/incidents/settings/severity",
132
+ );
133
+ // The member's own permissions, as getProjectMemberProps resolves them.
134
+ const MEMBER_PROPS: DatabaseCommonInteractionProps = {
135
+ userId: USER_ID,
136
+ tenantId: PROJECT_ID,
137
+ };
138
+
139
+ const FIRST_BUDGET: number = MICROSOFT_TEAMS_CARD_SIZE_BUDGETS_IN_BYTES[0]!;
140
+ const SECOND_BUDGET: number = MICROSOFT_TEAMS_CARD_SIZE_BUDGETS_IN_BYTES[1]!;
141
+ const LAST_BUDGET: number = MICROSOFT_TEAMS_CARD_SIZE_BUDGETS_IN_BYTES[2]!;
142
+
143
+ const NO_SEVERITIES_OPENING: string =
144
+ "An incident needs a severity, and this project has no incident severities yet.";
145
+
146
+ type CommandKind = "incident" | "maintenance";
147
+
148
+ type CreateCommandRequest = Parameters<
149
+ typeof MicrosoftTeamsCreateCommands.handleCreateIncidentCommand
150
+ >[0];
151
+
152
+ interface CommandProfile {
153
+ kind: CommandKind;
154
+ // What the user types to the bot.
155
+ command: string;
156
+ // The wording MicrosoftTeamsCreateCommands keeps for this form.
157
+ form: CreateFormText;
158
+ createLink: string;
159
+ titleInputId: string;
160
+ // The longest title the form takes; a longer typed title is cut to it.
161
+ titleMaxLength: number;
162
+ monitorsInputId: string;
163
+ // Every list the form offers a project that has some of each, in card order.
164
+ choiceSetIds: ReadonlyArray<string>;
165
+ submitAction: string;
166
+ // A title typed after the command, as MicrosoftTeams.ts hands it over.
167
+ typedTitle: string;
168
+ // Every reply the command gives instead of the form, word for word.
169
+ replies: {
170
+ senderUnknown: string;
171
+ accountNotLinked: string;
172
+ accountNotLinkedWithoutLink: string;
173
+ senderCheckFailed: string;
174
+ listsUnreadable: string;
175
+ listsUnreadableWithoutLink: string;
176
+ tooLarge: string;
177
+ tooLargeWithoutLink: string;
178
+ notInRoster: string;
179
+ };
180
+ }
181
+
182
+ const INCIDENT: CommandProfile = {
183
+ kind: "incident",
184
+ command: "create incident",
185
+ form: {
186
+ what: "an incident",
187
+ dashboardRoute: "/incidents/create",
188
+ purpose: "create incidents from Microsoft Teams",
189
+ lists: "monitors, labels and on-call policies",
190
+ },
191
+ createLink: CREATE_INCIDENT_LINK,
192
+ titleInputId: "incidentTitle",
193
+ titleMaxLength: ColumnLength.LongText,
194
+ monitorsInputId: "incidentMonitors",
195
+ choiceSetIds: [
196
+ "incidentSeverity",
197
+ "incidentMonitors",
198
+ "monitorStatus",
199
+ "onCallDutyPolicies",
200
+ "labels",
201
+ ],
202
+ submitAction: MicrosoftTeamsIncidentActionType.SubmitNewIncident,
203
+ typedTitle: "Checkout API returns 502 for EU customers",
204
+ replies: {
205
+ senderUnknown:
206
+ "Sorry, I couldn't tell who sent this message, so I can't open the form to create an incident. Please try again.",
207
+ accountNotLinked: `To create incidents from Microsoft Teams, first connect your Microsoft Teams account to OneUptime in [OneUptime → User Settings → Microsoft Teams](${TEAMS_SETTINGS_LINK}), then try again.`,
208
+ accountNotLinkedWithoutLink:
209
+ "To create incidents from Microsoft Teams, first connect your Microsoft Teams account to OneUptime in OneUptime under User Settings → Microsoft Teams, then try again.",
210
+ senderCheckFailed:
211
+ "Sorry, I couldn't open the form to create an incident because OneUptime could not check your account just now. Please try again in a minute.",
212
+ listsUnreadable: `Sorry, I couldn't open the form to create an incident because OneUptime could not load the lists it needs just now. Please try again in a minute, or create it in OneUptime instead: ${CREATE_INCIDENT_LINK}`,
213
+ listsUnreadableWithoutLink:
214
+ "Sorry, I couldn't open the form to create an incident because OneUptime could not load the lists it needs just now. Please try again in a minute, or create it in OneUptime instead.",
215
+ tooLarge: `Sorry, I couldn't open the form to create an incident: Microsoft Teams refused it as too large, even without the lists of monitors, labels and on-call policies. You can create it in OneUptime instead: ${CREATE_INCIDENT_LINK}`,
216
+ tooLargeWithoutLink:
217
+ "Sorry, I couldn't open the form to create an incident: Microsoft Teams refused it as too large, even without the lists of monitors, labels and on-call policies. You can create it in OneUptime instead.",
218
+ notInRoster: `Sorry, I couldn't open the form to create an incident: Microsoft Teams did not accept it (403 BotNotInConversationRoster). You can create it in OneUptime instead: ${CREATE_INCIDENT_LINK}`,
219
+ },
220
+ };
221
+
222
+ const MAINTENANCE: CommandProfile = {
223
+ kind: "maintenance",
224
+ command: "create maintenance",
225
+ form: {
226
+ what: "a scheduled maintenance event",
227
+ dashboardRoute: "/scheduled-maintenance-events/create",
228
+ purpose: "schedule maintenance from Microsoft Teams",
229
+ lists: "monitors and labels",
230
+ },
231
+ createLink: CREATE_MAINTENANCE_LINK,
232
+ titleInputId: "scheduledMaintenanceTitle",
233
+ titleMaxLength: ColumnLength.ShortText,
234
+ monitorsInputId: "scheduledMaintenanceMonitors",
235
+ choiceSetIds: ["scheduledMaintenanceMonitors", "monitorStatus", "labels"],
236
+ submitAction:
237
+ MicrosoftTeamsScheduledMaintenanceActionType.SubmitNewScheduledMaintenance,
238
+ typedTitle: "Postgres 16 upgrade",
239
+ replies: {
240
+ senderUnknown:
241
+ "Sorry, I couldn't tell who sent this message, so I can't open the form to create a scheduled maintenance event. Please try again.",
242
+ accountNotLinked: `To schedule maintenance from Microsoft Teams, first connect your Microsoft Teams account to OneUptime in [OneUptime → User Settings → Microsoft Teams](${TEAMS_SETTINGS_LINK}), then try again.`,
243
+ accountNotLinkedWithoutLink:
244
+ "To schedule maintenance from Microsoft Teams, first connect your Microsoft Teams account to OneUptime in OneUptime under User Settings → Microsoft Teams, then try again.",
245
+ senderCheckFailed:
246
+ "Sorry, I couldn't open the form to create a scheduled maintenance event because OneUptime could not check your account just now. Please try again in a minute.",
247
+ listsUnreadable: `Sorry, I couldn't open the form to create a scheduled maintenance event because OneUptime could not load the lists it needs just now. Please try again in a minute, or create it in OneUptime instead: ${CREATE_MAINTENANCE_LINK}`,
248
+ listsUnreadableWithoutLink:
249
+ "Sorry, I couldn't open the form to create a scheduled maintenance event because OneUptime could not load the lists it needs just now. Please try again in a minute, or create it in OneUptime instead.",
250
+ tooLarge: `Sorry, I couldn't open the form to create a scheduled maintenance event: Microsoft Teams refused it as too large, even without the lists of monitors and labels. You can create it in OneUptime instead: ${CREATE_MAINTENANCE_LINK}`,
251
+ tooLargeWithoutLink:
252
+ "Sorry, I couldn't open the form to create a scheduled maintenance event: Microsoft Teams refused it as too large, even without the lists of monitors and labels. You can create it in OneUptime instead.",
253
+ notInRoster: `Sorry, I couldn't open the form to create a scheduled maintenance event: Microsoft Teams did not accept it (403 BotNotInConversationRoster). You can create it in OneUptime instead: ${CREATE_MAINTENANCE_LINK}`,
254
+ },
255
+ };
256
+
257
+ const COMMANDS: ReadonlyArray<CommandProfile> = [INCIDENT, MAINTENANCE];
258
+
259
+ // ---- The project's lists ----
260
+
261
+ /*
262
+ * small: a few of everything, far below every budget.
263
+ * big: what the capped fetchers return for a large project - 250 of 1,200
264
+ * monitors, 100 labels and 100 of 140 on-call policies, every name 80
265
+ * characters long. Well over the first budget, so each budget gives a
266
+ * different card.
267
+ * listless: nothing but the required lists, so every budget gives the same
268
+ * card.
269
+ */
270
+ type ChoicesSize = "small" | "big" | "listless";
271
+
272
+ // A distinct, well-formed id per list and row.
273
+ function rowId(list: number, index: number): string {
274
+ return `00000000-0000-4000-8${String(list).padStart(3, "0")}-${String(
275
+ index,
276
+ ).padStart(12, "0")}`;
277
+ }
278
+
279
+ function namedList(
280
+ list: number,
281
+ names: ReadonlyArray<string>,
282
+ ): MicrosoftTeamsCardChoiceList {
283
+ return {
284
+ choices: names.map(
285
+ (name: string, index: number): MicrosoftTeamsCardChoice => {
286
+ return { title: name, value: rowId(list, index + 1) };
287
+ },
288
+ ),
289
+ totalCount: names.length,
290
+ };
291
+ }
292
+
293
+ function longList(data: {
294
+ list: number;
295
+ noun: string;
296
+ count: number;
297
+ totalCount: number;
298
+ }): MicrosoftTeamsCardChoiceList {
299
+ const names: Array<string> = [];
300
+
301
+ for (let index: number = 1; index <= data.count; index++) {
302
+ names.push(
303
+ `${data.noun} ${String(index).padStart(4, "0")} · checkout-api.eu-west-1.production.example.com / HTTPS health check`.substring(
304
+ 0,
305
+ 80,
306
+ ),
307
+ );
308
+ }
309
+
310
+ return {
311
+ choices: namedList(data.list, names).choices,
312
+ totalCount: data.totalCount,
313
+ };
314
+ }
315
+
316
+ function emptyList(): MicrosoftTeamsCardChoiceList {
317
+ return { choices: [], totalCount: 0 };
318
+ }
319
+
320
+ function severityList(): MicrosoftTeamsCardChoiceList {
321
+ return namedList(1, ["Critical", "Major", "Minor"]);
322
+ }
323
+
324
+ function monitorStatusList(): MicrosoftTeamsCardChoiceList {
325
+ return namedList(3, ["Operational", "Degraded", "Offline"]);
326
+ }
327
+
328
+ function monitorList(size: ChoicesSize): MicrosoftTeamsCardChoiceList {
329
+ if (size === "big") {
330
+ return longList({ list: 2, noun: "Monitor", count: 250, totalCount: 1200 });
331
+ }
332
+
333
+ return size === "small"
334
+ ? namedList(2, ["API", "Checkout", "Website"])
335
+ : emptyList();
336
+ }
337
+
338
+ function labelList(size: ChoicesSize): MicrosoftTeamsCardChoiceList {
339
+ if (size === "big") {
340
+ return longList({ list: 4, noun: "Label", count: 100, totalCount: 100 });
341
+ }
342
+
343
+ return size === "small"
344
+ ? namedList(4, ["eu-west-1", "payments"])
345
+ : emptyList();
346
+ }
347
+
348
+ function onCallPolicyList(size: ChoicesSize): MicrosoftTeamsCardChoiceList {
349
+ if (size === "big") {
350
+ return longList({ list: 5, noun: "Policy", count: 100, totalCount: 140 });
351
+ }
352
+
353
+ return size === "small" ? namedList(5, ["Primary on-call"]) : emptyList();
354
+ }
355
+
356
+ function incidentChoices(
357
+ size: ChoicesSize,
358
+ ): MicrosoftTeamsNewIncidentFormChoices {
359
+ return {
360
+ severities: severityList(),
361
+ monitors: monitorList(size),
362
+ monitorStatuses: monitorStatusList(),
363
+ labels: labelList(size),
364
+ onCallDutyPolicies: onCallPolicyList(size),
365
+ };
366
+ }
367
+
368
+ function maintenanceChoices(
369
+ size: ChoicesSize,
370
+ ): MicrosoftTeamsNewScheduledMaintenanceFormChoices {
371
+ return {
372
+ monitors: monitorList(size),
373
+ monitorStatuses: monitorStatusList(),
374
+ labels: labelList(size),
375
+ };
376
+ }
377
+
378
+ // ---- The form the command must send ----
379
+
380
+ /*
381
+ * The production card builders, kept before any test spies on them. The
382
+ * command must send exactly what they build from the project's lists, so a
383
+ * list, the typed title, the dashboard link or the time zone that goes missing
384
+ * on the way fails the test, not only a field a test happens to look at.
385
+ */
386
+ const buildIncidentForm: typeof MicrosoftTeamsIncidentActions.buildNewIncidentCardForBudget =
387
+ MicrosoftTeamsIncidentActions.buildNewIncidentCardForBudget.bind(
388
+ MicrosoftTeamsIncidentActions,
389
+ );
390
+ const buildMaintenanceForm: typeof MicrosoftTeamsScheduledMaintenanceActions.buildNewScheduledMaintenanceCardForBudget =
391
+ MicrosoftTeamsScheduledMaintenanceActions.buildNewScheduledMaintenanceCardForBudget.bind(
392
+ MicrosoftTeamsScheduledMaintenanceActions,
393
+ );
394
+
395
+ function expectedForm(
396
+ profile: CommandProfile,
397
+ data: {
398
+ size: ChoicesSize;
399
+ budgetInBytes: number;
400
+ initialTitle?: string | undefined;
401
+ createInOneUptimeUrl: string | null;
402
+ timezone?: string | undefined;
403
+ },
404
+ ): JSONObject {
405
+ if (profile.kind === "incident") {
406
+ return buildIncidentForm({
407
+ choices: incidentChoices(data.size),
408
+ budgetInBytes: data.budgetInBytes,
409
+ initialTitle: data.initialTitle,
410
+ createInOneUptimeUrl: data.createInOneUptimeUrl,
411
+ });
412
+ }
413
+
414
+ return buildMaintenanceForm({
415
+ choices: maintenanceChoices(data.size),
416
+ budgetInBytes: data.budgetInBytes,
417
+ initialTitle: data.initialTitle,
418
+ createInOneUptimeUrl: data.createInOneUptimeUrl,
419
+ timezone: data.timezone,
420
+ });
421
+ }
422
+
423
+ // ---- A fake Teams ----
424
+
425
+ /*
426
+ * What Teams does with one sendActivity call: undefined delivers it, anything
427
+ * else is thrown back at the bot, as the Bot Connector's RestError would be.
428
+ */
429
+ type TeamsAnswer = (reply: unknown) => unknown;
430
+
431
+ interface SendAttempt {
432
+ reply: unknown;
433
+ delivered: boolean;
434
+ }
435
+
436
+ interface FakeTeams {
437
+ turnContext: TurnContext;
438
+ // Every sendActivity call, in order, and whether it reached the user.
439
+ attempts: Array<SendAttempt>;
440
+ /*
441
+ * Every deleteActivity call. Recorded rather than refused: a refusal would
442
+ * be swallowed by a best-effort delete, and the test would never know.
443
+ */
444
+ deletions: Array<unknown>;
445
+ }
446
+
447
+ function createFakeTeams(data: {
448
+ activity: JSONObject;
449
+ answer?: TeamsAnswer | undefined;
450
+ }): FakeTeams {
451
+ const attempts: Array<SendAttempt> = [];
452
+ const deletions: Array<unknown> = [];
453
+
454
+ const turnContext: TurnContext = {
455
+ activity: data.activity,
456
+ sendActivity: async (reply: unknown): Promise<{ id: string }> => {
457
+ const refusal: unknown = data.answer ? data.answer(reply) : undefined;
458
+
459
+ attempts.push({ reply: reply, delivered: refusal === undefined });
460
+
461
+ if (refusal !== undefined) {
462
+ throw refusal;
463
+ }
464
+
465
+ return { id: `reply-${attempts.length}` };
466
+ },
467
+ deleteActivity: async (activityId: unknown): Promise<void> => {
468
+ deletions.push(activityId);
469
+ },
470
+ } as unknown as TurnContext;
471
+
472
+ return { turnContext: turnContext, attempts: attempts, deletions: deletions };
473
+ }
474
+
475
+ // What botbuilder's connector throws when Teams refuses a call.
476
+ function teamsRefusal(data: {
477
+ statusCode: number;
478
+ code?: string | undefined;
479
+ message: string;
480
+ }): Error {
481
+ return Object.assign(new Error(data.message), {
482
+ name: "RestError",
483
+ statusCode: data.statusCode,
484
+ code: data.code,
485
+ details: data.code
486
+ ? { error: { code: data.code, message: data.message } }
487
+ : undefined,
488
+ response: { status: data.statusCode },
489
+ });
490
+ }
491
+
492
+ function messageSizeTooBig(): Error {
493
+ return teamsRefusal({
494
+ statusCode: 413,
495
+ code: "MessageSizeTooBig",
496
+ message: "Message size too large.",
497
+ });
498
+ }
499
+
500
+ function botNotInConversationRoster(): Error {
501
+ return teamsRefusal({
502
+ statusCode: 403,
503
+ code: "BotNotInConversationRoster",
504
+ message: "The bot is not part of the conversation roster.",
505
+ });
506
+ }
507
+
508
+ // A connection Teams dropped: a network error code, no HTTP status.
509
+ function connectionReset(): Error {
510
+ return Object.assign(new Error("socket hang up"), { code: "ECONNRESET" });
511
+ }
512
+
513
+ function isCardReply(reply: unknown): boolean {
514
+ return (
515
+ typeof reply === "object" &&
516
+ reply !== null &&
517
+ Array.isArray((reply as { attachments?: unknown }).attachments)
518
+ );
519
+ }
520
+
521
+ // Refuses the first `times` cards (every card when not given); text passes.
522
+ function refuseCards(data: {
523
+ refusal: () => unknown;
524
+ times?: number | undefined;
525
+ }): TeamsAnswer {
526
+ let refusedCards: number = 0;
527
+
528
+ return (reply: unknown): unknown => {
529
+ if (!isCardReply(reply)) {
530
+ return undefined;
531
+ }
532
+
533
+ if (data.times !== undefined && refusedCards >= data.times) {
534
+ return undefined;
535
+ }
536
+
537
+ refusedCards++;
538
+ return data.refusal();
539
+ };
540
+ }
541
+
542
+ function refuseEverything(refusal: () => unknown): TeamsAnswer {
543
+ return (): unknown => {
544
+ return refusal();
545
+ };
546
+ }
547
+
548
+ // ---- Reading what the command sent ----
549
+
550
+ function deliveredReplies(teams: FakeTeams): Array<unknown> {
551
+ return teams.attempts
552
+ .filter((attempt: SendAttempt) => {
553
+ return attempt.delivered;
554
+ })
555
+ .map((attempt: SendAttempt) => {
556
+ return attempt.reply;
557
+ });
558
+ }
559
+
560
+ /*
561
+ * How many sendActivity calls there were, and how many reached the user. A
562
+ * create command never removes a message (not the user's, not a form), so
563
+ * there is never a deleteActivity call.
564
+ */
565
+ function expectSends(
566
+ teams: FakeTeams,
567
+ counts: { attempted: number; delivered: number },
568
+ ): void {
569
+ expect({
570
+ attempted: teams.attempts.length,
571
+ delivered: deliveredReplies(teams).length,
572
+ deleted: teams.deletions.length,
573
+ }).toEqual({ ...counts, deleted: 0 });
574
+ }
575
+
576
+ function cardOf(reply: unknown): JSONObject {
577
+ expect(isCardReply(reply)).toBe(true);
578
+
579
+ const attachments: Array<JSONObject> = (
580
+ reply as { attachments: Array<JSONObject> }
581
+ ).attachments;
582
+
583
+ expect(attachments).toHaveLength(1);
584
+ expect(attachments[0]!["contentType"]).toBe(
585
+ MICROSOFT_TEAMS_ADAPTIVE_CARD_CONTENT_TYPE,
586
+ );
587
+
588
+ return attachments[0]!["content"] as JSONObject;
589
+ }
590
+
591
+ // Every card the command tried to send, refused or not, in order.
592
+ function attemptedCards(teams: FakeTeams): Array<JSONObject> {
593
+ return teams.attempts
594
+ .filter((attempt: SendAttempt) => {
595
+ return isCardReply(attempt.reply);
596
+ })
597
+ .map((attempt: SendAttempt) => {
598
+ return cardOf(attempt.reply);
599
+ });
600
+ }
601
+
602
+ // The one reply that reached the user, which must be a form.
603
+ function theDeliveredCard(teams: FakeTeams): JSONObject {
604
+ const delivered: Array<unknown> = deliveredReplies(teams);
605
+
606
+ expect(delivered).toHaveLength(1);
607
+
608
+ return cardOf(delivered[0]);
609
+ }
610
+
611
+ // The one reply that reached the user, which must be text.
612
+ function theDeliveredText(teams: FakeTeams): string {
613
+ const delivered: Array<unknown> = deliveredReplies(teams);
614
+
615
+ expect(delivered).toHaveLength(1);
616
+ expect(typeof delivered[0]).toBe("string");
617
+
618
+ return delivered[0] as string;
619
+ }
620
+
621
+ function bodyOf(card: JSONObject): Array<JSONObject> {
622
+ return card["body"] as Array<JSONObject>;
623
+ }
624
+
625
+ function inputOf(card: JSONObject, id: string): JSONObject | undefined {
626
+ return bodyOf(card).find((element: JSONObject) => {
627
+ return element["id"] === id;
628
+ });
629
+ }
630
+
631
+ // The ids of the lists (Input.ChoiceSet) on the card, in order.
632
+ function choiceSetIdsOf(card: JSONObject): Array<string> {
633
+ return bodyOf(card)
634
+ .filter((element: JSONObject) => {
635
+ return element["type"] === "Input.ChoiceSet";
636
+ })
637
+ .map((element: JSONObject) => {
638
+ return String(element["id"]);
639
+ });
640
+ }
641
+
642
+ function textsOf(card: JSONObject): Array<string> {
643
+ return bodyOf(card)
644
+ .filter((element: JSONObject) => {
645
+ return element["type"] === "TextBlock";
646
+ })
647
+ .map((element: JSONObject) => {
648
+ return String(element["text"]);
649
+ });
650
+ }
651
+
652
+ function actionsOf(card: JSONObject): Array<JSONObject> {
653
+ return card["actions"] as Array<JSONObject>;
654
+ }
655
+
656
+ function submitDataOf(card: JSONObject): JSONObject {
657
+ const submit: JSONObject | undefined = actionsOf(card).find(
658
+ (action: JSONObject) => {
659
+ return action["type"] === "Action.Submit";
660
+ },
661
+ );
662
+
663
+ expect(submit).toBeDefined();
664
+
665
+ return submit!["data"] as JSONObject;
666
+ }
667
+
668
+ function openUrlsOf(card: JSONObject): Array<string> {
669
+ return actionsOf(card)
670
+ .filter((action: JSONObject) => {
671
+ return action["type"] === "Action.OpenUrl";
672
+ })
673
+ .map((action: JSONObject) => {
674
+ return String(action["url"]);
675
+ });
676
+ }
677
+
678
+ function sizeOf(card: JSONObject): number {
679
+ return MicrosoftTeamsMessageSize.getSizeInBytes(card);
680
+ }
681
+
682
+ function loggedErrors(): string {
683
+ return errorLogSpy.mock.calls
684
+ .map((call: Parameters<typeof logger.error>) => {
685
+ return String(call[0]);
686
+ })
687
+ .join("\n");
688
+ }
689
+
690
+ // What every log call of a create command carries: the project it ran in.
691
+ const PROJECT_LOG_ATTRIBUTES: LogAttributes = {
692
+ projectId: PROJECT_ID.toString(),
693
+ };
694
+
695
+ /*
696
+ * Every logger.error call, in order, each with the project it happened in: a
697
+ * string must be the very line, an error the very object that was thrown.
698
+ *
699
+ * A failure goes through MicrosoftTeamsReplies.logFailure: the line that says
700
+ * what failed and why, then the error itself, whose stack and class the line
701
+ * leaves out - unless the error carries an HTTP status, as Teams' RestError
702
+ * does, when the line (which names the status and code) is all there is.
703
+ */
704
+ function expectErrorLogs(entries: ReadonlyArray<string | Error>): void {
705
+ expect(errorLogSpy).toHaveBeenCalledTimes(entries.length);
706
+
707
+ entries.forEach((entry: string | Error, index: number): void => {
708
+ const call: Parameters<typeof logger.error> =
709
+ errorLogSpy.mock.calls[index]!;
710
+
711
+ expect(call[0]).toBe(entry);
712
+ expect(call[1]).toEqual(PROJECT_LOG_ATTRIBUTES);
713
+ });
714
+ }
715
+
716
+ // ---- Running a command ----
717
+
718
+ let messageCount: number = 0;
719
+
720
+ /*
721
+ * The message Teams delivers when the command is typed to the bot in a
722
+ * personal (1:1) chat, where the sender is the only one who can submit the
723
+ * form it answers with.
724
+ */
725
+ function messageActivity(
726
+ profile: CommandProfile,
727
+ fields?: JSONObject,
728
+ ): JSONObject {
729
+ messageCount++;
730
+
731
+ return {
732
+ type: "message",
733
+ id: `1727611200${String(messageCount).padStart(3, "0")}`,
734
+ text: `${profile.command} ${profile.typedTitle}`,
735
+ from: {
736
+ id: "29:1f2e3d4c5b6a7980",
737
+ name: "Jane Doe",
738
+ aadObjectId: AAD_OBJECT_ID,
739
+ },
740
+ recipient: { id: "28:oneuptime-bot", name: "OneUptime" },
741
+ conversation: {
742
+ id: "a:1x2y3z-personal-chat",
743
+ conversationType: "personal",
744
+ tenantId: "tenant-1",
745
+ },
746
+ channelData: { tenant: { id: "tenant-1" } },
747
+ ...(fields || {}),
748
+ };
749
+ }
750
+
751
+ /*
752
+ * A conversation other people are in, where whoever reads the form may submit
753
+ * it: what its messages carry in place of the personal chat's.
754
+ */
755
+ interface SharedConversation {
756
+ name: string;
757
+ fields: JSONObject;
758
+ }
759
+
760
+ const CHANNEL: SharedConversation = {
761
+ name: "channel",
762
+ fields: {
763
+ conversation: {
764
+ id: "19:8f3e2d1c0b9a4e5f6a7b@thread.tacv2;messageid=1727611200000",
765
+ conversationType: "channel",
766
+ tenantId: "tenant-1",
767
+ isGroup: true,
768
+ },
769
+ channelData: {
770
+ tenant: { id: "tenant-1" },
771
+ team: { id: "19:2a3b4c5d6e7f8091a2b3@thread.tacv2", name: "Engineering" },
772
+ channel: { id: "19:8f3e2d1c0b9a4e5f6a7b@thread.tacv2" },
773
+ },
774
+ },
775
+ };
776
+
777
+ const GROUP_CHAT: SharedConversation = {
778
+ name: "group chat",
779
+ fields: {
780
+ conversation: {
781
+ id: "19:6d5c4b3a2f1e0d9c8b7a@thread.v2",
782
+ conversationType: "groupChat",
783
+ tenantId: "tenant-1",
784
+ isGroup: true,
785
+ },
786
+ },
787
+ };
788
+
789
+ const SHARED_CONVERSATIONS: ReadonlyArray<SharedConversation> = [
790
+ CHANNEL,
791
+ GROUP_CHAT,
792
+ ];
793
+
794
+ /*
795
+ * The command typed there. The bot only hears a message that @mentions it
796
+ * outside a personal chat; MicrosoftTeams.ts strips the mention before it
797
+ * hands the command over.
798
+ */
799
+ function sharedConversationMessage(
800
+ profile: CommandProfile,
801
+ conversation: SharedConversation,
802
+ fields?: JSONObject,
803
+ ): JSONObject {
804
+ return messageActivity(profile, {
805
+ text: `<at>OneUptime</at> ${profile.command} ${profile.typedTitle}`,
806
+ entities: [
807
+ {
808
+ type: "mention",
809
+ text: "<at>OneUptime</at>",
810
+ mentioned: { id: "28:oneuptime-bot", name: "OneUptime" },
811
+ },
812
+ ],
813
+ ...conversation.fields,
814
+ ...(fields || {}),
815
+ });
816
+ }
817
+
818
+ /*
819
+ * Runs the command the way MicrosoftTeams.ts does: the message's activity is
820
+ * also the TurnContext's. The handler must resolve whatever happens: a throw
821
+ * here became an HTTP 500, and Teams delivered the message a second time.
822
+ */
823
+ async function runCommand(data: {
824
+ profile: CommandProfile;
825
+ activity?: JSONObject | undefined;
826
+ answer?: TeamsAnswer | undefined;
827
+ initialTitle?: string | undefined;
828
+ }): Promise<FakeTeams> {
829
+ const activity: JSONObject = data.activity || messageActivity(data.profile);
830
+ const teams: FakeTeams = createFakeTeams({
831
+ activity: activity,
832
+ answer: data.answer,
833
+ });
834
+
835
+ const request: CreateCommandRequest = {
836
+ turnContext: teams.turnContext,
837
+ activity: activity,
838
+ projectId: PROJECT_ID,
839
+ initialTitle: data.initialTitle,
840
+ };
841
+
842
+ await expect(
843
+ data.profile.kind === "incident"
844
+ ? MicrosoftTeamsCreateCommands.handleCreateIncidentCommand(request)
845
+ : MicrosoftTeamsCreateCommands.handleCreateScheduledMaintenanceCommand(
846
+ request,
847
+ ),
848
+ ).resolves.toBeUndefined();
849
+
850
+ return teams;
851
+ }
852
+
853
+ // ---- Stubs ----
854
+
855
+ // The real permission check, kept before any test spies on it.
856
+ const realAssertCanCreate: typeof WorkspaceActionAuthorization.assertCanCreate =
857
+ WorkspaceActionAuthorization.assertCanCreate.bind(
858
+ WorkspaceActionAuthorization,
859
+ );
860
+
861
+ // A member's props as getProjectMemberProps builds them, with these roles.
862
+ function memberWith(
863
+ permissions: ReadonlyArray<Permission>,
864
+ ): DatabaseCommonInteractionProps {
865
+ const tenantPermission: UserTenantAccessPermission = {
866
+ _type: "UserTenantAccessPermission",
867
+ projectId: PROJECT_ID,
868
+ permissions: permissions.map((permission: Permission): UserPermission => {
869
+ return {
870
+ _type: "UserPermission",
871
+ permission: permission,
872
+ isBlockPermission: false,
873
+ labelIds: [],
874
+ };
875
+ }),
876
+ };
877
+
878
+ return {
879
+ userId: USER_ID,
880
+ tenantId: PROJECT_ID,
881
+ userTeamIds: [],
882
+ userTenantAccessPermission: {
883
+ [PROJECT_ID.toString()]: tenantPermission,
884
+ },
885
+ };
886
+ }
887
+
888
+ let senderLookupSpy: SpyInstance<
889
+ typeof MicrosoftTeamsAuthAction.getOneUptimeUserIdFromTeamsUserId
890
+ >;
891
+ let memberPropsSpy: SpyInstance<
892
+ typeof WorkspaceActionAuthorization.getProjectMemberProps
893
+ >;
894
+ let assertCanCreateSpy: SpyInstance<
895
+ typeof WorkspaceActionAuthorization.assertCanCreate
896
+ >;
897
+ let incidentChoicesSpy: SpyInstance<
898
+ typeof MicrosoftTeamsIncidentActions.getNewIncidentFormChoices
899
+ >;
900
+ let maintenanceChoicesSpy: SpyInstance<
901
+ typeof MicrosoftTeamsScheduledMaintenanceActions.getNewScheduledMaintenanceFormChoices
902
+ >;
903
+ let dashboardLinkSpy: SpyInstance<
904
+ typeof MicrosoftTeamsReplies.getDashboardLink
905
+ >;
906
+ let incidentCardSpy: SpyInstance<
907
+ typeof MicrosoftTeamsIncidentActions.buildNewIncidentCardForBudget
908
+ >;
909
+ let maintenanceCardSpy: SpyInstance<
910
+ typeof MicrosoftTeamsScheduledMaintenanceActions.buildNewScheduledMaintenanceCardForBudget
911
+ >;
912
+ let errorLogSpy: SpyInstance<typeof logger.error>;
913
+ let warnLogSpy: SpyInstance<typeof logger.warn>;
914
+
915
+ function giveChoices(profile: CommandProfile, size: ChoicesSize): void {
916
+ if (profile.kind === "incident") {
917
+ incidentChoicesSpy.mockResolvedValue(incidentChoices(size));
918
+ return;
919
+ }
920
+
921
+ maintenanceChoicesSpy.mockResolvedValue(maintenanceChoices(size));
922
+ }
923
+
924
+ function failChoices(profile: CommandProfile, error: Error): void {
925
+ if (profile.kind === "incident") {
926
+ incidentChoicesSpy.mockRejectedValue(error);
927
+ return;
928
+ }
929
+
930
+ maintenanceChoicesSpy.mockRejectedValue(error);
931
+ }
932
+
933
+ function choicesSpyOf(
934
+ profile: CommandProfile,
935
+ ): SpyInstance<(projectId: ObjectID) => Promise<unknown>> {
936
+ return (profile.kind === "incident"
937
+ ? incidentChoicesSpy
938
+ : maintenanceChoicesSpy) as unknown as SpyInstance<
939
+ (projectId: ObjectID) => Promise<unknown>
940
+ >;
941
+ }
942
+
943
+ // The budget of every card the command built, in order.
944
+ function budgetsBuilt(profile: CommandProfile): Array<number> {
945
+ const calls: Array<[{ budgetInBytes: number }]> = (
946
+ profile.kind === "incident"
947
+ ? incidentCardSpy.mock.calls
948
+ : maintenanceCardSpy.mock.calls
949
+ ) as Array<[{ budgetInBytes: number }]>;
950
+
951
+ return calls.map((call: [{ budgetInBytes: number }]) => {
952
+ return call[0].budgetInBytes;
953
+ });
954
+ }
955
+
956
+ beforeEach((): void => {
957
+ senderLookupSpy = jest
958
+ .spyOn(MicrosoftTeamsAuthAction, "getOneUptimeUserIdFromTeamsUserId")
959
+ .mockResolvedValue(USER_ID);
960
+ memberPropsSpy = jest
961
+ .spyOn(WorkspaceActionAuthorization, "getProjectMemberProps")
962
+ .mockResolvedValue(MEMBER_PROPS);
963
+ assertCanCreateSpy = jest
964
+ .spyOn(WorkspaceActionAuthorization, "assertCanCreate")
965
+ .mockResolvedValue(undefined);
966
+ incidentChoicesSpy = jest
967
+ .spyOn(MicrosoftTeamsIncidentActions, "getNewIncidentFormChoices")
968
+ .mockResolvedValue(incidentChoices("small"));
969
+ maintenanceChoicesSpy = jest
970
+ .spyOn(
971
+ MicrosoftTeamsScheduledMaintenanceActions,
972
+ "getNewScheduledMaintenanceFormChoices",
973
+ )
974
+ .mockResolvedValue(maintenanceChoices("small"));
975
+ dashboardLinkSpy = jest
976
+ .spyOn(MicrosoftTeamsReplies, "getDashboardLink")
977
+ .mockImplementation(
978
+ async (data: {
979
+ projectId: ObjectID;
980
+ route: string;
981
+ }): Promise<string | null> => {
982
+ return `${DASHBOARD_URL}/${data.projectId.toString()}${data.route}`;
983
+ },
984
+ );
985
+
986
+ // The real card builders, watched for the budgets they are asked for.
987
+ incidentCardSpy = jest.spyOn(
988
+ MicrosoftTeamsIncidentActions,
989
+ "buildNewIncidentCardForBudget",
990
+ );
991
+ maintenanceCardSpy = jest.spyOn(
992
+ MicrosoftTeamsScheduledMaintenanceActions,
993
+ "buildNewScheduledMaintenanceCardForBudget",
994
+ );
995
+
996
+ errorLogSpy = jest.spyOn(logger, "error").mockImplementation((): void => {});
997
+ warnLogSpy = jest.spyOn(logger, "warn").mockImplementation((): void => {});
998
+ });
999
+
1000
+ afterEach((): void => {
1001
+ jest.restoreAllMocks();
1002
+ });
1003
+
1004
+ test("these tests count sends against three budgets: 40 KiB, 20 KiB, then no lists", (): void => {
1005
+ expect(MICROSOFT_TEAMS_CARD_SIZE_BUDGETS_IN_BYTES).toEqual([
1006
+ 40 * 1024,
1007
+ 20 * 1024,
1008
+ 0,
1009
+ ]);
1010
+ expect(LAST_BUDGET).toBe(0);
1011
+ });
1012
+
1013
+ describe.each(COMMANDS)("$command", (profile: CommandProfile): void => {
1014
+ // The builder's form for this command at one budget, with the dashboard link.
1015
+ function formFor(
1016
+ size: ChoicesSize,
1017
+ budgetInBytes: number,
1018
+ initialTitle?: string | undefined,
1019
+ ): JSONObject {
1020
+ return expectedForm(profile, {
1021
+ size: size,
1022
+ budgetInBytes: budgetInBytes,
1023
+ initialTitle: initialTitle,
1024
+ createInOneUptimeUrl: profile.createLink,
1025
+ });
1026
+ }
1027
+
1028
+ describe("the form", (): void => {
1029
+ test("sends one form, with the title typed after the command filled in", async (): Promise<void> => {
1030
+ const activity: JSONObject = messageActivity(profile);
1031
+
1032
+ const teams: FakeTeams = await runCommand({
1033
+ profile: profile,
1034
+ activity: activity,
1035
+ initialTitle: profile.typedTitle,
1036
+ });
1037
+
1038
+ expectSends(teams, { attempted: 1, delivered: 1 });
1039
+ const card: JSONObject = theDeliveredCard(teams);
1040
+
1041
+ expect(card["type"]).toBe("AdaptiveCard");
1042
+ expect(inputOf(card, profile.titleInputId)).toMatchObject({
1043
+ type: "Input.Text",
1044
+ isRequired: true,
1045
+ value: profile.typedTitle,
1046
+ });
1047
+ expect(submitDataOf(card)["action"]).toBe(profile.submitAction);
1048
+ // Every choice of a small project fits, so there is no dashboard button.
1049
+ expect(openUrlsOf(card)).toEqual([]);
1050
+ expect(
1051
+ (inputOf(card, profile.monitorsInputId)?.["choices"] as
1052
+ | Array<JSONObject>
1053
+ | undefined)!.map((choice: JSONObject) => {
1054
+ return choice["title"];
1055
+ }),
1056
+ ).toEqual(["API", "Checkout", "Website"]);
1057
+ // Every list the project has is offered, whole: the builder's own card.
1058
+ expect(choiceSetIdsOf(card)).toEqual(profile.choiceSetIds);
1059
+ expect(card).toEqual(formFor("small", FIRST_BUDGET, profile.typedTitle));
1060
+
1061
+ /*
1062
+ * In a personal chat, as here, the sender is checked as who they are in
1063
+ * this project, then the lists are read.
1064
+ */
1065
+ expect(senderLookupSpy).toHaveBeenCalledTimes(1);
1066
+ expect(senderLookupSpy).toHaveBeenCalledWith({
1067
+ teamsUserId: AAD_OBJECT_ID,
1068
+ projectId: PROJECT_ID,
1069
+ });
1070
+ expect(memberPropsSpy).toHaveBeenCalledTimes(1);
1071
+ expect(memberPropsSpy).toHaveBeenCalledWith({
1072
+ userId: USER_ID,
1073
+ projectId: PROJECT_ID,
1074
+ });
1075
+ expect(choicesSpyOf(profile)).toHaveBeenCalledTimes(1);
1076
+ expect(choicesSpyOf(profile)).toHaveBeenCalledWith(PROJECT_ID);
1077
+ expect(memberPropsSpy.mock.invocationCallOrder[0]!).toBeLessThan(
1078
+ choicesSpyOf(profile).mock.invocationCallOrder[0]!,
1079
+ );
1080
+
1081
+ expect(budgetsBuilt(profile)).toEqual([FIRST_BUDGET]);
1082
+ expect(errorLogSpy).not.toHaveBeenCalled();
1083
+ });
1084
+
1085
+ test("with nothing typed after the command, the title starts empty", async (): Promise<void> => {
1086
+ // MicrosoftTeams.ts passes "" for a bare "create incident" / "create maintenance".
1087
+ const teams: FakeTeams = await runCommand({
1088
+ profile: profile,
1089
+ initialTitle: "",
1090
+ });
1091
+
1092
+ expectSends(teams, { attempted: 1, delivered: 1 });
1093
+ const title: JSONObject | undefined = inputOf(
1094
+ theDeliveredCard(teams),
1095
+ profile.titleInputId,
1096
+ );
1097
+
1098
+ expect(title).toBeDefined();
1099
+ expect(title!["value"]).toBeUndefined();
1100
+ });
1101
+
1102
+ test("a title typed longer than the form takes is cut to fit, never through an emoji", async (): Promise<void> => {
1103
+ /*
1104
+ * The limit falls between the two UTF-16 halves of the emoji. Half an
1105
+ * emoji is not a character (Teams would show it as �), so the cut keeps
1106
+ * everything before the emoji and none of it.
1107
+ */
1108
+ const fits: string = "x".repeat(profile.titleMaxLength - 1);
1109
+ const typedTitle: string = `${fits}🔥 and more than the form takes`;
1110
+
1111
+ const teams: FakeTeams = await runCommand({
1112
+ profile: profile,
1113
+ initialTitle: typedTitle,
1114
+ });
1115
+
1116
+ expectSends(teams, { attempted: 1, delivered: 1 });
1117
+ const card: JSONObject = theDeliveredCard(teams);
1118
+
1119
+ expect(inputOf(card, profile.titleInputId)).toMatchObject({
1120
+ type: "Input.Text",
1121
+ maxLength: profile.titleMaxLength,
1122
+ value: fits,
1123
+ });
1124
+ expect(card).toEqual(formFor("small", FIRST_BUDGET, typedTitle));
1125
+ });
1126
+
1127
+ test("a big project's form fits the first budget and offers the dashboard's create page for what it leaves off", async (): Promise<void> => {
1128
+ giveChoices(profile, "big");
1129
+
1130
+ const teams: FakeTeams = await runCommand({
1131
+ profile: profile,
1132
+ initialTitle: profile.typedTitle,
1133
+ });
1134
+
1135
+ expectSends(teams, { attempted: 1, delivered: 1 });
1136
+ const card: JSONObject = theDeliveredCard(teams);
1137
+
1138
+ expect(sizeOf(card)).toBeLessThanOrEqual(FIRST_BUDGET);
1139
+ expect(openUrlsOf(card)).toEqual([profile.createLink]);
1140
+ expect(actionsOf(card)).toContainEqual({
1141
+ type: "Action.OpenUrl",
1142
+ title: "Create in OneUptime",
1143
+ url: profile.createLink,
1144
+ });
1145
+ expect(inputOf(card, profile.titleInputId)?.["value"]).toBe(
1146
+ profile.typedTitle,
1147
+ );
1148
+ expect(dashboardLinkSpy).toHaveBeenCalledWith({
1149
+ projectId: PROJECT_ID,
1150
+ route: profile.form.dashboardRoute,
1151
+ });
1152
+ expect(budgetsBuilt(profile)).toEqual([FIRST_BUDGET]);
1153
+
1154
+ // As many monitors as fit, not none, and the card says how many it left off.
1155
+ expect(inputOf(card, profile.monitorsInputId)).toBeDefined();
1156
+ expect(
1157
+ textsOf(card).some((text: string) => {
1158
+ return (
1159
+ text.startsWith("Showing the first ") &&
1160
+ text.includes(" of 1200 monitors, by name.")
1161
+ );
1162
+ }),
1163
+ ).toBe(true);
1164
+ expect(card).toEqual(formFor("big", FIRST_BUDGET, profile.typedTitle));
1165
+ });
1166
+ });
1167
+
1168
+ // Every message here comes from a personal chat (see messageActivity).
1169
+ describe("the sender check, in a personal chat", (): void => {
1170
+ test.each([
1171
+ { name: "no sender at all", from: undefined },
1172
+ { name: "a sender without an AAD object id", from: { id: "29:1f2e" } },
1173
+ {
1174
+ name: "an empty AAD object id",
1175
+ from: { id: "29:1f2e", aadObjectId: "" },
1176
+ },
1177
+ ])(
1178
+ "$name: one reply saying so, and nothing is looked up",
1179
+ async (row: {
1180
+ name: string;
1181
+ from: JSONObject | undefined;
1182
+ }): Promise<void> => {
1183
+ const activity: JSONObject = messageActivity(profile);
1184
+ delete activity["from"];
1185
+
1186
+ if (row.from) {
1187
+ activity["from"] = row.from;
1188
+ }
1189
+
1190
+ const teams: FakeTeams = await runCommand({
1191
+ profile: profile,
1192
+ activity: activity,
1193
+ });
1194
+
1195
+ expectSends(teams, { attempted: 1, delivered: 1 });
1196
+ expect(theDeliveredText(teams)).toBe(profile.replies.senderUnknown);
1197
+ expect(senderLookupSpy).not.toHaveBeenCalled();
1198
+ expect(memberPropsSpy).not.toHaveBeenCalled();
1199
+ expect(assertCanCreateSpy).not.toHaveBeenCalled();
1200
+ expect(choicesSpyOf(profile)).not.toHaveBeenCalled();
1201
+ // Worth a warning, not an error: nothing on the server failed.
1202
+ expect(warnLogSpy).toHaveBeenCalledTimes(1);
1203
+ expect(warnLogSpy).toHaveBeenCalledWith(
1204
+ "A Microsoft Teams create command arrived without the sender's AAD object id",
1205
+ { projectId: PROJECT_ID.toString() },
1206
+ );
1207
+ expect(errorLogSpy).not.toHaveBeenCalled();
1208
+ },
1209
+ );
1210
+
1211
+ test("an account nobody connected yet: one reply with the settings link, no form, no lists read", async (): Promise<void> => {
1212
+ senderLookupSpy.mockRejectedValue(
1213
+ new MicrosoftTeamsAccountNotLinkedException(
1214
+ "No OneUptime user linked to this Microsoft Teams user. Please authenticate with Microsoft Teams.",
1215
+ ),
1216
+ );
1217
+
1218
+ const teams: FakeTeams = await runCommand({ profile: profile });
1219
+
1220
+ expectSends(teams, { attempted: 1, delivered: 1 });
1221
+ expect(theDeliveredText(teams)).toBe(profile.replies.accountNotLinked);
1222
+ expect(dashboardLinkSpy).toHaveBeenCalledWith({
1223
+ projectId: PROJECT_ID,
1224
+ route: "/user-settings/microsoft-teams-integration",
1225
+ });
1226
+ expect(memberPropsSpy).not.toHaveBeenCalled();
1227
+ expect(assertCanCreateSpy).not.toHaveBeenCalled();
1228
+ expect(choicesSpyOf(profile)).not.toHaveBeenCalled();
1229
+ // Expected, not a server error.
1230
+ expect(errorLogSpy).not.toHaveBeenCalled();
1231
+ });
1232
+
1233
+ test("an account nobody connected yet, with no dashboard URL known: says where to connect it in words", async (): Promise<void> => {
1234
+ senderLookupSpy.mockRejectedValue(
1235
+ new MicrosoftTeamsAccountNotLinkedException("No linked user."),
1236
+ );
1237
+ dashboardLinkSpy.mockResolvedValue(null);
1238
+
1239
+ const teams: FakeTeams = await runCommand({ profile: profile });
1240
+
1241
+ expectSends(teams, { attempted: 1, delivered: 1 });
1242
+ expect(theDeliveredText(teams)).toBe(
1243
+ profile.replies.accountNotLinkedWithoutLink,
1244
+ );
1245
+ });
1246
+
1247
+ test("a sender who is not a member of the project: one reply with that message, verbatim", async (): Promise<void> => {
1248
+ memberPropsSpy.mockRejectedValue(
1249
+ new NotAuthorizedException(
1250
+ WorkspaceActionAuthorization.NOT_A_PROJECT_MEMBER_MESSAGE,
1251
+ ),
1252
+ );
1253
+
1254
+ const teams: FakeTeams = await runCommand({ profile: profile });
1255
+
1256
+ expectSends(teams, { attempted: 1, delivered: 1 });
1257
+ expect(theDeliveredText(teams)).toBe(
1258
+ "Your OneUptime account is not a member of this project. Ask a project admin to invite you, then try again.",
1259
+ );
1260
+ expect(assertCanCreateSpy).not.toHaveBeenCalled();
1261
+ expect(choicesSpyOf(profile)).not.toHaveBeenCalled();
1262
+ expect(errorLogSpy).not.toHaveBeenCalled();
1263
+ });
1264
+
1265
+ test.each([
1266
+ {
1267
+ name: "looking up the linked account fails (database down)",
1268
+ stage: "lookup",
1269
+ error: new Error("connect ECONNREFUSED 10.0.0.5:5432"),
1270
+ described: "Error: connect ECONNREFUSED 10.0.0.5:5432",
1271
+ },
1272
+ {
1273
+ name: "reading the project membership fails (database down)",
1274
+ stage: "membership",
1275
+ error: new Error("connect ECONNREFUSED 10.0.0.5:5432"),
1276
+ described: "Error: connect ECONNREFUSED 10.0.0.5:5432",
1277
+ },
1278
+ {
1279
+ name: "the account lookup refuses with a BadDataException that is not about a missing link",
1280
+ stage: "lookup",
1281
+ error: new BadDataException("Invalid workspace user id."),
1282
+ // OneUptime's exceptions keep Error's name; the log names their class.
1283
+ described: "BadDataException: Invalid workspace user id.",
1284
+ },
1285
+ ])(
1286
+ "$name: one reply that the account could not be checked, and the details go to the log",
1287
+ async (row: {
1288
+ name: string;
1289
+ stage: string;
1290
+ error: Error;
1291
+ described: string;
1292
+ }): Promise<void> => {
1293
+ if (row.stage === "lookup") {
1294
+ senderLookupSpy.mockRejectedValue(row.error);
1295
+ } else {
1296
+ memberPropsSpy.mockRejectedValue(row.error);
1297
+ }
1298
+
1299
+ const teams: FakeTeams = await runCommand({ profile: profile });
1300
+
1301
+ expectSends(teams, { attempted: 1, delivered: 1 });
1302
+ const reply: string = theDeliveredText(teams);
1303
+ expect(reply).toBe(profile.replies.senderCheckFailed);
1304
+ expect(reply).not.toContain(row.error.message);
1305
+ expect(choicesSpyOf(profile)).not.toHaveBeenCalled();
1306
+ // No HTTP status, so the error itself follows the line, for its stack.
1307
+ expectErrorLogs([
1308
+ `Could not check the Microsoft Teams sender of a create command: ${row.described}`,
1309
+ row.error,
1310
+ ]);
1311
+ },
1312
+ );
1313
+ });
1314
+
1315
+ /*
1316
+ * Anyone in a channel or group chat may submit the form, and the submit
1317
+ * checks whoever does (a linked project member, and for maintenance one who
1318
+ * may create the event), so a check of whoever asked for it would only
1319
+ * refuse the form to everyone else there.
1320
+ */
1321
+ describe("in a channel or group chat", (): void => {
1322
+ test.each(SHARED_CONVERSATIONS)(
1323
+ "in a $name, the form is posted for everyone there and nobody is looked up",
1324
+ async (conversation: SharedConversation): Promise<void> => {
1325
+ /*
1326
+ * Every check a personal chat makes would refuse this sender, so any
1327
+ * one of them running here would answer with a refusal, not the form.
1328
+ */
1329
+ senderLookupSpy.mockRejectedValue(
1330
+ new MicrosoftTeamsAccountNotLinkedException("No linked user."),
1331
+ );
1332
+ memberPropsSpy.mockRejectedValue(
1333
+ new NotAuthorizedException(
1334
+ WorkspaceActionAuthorization.NOT_A_PROJECT_MEMBER_MESSAGE,
1335
+ ),
1336
+ );
1337
+ assertCanCreateSpy.mockRejectedValue(
1338
+ new NotAuthorizedException(
1339
+ `You do not have permission to create ${profile.form.what}.`,
1340
+ ),
1341
+ );
1342
+
1343
+ const teams: FakeTeams = await runCommand({
1344
+ profile: profile,
1345
+ activity: sharedConversationMessage(profile, conversation),
1346
+ initialTitle: profile.typedTitle,
1347
+ });
1348
+
1349
+ expectSends(teams, { attempted: 1, delivered: 1 });
1350
+ expect(theDeliveredCard(teams)).toEqual(
1351
+ formFor("small", FIRST_BUDGET, profile.typedTitle),
1352
+ );
1353
+ expect(senderLookupSpy).not.toHaveBeenCalled();
1354
+ expect(memberPropsSpy).not.toHaveBeenCalled();
1355
+ expect(assertCanCreateSpy).not.toHaveBeenCalled();
1356
+ expect(choicesSpyOf(profile)).toHaveBeenCalledTimes(1);
1357
+ expect(choicesSpyOf(profile)).toHaveBeenCalledWith(PROJECT_ID);
1358
+ expect(budgetsBuilt(profile)).toEqual([FIRST_BUDGET]);
1359
+ expect(warnLogSpy).not.toHaveBeenCalled();
1360
+ expect(errorLogSpy).not.toHaveBeenCalled();
1361
+ },
1362
+ );
1363
+
1364
+ test.each(SHARED_CONVERSATIONS)(
1365
+ "in a $name, a message without the sender's AAD object id gets the form too: who asked for it does not decide who may submit it",
1366
+ async (conversation: SharedConversation): Promise<void> => {
1367
+ const teams: FakeTeams = await runCommand({
1368
+ profile: profile,
1369
+ activity: sharedConversationMessage(profile, conversation, {
1370
+ from: { id: "29:1f2e3d4c5b6a7980", name: "Jane Doe" },
1371
+ }),
1372
+ });
1373
+
1374
+ expectSends(teams, { attempted: 1, delivered: 1 });
1375
+ expect(theDeliveredCard(teams)).toEqual(formFor("small", FIRST_BUDGET));
1376
+ expect(senderLookupSpy).not.toHaveBeenCalled();
1377
+ // Nobody had to be told apart, so there is nothing to warn about.
1378
+ expect(warnLogSpy).not.toHaveBeenCalled();
1379
+ expect(errorLogSpy).not.toHaveBeenCalled();
1380
+ },
1381
+ );
1382
+ });
1383
+
1384
+ describe("the form's lists", (): void => {
1385
+ test.each([
1386
+ {
1387
+ name: "a query that timed out",
1388
+ error: new Error("Connection terminated due to connection timeout"),
1389
+ described: "Error: Connection terminated due to connection timeout",
1390
+ },
1391
+ {
1392
+ name: "no database connection",
1393
+ error: new DatabaseNotConnectedException(),
1394
+ // A OneUptime exception, named in the log by its class.
1395
+ described: "DatabaseNotConnectedException: Database not connected",
1396
+ },
1397
+ ])(
1398
+ "lists that cannot be read ($name): one reply with the dashboard's create page, and the details go to the log",
1399
+ async (row: {
1400
+ name: string;
1401
+ error: Error;
1402
+ described: string;
1403
+ }): Promise<void> => {
1404
+ failChoices(profile, row.error);
1405
+
1406
+ const teams: FakeTeams = await runCommand({ profile: profile });
1407
+
1408
+ expectSends(teams, { attempted: 1, delivered: 1 });
1409
+ const reply: string = theDeliveredText(teams);
1410
+ expect(reply).toBe(profile.replies.listsUnreadable);
1411
+ expect(reply).not.toContain(row.error.message);
1412
+ // No HTTP status, so the error itself follows the line, for its stack.
1413
+ expectErrorLogs([
1414
+ `Could not load the Microsoft Teams form to create ${profile.form.what}: ${row.described}`,
1415
+ row.error,
1416
+ ]);
1417
+ expect(budgetsBuilt(profile)).toEqual([]);
1418
+ },
1419
+ );
1420
+
1421
+ test("lists that cannot be read, with no dashboard URL known: the same reply without a link", async (): Promise<void> => {
1422
+ failChoices(profile, new Error("Connection terminated"));
1423
+ dashboardLinkSpy.mockResolvedValue(null);
1424
+
1425
+ const teams: FakeTeams = await runCommand({ profile: profile });
1426
+
1427
+ expectSends(teams, { attempted: 1, delivered: 1 });
1428
+ expect(theDeliveredText(teams)).toBe(
1429
+ profile.replies.listsUnreadableWithoutLink,
1430
+ );
1431
+ });
1432
+ });
1433
+
1434
+ describe("a form Teams refuses", (): void => {
1435
+ test("413 on the first card: the smaller card for the next budget goes out, and that is the only reply", async (): Promise<void> => {
1436
+ giveChoices(profile, "big");
1437
+
1438
+ const teams: FakeTeams = await runCommand({
1439
+ profile: profile,
1440
+ initialTitle: profile.typedTitle,
1441
+ answer: refuseCards({ refusal: messageSizeTooBig, times: 1 }),
1442
+ });
1443
+
1444
+ expectSends(teams, { attempted: 2, delivered: 1 });
1445
+ const [refused, sent]: Array<JSONObject> = attemptedCards(teams);
1446
+
1447
+ expect(sizeOf(refused!)).toBeLessThanOrEqual(FIRST_BUDGET);
1448
+ expect(sizeOf(refused!)).toBeGreaterThan(SECOND_BUDGET);
1449
+ expect(sizeOf(sent!)).toBeLessThanOrEqual(SECOND_BUDGET);
1450
+ expect(theDeliveredCard(teams)).toEqual(sent);
1451
+
1452
+ // Still the same form: the typed title, the submit, the dashboard button.
1453
+ expect(inputOf(sent!, profile.titleInputId)?.["value"]).toBe(
1454
+ profile.typedTitle,
1455
+ );
1456
+ expect(submitDataOf(sent!)["action"]).toBe(profile.submitAction);
1457
+ expect(openUrlsOf(sent!)).toEqual([profile.createLink]);
1458
+ expect(inputOf(sent!, profile.monitorsInputId)).toBeDefined();
1459
+
1460
+ // Each is the builder's card for its budget, from the same lists and title.
1461
+ expect([refused, sent]).toEqual([
1462
+ formFor("big", FIRST_BUDGET, profile.typedTitle),
1463
+ formFor("big", SECOND_BUDGET, profile.typedTitle),
1464
+ ]);
1465
+ expect(budgetsBuilt(profile)).toEqual([FIRST_BUDGET, SECOND_BUDGET]);
1466
+
1467
+ // Recovered from, so a warning for the operator and not an error.
1468
+ expect(errorLogSpy).not.toHaveBeenCalled();
1469
+ expect(warnLogSpy).toHaveBeenCalledTimes(1);
1470
+ expect(String(warnLogSpy.mock.calls[0]![0])).toContain(
1471
+ `card as too large (budget ${FIRST_BUDGET} bytes); trying a smaller one.`,
1472
+ );
1473
+ });
1474
+
1475
+ test("413 on a small project's form: the 20 KiB card would be the same, so the card without lists goes out next", async (): Promise<void> => {
1476
+ const teams: FakeTeams = await runCommand({
1477
+ profile: profile,
1478
+ answer: refuseCards({ refusal: messageSizeTooBig, times: 1 }),
1479
+ });
1480
+
1481
+ expectSends(teams, { attempted: 2, delivered: 1 });
1482
+ const [refused, sent]: Array<JSONObject> = attemptedCards(teams);
1483
+
1484
+ expect(inputOf(refused!, profile.monitorsInputId)).toBeDefined();
1485
+ expect(inputOf(sent!, profile.monitorsInputId)).toBeUndefined();
1486
+ expect(
1487
+ textsOf(sent!).some((text: string) => {
1488
+ return text.startsWith(
1489
+ "This project has 3 monitors, too many to list in Microsoft Teams.",
1490
+ );
1491
+ }),
1492
+ ).toBe(true);
1493
+ expect(openUrlsOf(sent!)).toEqual([profile.createLink]);
1494
+ expect(theDeliveredCard(teams)).toEqual(sent);
1495
+ expect([refused, sent]).toEqual([
1496
+ formFor("small", FIRST_BUDGET),
1497
+ formFor("small", LAST_BUDGET),
1498
+ ]);
1499
+
1500
+ // The 20 KiB card was built, found identical to the refused one, and skipped.
1501
+ expect(budgetsBuilt(profile)).toEqual([
1502
+ FIRST_BUDGET,
1503
+ SECOND_BUDGET,
1504
+ LAST_BUDGET,
1505
+ ]);
1506
+ });
1507
+
1508
+ test("413 on every card: three ever smaller cards, then one reply that Teams refused it as too large, with the dashboard's create page", async (): Promise<void> => {
1509
+ giveChoices(profile, "big");
1510
+
1511
+ const teams: FakeTeams = await runCommand({
1512
+ profile: profile,
1513
+ answer: refuseCards({ refusal: messageSizeTooBig }),
1514
+ });
1515
+
1516
+ expectSends(teams, { attempted: 4, delivered: 1 });
1517
+ expect(theDeliveredText(teams)).toBe(profile.replies.tooLarge);
1518
+
1519
+ const cards: Array<JSONObject> = attemptedCards(teams);
1520
+ expect(cards).toHaveLength(3);
1521
+ expect(sizeOf(cards[0]!)).toBeGreaterThan(sizeOf(cards[1]!));
1522
+ expect(sizeOf(cards[1]!)).toBeGreaterThan(sizeOf(cards[2]!));
1523
+ // The last one left every list off.
1524
+ expect(inputOf(cards[2]!, profile.monitorsInputId)).toBeUndefined();
1525
+ expect(inputOf(cards[2]!, "labels")).toBeUndefined();
1526
+ expect(cards).toEqual([
1527
+ formFor("big", FIRST_BUDGET),
1528
+ formFor("big", SECOND_BUDGET),
1529
+ formFor("big", LAST_BUDGET),
1530
+ ]);
1531
+ expect(budgetsBuilt(profile)).toEqual([
1532
+ FIRST_BUDGET,
1533
+ SECOND_BUDGET,
1534
+ LAST_BUDGET,
1535
+ ]);
1536
+ // The reply is the last thing sent.
1537
+ expect(isCardReply(teams.attempts[3]!.reply)).toBe(false);
1538
+
1539
+ /*
1540
+ * Teams' refusal carries its HTTP status, which the line names with its
1541
+ * code, so the line is all that is logged, not the whole RestError.
1542
+ */
1543
+ expectErrorLogs([
1544
+ `Microsoft Teams did not accept the form to create ${profile.form.what}: RestError 413 MessageSizeTooBig: Message size too large.`,
1545
+ ]);
1546
+ // Each refused card was worth a warning on the way down.
1547
+ expect(warnLogSpy).toHaveBeenCalledTimes(3);
1548
+ });
1549
+
1550
+ test("413 on every card, with no dashboard URL known: the too-large reply without a link", async (): Promise<void> => {
1551
+ giveChoices(profile, "big");
1552
+ dashboardLinkSpy.mockResolvedValue(null);
1553
+
1554
+ const teams: FakeTeams = await runCommand({
1555
+ profile: profile,
1556
+ answer: refuseCards({ refusal: messageSizeTooBig }),
1557
+ });
1558
+
1559
+ expectSends(teams, { attempted: 4, delivered: 1 });
1560
+ expect(theDeliveredText(teams)).toBe(profile.replies.tooLargeWithoutLink);
1561
+ // With no dashboard URL to point to, no card has the button either.
1562
+ expect(
1563
+ attemptedCards(teams).every((card: JSONObject) => {
1564
+ return openUrlsOf(card).length === 0;
1565
+ }),
1566
+ ).toBe(true);
1567
+ expect(attemptedCards(teams)).toEqual(
1568
+ MICROSOFT_TEAMS_CARD_SIZE_BUDGETS_IN_BYTES.map(
1569
+ (budgetInBytes: number): JSONObject => {
1570
+ return expectedForm(profile, {
1571
+ size: "big",
1572
+ budgetInBytes: budgetInBytes,
1573
+ createInOneUptimeUrl: null,
1574
+ });
1575
+ },
1576
+ ),
1577
+ );
1578
+ });
1579
+
1580
+ test("413 on a card with nothing to shorten: it is not sent again, and the too-large reply follows", async (): Promise<void> => {
1581
+ giveChoices(profile, "listless");
1582
+
1583
+ const teams: FakeTeams = await runCommand({
1584
+ profile: profile,
1585
+ answer: refuseCards({ refusal: messageSizeTooBig }),
1586
+ });
1587
+
1588
+ expectSends(teams, { attempted: 2, delivered: 1 });
1589
+ expect(attemptedCards(teams)).toEqual([
1590
+ formFor("listless", FIRST_BUDGET),
1591
+ ]);
1592
+ expect(theDeliveredText(teams)).toBe(profile.replies.tooLarge);
1593
+ // Each smaller budget was built, found no different, and not sent.
1594
+ expect(budgetsBuilt(profile)).toEqual([
1595
+ FIRST_BUDGET,
1596
+ SECOND_BUDGET,
1597
+ LAST_BUDGET,
1598
+ ]);
1599
+ // One refused card, one warning; the refusal's line is the one error.
1600
+ expect(warnLogSpy).toHaveBeenCalledTimes(1);
1601
+ expectErrorLogs([
1602
+ `Microsoft Teams did not accept the form to create ${profile.form.what}: RestError 413 MessageSizeTooBig: Message size too large.`,
1603
+ ]);
1604
+ });
1605
+
1606
+ test("403 BotNotInConversationRoster: no smaller card is tried, and one reply names Teams' status and code", async (): Promise<void> => {
1607
+ giveChoices(profile, "big");
1608
+
1609
+ const teams: FakeTeams = await runCommand({
1610
+ profile: profile,
1611
+ answer: refuseCards({ refusal: botNotInConversationRoster }),
1612
+ });
1613
+
1614
+ expectSends(teams, { attempted: 2, delivered: 1 });
1615
+ expect(attemptedCards(teams)).toEqual([formFor("big", FIRST_BUDGET)]);
1616
+ expect(budgetsBuilt(profile)).toEqual([FIRST_BUDGET]);
1617
+
1618
+ const reply: string = theDeliveredText(teams);
1619
+ expect(reply).toBe(profile.replies.notInRoster);
1620
+ expect(reply).toBe(
1621
+ MicrosoftTeamsCreateCommands.getFormNotAcceptedMessage({
1622
+ form: profile.form,
1623
+ error: botNotInConversationRoster(),
1624
+ createInOneUptimeUrl: profile.createLink,
1625
+ }),
1626
+ );
1627
+ expect(reply).not.toContain("roster.");
1628
+ // An HTTP refusal: its line, naming the status and code, is all there is.
1629
+ expectErrorLogs([
1630
+ `Microsoft Teams did not accept the form to create ${profile.form.what}: RestError 403 BotNotInConversationRoster: The bot is not part of the conversation roster.`,
1631
+ ]);
1632
+ // Not a size problem, so there was nothing smaller to try and no warning.
1633
+ expect(warnLogSpy).not.toHaveBeenCalled();
1634
+ });
1635
+
1636
+ test("413 on the first card, then a dropped connection on the smaller one: answered at once, without trying the last budget", async (): Promise<void> => {
1637
+ giveChoices(profile, "big");
1638
+ let cardsSeen: number = 0;
1639
+ const droppedConnections: Array<Error> = [];
1640
+
1641
+ const teams: FakeTeams = await runCommand({
1642
+ profile: profile,
1643
+ answer: (reply: unknown): unknown => {
1644
+ if (!isCardReply(reply)) {
1645
+ return undefined;
1646
+ }
1647
+
1648
+ cardsSeen++;
1649
+
1650
+ if (cardsSeen === 1) {
1651
+ return messageSizeTooBig();
1652
+ }
1653
+
1654
+ const droppedConnection: Error = connectionReset();
1655
+ droppedConnections.push(droppedConnection);
1656
+ return droppedConnection;
1657
+ },
1658
+ });
1659
+
1660
+ expectSends(teams, { attempted: 3, delivered: 1 });
1661
+ expect(attemptedCards(teams)).toEqual([
1662
+ formFor("big", FIRST_BUDGET),
1663
+ formFor("big", SECOND_BUDGET),
1664
+ ]);
1665
+ expect(budgetsBuilt(profile)).toEqual([FIRST_BUDGET, SECOND_BUDGET]);
1666
+ expect(theDeliveredText(teams)).toBe(
1667
+ `Sorry, I couldn't open the form to create ${profile.form.what}: Microsoft Teams did not accept it (ECONNRESET). You can create it in OneUptime instead: ${profile.createLink}`,
1668
+ );
1669
+ /*
1670
+ * A dropped connection has a network code but no HTTP status, so the
1671
+ * error itself follows the line, for its stack.
1672
+ */
1673
+ expect(droppedConnections).toHaveLength(1);
1674
+ expectErrorLogs([
1675
+ `Microsoft Teams did not accept the form to create ${profile.form.what}: Error ECONNRESET: socket hang up`,
1676
+ droppedConnections[0]!,
1677
+ ]);
1678
+ });
1679
+
1680
+ test("a form that cannot be built still gets one reply, with the dashboard's create page, and nothing is thrown", async (): Promise<void> => {
1681
+ const builderError: TypeError = new TypeError(
1682
+ "Cannot read properties of undefined (reading 'choices')",
1683
+ );
1684
+ const builderFailure: () => never = (): never => {
1685
+ throw builderError;
1686
+ };
1687
+
1688
+ if (profile.kind === "incident") {
1689
+ incidentCardSpy.mockImplementation(builderFailure);
1690
+ } else {
1691
+ maintenanceCardSpy.mockImplementation(builderFailure);
1692
+ }
1693
+
1694
+ const teams: FakeTeams = await runCommand({ profile: profile });
1695
+
1696
+ expectSends(teams, { attempted: 1, delivered: 1 });
1697
+ const reply: string = theDeliveredText(teams);
1698
+ expect(
1699
+ reply.startsWith(
1700
+ `Sorry, I couldn't open the form to create ${profile.form.what}`,
1701
+ ),
1702
+ ).toBe(true);
1703
+ expect(reply.endsWith(profile.createLink)).toBe(true);
1704
+ expect(reply).not.toContain("Cannot read properties");
1705
+ // Not a size problem, so no smaller card was tried.
1706
+ expect(budgetsBuilt(profile)).toEqual([FIRST_BUDGET]);
1707
+ /*
1708
+ * The operator is told what broke, and in which project, then gets the
1709
+ * TypeError itself, with the stack that says where.
1710
+ */
1711
+ expect(errorLogSpy).toHaveBeenCalledTimes(2);
1712
+ expect(errorLogSpy).toHaveBeenNthCalledWith(
1713
+ 1,
1714
+ expect.stringContaining(
1715
+ ": TypeError: Cannot read properties of undefined (reading 'choices')",
1716
+ ),
1717
+ PROJECT_LOG_ATTRIBUTES,
1718
+ );
1719
+ expect(errorLogSpy.mock.calls[1]![0]).toBe(builderError);
1720
+ expect(errorLogSpy.mock.calls[1]![1]).toEqual(PROJECT_LOG_ATTRIBUTES);
1721
+ });
1722
+ });
1723
+
1724
+ describe("a reply Teams refuses too", (): void => {
1725
+ interface RefusedReplyCase {
1726
+ name: string;
1727
+ // Sets the scene and returns the message the command answers.
1728
+ arrange: () => JSONObject;
1729
+ answer: () => TeamsAnswer;
1730
+ attempted: number;
1731
+ }
1732
+
1733
+ const REFUSED_REPLY_CASES: ReadonlyArray<RefusedReplyCase> = [
1734
+ {
1735
+ name: "the reply that the sender is unknown",
1736
+ arrange: (): JSONObject => {
1737
+ return messageActivity(profile, { from: { id: "29:1f2e" } });
1738
+ },
1739
+ answer: (): TeamsAnswer => {
1740
+ return refuseEverything(botNotInConversationRoster);
1741
+ },
1742
+ attempted: 1,
1743
+ },
1744
+ {
1745
+ name: "the account-not-linked reply",
1746
+ arrange: (): JSONObject => {
1747
+ senderLookupSpy.mockRejectedValue(
1748
+ new MicrosoftTeamsAccountNotLinkedException("No linked user."),
1749
+ );
1750
+ return messageActivity(profile);
1751
+ },
1752
+ answer: (): TeamsAnswer => {
1753
+ return refuseEverything(botNotInConversationRoster);
1754
+ },
1755
+ attempted: 1,
1756
+ },
1757
+ {
1758
+ name: "the not-a-member reply",
1759
+ arrange: (): JSONObject => {
1760
+ memberPropsSpy.mockRejectedValue(
1761
+ new NotAuthorizedException(
1762
+ WorkspaceActionAuthorization.NOT_A_PROJECT_MEMBER_MESSAGE,
1763
+ ),
1764
+ );
1765
+ return messageActivity(profile);
1766
+ },
1767
+ answer: (): TeamsAnswer => {
1768
+ return refuseEverything(botNotInConversationRoster);
1769
+ },
1770
+ attempted: 1,
1771
+ },
1772
+ {
1773
+ name: "the could-not-check-your-account reply",
1774
+ arrange: (): JSONObject => {
1775
+ senderLookupSpy.mockRejectedValue(new Error("ECONNREFUSED"));
1776
+ return messageActivity(profile);
1777
+ },
1778
+ answer: (): TeamsAnswer => {
1779
+ return refuseEverything(connectionReset);
1780
+ },
1781
+ attempted: 1,
1782
+ },
1783
+ {
1784
+ name: "the could-not-read-the-lists reply",
1785
+ arrange: (): JSONObject => {
1786
+ failChoices(profile, new Error("Connection terminated"));
1787
+ return messageActivity(profile);
1788
+ },
1789
+ answer: (): TeamsAnswer => {
1790
+ return refuseEverything(connectionReset);
1791
+ },
1792
+ attempted: 1,
1793
+ },
1794
+ {
1795
+ name: "the form, and then the did-not-accept reply",
1796
+ arrange: (): JSONObject => {
1797
+ return messageActivity(profile);
1798
+ },
1799
+ answer: (): TeamsAnswer => {
1800
+ return refuseEverything(botNotInConversationRoster);
1801
+ },
1802
+ attempted: 2,
1803
+ },
1804
+ {
1805
+ name: "every card, and then the too-large reply",
1806
+ arrange: (): JSONObject => {
1807
+ giveChoices(profile, "big");
1808
+ return messageActivity(profile);
1809
+ },
1810
+ answer: (): TeamsAnswer => {
1811
+ return refuseEverything(messageSizeTooBig);
1812
+ },
1813
+ attempted: 4,
1814
+ },
1815
+ ];
1816
+
1817
+ test.each(REFUSED_REPLY_CASES)(
1818
+ "Teams refusing $name: nothing reaches the user, nothing is thrown, and the failed send is logged",
1819
+ async (row: RefusedReplyCase): Promise<void> => {
1820
+ const activity: JSONObject = row.arrange();
1821
+
1822
+ const teams: FakeTeams = await runCommand({
1823
+ profile: profile,
1824
+ activity: activity,
1825
+ answer: row.answer(),
1826
+ });
1827
+
1828
+ expectSends(teams, { attempted: row.attempted, delivered: 0 });
1829
+ expect(loggedErrors()).toContain(
1830
+ "Could not send a Microsoft Teams reply:",
1831
+ );
1832
+ },
1833
+ );
1834
+ });
1835
+ });
1836
+
1837
+ describe("create incident", (): void => {
1838
+ /*
1839
+ * DELIBERATE, not an oversight: creating an incident from Microsoft Teams
1840
+ * asks for no incident-create permission, only a linked account of a
1841
+ * current project member. It is the product's rule for incidents raised
1842
+ * from chat - Slack lets "anyone in the company" create one
1843
+ * (slackActionTypesThatDoNotRequireUserSlackAccountToBeConnectedToOneUptime
1844
+ * in Server/Utils/Workspace/Slack/Actions/Auth.ts) - and commit 7ac7a3a03f,
1845
+ * which made every other chat write check the linked member's own
1846
+ * permissions, left incident creation out of it. Membership is still
1847
+ * required: by this command in a personal chat (a non-member is refused,
1848
+ * see "the sender check, in a personal chat") and by every submit
1849
+ * (MicrosoftTeamsUtil.handleBotInvokeActivity). Change these two tests only
1850
+ * together with that product decision.
1851
+ */
1852
+ test("never asks for a create permission: its submit only needs a project member", async (): Promise<void> => {
1853
+ assertCanCreateSpy.mockRejectedValue(
1854
+ new NotAuthorizedException(
1855
+ "You do not have permission to create an incident.",
1856
+ ),
1857
+ );
1858
+
1859
+ const teams: FakeTeams = await runCommand({ profile: INCIDENT });
1860
+
1861
+ expectSends(teams, { attempted: 1, delivered: 1 });
1862
+ expect(theDeliveredCard(teams)["type"]).toBe("AdaptiveCard");
1863
+ expect(assertCanCreateSpy).not.toHaveBeenCalled();
1864
+ });
1865
+
1866
+ test("a linked member the permission model would not let create an incident still gets the form, as the policy above says", async (): Promise<void> => {
1867
+ assertCanCreateSpy.mockImplementation(realAssertCanCreate);
1868
+ const viewer: DatabaseCommonInteractionProps = memberWith([
1869
+ Permission.Viewer,
1870
+ ]);
1871
+ memberPropsSpy.mockResolvedValue(viewer);
1872
+
1873
+ // Asked, the permission model itself would refuse this member an incident.
1874
+ await expect(
1875
+ realAssertCanCreate({
1876
+ props: viewer,
1877
+ modelType: Incident,
1878
+ action: "create an incident",
1879
+ }),
1880
+ ).rejects.toBeInstanceOf(NotAuthorizedException);
1881
+
1882
+ const teams: FakeTeams = await runCommand({
1883
+ profile: INCIDENT,
1884
+ initialTitle: INCIDENT.typedTitle,
1885
+ });
1886
+
1887
+ expectSends(teams, { attempted: 1, delivered: 1 });
1888
+ expect(theDeliveredCard(teams)).toEqual(
1889
+ expectedForm(INCIDENT, {
1890
+ size: "small",
1891
+ budgetInBytes: FIRST_BUDGET,
1892
+ initialTitle: INCIDENT.typedTitle,
1893
+ createInOneUptimeUrl: CREATE_INCIDENT_LINK,
1894
+ }),
1895
+ );
1896
+ // Checked as a member of the project, and for nothing more.
1897
+ expect(memberPropsSpy).toHaveBeenCalledTimes(1);
1898
+ expect(assertCanCreateSpy).not.toHaveBeenCalled();
1899
+ expect(errorLogSpy).not.toHaveBeenCalled();
1900
+ });
1901
+
1902
+ test("offers the severities, which are never shortened, even on the card without lists", async (): Promise<void> => {
1903
+ giveChoices(INCIDENT, "big");
1904
+
1905
+ const teams: FakeTeams = await runCommand({
1906
+ profile: INCIDENT,
1907
+ answer: refuseCards({ refusal: messageSizeTooBig, times: 2 }),
1908
+ });
1909
+
1910
+ expectSends(teams, { attempted: 3, delivered: 1 });
1911
+ const card: JSONObject = theDeliveredCard(teams);
1912
+ expect(inputOf(card, "incidentMonitors")).toBeUndefined();
1913
+ expect(inputOf(card, "onCallDutyPolicies")).toBeUndefined();
1914
+ expect(
1915
+ (inputOf(card, "incidentSeverity")?.["choices"] as Array<JSONObject>).map(
1916
+ (choice: JSONObject) => {
1917
+ return choice["title"];
1918
+ },
1919
+ ),
1920
+ ).toEqual(["Critical", "Major", "Minor"]);
1921
+ expect(card).toEqual(
1922
+ expectedForm(INCIDENT, {
1923
+ size: "big",
1924
+ budgetInBytes: LAST_BUDGET,
1925
+ createInOneUptimeUrl: CREATE_INCIDENT_LINK,
1926
+ }),
1927
+ );
1928
+ });
1929
+
1930
+ test("a project without incident severities gets one reply saying so, linking the page where they are added, and no form", async (): Promise<void> => {
1931
+ incidentChoicesSpy.mockResolvedValue({
1932
+ ...incidentChoices("small"),
1933
+ severities: emptyList(),
1934
+ });
1935
+
1936
+ const teams: FakeTeams = await runCommand({ profile: INCIDENT });
1937
+
1938
+ expectSends(teams, { attempted: 1, delivered: 1 });
1939
+ expect(theDeliveredText(teams)).toBe(
1940
+ `${NO_SEVERITIES_OPENING} Add one in OneUptime under [Incidents → Settings → Incident Severity](${SEVERITY_SETTINGS_LINK}), then try again.`,
1941
+ );
1942
+ // The one link asked for is that page's, not the create page's.
1943
+ expect(dashboardLinkSpy).toHaveBeenCalledTimes(1);
1944
+ expect(dashboardLinkSpy).toHaveBeenCalledWith({
1945
+ projectId: PROJECT_ID,
1946
+ route: "/incidents/settings/severity",
1947
+ });
1948
+ expect(incidentCardSpy).not.toHaveBeenCalled();
1949
+ expect(errorLogSpy).not.toHaveBeenCalled();
1950
+ });
1951
+
1952
+ test("a project without incident severities, with no dashboard URL known: names that page in words", async (): Promise<void> => {
1953
+ incidentChoicesSpy.mockResolvedValue({
1954
+ ...incidentChoices("small"),
1955
+ severities: emptyList(),
1956
+ });
1957
+ dashboardLinkSpy.mockResolvedValue(null);
1958
+
1959
+ const teams: FakeTeams = await runCommand({ profile: INCIDENT });
1960
+
1961
+ expectSends(teams, { attempted: 1, delivered: 1 });
1962
+ expect(theDeliveredText(teams)).toBe(
1963
+ `${NO_SEVERITIES_OPENING} Add one in OneUptime under Incidents → Settings → Incident Severity, then try again.`,
1964
+ );
1965
+ expect(incidentCardSpy).not.toHaveBeenCalled();
1966
+ });
1967
+
1968
+ test("a project without incident severities is sent to where severities are added: Incidents → Settings → Incident Severity", async (): Promise<void> => {
1969
+ /*
1970
+ * Incident severities are managed under Incidents → Settings → Incident
1971
+ * Severity (/dashboard/<project>/incidents/settings/severity). Project
1972
+ * Settings has no such page; the docs (incidents/settings.md) say
1973
+ * incident configuration "does not live in Project Settings".
1974
+ */
1975
+ incidentChoicesSpy.mockResolvedValue({
1976
+ ...incidentChoices("small"),
1977
+ severities: emptyList(),
1978
+ });
1979
+
1980
+ const teams: FakeTeams = await runCommand({ profile: INCIDENT });
1981
+
1982
+ expectSends(teams, { attempted: 1, delivered: 1 });
1983
+ const reply: string = theDeliveredText(teams);
1984
+ expect(reply).not.toContain("Project Settings → Incident Severity");
1985
+ // Named as the dashboard names it, or linked to the page itself.
1986
+ expect(reply).toMatch(
1987
+ /Incidents → Settings → Incident Severity|\/incidents\/settings\/severity/,
1988
+ );
1989
+ });
1990
+
1991
+ test("Teams refusing the no-severities reply: nothing reaches the user and nothing is thrown", async (): Promise<void> => {
1992
+ incidentChoicesSpy.mockResolvedValue({
1993
+ ...incidentChoices("small"),
1994
+ severities: emptyList(),
1995
+ });
1996
+
1997
+ const teams: FakeTeams = await runCommand({
1998
+ profile: INCIDENT,
1999
+ answer: refuseEverything(botNotInConversationRoster),
2000
+ });
2001
+
2002
+ expectSends(teams, { attempted: 1, delivered: 0 });
2003
+ expect(loggedErrors()).toContain("Could not send a Microsoft Teams reply:");
2004
+ });
2005
+ });
2006
+
2007
+ describe("create maintenance", (): void => {
2008
+ /*
2009
+ * In a personal chat the sender is the only one who can submit the form,
2010
+ * so the permission its submit checks is checked first. In a channel or
2011
+ * group chat it is not (see "in a channel or group chat" above).
2012
+ */
2013
+ describe("in a personal chat", (): void => {
2014
+ test("asks for the permission its submit needs, with the member's own props, before reading any list", async (): Promise<void> => {
2015
+ const teams: FakeTeams = await runCommand({ profile: MAINTENANCE });
2016
+
2017
+ expectSends(teams, { attempted: 1, delivered: 1 });
2018
+ expect(assertCanCreateSpy).toHaveBeenCalledTimes(1);
2019
+ const check: Parameters<
2020
+ typeof WorkspaceActionAuthorization.assertCanCreate
2021
+ >[0] = assertCanCreateSpy.mock.calls[0]![0];
2022
+ expect(check.props).toBe(MEMBER_PROPS);
2023
+ expect(check.modelType).toBe(ScheduledMaintenance);
2024
+ expect(check.action).toBe("create a scheduled maintenance event");
2025
+ expect(memberPropsSpy.mock.invocationCallOrder[0]!).toBeLessThan(
2026
+ assertCanCreateSpy.mock.invocationCallOrder[0]!,
2027
+ );
2028
+ expect(assertCanCreateSpy.mock.invocationCallOrder[0]!).toBeLessThan(
2029
+ maintenanceChoicesSpy.mock.invocationCallOrder[0]!,
2030
+ );
2031
+ });
2032
+
2033
+ test("a member without permission to create the event: one reply with that message, verbatim, and no form", async (): Promise<void> => {
2034
+ const refusal: string =
2035
+ "You do not have permission to create a scheduled maintenance event. You need one of these permissions: Project Owner, Project Admin, Create Scheduled Maintenance.";
2036
+ assertCanCreateSpy.mockRejectedValue(new NotAuthorizedException(refusal));
2037
+
2038
+ const teams: FakeTeams = await runCommand({ profile: MAINTENANCE });
2039
+
2040
+ expectSends(teams, { attempted: 1, delivered: 1 });
2041
+ expect(theDeliveredText(teams)).toBe(refusal);
2042
+ expect(maintenanceChoicesSpy).not.toHaveBeenCalled();
2043
+ expect(maintenanceCardSpy).not.toHaveBeenCalled();
2044
+ expect(errorLogSpy).not.toHaveBeenCalled();
2045
+ });
2046
+
2047
+ test("the permission check itself failing: one reply that the account could not be checked", async (): Promise<void> => {
2048
+ const permissionError: Error = new Error(
2049
+ "Redis connection lost while reading permissions",
2050
+ );
2051
+ assertCanCreateSpy.mockRejectedValue(permissionError);
2052
+
2053
+ const teams: FakeTeams = await runCommand({ profile: MAINTENANCE });
2054
+
2055
+ expectSends(teams, { attempted: 1, delivered: 1 });
2056
+ expect(theDeliveredText(teams)).toBe(
2057
+ MAINTENANCE.replies.senderCheckFailed,
2058
+ );
2059
+ expect(maintenanceChoicesSpy).not.toHaveBeenCalled();
2060
+ // No HTTP status, so the error itself follows the line, for its stack.
2061
+ expectErrorLogs([
2062
+ "Could not check the Microsoft Teams sender of a create command: Error: Redis connection lost while reading permissions",
2063
+ permissionError,
2064
+ ]);
2065
+ });
2066
+
2067
+ describe("with the real permission model deciding", (): void => {
2068
+ beforeEach((): void => {
2069
+ assertCanCreateSpy.mockImplementation(realAssertCanCreate);
2070
+ });
2071
+
2072
+ test("a read-only member is refused in the permission model's own words, before any list is read", async (): Promise<void> => {
2073
+ const viewer: DatabaseCommonInteractionProps = memberWith([
2074
+ Permission.Viewer,
2075
+ ]);
2076
+ memberPropsSpy.mockResolvedValue(viewer);
2077
+
2078
+ // What the permission model itself says to this member about this event.
2079
+ const refusal: unknown = await realAssertCanCreate({
2080
+ props: viewer,
2081
+ modelType: ScheduledMaintenance,
2082
+ action: "create a scheduled maintenance event",
2083
+ }).then(
2084
+ (): null => {
2085
+ return null;
2086
+ },
2087
+ (error: unknown): unknown => {
2088
+ return error;
2089
+ },
2090
+ );
2091
+ expect(refusal).toBeInstanceOf(NotAuthorizedException);
2092
+
2093
+ const teams: FakeTeams = await runCommand({ profile: MAINTENANCE });
2094
+
2095
+ expectSends(teams, { attempted: 1, delivered: 1 });
2096
+ const reply: string = theDeliveredText(teams);
2097
+ expect(reply).toBe((refusal as NotAuthorizedException).message);
2098
+ expect(
2099
+ reply.startsWith(
2100
+ "You do not have permission to create a scheduled maintenance event.",
2101
+ ),
2102
+ ).toBe(true);
2103
+ expect(maintenanceChoicesSpy).not.toHaveBeenCalled();
2104
+ expect(errorLogSpy).not.toHaveBeenCalled();
2105
+ });
2106
+
2107
+ test.each([
2108
+ Permission.ScheduledMaintenanceMember,
2109
+ Permission.CreateProjectScheduledMaintenance,
2110
+ ])(
2111
+ "a member with %s gets the form",
2112
+ async (permission: Permission): Promise<void> => {
2113
+ memberPropsSpy.mockResolvedValue(memberWith([permission]));
2114
+
2115
+ const teams: FakeTeams = await runCommand({ profile: MAINTENANCE });
2116
+
2117
+ expectSends(teams, { attempted: 1, delivered: 1 });
2118
+ expect(theDeliveredCard(teams)).toEqual(
2119
+ expectedForm(MAINTENANCE, {
2120
+ size: "small",
2121
+ budgetInBytes: FIRST_BUDGET,
2122
+ createInOneUptimeUrl: CREATE_MAINTENANCE_LINK,
2123
+ }),
2124
+ );
2125
+ expect(assertCanCreateSpy).toHaveBeenCalledTimes(1);
2126
+ },
2127
+ );
2128
+ });
2129
+ });
2130
+
2131
+ test("a project with no monitors or labels still gets the form: nothing on it is required from them", async (): Promise<void> => {
2132
+ giveChoices(MAINTENANCE, "listless");
2133
+
2134
+ const teams: FakeTeams = await runCommand({ profile: MAINTENANCE });
2135
+
2136
+ expectSends(teams, { attempted: 1, delivered: 1 });
2137
+ const card: JSONObject = theDeliveredCard(teams);
2138
+ expect(inputOf(card, "scheduledMaintenanceMonitors")).toBeUndefined();
2139
+ expect(inputOf(card, "startDate")).toBeDefined();
2140
+ expect(inputOf(card, "endTime")).toBeDefined();
2141
+ expect(card).toEqual(
2142
+ expectedForm(MAINTENANCE, {
2143
+ size: "listless",
2144
+ budgetInBytes: FIRST_BUDGET,
2145
+ createInOneUptimeUrl: CREATE_MAINTENANCE_LINK,
2146
+ }),
2147
+ );
2148
+ });
2149
+
2150
+ describe("the sender's time zone", (): void => {
2151
+ const KOLKATA_CLIENT_INFO: JSONObject = {
2152
+ type: "clientInfo",
2153
+ locale: "en-IN",
2154
+ country: "IN",
2155
+ platform: "Web",
2156
+ timezone: "Asia/Kolkata",
2157
+ };
2158
+
2159
+ const GENERIC_ZONE_NOTE: string =
2160
+ "Start and end times are in the time zone Microsoft Teams reports for you, or in UTC if it does not report one.";
2161
+
2162
+ test("the server really is in Asia/Tokyo here, so its zone cannot pass for the sender's", (): void => {
2163
+ expect(Intl.DateTimeFormat().resolvedOptions().timeZone).toBe(
2164
+ "Asia/Tokyo",
2165
+ );
2166
+ expect(new Date("2026-09-29T00:00:00.000Z").getTimezoneOffset()).toBe(
2167
+ -540,
2168
+ );
2169
+ });
2170
+
2171
+ test.each([
2172
+ {
2173
+ name: "localTimezone",
2174
+ fields: { localTimezone: "America/New_York" },
2175
+ zone: "America/New_York",
2176
+ },
2177
+ {
2178
+ name: "the clientInfo entity, when localTimezone is missing",
2179
+ fields: { entities: [KOLKATA_CLIENT_INFO] },
2180
+ zone: "Asia/Kolkata",
2181
+ },
2182
+ {
2183
+ name: "localTimezone, over the clientInfo entity",
2184
+ fields: {
2185
+ localTimezone: "Europe/Berlin",
2186
+ entities: [KOLKATA_CLIENT_INFO],
2187
+ },
2188
+ zone: "Europe/Berlin",
2189
+ },
2190
+ ])(
2191
+ "the zone from $name is named on the form and travels with its submit",
2192
+ async (row: {
2193
+ name: string;
2194
+ fields: JSONObject;
2195
+ zone: string;
2196
+ }): Promise<void> => {
2197
+ const teams: FakeTeams = await runCommand({
2198
+ profile: MAINTENANCE,
2199
+ activity: messageActivity(MAINTENANCE, row.fields),
2200
+ });
2201
+
2202
+ expectSends(teams, { attempted: 1, delivered: 1 });
2203
+ const card: JSONObject = theDeliveredCard(teams);
2204
+ expect(bodyOf(card)).toContainEqual(
2205
+ MicrosoftTeamsCardChoices.buildNoteElement(
2206
+ `Start and end times are in ${row.zone}.`,
2207
+ ),
2208
+ );
2209
+ /*
2210
+ * The zone is named, not called "your time zone": in a channel,
2211
+ * whoever fills the form in may be somewhere else.
2212
+ */
2213
+ expect(JSON.stringify(card)).not.toContain("your time zone");
2214
+ expect(submitDataOf(card)).toEqual({
2215
+ action:
2216
+ MicrosoftTeamsScheduledMaintenanceActionType.SubmitNewScheduledMaintenance,
2217
+ timezone: row.zone,
2218
+ });
2219
+ expect(maintenanceCardSpy.mock.calls[0]![0].timezone).toBe(row.zone);
2220
+ expect(card).toEqual(
2221
+ expectedForm(MAINTENANCE, {
2222
+ size: "small",
2223
+ budgetInBytes: FIRST_BUDGET,
2224
+ createInOneUptimeUrl: CREATE_MAINTENANCE_LINK,
2225
+ timezone: row.zone,
2226
+ }),
2227
+ );
2228
+ },
2229
+ );
2230
+
2231
+ test.each([
2232
+ { name: "no zone at all", fields: {} },
2233
+ {
2234
+ name: "a zone name nobody knows",
2235
+ fields: { localTimezone: "Mars/Olympus_Mons" },
2236
+ },
2237
+ { name: "a zone that is not a name", fields: { localTimezone: 330 } },
2238
+ {
2239
+ /*
2240
+ * iOS has been seen to leave localTimezone out. An offset is not a
2241
+ * zone (it knows nothing of daylight saving time), so the form does
2242
+ * not name one; the submit reads the times at the offset its own
2243
+ * activity carries instead.
2244
+ */
2245
+ name: "only the UTC offset of the local timestamp",
2246
+ fields: {
2247
+ localTimestamp: "2026-09-29T14:30:00.000+05:30",
2248
+ rawLocalTimestamp: "2026-09-29T14:30:00.000+05:30",
2249
+ },
2250
+ },
2251
+ ])(
2252
+ "$name: the form says which zone applies, carries none, and never the server's",
2253
+ async (row: { name: string; fields: JSONObject }): Promise<void> => {
2254
+ const teams: FakeTeams = await runCommand({
2255
+ profile: MAINTENANCE,
2256
+ activity: messageActivity(MAINTENANCE, row.fields),
2257
+ });
2258
+
2259
+ expectSends(teams, { attempted: 1, delivered: 1 });
2260
+ const card: JSONObject = theDeliveredCard(teams);
2261
+ expect(textsOf(card)).toContain(GENERIC_ZONE_NOTE);
2262
+ expect(submitDataOf(card)).toEqual({
2263
+ action:
2264
+ MicrosoftTeamsScheduledMaintenanceActionType.SubmitNewScheduledMaintenance,
2265
+ });
2266
+ expect(JSON.stringify(card)).not.toContain("Asia/Tokyo");
2267
+ expect(card).toEqual(
2268
+ expectedForm(MAINTENANCE, {
2269
+ size: "small",
2270
+ budgetInBytes: FIRST_BUDGET,
2271
+ createInOneUptimeUrl: CREATE_MAINTENANCE_LINK,
2272
+ }),
2273
+ );
2274
+ },
2275
+ );
2276
+
2277
+ test("the smaller card sent after a 413 still names and carries the zone", async (): Promise<void> => {
2278
+ giveChoices(MAINTENANCE, "big");
2279
+
2280
+ const teams: FakeTeams = await runCommand({
2281
+ profile: MAINTENANCE,
2282
+ activity: messageActivity(MAINTENANCE, {
2283
+ localTimezone: "America/Los_Angeles",
2284
+ }),
2285
+ answer: refuseCards({ refusal: messageSizeTooBig, times: 1 }),
2286
+ });
2287
+
2288
+ expectSends(teams, { attempted: 2, delivered: 1 });
2289
+ const card: JSONObject = theDeliveredCard(teams);
2290
+ expect(sizeOf(card)).toBeLessThanOrEqual(SECOND_BUDGET);
2291
+ expect(bodyOf(card)).toContainEqual(
2292
+ MicrosoftTeamsCardChoices.buildNoteElement(
2293
+ "Start and end times are in America/Los_Angeles.",
2294
+ ),
2295
+ );
2296
+ expect(submitDataOf(card)["timezone"]).toBe("America/Los_Angeles");
2297
+ expect(attemptedCards(teams)).toEqual(
2298
+ [FIRST_BUDGET, SECOND_BUDGET].map(
2299
+ (budgetInBytes: number): JSONObject => {
2300
+ return expectedForm(MAINTENANCE, {
2301
+ size: "big",
2302
+ budgetInBytes: budgetInBytes,
2303
+ createInOneUptimeUrl: CREATE_MAINTENANCE_LINK,
2304
+ timezone: "America/Los_Angeles",
2305
+ });
2306
+ },
2307
+ ),
2308
+ );
2309
+ });
2310
+
2311
+ test.each(SHARED_CONVERSATIONS)(
2312
+ "in a $name, the form names the zone of whoever asked for it, for everyone who may fill it in, and carries it to the submit",
2313
+ async (conversation: SharedConversation): Promise<void> => {
2314
+ const teams: FakeTeams = await runCommand({
2315
+ profile: MAINTENANCE,
2316
+ activity: sharedConversationMessage(MAINTENANCE, conversation, {
2317
+ localTimezone: "America/New_York",
2318
+ }),
2319
+ });
2320
+
2321
+ expectSends(teams, { attempted: 1, delivered: 1 });
2322
+ const card: JSONObject = theDeliveredCard(teams);
2323
+ /*
2324
+ * Whoever fills the form in types the times in the zone it names, and
2325
+ * the submit reads them in the zone its data carries before the
2326
+ * submitter's own (MicrosoftTeamsTimezone.resolve).
2327
+ */
2328
+ expect(bodyOf(card)).toContainEqual(
2329
+ MicrosoftTeamsCardChoices.buildNoteElement(
2330
+ "Start and end times are in America/New_York.",
2331
+ ),
2332
+ );
2333
+ expect(JSON.stringify(card)).not.toContain("your time zone");
2334
+ expect(submitDataOf(card)).toEqual({
2335
+ action:
2336
+ MicrosoftTeamsScheduledMaintenanceActionType.SubmitNewScheduledMaintenance,
2337
+ timezone: "America/New_York",
2338
+ });
2339
+ expect(card).toEqual(
2340
+ expectedForm(MAINTENANCE, {
2341
+ size: "small",
2342
+ budgetInBytes: FIRST_BUDGET,
2343
+ createInOneUptimeUrl: CREATE_MAINTENANCE_LINK,
2344
+ timezone: "America/New_York",
2345
+ }),
2346
+ );
2347
+ },
2348
+ );
2349
+ });
2350
+ });
2351
+
2352
+ describe("getFormNotAcceptedMessage", (): void => {
2353
+ test.each([
2354
+ {
2355
+ name: "the connector's RestError (413 MessageSizeTooBig)",
2356
+ error: messageSizeTooBig(),
2357
+ },
2358
+ {
2359
+ name: "MessageSizeTooBig only in the Teams error body",
2360
+ error: Object.assign(new Error("Message size too large."), {
2361
+ details: {
2362
+ error: {
2363
+ code: "MessageSizeTooBig",
2364
+ message: "Message size too large.",
2365
+ },
2366
+ },
2367
+ }),
2368
+ },
2369
+ {
2370
+ name: "MessageSizeTooBig only as the error's code",
2371
+ error: Object.assign(new Error("Message size too large."), {
2372
+ code: "MessageSizeTooBig",
2373
+ }),
2374
+ },
2375
+ {
2376
+ name: "a bare 413 from a proxy in front of the connector",
2377
+ error: Object.assign(new Error("Request Entity Too Large"), {
2378
+ response: { status: 413 },
2379
+ }),
2380
+ },
2381
+ { name: "a plain object with statusCode 413", error: { statusCode: 413 } },
2382
+ ])(
2383
+ "$name: Teams refused it as too large, even without the lists",
2384
+ (row: { name: string; error: unknown }): void => {
2385
+ expect(
2386
+ MicrosoftTeamsCreateCommands.getFormNotAcceptedMessage({
2387
+ form: INCIDENT.form,
2388
+ error: row.error,
2389
+ createInOneUptimeUrl: CREATE_INCIDENT_LINK,
2390
+ }),
2391
+ ).toBe(
2392
+ `Sorry, I couldn't open the form to create an incident: Microsoft Teams refused it as too large, even without the lists of monitors, labels and on-call policies. You can create it in OneUptime instead: ${CREATE_INCIDENT_LINK}`,
2393
+ );
2394
+ },
2395
+ );
2396
+
2397
+ test("too large, for the maintenance form: names that form's own lists", (): void => {
2398
+ expect(
2399
+ MicrosoftTeamsCreateCommands.getFormNotAcceptedMessage({
2400
+ form: MAINTENANCE.form,
2401
+ error: messageSizeTooBig(),
2402
+ createInOneUptimeUrl: CREATE_MAINTENANCE_LINK,
2403
+ }),
2404
+ ).toBe(MAINTENANCE.replies.tooLarge);
2405
+ });
2406
+
2407
+ test("too large, with no dashboard URL known: ends without a link", (): void => {
2408
+ expect(
2409
+ MicrosoftTeamsCreateCommands.getFormNotAcceptedMessage({
2410
+ form: INCIDENT.form,
2411
+ error: messageSizeTooBig(),
2412
+ createInOneUptimeUrl: null,
2413
+ }),
2414
+ ).toBe(INCIDENT.replies.tooLargeWithoutLink);
2415
+ });
2416
+
2417
+ test.each([
2418
+ {
2419
+ name: "403 BotNotInConversationRoster",
2420
+ error: botNotInConversationRoster(),
2421
+ reason: "(403 BotNotInConversationRoster)",
2422
+ },
2423
+ {
2424
+ name: "a status code alone (a proxy's 502 without a Teams body)",
2425
+ error: teamsRefusal({ statusCode: 502, message: "Bad Gateway" }),
2426
+ reason: "(502)",
2427
+ },
2428
+ {
2429
+ name: "status and code only on response and details",
2430
+ error: Object.assign(new Error("Service Unavailable"), {
2431
+ response: { status: 503 },
2432
+ details: {
2433
+ error: { code: "ServiceUnavailable", message: "Try again later." },
2434
+ },
2435
+ }),
2436
+ reason: "(503 ServiceUnavailable)",
2437
+ },
2438
+ {
2439
+ name: "a network error's code alone",
2440
+ error: connectionReset(),
2441
+ reason: "(ECONNRESET)",
2442
+ },
2443
+ {
2444
+ name: "a blank code",
2445
+ error: Object.assign(new Error("Something odd"), { code: "" }),
2446
+ reason: null,
2447
+ },
2448
+ {
2449
+ name: "an Error with neither status nor code",
2450
+ error: new Error("socket hang up"),
2451
+ reason: null,
2452
+ },
2453
+ { name: "a thrown string", error: "boom", reason: null },
2454
+ { name: "undefined", error: undefined, reason: null },
2455
+ { name: "null", error: null, reason: null },
2456
+ ])(
2457
+ "$name: Teams did not accept it, with the status and code it gave",
2458
+ (row: { name: string; error: unknown; reason: string | null }): void => {
2459
+ const expectedEnd: string = `You can create it in OneUptime instead: ${CREATE_INCIDENT_LINK}`;
2460
+ const message: string =
2461
+ MicrosoftTeamsCreateCommands.getFormNotAcceptedMessage({
2462
+ form: INCIDENT.form,
2463
+ error: row.error,
2464
+ createInOneUptimeUrl: CREATE_INCIDENT_LINK,
2465
+ });
2466
+
2467
+ expect(message).toBe(
2468
+ row.reason
2469
+ ? `Sorry, I couldn't open the form to create an incident: Microsoft Teams did not accept it ${row.reason}. ${expectedEnd}`
2470
+ : `Sorry, I couldn't open the form to create an incident: Microsoft Teams did not accept it. ${expectedEnd}`,
2471
+ );
2472
+ // The error's own text is for the log, not the user.
2473
+ expect(message).not.toContain("socket hang up");
2474
+ expect(message).not.toContain("roster.");
2475
+ expect(message).not.toContain("Try again later.");
2476
+ },
2477
+ );
2478
+
2479
+ test("not accepted, for the maintenance form, with no dashboard URL known: ends without a link", (): void => {
2480
+ expect(
2481
+ MicrosoftTeamsCreateCommands.getFormNotAcceptedMessage({
2482
+ form: MAINTENANCE.form,
2483
+ error: botNotInConversationRoster(),
2484
+ createInOneUptimeUrl: null,
2485
+ }),
2486
+ ).toBe(
2487
+ "Sorry, I couldn't open the form to create a scheduled maintenance event: Microsoft Teams did not accept it (403 BotNotInConversationRoster). You can create it in OneUptime instead.",
2488
+ );
2489
+ });
2490
+ });