@oneuptime/common 14.0.9 → 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 (383) 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 +2 -0
  9. package/Models/DatabaseModels/PodmanHost.ts +213 -0
  10. package/Models/DatabaseModels/ProxmoxCluster.ts +213 -0
  11. package/Models/DatabaseModels/ResourceAiAgent.ts +419 -0
  12. package/Models/DatabaseModels/RunnerJob.ts +144 -0
  13. package/Models/DatabaseModels/VMwareVCenter.ts +213 -0
  14. package/Server/API/AutoRemediationAPI.ts +392 -1
  15. package/Server/API/ResourceAiAccessAPI.ts +1669 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.ts +423 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  18. package/Server/Infrastructure/Semaphore.ts +22 -0
  19. package/Server/Middleware/TelemetryIngest.ts +15 -5
  20. package/Server/Services/AnalyticsDatabaseService.ts +45 -1
  21. package/Server/Services/AutoRemediationRuleEngineService.ts +940 -112
  22. package/Server/Services/CephClusterService.ts +109 -1
  23. package/Server/Services/DatabaseServerService.ts +116 -1
  24. package/Server/Services/DockerHostService.ts +109 -1
  25. package/Server/Services/DockerSwarmClusterService.ts +113 -1
  26. package/Server/Services/HostService.ts +126 -4
  27. package/Server/Services/MetricRecordingRuleService.ts +47 -0
  28. package/Server/Services/PodmanHostService.ts +109 -1
  29. package/Server/Services/ProxmoxClusterService.ts +110 -1
  30. package/Server/Services/ResourceAiAccessService.ts +1439 -0
  31. package/Server/Services/ResourceAiAgentJobService.ts +202 -0
  32. package/Server/Services/ResourceAiAgentService.ts +2432 -0
  33. package/Server/Services/RunnerJobService.ts +611 -4
  34. package/Server/Services/TelemetryUsageBillingService.ts +28 -0
  35. package/Server/Services/TraceRecordingRuleService.ts +33 -0
  36. package/Server/Services/VMwareVCenterService.ts +110 -1
  37. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +1434 -32
  38. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +935 -21
  39. package/Server/Utils/AI/Remediation/RemediationPlanRunner.ts +25 -1
  40. package/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.ts +577 -0
  41. package/Server/Utils/AI/ResourceAccess/ResourceAccessContext.ts +271 -0
  42. package/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.ts +45 -0
  43. package/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.ts +964 -0
  44. package/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.ts +333 -0
  45. package/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.ts +873 -0
  46. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +72 -2
  47. package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +72 -5
  48. package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +72 -5
  49. package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +396 -13
  50. package/Server/Utils/AutoRemediation/RemediationVerifier.ts +35 -2
  51. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +9 -1
  52. package/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.ts +920 -0
  53. package/Server/Utils/SessionReplay/SessionReplayUsage.ts +84 -6
  54. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +35 -15
  55. package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +33 -15
  56. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +21 -1
  57. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +298 -224
  58. package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +35 -15
  59. package/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.ts +389 -175
  60. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +366 -143
  61. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.ts +163 -0
  62. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.ts +396 -0
  63. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.ts +418 -0
  64. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.ts +152 -0
  65. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.ts +251 -0
  66. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.ts +214 -0
  67. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +93 -0
  68. package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +18 -6
  69. package/Tests/App/Dashboard/InvestigationInfrastructureTools.test.tsx +506 -0
  70. package/Tests/App/Dashboard/RemediationSuggestionCardDescription.test.tsx +209 -0
  71. package/Tests/App/Dashboard/RemediationSuggestionCardResource.test.tsx +317 -0
  72. package/Tests/App/Dashboard/ResourceAiAccessSettingsUtil.test.ts +921 -0
  73. package/Tests/App/Dashboard/ResourceAiAgentInstall.test.ts +860 -0
  74. package/Tests/App/Dashboard/ResourceAiAgentPage.test.tsx +1721 -0
  75. package/Tests/App/Dashboard/ResourceAiAgentStatus.test.ts +1002 -0
  76. package/Tests/App/Dashboard/ResourceAiInsightsPage.test.tsx +931 -0
  77. package/Tests/App/Dashboard/ResourceAiNavigation.test.tsx +575 -0
  78. package/Tests/App/Dashboard/RunbookStepTypeMaps.test.ts +38 -7
  79. package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +33 -1
  80. package/Tests/Models/DatabaseModels/ResourceAiAccessColumns.test.ts +738 -0
  81. package/Tests/Models/DatabaseModels/ResourceAiAgentModel.test.ts +842 -0
  82. package/Tests/Server/API/AutoRemediationApproveResourceRound.test.ts +835 -0
  83. package/Tests/Server/API/ResourceAiAccessAPI.test.ts +2730 -0
  84. package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +60 -0
  85. package/Tests/Server/Infrastructure/Postgres/AddResourceAiAgentsMigration.test.ts +659 -0
  86. package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +31 -0
  87. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +63 -5
  88. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +103 -5
  89. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +19 -1
  90. package/Tests/Server/Services/AutoRemediationResourceRuleEngine.test.ts +1279 -0
  91. package/Tests/Server/Services/DatabaseServerService.test.ts +55 -0
  92. package/Tests/Server/Services/GroupTelemetryUsageExcludeNames.test.ts +299 -0
  93. package/Tests/Server/Services/HostServiceFindOrCreateMemo.test.ts +44 -0
  94. package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +45 -6
  95. package/Tests/Server/Services/RecordingRuleReservedMetricName.test.ts +171 -0
  96. package/Tests/Server/Services/ResourceAiAccessChangeAuthorization.test.ts +256 -0
  97. package/Tests/Server/Services/ResourceAiAccessService.test.ts +1373 -0
  98. package/Tests/Server/Services/ResourceAiAgentJobService.test.ts +375 -0
  99. package/Tests/Server/Services/ResourceAiAgentServiceHelpers.test.ts +1114 -0
  100. package/Tests/Server/Services/ResourceAiAgentServiceLifecycle.test.ts +1050 -0
  101. package/Tests/Server/Services/ResourceAiAgentServiceRegister.test.ts +2251 -0
  102. package/Tests/Server/Services/ResourceAiSettingsCreate.test.ts +373 -0
  103. package/Tests/Server/Services/ResourceAiSettingsPermission.test.ts +1042 -0
  104. package/Tests/Server/Services/ResourceServiceDeleteCleansUpAi.test.ts +319 -0
  105. package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +105 -0
  106. package/Tests/Server/Services/RunnerJobEnqueueResourceCommand.test.ts +986 -0
  107. package/Tests/Server/Services/RunnerJobResourceCommandLane.test.ts +211 -0
  108. package/Tests/Server/Services/RunnerJobResourceCommandTimeoutAndRedaction.test.ts +352 -0
  109. package/Tests/Server/Services/TelemetryUsageBillingSloExclusion.test.ts +218 -0
  110. package/Tests/Server/TestingUtils/Services/FakeRunnerJobCount.ts +145 -0
  111. package/Tests/Server/Utils/AI/InvestigationInfrastructureAccessWiring.test.ts +453 -0
  112. package/Tests/Server/Utils/AI/InvestigationInfrastructureReport.test.ts +721 -0
  113. package/Tests/Server/Utils/AI/RemediationCommandTools.test.ts +94 -1
  114. package/Tests/Server/Utils/AI/RemediationCommandToolsResource.test.ts +1530 -0
  115. package/Tests/Server/Utils/AI/RemediationExecutionRunnerResourceMode.test.ts +1249 -0
  116. package/Tests/Server/Utils/AI/RemediationPlanRunner.test.ts +117 -0
  117. package/Tests/Server/Utils/AI/RemediationResourceCopyParity.test.ts +463 -0
  118. package/Tests/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.test.ts +722 -0
  119. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAccessContext.test.ts +266 -0
  120. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.test.ts +1220 -0
  121. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.test.ts +569 -0
  122. package/Tests/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.test.ts +830 -0
  123. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorResource.test.ts +717 -0
  124. package/Tests/Server/Utils/AutoRemediation/RemediationVerifierResourceFollowUp.test.ts +338 -0
  125. package/Tests/Server/Utils/Monitor/Criteria/SessionReplayBudgetTemplateCriteria.test.ts +422 -0
  126. package/Tests/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.test.ts +1647 -0
  127. package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +143 -1
  128. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +33 -3
  129. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsAccountNotLinked.test.ts +1093 -0
  130. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.test.ts +1049 -0
  131. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.test.ts +1676 -0
  132. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCards.test.ts +2884 -0
  133. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.test.ts +2490 -0
  134. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmit.test.ts +2293 -0
  135. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmitServerTimezone.test.ts +386 -0
  136. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.test.ts +1477 -0
  137. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.test.ts +1967 -0
  138. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsStripHtmlTags.test.ts +92 -0
  139. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsSubmittedFormRemoval.test.ts +1819 -0
  140. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.test.ts +1033 -0
  141. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezoneServerZone.test.ts +610 -0
  142. package/Tests/Server/Utils/Workspace/MicrosoftTeamsBotMessageHandling.test.ts +2610 -0
  143. package/Tests/Server/Utils/Workspace/MicrosoftTeamsCreateCommandsEndToEnd.test.ts +4672 -0
  144. package/Tests/Server/Utils/Workspace/WorkspaceCreateProjectReferences.test.ts +190 -65
  145. package/Tests/Types/AutoRemediation/AiRemediationCommandPlan.test.ts +10 -4
  146. package/Tests/Types/AutoRemediation/AiRemediationCommandPlanResourceCommand.test.ts +305 -0
  147. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +277 -13
  148. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +113 -0
  149. package/Tests/Types/Monitor/RumAlertTemplates.test.ts +773 -2
  150. package/Tests/Types/ResourceAiAgent/AiResourceType.test.ts +419 -0
  151. package/Tests/Types/ResourceAiAgent/ResourceAiAccess.test.ts +517 -0
  152. package/Tests/Types/Runbook/RunbookStepType.test.ts +147 -8
  153. package/Tests/UI/Rum/RecordingHealthDashboard.test.tsx +769 -1
  154. package/Tests/Utils/AiRemediation/Resource/CephCommandPolicy.test.ts +1653 -0
  155. package/Tests/Utils/AiRemediation/Resource/CephOutputRedaction.test.ts +204 -0
  156. package/Tests/Utils/AiRemediation/Resource/DatabaseCommandPolicy.test.ts +1077 -0
  157. package/Tests/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.test.ts +558 -0
  158. package/Tests/Utils/AiRemediation/Resource/DatabaseQueryRedactor.test.ts +834 -0
  159. package/Tests/Utils/AiRemediation/Resource/DockerCliGrammar.test.ts +747 -0
  160. package/Tests/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.test.ts +1259 -0
  161. package/Tests/Utils/AiRemediation/Resource/DockerOutputRedaction.test.ts +386 -0
  162. package/Tests/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.test.ts +782 -0
  163. package/Tests/Utils/AiRemediation/Resource/GovcCommandPolicy.test.ts +2259 -0
  164. package/Tests/Utils/AiRemediation/Resource/HostCommandPolicy.test.ts +2019 -0
  165. package/Tests/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.test.ts +2497 -0
  166. package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicy.test.ts +1305 -0
  167. package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.test.ts +585 -0
  168. package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactor.test.ts +334 -0
  169. package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactorCopyParity.test.ts +98 -0
  170. package/Tests/Utils/AiRemediation/Resource/ResourcePolicyImportClosure.test.ts +391 -0
  171. package/Tests/Utils/AiRemediation/ResourceAiAgentPolicyCopyParity.test.ts +497 -0
  172. package/Tests/Utils/SessionReplay/SessionReplayBudgetMetricType.test.ts +383 -0
  173. package/Types/AI/ResourceAiAccessApi.ts +140 -0
  174. package/Types/AI/ResourceAiAccessPermissions.ts +126 -0
  175. package/Types/AutoRemediation/AiRemediationCommandPlan.ts +183 -2
  176. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +55 -22
  177. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +35 -3
  178. package/Types/Monitor/RumAlertTemplates.ts +351 -4
  179. package/Types/ResourceAiAgent/AiResourceType.ts +310 -0
  180. package/Types/ResourceAiAgent/ResourceAiAccess.ts +574 -0
  181. package/Types/Rum/SessionReplayBudgetMetricType.ts +47 -0
  182. package/Types/Runbook/RunbookStepType.ts +29 -0
  183. package/Types/Telemetry/TelemetryIngestSurface.ts +14 -2
  184. package/Utils/AI/InvestigationReport.ts +121 -15
  185. package/Utils/AiRemediation/Resource/CephCommandPolicy.ts +1936 -0
  186. package/Utils/AiRemediation/Resource/DatabaseCommandPolicy.ts +166 -0
  187. package/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.ts +1532 -0
  188. package/Utils/AiRemediation/Resource/DatabaseQueryRedactor.ts +1394 -0
  189. package/Utils/AiRemediation/Resource/DockerCliGrammar.ts +1839 -0
  190. package/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.ts +490 -0
  191. package/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.ts +687 -0
  192. package/Utils/AiRemediation/Resource/GovcCommandPolicy.ts +2096 -0
  193. package/Utils/AiRemediation/Resource/HostCommandPolicy.ts +2982 -0
  194. package/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.ts +1679 -0
  195. package/Utils/AiRemediation/Resource/ResourceCommandPolicy.ts +851 -0
  196. package/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.ts +593 -0
  197. package/Utils/AiRemediation/Resource/ResourceOutputRedactor.ts +2812 -0
  198. package/Utils/SessionReplay/SessionReplayBudgetMetricType.ts +184 -0
  199. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js +62 -0
  200. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js.map +1 -1
  201. package/build/dist/Models/DatabaseModels/CephCluster.js +219 -0
  202. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  203. package/build/dist/Models/DatabaseModels/DatabaseServer.js +219 -0
  204. package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -1
  205. package/build/dist/Models/DatabaseModels/DockerHost.js +219 -0
  206. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  207. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js +219 -0
  208. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js.map +1 -1
  209. package/build/dist/Models/DatabaseModels/GlobalConfig.js +1 -1
  210. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  211. package/build/dist/Models/DatabaseModels/Host.js +219 -0
  212. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  213. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  214. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  215. package/build/dist/Models/DatabaseModels/PodmanHost.js +219 -0
  216. package/build/dist/Models/DatabaseModels/PodmanHost.js.map +1 -1
  217. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +219 -0
  218. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  219. package/build/dist/Models/DatabaseModels/ResourceAiAgent.js +439 -0
  220. package/build/dist/Models/DatabaseModels/ResourceAiAgent.js.map +1 -0
  221. package/build/dist/Models/DatabaseModels/RunnerJob.js +145 -0
  222. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  223. package/build/dist/Models/DatabaseModels/VMwareVCenter.js +219 -0
  224. package/build/dist/Models/DatabaseModels/VMwareVCenter.js.map +1 -1
  225. package/build/dist/Server/API/AutoRemediationAPI.js +231 -1
  226. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  227. package/build/dist/Server/API/ResourceAiAccessAPI.js +1119 -0
  228. package/build/dist/Server/API/ResourceAiAccessAPI.js.map +1 -0
  229. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js +168 -0
  230. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js.map +1 -0
  231. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  232. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  233. package/build/dist/Server/Infrastructure/Semaphore.js +6 -0
  234. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  235. package/build/dist/Server/Middleware/TelemetryIngest.js +12 -5
  236. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  237. package/build/dist/Server/Services/AnalyticsDatabaseService.js +26 -1
  238. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  239. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +601 -3
  240. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
  241. package/build/dist/Server/Services/CephClusterService.js +104 -0
  242. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  243. package/build/dist/Server/Services/DatabaseServerService.js +99 -0
  244. package/build/dist/Server/Services/DatabaseServerService.js.map +1 -1
  245. package/build/dist/Server/Services/DockerHostService.js +104 -0
  246. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  247. package/build/dist/Server/Services/DockerSwarmClusterService.js +104 -0
  248. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  249. package/build/dist/Server/Services/HostService.js +114 -2
  250. package/build/dist/Server/Services/HostService.js.map +1 -1
  251. package/build/dist/Server/Services/MetricRecordingRuleService.js +46 -0
  252. package/build/dist/Server/Services/MetricRecordingRuleService.js.map +1 -1
  253. package/build/dist/Server/Services/PodmanHostService.js +104 -0
  254. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  255. package/build/dist/Server/Services/ProxmoxClusterService.js +104 -0
  256. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  257. package/build/dist/Server/Services/ResourceAiAccessService.js +1028 -0
  258. package/build/dist/Server/Services/ResourceAiAccessService.js.map +1 -0
  259. package/build/dist/Server/Services/ResourceAiAgentJobService.js +173 -0
  260. package/build/dist/Server/Services/ResourceAiAgentJobService.js.map +1 -0
  261. package/build/dist/Server/Services/ResourceAiAgentService.js +1687 -0
  262. package/build/dist/Server/Services/ResourceAiAgentService.js.map +1 -0
  263. package/build/dist/Server/Services/RunnerJobService.js +344 -5
  264. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  265. package/build/dist/Server/Services/TelemetryUsageBillingService.js +26 -0
  266. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  267. package/build/dist/Server/Services/TraceRecordingRuleService.js +37 -0
  268. package/build/dist/Server/Services/TraceRecordingRuleService.js.map +1 -1
  269. package/build/dist/Server/Services/VMwareVCenterService.js +104 -0
  270. package/build/dist/Server/Services/VMwareVCenterService.js.map +1 -1
  271. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +1029 -30
  272. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  273. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +633 -28
  274. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  275. package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js +21 -1
  276. package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js.map +1 -1
  277. package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js +330 -0
  278. package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js.map +1 -0
  279. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js +160 -0
  280. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js.map +1 -0
  281. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js +33 -0
  282. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js.map +1 -0
  283. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js +640 -0
  284. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js.map +1 -0
  285. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js +226 -0
  286. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js.map +1 -0
  287. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js +600 -0
  288. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js.map +1 -0
  289. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +50 -4
  290. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  291. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +55 -4
  292. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
  293. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +55 -4
  294. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
  295. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +290 -13
  296. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
  297. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js +29 -1
  298. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js.map +1 -1
  299. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +9 -1
  300. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  301. package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js +627 -0
  302. package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js.map +1 -0
  303. package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js +60 -5
  304. package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js.map +1 -1
  305. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +19 -15
  306. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  307. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +19 -15
  308. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -1
  309. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +17 -1
  310. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  311. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +183 -209
  312. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  313. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +19 -15
  314. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
  315. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js +233 -167
  316. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js.map +1 -1
  317. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +263 -110
  318. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  319. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js +129 -0
  320. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js.map +1 -0
  321. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js +266 -0
  322. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js.map +1 -0
  323. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js +258 -0
  324. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js.map +1 -0
  325. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js +104 -0
  326. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js.map +1 -0
  327. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js +183 -0
  328. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js.map +1 -0
  329. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js +123 -0
  330. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js.map +1 -0
  331. package/build/dist/Types/AI/ResourceAiAccessApi.js +30 -0
  332. package/build/dist/Types/AI/ResourceAiAccessApi.js.map +1 -0
  333. package/build/dist/Types/AI/ResourceAiAccessPermissions.js +98 -0
  334. package/build/dist/Types/AI/ResourceAiAccessPermissions.js.map +1 -0
  335. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +116 -29
  336. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
  337. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +47 -21
  338. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  339. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +5 -0
  340. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  341. package/build/dist/Types/Monitor/RumAlertTemplates.js +199 -3
  342. package/build/dist/Types/Monitor/RumAlertTemplates.js.map +1 -1
  343. package/build/dist/Types/ResourceAiAgent/AiResourceType.js +242 -0
  344. package/build/dist/Types/ResourceAiAgent/AiResourceType.js.map +1 -0
  345. package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js +275 -0
  346. package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js.map +1 -0
  347. package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js +45 -0
  348. package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js.map +1 -0
  349. package/build/dist/Types/Runbook/RunbookStepType.js +27 -0
  350. package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
  351. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +13 -2
  352. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
  353. package/build/dist/Utils/AI/InvestigationReport.js +71 -11
  354. package/build/dist/Utils/AI/InvestigationReport.js.map +1 -1
  355. package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js +1462 -0
  356. package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js.map +1 -0
  357. package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js +122 -0
  358. package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js.map +1 -0
  359. package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js +1139 -0
  360. package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js.map +1 -0
  361. package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js +1060 -0
  362. package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js.map +1 -0
  363. package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js +1301 -0
  364. package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js.map +1 -0
  365. package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js +370 -0
  366. package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js.map +1 -0
  367. package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js +498 -0
  368. package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js.map +1 -0
  369. package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js +1565 -0
  370. package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js.map +1 -0
  371. package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js +2206 -0
  372. package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js.map +1 -0
  373. package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js +1129 -0
  374. package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js.map +1 -0
  375. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js +565 -0
  376. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js.map +1 -0
  377. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js +408 -0
  378. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js.map +1 -0
  379. package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js +1910 -0
  380. package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js.map +1 -0
  381. package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js +160 -0
  382. package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js.map +1 -0
  383. package/package.json +1 -1
@@ -0,0 +1,1033 @@
1
+ /** @timezone UTC */
2
+
3
+ import { describe, expect, test } from "@jest/globals";
4
+ import MicrosoftTeamsTimezone, {
5
+ MicrosoftTeamsUserTimezone,
6
+ } from "../../../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone";
7
+ import OneUptimeDate from "../../../../../Types/Date";
8
+ import { JSONObject, JSONValue } from "../../../../../Types/JSON";
9
+
10
+ /*
11
+ * Issue #4111, the maintenance half. The "create maintenance" form in
12
+ * Microsoft Teams submits Input.Date and Input.Time as a bare "2026-10-01"
13
+ * and "14:00", with no zone. The old handler read them in the server's zone
14
+ * (UTC in the containers), so 14:00 typed in New York became 10:00 in New
15
+ * York. MicrosoftTeamsTimezone works out the zone the times were typed in,
16
+ * reads the date and time in it, and names it back to the user.
17
+ *
18
+ * That zone is the one the form named, which the card carries in its submit
19
+ * data: the form tells whoever fills it in which zone its times are in, and
20
+ * in a channel that person may be somewhere else than the one who asked for
21
+ * it. A form that named no zone is read in the submitting activity's
22
+ * localTimezone, then its clientInfo entity's zone, then at the UTC offset of
23
+ * its local timestamp, then in UTC. An offset is only today's, so when that
24
+ * is all there is (isUtcOffsetOnly) the confirmation warns that a daylight
25
+ * saving change could move the times.
26
+ *
27
+ * The docblock runs this file in UTC, the zone the containers run in.
28
+ * MicrosoftTeamsTimezoneServerZone.test.ts runs the same arithmetic with the
29
+ * server in Asia/Tokyo and expects the same instants.
30
+ */
31
+
32
+ const NEW_YORK: MicrosoftTeamsUserTimezone = {
33
+ timezone: "America/New_York",
34
+ label: "America/New_York",
35
+ };
36
+ const KOLKATA: MicrosoftTeamsUserTimezone = {
37
+ timezone: "Asia/Kolkata",
38
+ label: "Asia/Kolkata",
39
+ };
40
+ const UTC_PLUS_05_30: MicrosoftTeamsUserTimezone = {
41
+ utcOffsetInMinutes: 330,
42
+ label: "UTC+05:30",
43
+ };
44
+ const UTC_MINUS_07_00: MicrosoftTeamsUserTimezone = {
45
+ utcOffsetInMinutes: -420,
46
+ label: "UTC-07:00",
47
+ };
48
+ // Spelled out, not MicrosoftTeamsTimezone.UTC: no expectation leans on the constant.
49
+ const UTC: MicrosoftTeamsUserTimezone = { timezone: "UTC", label: "UTC" };
50
+
51
+ // Values Teams, a client or a stale card could put where a zone name goes.
52
+ const NOT_A_KNOWN_ZONE: Array<JSONValue> = [
53
+ "Eastern Standard Time",
54
+ "India Standard Time",
55
+ "Mars/Olympus_Mons",
56
+ "",
57
+ " ",
58
+ 42,
59
+ true,
60
+ null,
61
+ { timezone: "America/New_York" },
62
+ ["America/New_York"],
63
+ ];
64
+
65
+ function clientInfo(timezone: JSONValue): JSONObject {
66
+ return {
67
+ type: "clientInfo",
68
+ locale: "en-US",
69
+ country: "US",
70
+ platform: "Web",
71
+ timezone: timezone,
72
+ };
73
+ }
74
+
75
+ /*
76
+ * A message activity as botbuilder hands it to the bot: it has turned
77
+ * localTimestamp into a Date and kept the string as rawLocalTimestamp.
78
+ */
79
+ function webClientActivity(parts: JSONObject): JSONObject {
80
+ return {
81
+ type: "message",
82
+ id: "1727712345678",
83
+ channelId: "msteams",
84
+ conversation: { id: "a:1pQ3dFxkGqKZ8Yv0cWm7bT2nR5sL9hJ4uE6oA" },
85
+ ...parts,
86
+ };
87
+ }
88
+
89
+ function toIso(
90
+ date: string,
91
+ time: string,
92
+ timezone: MicrosoftTeamsUserTimezone,
93
+ ): string | null {
94
+ const instant: Date | null = MicrosoftTeamsTimezone.toDate({
95
+ date: date,
96
+ time: time,
97
+ timezone: timezone,
98
+ });
99
+
100
+ return instant ? instant.toISOString() : null;
101
+ }
102
+
103
+ function format(iso: string, timezone: MicrosoftTeamsUserTimezone): string {
104
+ return MicrosoftTeamsTimezone.format(new Date(iso), timezone);
105
+ }
106
+
107
+ describe("MicrosoftTeamsTimezone.getKnownTimezone", () => {
108
+ test("returns an IANA zone name moment-timezone knows", () => {
109
+ for (const name of [
110
+ "America/New_York",
111
+ "Asia/Kolkata",
112
+ "Asia/Kathmandu",
113
+ "Pacific/Chatham",
114
+ "Europe/London",
115
+ "UTC",
116
+ ]) {
117
+ expect(MicrosoftTeamsTimezone.getKnownTimezone(name)).toBe(name);
118
+ }
119
+ });
120
+
121
+ test("trims the name", () => {
122
+ expect(
123
+ MicrosoftTeamsTimezone.getKnownTimezone(" America/New_York\t"),
124
+ ).toBe("America/New_York");
125
+ });
126
+
127
+ test("ignores Windows zone names, unknown names, blanks and anything that is not a string", () => {
128
+ for (const value of NOT_A_KNOWN_ZONE) {
129
+ expect(MicrosoftTeamsTimezone.getKnownTimezone(value)).toBeUndefined();
130
+ }
131
+
132
+ expect(MicrosoftTeamsTimezone.getKnownTimezone(undefined)).toBeUndefined();
133
+ expect(MicrosoftTeamsTimezone.getKnownTimezone(new Date())).toBeUndefined();
134
+ });
135
+ });
136
+
137
+ describe("MicrosoftTeamsTimezone.getTimezoneFromActivity", () => {
138
+ test("reads the activity's localTimezone first", () => {
139
+ expect(
140
+ MicrosoftTeamsTimezone.getTimezoneFromActivity(
141
+ webClientActivity({
142
+ localTimezone: "America/New_York",
143
+ entities: [clientInfo("Europe/Berlin")],
144
+ }),
145
+ ),
146
+ ).toBe("America/New_York");
147
+ });
148
+
149
+ test("falls back to the clientInfo entity when localTimezone is missing", () => {
150
+ expect(
151
+ MicrosoftTeamsTimezone.getTimezoneFromActivity(
152
+ webClientActivity({
153
+ entities: [
154
+ { type: "mention", text: "<at>OneUptime</at>" },
155
+ clientInfo("Asia/Kolkata"),
156
+ ],
157
+ }),
158
+ ),
159
+ ).toBe("Asia/Kolkata");
160
+ });
161
+
162
+ test("falls back to the clientInfo entity when localTimezone is not a zone it knows", () => {
163
+ for (const value of NOT_A_KNOWN_ZONE) {
164
+ expect(
165
+ MicrosoftTeamsTimezone.getTimezoneFromActivity(
166
+ webClientActivity({
167
+ localTimezone: value,
168
+ entities: [clientInfo("Asia/Kolkata")],
169
+ }),
170
+ ),
171
+ ).toBe("Asia/Kolkata");
172
+ }
173
+ });
174
+
175
+ test("takes the first clientInfo entity with a usable zone, and only clientInfo entities", () => {
176
+ expect(
177
+ MicrosoftTeamsTimezone.getTimezoneFromActivity(
178
+ webClientActivity({
179
+ entities: [
180
+ null,
181
+ { type: "mention", timezone: "Europe/Paris" },
182
+ clientInfo("Pacific Standard Time"),
183
+ { type: "clientInfo", locale: "en-US" },
184
+ clientInfo("Asia/Tokyo"),
185
+ clientInfo("Europe/Berlin"),
186
+ ],
187
+ }),
188
+ ),
189
+ ).toBe("Asia/Tokyo");
190
+ });
191
+
192
+ test("is undefined when the activity names no zone (iOS has been seen to send none)", () => {
193
+ expect(
194
+ MicrosoftTeamsTimezone.getTimezoneFromActivity(webClientActivity({})),
195
+ ).toBeUndefined();
196
+ expect(
197
+ MicrosoftTeamsTimezone.getTimezoneFromActivity(
198
+ webClientActivity({
199
+ localTimezone: "",
200
+ entities: [clientInfo("")],
201
+ rawLocalTimestamp: "2026-10-01T14:00:00.000-04:00",
202
+ }),
203
+ ),
204
+ ).toBeUndefined();
205
+ });
206
+ });
207
+
208
+ describe("MicrosoftTeamsTimezone.getUtcOffsetFromActivity", () => {
209
+ test("reads the offset of rawLocalTimestamp, the string botbuilder keeps", () => {
210
+ const offsets: Array<{ timestamp: string; minutes: number }> = [
211
+ { timestamp: "2026-10-01T14:00:00.000-04:00", minutes: -240 },
212
+ { timestamp: "2026-10-01T14:00:00.000+05:30", minutes: 330 },
213
+ { timestamp: "2026-10-01T14:00:00.000+05:45", minutes: 345 },
214
+ { timestamp: "2026-10-01T14:00:00.000-03:30", minutes: -210 },
215
+ { timestamp: "2026-10-01T14:00:00.000+14:00", minutes: 840 },
216
+ { timestamp: "2026-10-01T14:00:00.000Z", minutes: 0 },
217
+ // The seven fractional digits the Bot Framework sends.
218
+ { timestamp: "2026-10-01T07:00:00.1015458-07:00", minutes: -420 },
219
+ // The basic form of the offset, without a colon.
220
+ { timestamp: "2026-10-01T14:00:00+0530", minutes: 330 },
221
+ { timestamp: " 2026-10-01T14:00:00-04:00 ", minutes: -240 },
222
+ ];
223
+
224
+ for (const offset of offsets) {
225
+ expect(
226
+ MicrosoftTeamsTimezone.getUtcOffsetFromActivity(
227
+ webClientActivity({ rawLocalTimestamp: offset.timestamp }),
228
+ ),
229
+ ).toBe(offset.minutes);
230
+ }
231
+ });
232
+
233
+ test("prefers rawLocalTimestamp to a localTimestamp string", () => {
234
+ expect(
235
+ MicrosoftTeamsTimezone.getUtcOffsetFromActivity(
236
+ webClientActivity({
237
+ rawLocalTimestamp: "2026-10-01T23:00:00.000+09:00",
238
+ localTimestamp: "2026-10-01T07:00:00.000-07:00",
239
+ }),
240
+ ),
241
+ ).toBe(540);
242
+ });
243
+
244
+ test("reads a localTimestamp string when there is no usable rawLocalTimestamp", () => {
245
+ expect(
246
+ MicrosoftTeamsTimezone.getUtcOffsetFromActivity(
247
+ webClientActivity({
248
+ localTimestamp: "2026-10-01T07:00:00.000-07:00",
249
+ }),
250
+ ),
251
+ ).toBe(-420);
252
+
253
+ expect(
254
+ MicrosoftTeamsTimezone.getUtcOffsetFromActivity(
255
+ webClientActivity({
256
+ rawLocalTimestamp: "not a timestamp+05:00",
257
+ localTimestamp: "2026-10-01T07:00:00.000-07:00",
258
+ }),
259
+ ),
260
+ ).toBe(-420);
261
+ });
262
+
263
+ test("ignores the Date botbuilder makes of localTimestamp: a Date has no offset", () => {
264
+ expect(
265
+ MicrosoftTeamsTimezone.getUtcOffsetFromActivity(
266
+ webClientActivity({
267
+ localTimestamp: new Date("2026-10-01T18:00:00.000Z"),
268
+ }),
269
+ ),
270
+ ).toBeUndefined();
271
+ });
272
+
273
+ test("does not take a timestamp without an offset for UTC, and ignores what does not parse", () => {
274
+ for (const timestamp of [
275
+ "2026-10-01T14:00:00",
276
+ "2026-10-01T14:00:00.000",
277
+ "not a timestamp+05:00",
278
+ "2026-13-01T14:00:00+05:00",
279
+ "",
280
+ ]) {
281
+ expect(
282
+ MicrosoftTeamsTimezone.getUtcOffsetFromActivity(
283
+ webClientActivity({
284
+ rawLocalTimestamp: timestamp,
285
+ localTimestamp: timestamp,
286
+ }),
287
+ ),
288
+ ).toBeUndefined();
289
+ }
290
+
291
+ expect(
292
+ MicrosoftTeamsTimezone.getUtcOffsetFromActivity(
293
+ webClientActivity({ rawLocalTimestamp: 1727791200000 }),
294
+ ),
295
+ ).toBeUndefined();
296
+ expect(
297
+ MicrosoftTeamsTimezone.getUtcOffsetFromActivity(webClientActivity({})),
298
+ ).toBeUndefined();
299
+ });
300
+ });
301
+
302
+ describe("MicrosoftTeamsTimezone.resolve", () => {
303
+ /*
304
+ * Every source names a different zone, so each test can tell which one
305
+ * won. The sources are taken away one at a time.
306
+ */
307
+ const EVERY_SOURCE: JSONObject = webClientActivity({
308
+ localTimezone: "America/New_York",
309
+ entities: [clientInfo("Europe/Berlin")],
310
+ rawLocalTimestamp: "2026-10-01T23:00:00.000+09:00",
311
+ localTimestamp: "2026-10-01T07:00:00.000-07:00",
312
+ });
313
+
314
+ function without(...fields: Array<string>): JSONObject {
315
+ const activity: JSONObject = { ...EVERY_SOURCE };
316
+
317
+ for (const field of fields) {
318
+ delete activity[field];
319
+ }
320
+
321
+ return activity;
322
+ }
323
+
324
+ test("1. the zone the form named, carried in the card's submit data, comes before everything else", () => {
325
+ expect(
326
+ MicrosoftTeamsTimezone.resolve({
327
+ activity: EVERY_SOURCE,
328
+ timezoneFromCard: "Asia/Kolkata",
329
+ }),
330
+ ).toEqual({ timezone: "Asia/Kolkata", label: "Asia/Kolkata" });
331
+ });
332
+
333
+ test("2. then the activity's localTimezone", () => {
334
+ expect(MicrosoftTeamsTimezone.resolve({ activity: EVERY_SOURCE })).toEqual({
335
+ timezone: "America/New_York",
336
+ label: "America/New_York",
337
+ });
338
+ expect(
339
+ MicrosoftTeamsTimezone.resolve({
340
+ activity: EVERY_SOURCE,
341
+ timezoneFromCard: undefined,
342
+ }),
343
+ ).toEqual({ timezone: "America/New_York", label: "America/New_York" });
344
+ });
345
+
346
+ test("3. then the activity's clientInfo entity", () => {
347
+ expect(
348
+ MicrosoftTeamsTimezone.resolve({
349
+ activity: without("localTimezone"),
350
+ }),
351
+ ).toEqual({ timezone: "Europe/Berlin", label: "Europe/Berlin" });
352
+ });
353
+
354
+ test("4. then the UTC offset of rawLocalTimestamp", () => {
355
+ expect(
356
+ MicrosoftTeamsTimezone.resolve({
357
+ activity: without("localTimezone", "entities"),
358
+ }),
359
+ ).toEqual({ utcOffsetInMinutes: 540, label: "UTC+09:00" });
360
+ });
361
+
362
+ test("5. then the UTC offset of a localTimestamp string", () => {
363
+ expect(
364
+ MicrosoftTeamsTimezone.resolve({
365
+ activity: without("localTimezone", "entities", "rawLocalTimestamp"),
366
+ }),
367
+ ).toEqual({ utcOffsetInMinutes: -420, label: "UTC-07:00" });
368
+ });
369
+
370
+ test("6. and UTC when nothing names a zone or an offset", () => {
371
+ expect(
372
+ MicrosoftTeamsTimezone.resolve({
373
+ activity: without(
374
+ "localTimezone",
375
+ "entities",
376
+ "rawLocalTimestamp",
377
+ "localTimestamp",
378
+ ),
379
+ }),
380
+ ).toEqual({ timezone: "UTC", label: "UTC" });
381
+
382
+ expect(MicrosoftTeamsTimezone.resolve({ activity: {} })).toEqual(UTC);
383
+ });
384
+
385
+ test("a zone name it does not know is skipped wherever it appears", () => {
386
+ for (const value of NOT_A_KNOWN_ZONE) {
387
+ // On the card: the activity's localTimezone decides.
388
+ expect(
389
+ MicrosoftTeamsTimezone.resolve({
390
+ activity: EVERY_SOURCE,
391
+ timezoneFromCard: value,
392
+ }),
393
+ ).toEqual({ timezone: "America/New_York", label: "America/New_York" });
394
+
395
+ // As localTimezone too: the clientInfo entity decides.
396
+ expect(
397
+ MicrosoftTeamsTimezone.resolve({
398
+ activity: { ...EVERY_SOURCE, localTimezone: value },
399
+ timezoneFromCard: value,
400
+ }),
401
+ ).toEqual({ timezone: "Europe/Berlin", label: "Europe/Berlin" });
402
+
403
+ // In clientInfo as well: the local timestamp's offset decides.
404
+ expect(
405
+ MicrosoftTeamsTimezone.resolve({
406
+ activity: {
407
+ ...EVERY_SOURCE,
408
+ localTimezone: value,
409
+ entities: [clientInfo(value)],
410
+ },
411
+ timezoneFromCard: value,
412
+ }),
413
+ ).toEqual({ utcOffsetInMinutes: 540, label: "UTC+09:00" });
414
+ }
415
+ });
416
+
417
+ /*
418
+ * The form tells whoever fills it in that its times are in the zone it
419
+ * names, so that is the zone they typed them in. In a channel the one who
420
+ * submits it need not be the one it was sent for.
421
+ */
422
+ test("the zone the form named beats the submitter's own, wherever Teams says the submitter is", () => {
423
+ // Sent for someone in New York; a colleague in Kolkata submitted it.
424
+ const fromKolkata: Array<JSONObject> = [
425
+ webClientActivity({ localTimezone: "Asia/Kolkata" }),
426
+ webClientActivity({ entities: [clientInfo("Asia/Kolkata")] }),
427
+ webClientActivity({
428
+ rawLocalTimestamp: "2026-10-01T19:30:00.000+05:30",
429
+ }),
430
+ webClientActivity({ localTimestamp: "2026-10-01T19:30:00.000+05:30" }),
431
+ webClientActivity({
432
+ localTimezone: "Asia/Kolkata",
433
+ entities: [clientInfo("Asia/Kolkata")],
434
+ rawLocalTimestamp: "2026-10-01T19:30:00.000+05:30",
435
+ }),
436
+ ];
437
+
438
+ for (const activity of fromKolkata) {
439
+ expect(
440
+ MicrosoftTeamsTimezone.resolve({
441
+ activity: activity,
442
+ timezoneFromCard: "America/New_York",
443
+ }),
444
+ ).toEqual({ timezone: "America/New_York", label: "America/New_York" });
445
+ }
446
+ });
447
+
448
+ test("the card's zone covers a submit from a client that names no zone (iOS)", () => {
449
+ expect(
450
+ MicrosoftTeamsTimezone.resolve({
451
+ activity: webClientActivity({
452
+ localTimestamp: new Date("2026-10-01T18:00:00.000Z"),
453
+ rawLocalTimestamp: "2026-10-01T14:00:00.000-04:00",
454
+ }),
455
+ timezoneFromCard: " America/New_York ",
456
+ }),
457
+ ).toEqual({ timezone: "America/New_York", label: "America/New_York" });
458
+ });
459
+
460
+ test("a Date-typed localTimestamp is not an offset: without anything else it is UTC", () => {
461
+ expect(
462
+ MicrosoftTeamsTimezone.resolve({
463
+ activity: webClientActivity({
464
+ localTimestamp: new Date("2026-10-01T18:00:00.000Z"),
465
+ }),
466
+ }),
467
+ ).toEqual(UTC);
468
+ });
469
+
470
+ test("a zero offset is named UTC", () => {
471
+ expect(
472
+ MicrosoftTeamsTimezone.resolve({
473
+ activity: webClientActivity({
474
+ rawLocalTimestamp: "2026-10-01T14:00:00.000Z",
475
+ }),
476
+ }),
477
+ ).toEqual({ utcOffsetInMinutes: 0, label: "UTC" });
478
+ });
479
+
480
+ test("the offsets it names are the ones toDate and format are given below", () => {
481
+ expect(
482
+ MicrosoftTeamsTimezone.resolve({
483
+ activity: webClientActivity({
484
+ rawLocalTimestamp: "2026-10-01T14:00:00.000+05:30",
485
+ }),
486
+ }),
487
+ ).toEqual(UTC_PLUS_05_30);
488
+ expect(
489
+ MicrosoftTeamsTimezone.resolve({
490
+ activity: webClientActivity({
491
+ rawLocalTimestamp: "2026-10-01T14:00:00.000-07:00",
492
+ }),
493
+ }),
494
+ ).toEqual(UTC_MINUS_07_00);
495
+ });
496
+ });
497
+
498
+ /*
499
+ * The maintenance confirmation adds a warning when only an offset is known:
500
+ * the offset is the one the activity's local timestamp has today, so a date
501
+ * past a daylight saving change can be an hour out.
502
+ */
503
+ describe("MicrosoftTeamsTimezone.isUtcOffsetOnly", () => {
504
+ test("is true when only a UTC offset is known", () => {
505
+ expect(MicrosoftTeamsTimezone.isUtcOffsetOnly(UTC_PLUS_05_30)).toBe(true);
506
+ expect(MicrosoftTeamsTimezone.isUtcOffsetOnly(UTC_MINUS_07_00)).toBe(true);
507
+ // Still only an offset: London is UTC+00:00 in winter and UTC+01:00 in summer.
508
+ expect(
509
+ MicrosoftTeamsTimezone.isUtcOffsetOnly({
510
+ utcOffsetInMinutes: 0,
511
+ label: "UTC",
512
+ }),
513
+ ).toBe(true);
514
+ // An empty name is no name: toDate reads the offset then.
515
+ expect(
516
+ MicrosoftTeamsTimezone.isUtcOffsetOnly({
517
+ timezone: "",
518
+ utcOffsetInMinutes: 330,
519
+ label: "UTC+05:30",
520
+ }),
521
+ ).toBe(true);
522
+ });
523
+
524
+ test("is false when a zone name is known, with or without an offset beside it", () => {
525
+ expect(MicrosoftTeamsTimezone.isUtcOffsetOnly(NEW_YORK)).toBe(false);
526
+ expect(MicrosoftTeamsTimezone.isUtcOffsetOnly(KOLKATA)).toBe(false);
527
+ expect(
528
+ MicrosoftTeamsTimezone.isUtcOffsetOnly({
529
+ timezone: "America/New_York",
530
+ utcOffsetInMinutes: -240,
531
+ label: "America/New_York",
532
+ }),
533
+ ).toBe(false);
534
+ // What resolve falls back on is the UTC zone, which has no daylight saving time.
535
+ expect(MicrosoftTeamsTimezone.isUtcOffsetOnly(UTC)).toBe(false);
536
+ expect(
537
+ MicrosoftTeamsTimezone.isUtcOffsetOnly(MicrosoftTeamsTimezone.UTC),
538
+ ).toBe(false);
539
+ });
540
+
541
+ test("is false when neither a zone name nor an offset is known", () => {
542
+ expect(MicrosoftTeamsTimezone.isUtcOffsetOnly({ label: "UTC" })).toBe(
543
+ false,
544
+ );
545
+ expect(
546
+ MicrosoftTeamsTimezone.isUtcOffsetOnly({
547
+ timezone: undefined,
548
+ utcOffsetInMinutes: undefined,
549
+ label: "UTC",
550
+ }),
551
+ ).toBe(false);
552
+ });
553
+
554
+ test("is true for what resolve reads from a local timestamp, and only for that", () => {
555
+ const submits: Array<{
556
+ source: string;
557
+ activity: JSONObject;
558
+ timezoneFromCard?: string;
559
+ isUtcOffsetOnly: boolean;
560
+ }> = [
561
+ {
562
+ source: "the zone the form named",
563
+ activity: webClientActivity({
564
+ rawLocalTimestamp: "2026-10-01T14:00:00.000-04:00",
565
+ }),
566
+ timezoneFromCard: "America/New_York",
567
+ isUtcOffsetOnly: false,
568
+ },
569
+ {
570
+ source: "localTimezone",
571
+ activity: webClientActivity({
572
+ localTimezone: "America/New_York",
573
+ rawLocalTimestamp: "2026-10-01T14:00:00.000-04:00",
574
+ }),
575
+ isUtcOffsetOnly: false,
576
+ },
577
+ {
578
+ source: "clientInfo",
579
+ activity: webClientActivity({
580
+ entities: [clientInfo("America/New_York")],
581
+ rawLocalTimestamp: "2026-10-01T14:00:00.000-04:00",
582
+ }),
583
+ isUtcOffsetOnly: false,
584
+ },
585
+ {
586
+ source: "rawLocalTimestamp",
587
+ activity: webClientActivity({
588
+ rawLocalTimestamp: "2026-10-01T14:00:00.000-04:00",
589
+ }),
590
+ isUtcOffsetOnly: true,
591
+ },
592
+ {
593
+ source: "a localTimestamp string",
594
+ activity: webClientActivity({
595
+ localTimestamp: "2026-10-01T14:00:00.000+05:30",
596
+ }),
597
+ isUtcOffsetOnly: true,
598
+ },
599
+ {
600
+ source: "a zero offset",
601
+ activity: webClientActivity({
602
+ rawLocalTimestamp: "2026-10-01T14:00:00.000Z",
603
+ }),
604
+ isUtcOffsetOnly: true,
605
+ },
606
+ {
607
+ source: "nothing: UTC",
608
+ activity: webClientActivity({
609
+ localTimestamp: new Date("2026-10-01T18:00:00.000Z"),
610
+ }),
611
+ isUtcOffsetOnly: false,
612
+ },
613
+ ];
614
+
615
+ // Compared as one list, so a failure names every source that changed.
616
+ expect(
617
+ submits.map(
618
+ (submit: {
619
+ source: string;
620
+ activity: JSONObject;
621
+ timezoneFromCard?: string;
622
+ }) => {
623
+ return {
624
+ source: submit.source,
625
+ isUtcOffsetOnly: MicrosoftTeamsTimezone.isUtcOffsetOnly(
626
+ MicrosoftTeamsTimezone.resolve({
627
+ activity: submit.activity,
628
+ timezoneFromCard: submit.timezoneFromCard,
629
+ }),
630
+ ),
631
+ };
632
+ },
633
+ ),
634
+ ).toEqual(
635
+ submits.map((submit: { source: string; isUtcOffsetOnly: boolean }) => {
636
+ return {
637
+ source: submit.source,
638
+ isUtcOffsetOnly: submit.isUtcOffsetOnly,
639
+ };
640
+ }),
641
+ );
642
+ });
643
+
644
+ test("why it is worth a warning: an offset read in October is an hour out for a date in New York's winter", () => {
645
+ // A New York user on a client that names no zone, in October (EDT).
646
+ const timezone: MicrosoftTeamsUserTimezone = MicrosoftTeamsTimezone.resolve(
647
+ {
648
+ activity: webClientActivity({
649
+ rawLocalTimestamp: "2026-10-01T14:00:00.000-04:00",
650
+ }),
651
+ },
652
+ );
653
+
654
+ expect(timezone).toEqual({ utcOffsetInMinutes: -240, label: "UTC-04:00" });
655
+ expect(MicrosoftTeamsTimezone.isUtcOffsetOnly(timezone)).toBe(true);
656
+
657
+ const startsAt: Date | null = MicrosoftTeamsTimezone.toDate({
658
+ date: "2026-12-24",
659
+ time: "23:15",
660
+ timezone: timezone,
661
+ });
662
+
663
+ expect(startsAt?.toISOString()).toBe("2026-12-25T03:15:00.000Z");
664
+ // The confirmation reads back what was typed, at the offset it was read at...
665
+ expect(MicrosoftTeamsTimezone.format(startsAt!, timezone)).toBe(
666
+ "Dec 24, 2026, 23:15 (UTC-04:00)",
667
+ );
668
+ // ...which New York's clocks, on EST by then, show an hour earlier.
669
+ expect(MicrosoftTeamsTimezone.format(startsAt!, NEW_YORK)).toBe(
670
+ "Dec 24, 2026, 22:15 (America/New_York)",
671
+ );
672
+ });
673
+ });
674
+
675
+ describe("MicrosoftTeamsTimezone.formatUtcOffset", () => {
676
+ test("names an offset from UTC in hours and minutes", () => {
677
+ expect(MicrosoftTeamsTimezone.formatUtcOffset(0)).toBe("UTC");
678
+ expect(MicrosoftTeamsTimezone.formatUtcOffset(330)).toBe("UTC+05:30");
679
+ expect(MicrosoftTeamsTimezone.formatUtcOffset(-420)).toBe("UTC-07:00");
680
+ expect(MicrosoftTeamsTimezone.formatUtcOffset(45)).toBe("UTC+00:45");
681
+ expect(MicrosoftTeamsTimezone.formatUtcOffset(-45)).toBe("UTC-00:45");
682
+ expect(MicrosoftTeamsTimezone.formatUtcOffset(-210)).toBe("UTC-03:30");
683
+ expect(MicrosoftTeamsTimezone.formatUtcOffset(345)).toBe("UTC+05:45");
684
+ expect(MicrosoftTeamsTimezone.formatUtcOffset(765)).toBe("UTC+12:45");
685
+ expect(MicrosoftTeamsTimezone.formatUtcOffset(840)).toBe("UTC+14:00");
686
+ expect(MicrosoftTeamsTimezone.formatUtcOffset(-720)).toBe("UTC-12:00");
687
+ });
688
+ });
689
+
690
+ describe("MicrosoftTeamsTimezone.toDate", () => {
691
+ test("reads 14:00 in New York as 18:00 UTC in October (EDT) and 19:00 UTC in January (EST)", () => {
692
+ expect(toIso("2026-10-01", "14:00", NEW_YORK)).toBe(
693
+ "2026-10-01T18:00:00.000Z",
694
+ );
695
+ expect(toIso("2027-01-15", "14:00", NEW_YORK)).toBe(
696
+ "2027-01-15T19:00:00.000Z",
697
+ );
698
+ });
699
+
700
+ test("reads the date and time in a zone with a half-hour offset", () => {
701
+ expect(toIso("2026-10-01", "14:00", KOLKATA)).toBe(
702
+ "2026-10-01T08:30:00.000Z",
703
+ );
704
+ // Before 05:30 in Kolkata it is still the day before in UTC.
705
+ expect(toIso("2026-10-01", "02:00", KOLKATA)).toBe(
706
+ "2026-09-30T20:30:00.000Z",
707
+ );
708
+ });
709
+
710
+ test("reads the date and time as UTC in UTC", () => {
711
+ expect(toIso("2026-10-01", "14:00", UTC)).toBe("2026-10-01T14:00:00.000Z");
712
+ expect(
713
+ toIso("2026-10-01", "14:00", { utcOffsetInMinutes: 0, label: "UTC" }),
714
+ ).toBe("2026-10-01T14:00:00.000Z");
715
+ });
716
+
717
+ test("reads the date and time at a bare UTC offset", () => {
718
+ expect(toIso("2026-10-01", "14:00", UTC_PLUS_05_30)).toBe(
719
+ "2026-10-01T08:30:00.000Z",
720
+ );
721
+ expect(toIso("2026-10-01", "14:00", UTC_MINUS_07_00)).toBe(
722
+ "2026-10-01T21:00:00.000Z",
723
+ );
724
+ // 20:00 at UTC-07:00 is already the next day in UTC.
725
+ expect(toIso("2026-10-01", "20:00", UTC_MINUS_07_00)).toBe(
726
+ "2026-10-02T03:00:00.000Z",
727
+ );
728
+ });
729
+
730
+ test("a zone name comes before an offset when both are given", () => {
731
+ expect(
732
+ toIso("2026-10-01", "14:00", {
733
+ timezone: "America/New_York",
734
+ utcOffsetInMinutes: 330,
735
+ label: "America/New_York",
736
+ }),
737
+ ).toBe("2026-10-01T18:00:00.000Z");
738
+ });
739
+
740
+ test("accepts seconds (HH:mm:ss)", () => {
741
+ expect(toIso("2026-10-01", "14:00:30", NEW_YORK)).toBe(
742
+ "2026-10-01T18:00:30.000Z",
743
+ );
744
+ expect(toIso("2026-10-01", "14:00:30", UTC_PLUS_05_30)).toBe(
745
+ "2026-10-01T08:30:30.000Z",
746
+ );
747
+ expect(toIso("2026-10-01", "14:00:00", UTC)).toBe(
748
+ "2026-10-01T14:00:00.000Z",
749
+ );
750
+ });
751
+
752
+ test("trims the date and the time", () => {
753
+ expect(toIso(" 2026-10-01 ", " 14:00 ", NEW_YORK)).toBe(
754
+ "2026-10-01T18:00:00.000Z",
755
+ );
756
+ expect(toIso(" 2026-10-01 ", " 14:00 ", UTC_PLUS_05_30)).toBe(
757
+ "2026-10-01T08:30:00.000Z",
758
+ );
759
+ });
760
+
761
+ test("reads February 29 only in a leap year", () => {
762
+ expect(toIso("2028-02-29", "14:00", NEW_YORK)).toBe(
763
+ "2028-02-29T19:00:00.000Z",
764
+ );
765
+ expect(toIso("2026-02-29", "14:00", NEW_YORK)).toBeNull();
766
+ expect(toIso("2026-02-29", "14:00", UTC_PLUS_05_30)).toBeNull();
767
+ });
768
+
769
+ test("is null for a date that is not a real YYYY-MM-DD date", () => {
770
+ for (const date of [
771
+ "2026-13-01",
772
+ "2026-00-10",
773
+ "2026-02-30",
774
+ "2026-04-31",
775
+ "2026-10-1",
776
+ "26-10-01",
777
+ "2026/10/01",
778
+ "10/01/2026",
779
+ "01-10-2026",
780
+ "2026-10-01T14:00",
781
+ "tomorrow",
782
+ "",
783
+ ]) {
784
+ for (const timezone of [NEW_YORK, UTC_PLUS_05_30, UTC]) {
785
+ expect(toIso(date, "14:00", timezone)).toBeNull();
786
+ }
787
+ }
788
+ });
789
+
790
+ test("is null for a time that is not a real HH:mm or HH:mm:ss time", () => {
791
+ for (const time of [
792
+ "25:00",
793
+ "14:60",
794
+ "14:00:60",
795
+ "9:00",
796
+ "14:5",
797
+ "14",
798
+ "2 PM",
799
+ "14:00 PM",
800
+ "14h00",
801
+ "14:00Z",
802
+ "14:00+02:00",
803
+ "14:00:00.000",
804
+ "",
805
+ ]) {
806
+ for (const timezone of [NEW_YORK, UTC_PLUS_05_30, UTC]) {
807
+ expect(toIso("2026-10-01", time, timezone)).toBeNull();
808
+ }
809
+ }
810
+ });
811
+
812
+ /*
813
+ * New York's clocks jump from 02:00 to 03:00 on March 8, 2026, and fall
814
+ * back from 02:00 to 01:00 on November 1, 2026. A time in the gap does
815
+ * not exist and a time in the overlap exists twice. Both are read the way
816
+ * RFC 5545 reads them: a time in the gap with the offset from before the
817
+ * gap (02:30 becomes 03:30 EDT), a repeated time as its first occurrence
818
+ * (01:30 EDT).
819
+ */
820
+ test("a time skipped by the spring-forward gap is read with the offset from before the gap", () => {
821
+ expect(toIso("2026-03-08", "01:59", NEW_YORK)).toBe(
822
+ "2026-03-08T06:59:00.000Z",
823
+ );
824
+ expect(toIso("2026-03-08", "02:30", NEW_YORK)).toBe(
825
+ "2026-03-08T07:30:00.000Z",
826
+ );
827
+ expect(toIso("2026-03-08", "03:00", NEW_YORK)).toBe(
828
+ "2026-03-08T07:00:00.000Z",
829
+ );
830
+ expect(toIso("2026-03-08", "03:30", NEW_YORK)).toBe(
831
+ "2026-03-08T07:30:00.000Z",
832
+ );
833
+ });
834
+
835
+ test("a time repeated by the fall-back overlap is read as its first occurrence", () => {
836
+ expect(toIso("2026-11-01", "00:59", NEW_YORK)).toBe(
837
+ "2026-11-01T04:59:00.000Z",
838
+ );
839
+ expect(toIso("2026-11-01", "01:30", NEW_YORK)).toBe(
840
+ "2026-11-01T05:30:00.000Z",
841
+ );
842
+ expect(toIso("2026-11-01", "02:00", NEW_YORK)).toBe(
843
+ "2026-11-01T07:00:00.000Z",
844
+ );
845
+ });
846
+ });
847
+
848
+ describe("MicrosoftTeamsTimezone.format", () => {
849
+ test("shows the time on the user's clock with the zone's name", () => {
850
+ expect(format("2026-10-01T18:00:00.000Z", NEW_YORK)).toBe(
851
+ "Oct 1, 2026, 14:00 (America/New_York)",
852
+ );
853
+ expect(format("2027-01-15T19:00:00.000Z", NEW_YORK)).toBe(
854
+ "Jan 15, 2027, 14:00 (America/New_York)",
855
+ );
856
+ expect(format("2026-10-01T08:30:00.000Z", KOLKATA)).toBe(
857
+ "Oct 1, 2026, 14:00 (Asia/Kolkata)",
858
+ );
859
+ expect(format("2026-10-01T14:00:00.000Z", UTC)).toBe(
860
+ "Oct 1, 2026, 14:00 (UTC)",
861
+ );
862
+ });
863
+
864
+ test("shows the time at a bare offset with the offset's name", () => {
865
+ expect(format("2026-10-01T08:30:00.000Z", UTC_PLUS_05_30)).toBe(
866
+ "Oct 1, 2026, 14:00 (UTC+05:30)",
867
+ );
868
+ expect(format("2026-10-01T21:00:00.000Z", UTC_MINUS_07_00)).toBe(
869
+ "Oct 1, 2026, 14:00 (UTC-07:00)",
870
+ );
871
+ expect(
872
+ format("2026-10-01T14:00:00.000Z", {
873
+ utcOffsetInMinutes: 0,
874
+ label: "UTC",
875
+ }),
876
+ ).toBe("Oct 1, 2026, 14:00 (UTC)");
877
+ });
878
+
879
+ test("the day, month and year are the user's, not UTC's", () => {
880
+ expect(format("2026-10-01T02:00:00.000Z", NEW_YORK)).toBe(
881
+ "Sep 30, 2026, 22:00 (America/New_York)",
882
+ );
883
+ expect(format("2026-12-31T20:00:00.000Z", UTC_PLUS_05_30)).toBe(
884
+ "Jan 1, 2027, 01:30 (UTC+05:30)",
885
+ );
886
+ expect(format("2026-10-01T03:00:00.000Z", UTC_MINUS_07_00)).toBe(
887
+ "Sep 30, 2026, 20:00 (UTC-07:00)",
888
+ );
889
+ });
890
+
891
+ test("uses a 24-hour clock and leaves the seconds out", () => {
892
+ expect(format("2026-10-01T04:05:00.000Z", NEW_YORK)).toBe(
893
+ "Oct 1, 2026, 00:05 (America/New_York)",
894
+ );
895
+ expect(format("2026-10-01T18:00:30.000Z", NEW_YORK)).toBe(
896
+ "Oct 1, 2026, 14:00 (America/New_York)",
897
+ );
898
+ });
899
+
900
+ test("echoes the time a spring-forward gap moved it to, so the user sees what was scheduled", () => {
901
+ const startsAt: Date | null = MicrosoftTeamsTimezone.toDate({
902
+ date: "2026-03-08",
903
+ time: "02:30",
904
+ timezone: NEW_YORK,
905
+ });
906
+
907
+ expect(startsAt).not.toBeNull();
908
+ expect(MicrosoftTeamsTimezone.format(startsAt!, NEW_YORK)).toBe(
909
+ "Mar 8, 2026, 03:30 (America/New_York)",
910
+ );
911
+ });
912
+
913
+ test("gives back the date and time the user typed, in the zone the submit resolves to", () => {
914
+ // MicrosoftTeamsTimezoneServerZone.test.ts expects this same table.
915
+ const submits: Array<{
916
+ activity: JSONObject;
917
+ timezoneFromCard?: string;
918
+ label: string;
919
+ startsAt: string;
920
+ isUtcOffsetOnly: boolean;
921
+ }> = [
922
+ {
923
+ activity: webClientActivity({ localTimezone: "America/New_York" }),
924
+ label: "America/New_York",
925
+ startsAt: "2026-12-25T04:15:00.000Z",
926
+ isUtcOffsetOnly: false,
927
+ },
928
+ {
929
+ activity: webClientActivity({
930
+ entities: [clientInfo("Australia/Adelaide")],
931
+ }),
932
+ label: "Australia/Adelaide",
933
+ startsAt: "2026-12-24T12:45:00.000Z",
934
+ isUtcOffsetOnly: false,
935
+ },
936
+ {
937
+ // iOS: no zone on the submit, the one the form named.
938
+ activity: webClientActivity({
939
+ localTimestamp: new Date("2026-10-01T18:00:00.000Z"),
940
+ rawLocalTimestamp: "2026-10-01T14:00:00.000-04:00",
941
+ }),
942
+ timezoneFromCard: "America/New_York",
943
+ label: "America/New_York",
944
+ startsAt: "2026-12-25T04:15:00.000Z",
945
+ isUtcOffsetOnly: false,
946
+ },
947
+ {
948
+ // A channel: the form named New York; a colleague in Kolkata submitted it.
949
+ activity: webClientActivity({
950
+ localTimezone: "Asia/Kolkata",
951
+ entities: [clientInfo("Asia/Kolkata")],
952
+ rawLocalTimestamp: "2026-10-01T19:30:00.000+05:30",
953
+ }),
954
+ timezoneFromCard: "America/New_York",
955
+ label: "America/New_York",
956
+ startsAt: "2026-12-25T04:15:00.000Z",
957
+ isUtcOffsetOnly: false,
958
+ },
959
+ {
960
+ activity: webClientActivity({
961
+ rawLocalTimestamp: "2026-10-01T14:00:00.000+05:45",
962
+ }),
963
+ label: "UTC+05:45",
964
+ startsAt: "2026-12-24T17:30:00.000Z",
965
+ isUtcOffsetOnly: true,
966
+ },
967
+ {
968
+ activity: webClientActivity({
969
+ localTimestamp: "2026-10-01T14:00:00.000-03:30",
970
+ }),
971
+ label: "UTC-03:30",
972
+ startsAt: "2026-12-25T02:45:00.000Z",
973
+ isUtcOffsetOnly: true,
974
+ },
975
+ {
976
+ activity: webClientActivity({}),
977
+ label: "UTC",
978
+ startsAt: "2026-12-24T23:15:00.000Z",
979
+ isUtcOffsetOnly: false,
980
+ },
981
+ ];
982
+
983
+ for (const submit of submits) {
984
+ const timezone: MicrosoftTeamsUserTimezone =
985
+ MicrosoftTeamsTimezone.resolve({
986
+ activity: submit.activity,
987
+ timezoneFromCard: submit.timezoneFromCard,
988
+ });
989
+ const startsAt: Date | null = MicrosoftTeamsTimezone.toDate({
990
+ date: "2026-12-24",
991
+ time: "23:15",
992
+ timezone: timezone,
993
+ });
994
+
995
+ expect(timezone.label).toBe(submit.label);
996
+ expect(startsAt?.toISOString()).toBe(submit.startsAt);
997
+ expect(MicrosoftTeamsTimezone.format(startsAt!, timezone)).toBe(
998
+ `Dec 24, 2026, 23:15 (${submit.label})`,
999
+ );
1000
+ expect(MicrosoftTeamsTimezone.isUtcOffsetOnly(timezone)).toBe(
1001
+ submit.isUtcOffsetOnly,
1002
+ );
1003
+ }
1004
+ });
1005
+ });
1006
+
1007
+ describe("MicrosoftTeamsTimezone against the read it replaced", () => {
1008
+ test("14:00 typed in New York is 14:00 in New York; the old read made it 10:00 on a UTC server", () => {
1009
+ /*
1010
+ * The old handler: OneUptimeDate.fromString(`${startDate}T${startTime}`),
1011
+ * which reads a date and time without a zone on the server's clock.
1012
+ */
1013
+ const oldRead: Date = OneUptimeDate.fromString("2026-10-01T14:00");
1014
+
1015
+ expect(oldRead.toISOString()).toBe("2026-10-01T14:00:00.000Z");
1016
+ expect(MicrosoftTeamsTimezone.format(oldRead, NEW_YORK)).toBe(
1017
+ "Oct 1, 2026, 10:00 (America/New_York)",
1018
+ );
1019
+
1020
+ const startsAt: Date | null = MicrosoftTeamsTimezone.toDate({
1021
+ date: "2026-10-01",
1022
+ time: "14:00",
1023
+ timezone: MicrosoftTeamsTimezone.resolve({
1024
+ activity: webClientActivity({ localTimezone: "America/New_York" }),
1025
+ }),
1026
+ });
1027
+
1028
+ expect(startsAt?.toISOString()).toBe("2026-10-01T18:00:00.000Z");
1029
+ expect(MicrosoftTeamsTimezone.format(startsAt!, NEW_YORK)).toBe(
1030
+ "Oct 1, 2026, 14:00 (America/New_York)",
1031
+ );
1032
+ });
1033
+ });