@oneuptime/common 11.3.25 → 11.3.28

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 (397) hide show
  1. package/Models/DatabaseModels/AIConversation.ts +322 -0
  2. package/Models/DatabaseModels/AIConversationMessage.ts +495 -0
  3. package/Models/DatabaseModels/AIRun.ts +584 -0
  4. package/Models/DatabaseModels/AIRunEvent.ts +443 -0
  5. package/Models/DatabaseModels/IncidentEpisodeRoleMember.ts +1 -1
  6. package/Models/DatabaseModels/IncidentMember.ts +1 -1
  7. package/Models/DatabaseModels/IncomingCallPolicy.ts +13 -0
  8. package/Models/DatabaseModels/Index.ts +11 -0
  9. package/Models/DatabaseModels/LlmLog.ts +26 -0
  10. package/Models/DatabaseModels/LlmProvider.ts +2 -2
  11. package/Models/DatabaseModels/MigrationFailure.ts +170 -0
  12. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +51 -2
  13. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +44 -0
  14. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +6 -0
  15. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.ts +6 -0
  16. package/Models/DatabaseModels/OnCallDutyPolicyTimeLog.ts +1 -1
  17. package/Models/DatabaseModels/OnCallDutyPolicyUserOverride.ts +2 -2
  18. package/Server/API/AIChatAPI.ts +693 -0
  19. package/Server/API/AlertAPI.ts +1 -1
  20. package/Server/API/IncidentAPI.ts +2 -2
  21. package/Server/API/IncidentEpisodeAPI.ts +1 -1
  22. package/Server/API/LlmProviderAPI.ts +170 -0
  23. package/Server/API/MicrosoftTeamsAPI.ts +5 -0
  24. package/Server/API/ScheduledMaintenanceAPI.ts +1 -1
  25. package/Server/API/SlackAPI.ts +663 -0
  26. package/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.ts +245 -0
  27. package/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.ts +36 -0
  28. package/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.ts +46 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.ts +119 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.ts +25 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.ts +81 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.ts +25 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.ts +35 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +16 -0
  35. package/Server/Infrastructure/PostgresDatabase.ts +11 -0
  36. package/Server/Services/AIConversationMessageService.ts +39 -0
  37. package/Server/Services/AIConversationService.ts +63 -0
  38. package/Server/Services/AIRunEventService.ts +39 -0
  39. package/Server/Services/AIRunService.ts +39 -0
  40. package/Server/Services/AIService.ts +127 -33
  41. package/Server/Services/AnalyticsDatabaseService.ts +10 -1
  42. package/Server/Services/DatabaseService.ts +29 -1
  43. package/Server/Services/IncidentService.ts +1 -1
  44. package/Server/Services/IncomingCallPolicyEscalationRuleService.ts +75 -2
  45. package/Server/Services/IncomingCallPolicyService.ts +42 -1
  46. package/Server/Services/Index.ts +2 -0
  47. package/Server/Services/LlmProviderService.ts +110 -0
  48. package/Server/Services/MigrationFailureService.ts +9 -0
  49. package/Server/Services/MonitorProbeService.ts +33 -7
  50. package/Server/Services/MonitorService.ts +19 -7
  51. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  52. package/Server/Services/OnCallDutyPolicyEscalationRuleService.ts +185 -41
  53. package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +23 -15
  54. package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +39 -0
  55. package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +148 -0
  56. package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +11 -2
  57. package/Server/Services/OnCallDutyPolicyScheduleService.ts +502 -26
  58. package/Server/Services/OnCallDutyPolicyTimeLogService.ts +49 -7
  59. package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +218 -4
  60. package/Server/Services/ProjectCallSMSConfigService.ts +82 -1
  61. package/Server/Services/ProjectService.ts +24 -0
  62. package/Server/Services/TeamMemberService.ts +6 -0
  63. package/Server/Services/UserNotificationRuleService.ts +13 -42
  64. package/Server/Services/UserOnCallLogService.ts +90 -0
  65. package/Server/Services/UserOnCallLogTimelineService.ts +43 -1
  66. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +74 -9
  67. package/Server/Types/Database/QueryUtil.ts +57 -0
  68. package/Server/Utils/AI/AIChatPrivacyFilter.ts +28 -0
  69. package/Server/Utils/AI/AlertAIContextBuilder.ts +6 -1
  70. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +1066 -0
  71. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +239 -0
  72. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +51 -0
  73. package/Server/Utils/AI/IncidentAIContextBuilder.ts +18 -3
  74. package/Server/Utils/AI/IncidentEpisodeAIContextBuilder.ts +6 -1
  75. package/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.ts +12 -2
  76. package/Server/Utils/AI/Toolbox/AlertTools.ts +201 -0
  77. package/Server/Utils/AI/Toolbox/AlertWriteTools.ts +174 -0
  78. package/Server/Utils/AI/Toolbox/ContextTools.ts +189 -0
  79. package/Server/Utils/AI/Toolbox/ExceptionTools.ts +149 -0
  80. package/Server/Utils/AI/Toolbox/IncidentTools.ts +386 -0
  81. package/Server/Utils/AI/Toolbox/IncidentWriteTools.ts +350 -0
  82. package/Server/Utils/AI/Toolbox/Index.ts +231 -0
  83. package/Server/Utils/AI/Toolbox/LogTools.ts +339 -0
  84. package/Server/Utils/AI/Toolbox/MetricTools.ts +179 -0
  85. package/Server/Utils/AI/Toolbox/MonitorTools.ts +193 -0
  86. package/Server/Utils/AI/Toolbox/RecentChangesTools.ts +208 -0
  87. package/Server/Utils/AI/Toolbox/Serializer.ts +299 -0
  88. package/Server/Utils/AI/Toolbox/ToolTypes.ts +249 -0
  89. package/Server/Utils/AI/Toolbox/TraceTools.ts +402 -0
  90. package/Server/Utils/AI/Toolbox/WidgetBuilder.ts +189 -0
  91. package/Server/Utils/Database/MigrationFailureLog.ts +240 -0
  92. package/Server/Utils/IncomingCallPhoneNumber.ts +41 -0
  93. package/Server/Utils/LLM/LLMService.ts +605 -96
  94. package/Server/Utils/Telemetry/IoTSnapshotScan.ts +12 -9
  95. package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +12 -9
  96. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +512 -19
  97. package/Server/Utils/Workspace/Slack/Slack.ts +80 -0
  98. package/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
  99. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +0 -8
  100. package/Tests/Server/Services/OnCallDutyPolicyScheduleLayerUserReorder.test.ts +233 -0
  101. package/Tests/Server/Services/OnCallDutyPolicyTimeLogServiceScoping.test.ts +242 -0
  102. package/Tests/Server/Services/OnCallDutyPolicyUserOverrideEdit.test.ts +198 -0
  103. package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +495 -0
  104. package/Tests/Server/Types/Database/QueryUtil.test.ts +74 -0
  105. package/Tests/Server/Utils/AI/AIChatModelACL.test.ts +42 -0
  106. package/Tests/Server/Utils/AI/ChatAgentHelpers.test.ts +89 -0
  107. package/Tests/Server/Utils/AI/LLMServiceBaseUrl.test.ts +139 -0
  108. package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +386 -0
  109. package/Tests/Server/Utils/AI/ToolArgsGetTimeRange.test.ts +62 -0
  110. package/Tests/Server/Utils/AI/ToolArgsScopeServiceIds.test.ts +79 -0
  111. package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +223 -0
  112. package/Tests/Types/Billing/SubscriptionStatus.test.ts +127 -0
  113. package/Tests/Types/DateExhaustiveTimezone.test.ts +700 -0
  114. package/Tests/Types/DateTimezoneWallClock.test.ts +99 -0
  115. package/Tests/Types/Events/Recurring.test.ts +22 -9
  116. package/Tests/Types/Metrics/RecordingRuleDefinition.test.ts +213 -0
  117. package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixes.test.ts +291 -0
  118. package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixesRound2.test.ts +398 -0
  119. package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeekendGap.test.ts +114 -0
  120. package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeeklyDayShift.test.ts +76 -0
  121. package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyMutation.test.ts +128 -0
  122. package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyProbe.test.ts +203 -0
  123. package/Tests/Types/OnCallDutyPolicy/LayerUtilDiffFuzz.test.ts +322 -0
  124. package/Tests/Types/OnCallDutyPolicy/LayerUtilEdgeCases.test.ts +280 -0
  125. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveCurrentUser.test.ts +863 -0
  126. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveDaily.test.ts +900 -0
  127. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveMultiLayer.test.ts +1104 -0
  128. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +957 -0
  129. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveWeekly.test.ts +1095 -0
  130. package/Tests/Types/OnCallDutyPolicy/LayerUtilInvariants.test.ts +232 -0
  131. package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +227 -0
  132. package/Tests/Types/OnCallDutyPolicy/LayerUtilMonthYearAudit.test.ts +255 -0
  133. package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerAudit.test.ts +388 -0
  134. package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerEdge.test.ts +300 -0
  135. package/Tests/Types/OnCallDutyPolicy/LayerUtilOvernightDSTAudit.test.ts +158 -0
  136. package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapFix.test.ts +253 -0
  137. package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapRepro.test.ts +157 -0
  138. package/Tests/Types/OnCallDutyPolicy/LayerUtilRotationFixes.test.ts +414 -0
  139. package/Tests/Types/OnCallDutyPolicy/LayerUtilTimezone.test.ts +243 -0
  140. package/Tests/Types/OnCallDutyPolicy/LayerUtilWeekendGapRepro.test.ts +112 -0
  141. package/Tests/Types/OnCallDutyPolicy/OverrideLensAudit.test.ts +117 -0
  142. package/Tests/Types/OnCallDutyPolicy/RestrictionTimes.test.ts +8 -6
  143. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesDefaultDayAudit.test.ts +112 -0
  144. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesExhaustive.test.ts +821 -0
  145. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesMutationInvariant.test.ts +886 -0
  146. package/Tests/Types/OnCallDutyPolicy/UserOverrideUtil.test.ts +255 -0
  147. package/Tests/Types/OnCallDutyPolicy/UserOverrideUtilExhaustive.test.ts +1126 -0
  148. package/Types/AI/AIChatMessageRole.ts +6 -0
  149. package/Types/AI/AIChatMessageStatus.ts +32 -0
  150. package/Types/AI/AIChatPermissionMode.ts +69 -0
  151. package/Types/AI/AIChatTypes.ts +231 -0
  152. package/Types/AI/AIRunEventType.ts +17 -0
  153. package/Types/AI/AIRunStatus.ts +28 -0
  154. package/Types/AI/AIRunType.ts +6 -0
  155. package/Types/Date.ts +200 -15
  156. package/Types/LLM/LlmType.ts +6 -0
  157. package/Types/OnCallDutyPolicy/Layer.ts +790 -260
  158. package/Types/OnCallDutyPolicy/RestrictionTimes.ts +39 -13
  159. package/Types/OnCallDutyPolicy/UserOverrideUtil.ts +21 -5
  160. package/UI/Components/Events/RecurringFieldElement.tsx +16 -0
  161. package/UI/Components/Header/HeaderIconDropdownButton.tsx +22 -12
  162. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +35 -3
  163. package/UI/Components/ModelFormModal/ModelFormModal.tsx +8 -0
  164. package/UI/Components/Page/Page.tsx +14 -0
  165. package/UI/Components/RadioButtons/BasicRadioButtons.tsx +1 -1
  166. package/UI/Utils/LlmTypeDropdownOptions.ts +40 -0
  167. package/UI/Utils/TestLLMProvider.ts +59 -0
  168. package/build/dist/Models/DatabaseModels/AIConversation.js +345 -0
  169. package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -0
  170. package/build/dist/Models/DatabaseModels/AIConversationMessage.js +521 -0
  171. package/build/dist/Models/DatabaseModels/AIConversationMessage.js.map +1 -0
  172. package/build/dist/Models/DatabaseModels/AIRun.js +619 -0
  173. package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -0
  174. package/build/dist/Models/DatabaseModels/AIRunEvent.js +469 -0
  175. package/build/dist/Models/DatabaseModels/AIRunEvent.js.map +1 -0
  176. package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js +1 -1
  177. package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js.map +1 -1
  178. package/build/dist/Models/DatabaseModels/IncidentMember.js +1 -1
  179. package/build/dist/Models/DatabaseModels/IncidentMember.js.map +1 -1
  180. package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js +10 -0
  181. package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js.map +1 -1
  182. package/build/dist/Models/DatabaseModels/Index.js +10 -0
  183. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  184. package/build/dist/Models/DatabaseModels/LlmLog.js +28 -0
  185. package/build/dist/Models/DatabaseModels/LlmLog.js.map +1 -1
  186. package/build/dist/Models/DatabaseModels/LlmProvider.js +2 -2
  187. package/build/dist/Models/DatabaseModels/LlmProvider.js.map +1 -1
  188. package/build/dist/Models/DatabaseModels/MigrationFailure.js +196 -0
  189. package/build/dist/Models/DatabaseModels/MigrationFailure.js.map +1 -0
  190. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +52 -2
  191. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  192. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +45 -0
  193. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  194. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +6 -0
  195. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
  196. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js +6 -0
  197. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js.map +1 -1
  198. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js +1 -1
  199. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js.map +1 -1
  200. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js +2 -2
  201. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js.map +1 -1
  202. package/build/dist/Server/API/AIChatAPI.js +498 -0
  203. package/build/dist/Server/API/AIChatAPI.js.map +1 -0
  204. package/build/dist/Server/API/AlertAPI.js +1 -1
  205. package/build/dist/Server/API/IncidentAPI.js +2 -2
  206. package/build/dist/Server/API/IncidentEpisodeAPI.js +1 -1
  207. package/build/dist/Server/API/LlmProviderAPI.js +108 -1
  208. package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
  209. package/build/dist/Server/API/MicrosoftTeamsAPI.js +4 -0
  210. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  211. package/build/dist/Server/API/ScheduledMaintenanceAPI.js +1 -1
  212. package/build/dist/Server/API/SlackAPI.js +442 -0
  213. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  214. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js +96 -0
  215. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js.map +1 -0
  216. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js +25 -0
  217. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js.map +1 -0
  218. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js +31 -0
  219. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js.map +1 -0
  220. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js +46 -0
  221. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js.map +1 -0
  222. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js +18 -0
  223. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js.map +1 -0
  224. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js +38 -0
  225. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js.map +1 -0
  226. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js +16 -0
  227. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js.map +1 -0
  228. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js +18 -0
  229. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js.map +1 -0
  230. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +16 -0
  231. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  232. package/build/dist/Server/Infrastructure/PostgresDatabase.js +9 -0
  233. package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
  234. package/build/dist/Server/Services/AIConversationMessageService.js +34 -0
  235. package/build/dist/Server/Services/AIConversationMessageService.js.map +1 -0
  236. package/build/dist/Server/Services/AIConversationService.js +42 -0
  237. package/build/dist/Server/Services/AIConversationService.js.map +1 -0
  238. package/build/dist/Server/Services/AIRunEventService.js +34 -0
  239. package/build/dist/Server/Services/AIRunEventService.js.map +1 -0
  240. package/build/dist/Server/Services/AIRunService.js +34 -0
  241. package/build/dist/Server/Services/AIRunService.js.map +1 -0
  242. package/build/dist/Server/Services/AIService.js +84 -25
  243. package/build/dist/Server/Services/AIService.js.map +1 -1
  244. package/build/dist/Server/Services/AnalyticsDatabaseService.js +10 -1
  245. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  246. package/build/dist/Server/Services/DatabaseService.js +20 -1
  247. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  248. package/build/dist/Server/Services/IncidentService.js +1 -1
  249. package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js +55 -2
  250. package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js.map +1 -1
  251. package/build/dist/Server/Services/IncomingCallPolicyService.js +40 -0
  252. package/build/dist/Server/Services/IncomingCallPolicyService.js.map +1 -1
  253. package/build/dist/Server/Services/Index.js +2 -0
  254. package/build/dist/Server/Services/Index.js.map +1 -1
  255. package/build/dist/Server/Services/LlmProviderService.js +108 -0
  256. package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
  257. package/build/dist/Server/Services/MigrationFailureService.js +9 -0
  258. package/build/dist/Server/Services/MigrationFailureService.js.map +1 -0
  259. package/build/dist/Server/Services/MonitorProbeService.js +33 -7
  260. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  261. package/build/dist/Server/Services/MonitorService.js +12 -5
  262. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  263. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +81 -22
  264. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  265. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js +134 -31
  266. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js.map +1 -1
  267. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +21 -13
  268. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
  269. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +36 -0
  270. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
  271. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +121 -0
  272. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
  273. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +11 -2
  274. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
  275. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +409 -36
  276. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  277. package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js +52 -8
  278. package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js.map +1 -1
  279. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +170 -3
  280. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
  281. package/build/dist/Server/Services/ProjectCallSMSConfigService.js +72 -0
  282. package/build/dist/Server/Services/ProjectCallSMSConfigService.js.map +1 -1
  283. package/build/dist/Server/Services/ProjectService.js +25 -0
  284. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  285. package/build/dist/Server/Services/TeamMemberService.js +6 -0
  286. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  287. package/build/dist/Server/Services/UserNotificationRuleService.js +12 -30
  288. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  289. package/build/dist/Server/Services/UserOnCallLogService.js +75 -0
  290. package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
  291. package/build/dist/Server/Services/UserOnCallLogTimelineService.js +31 -1
  292. package/build/dist/Server/Services/UserOnCallLogTimelineService.js.map +1 -1
  293. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +50 -7
  294. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  295. package/build/dist/Server/Types/Database/QueryUtil.js +45 -0
  296. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  297. package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js +18 -0
  298. package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js.map +1 -0
  299. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js +6 -1
  300. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js.map +1 -1
  301. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +756 -0
  302. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -0
  303. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +165 -0
  304. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -0
  305. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +44 -0
  306. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -0
  307. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js +18 -3
  308. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js.map +1 -1
  309. package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js +6 -1
  310. package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js.map +1 -1
  311. package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js +12 -2
  312. package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js.map +1 -1
  313. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +167 -0
  314. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -0
  315. package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js +136 -0
  316. package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js.map +1 -0
  317. package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js +141 -0
  318. package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js.map +1 -0
  319. package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js +117 -0
  320. package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js.map +1 -0
  321. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +318 -0
  322. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -0
  323. package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js +280 -0
  324. package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js.map +1 -0
  325. package/build/dist/Server/Utils/AI/Toolbox/Index.js +153 -0
  326. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -0
  327. package/build/dist/Server/Utils/AI/Toolbox/LogTools.js +246 -0
  328. package/build/dist/Server/Utils/AI/Toolbox/LogTools.js.map +1 -0
  329. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +120 -0
  330. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -0
  331. package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js +158 -0
  332. package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js.map +1 -0
  333. package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js +165 -0
  334. package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js.map +1 -0
  335. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +228 -0
  336. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -0
  337. package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js +142 -0
  338. package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js.map +1 -0
  339. package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js +309 -0
  340. package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js.map +1 -0
  341. package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js +120 -0
  342. package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js.map +1 -0
  343. package/build/dist/Server/Utils/Database/MigrationFailureLog.js +174 -0
  344. package/build/dist/Server/Utils/Database/MigrationFailureLog.js.map +1 -0
  345. package/build/dist/Server/Utils/IncomingCallPhoneNumber.js +30 -0
  346. package/build/dist/Server/Utils/IncomingCallPhoneNumber.js.map +1 -0
  347. package/build/dist/Server/Utils/LLM/LLMService.js +452 -92
  348. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  349. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +11 -8
  350. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
  351. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +11 -8
  352. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
  353. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +378 -15
  354. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  355. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +59 -0
  356. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  357. package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
  358. package/build/dist/Types/AI/AIChatMessageRole.js +7 -0
  359. package/build/dist/Types/AI/AIChatMessageRole.js.map +1 -0
  360. package/build/dist/Types/AI/AIChatMessageStatus.js +27 -0
  361. package/build/dist/Types/AI/AIChatMessageStatus.js.map +1 -0
  362. package/build/dist/Types/AI/AIChatPermissionMode.js +53 -0
  363. package/build/dist/Types/AI/AIChatPermissionMode.js.map +1 -0
  364. package/build/dist/Types/AI/AIChatTypes.js +74 -0
  365. package/build/dist/Types/AI/AIChatTypes.js.map +1 -0
  366. package/build/dist/Types/AI/AIRunEventType.js +18 -0
  367. package/build/dist/Types/AI/AIRunEventType.js.map +1 -0
  368. package/build/dist/Types/AI/AIRunStatus.js +26 -0
  369. package/build/dist/Types/AI/AIRunStatus.js.map +1 -0
  370. package/build/dist/Types/AI/AIRunType.js +7 -0
  371. package/build/dist/Types/AI/AIRunType.js.map +1 -0
  372. package/build/dist/Types/Date.js +153 -15
  373. package/build/dist/Types/Date.js.map +1 -1
  374. package/build/dist/Types/LLM/LlmType.js +6 -0
  375. package/build/dist/Types/LLM/LlmType.js.map +1 -1
  376. package/build/dist/Types/OnCallDutyPolicy/Layer.js +602 -171
  377. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  378. package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js +27 -13
  379. package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js.map +1 -1
  380. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js +20 -5
  381. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js.map +1 -1
  382. package/build/dist/UI/Components/Events/RecurringFieldElement.js +13 -0
  383. package/build/dist/UI/Components/Events/RecurringFieldElement.js.map +1 -1
  384. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +10 -5
  385. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  386. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +18 -4
  387. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  388. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  389. package/build/dist/UI/Components/Page/Page.js +3 -1
  390. package/build/dist/UI/Components/Page/Page.js.map +1 -1
  391. package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js +1 -1
  392. package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js.map +1 -1
  393. package/build/dist/UI/Utils/LlmTypeDropdownOptions.js +38 -0
  394. package/build/dist/UI/Utils/LlmTypeDropdownOptions.js.map +1 -0
  395. package/build/dist/UI/Utils/TestLLMProvider.js +37 -0
  396. package/build/dist/UI/Utils/TestLLMProvider.js.map +1 -0
  397. package/package.json +1 -1
@@ -0,0 +1,198 @@
1
+ import OnCallDutyPolicyUserOverrideService from "../../../Server/Services/OnCallDutyPolicyUserOverrideService";
2
+ import OnCallDutyPolicyScheduleService from "../../../Server/Services/OnCallDutyPolicyScheduleService";
3
+ import ObjectID from "../../../Types/ObjectID";
4
+ import logger from "../../../Server/Utils/Logger";
5
+ import { describe, expect, test, afterEach } from "@jest/globals";
6
+
7
+ /*
8
+ * Audit finding M2: an override edit that changes overrideUserId from A to A2
9
+ * used to refresh only A2's schedules. Schedules that contained A (but NOT A2)
10
+ * therefore kept showing the substitute forever — until the next natural
11
+ * handoff — because refreshRostersForUserInProject selects schedules by the
12
+ * NEW user only.
13
+ *
14
+ * The fix captures each affected override's PRE-update {projectId,
15
+ * overrideUserId} in onBeforeUpdate (carryForward), and onUpdateSuccess then
16
+ * refreshes rosters for BOTH the OLD users (from carryForward) and the NEW
17
+ * users (looked up post-update by id), deduped by "projectId:overrideUserId".
18
+ *
19
+ * The private refreshRostersForOverrideUser lazy-requires
20
+ * OnCallDutyPolicyScheduleService and calls its
21
+ * refreshRostersForUserInProject({projectId, userId}). Because the test imports
22
+ * the very same singleton the lazy require resolves to, spying that public
23
+ * method observes exactly which {projectId, userId} pairs are refreshed.
24
+ *
25
+ * The hooks are protected, so they are invoked via (service as any).
26
+ */
27
+
28
+ // Fixed ids so assertions read clearly. ObjectID accepts arbitrary strings.
29
+ const P1: ObjectID = new ObjectID("p1");
30
+ const USER_A: ObjectID = new ObjectID("A");
31
+ const USER_A2: ObjectID = new ObjectID("A2");
32
+ const OV1: ObjectID = new ObjectID("ov1");
33
+
34
+ // Build a minimal root UpdateBy the hooks accept.
35
+ function fakeUpdateBy(): any {
36
+ return {
37
+ query: { _id: OV1 },
38
+ data: {},
39
+ props: { isRoot: true },
40
+ };
41
+ }
42
+
43
+ // The set of userId strings refreshRostersForUserInProject was called with.
44
+ function refreshedUserIds(spy: jest.SpyInstance): Array<string> {
45
+ return spy.mock.calls.map((call: Array<unknown>): string => {
46
+ const arg: { projectId: ObjectID; userId: ObjectID } = call[0] as {
47
+ projectId: ObjectID;
48
+ userId: ObjectID;
49
+ };
50
+ return arg.userId.toString();
51
+ });
52
+ }
53
+
54
+ describe("OnCallDutyPolicyUserOverrideService update-roster refresh (audit M2)", () => {
55
+ afterEach(() => {
56
+ jest.restoreAllMocks();
57
+ });
58
+
59
+ // Keep best-effort logger.error noise out of the test output.
60
+ function silenceLoggerError(): void {
61
+ jest.spyOn(logger, "error").mockImplementation((): void => {
62
+ return undefined;
63
+ });
64
+ }
65
+
66
+ /*
67
+ * ----------------------------------------------------------------------- *
68
+ * (1) onBeforeUpdate captures the OLD override user into carryForward.
69
+ * ----------------------------------------------------------------------- *
70
+ */
71
+ test("onBeforeUpdate captures the pre-update {projectId, overrideUserId} into carryForward", async () => {
72
+ silenceLoggerError();
73
+
74
+ // findBy returns the row(s) that match the update query, PRE-update.
75
+ const findBy: jest.SpyInstance = jest
76
+ .spyOn(OnCallDutyPolicyUserOverrideService, "findBy")
77
+ .mockResolvedValue([{ projectId: P1, overrideUserId: USER_A }] as any);
78
+
79
+ const onUpdate: any = await (
80
+ OnCallDutyPolicyUserOverrideService as any
81
+ ).onBeforeUpdate(fakeUpdateBy());
82
+
83
+ expect(findBy).toHaveBeenCalledTimes(1);
84
+
85
+ const carryForward: Array<{
86
+ projectId: ObjectID;
87
+ overrideUserId: ObjectID;
88
+ }> = onUpdate.carryForward;
89
+
90
+ expect(Array.isArray(carryForward)).toBe(true);
91
+ expect(carryForward).toHaveLength(1);
92
+ expect(carryForward[0]!.projectId.toString()).toBe(P1.toString());
93
+ expect(carryForward[0]!.overrideUserId.toString()).toBe(USER_A.toString());
94
+
95
+ // The original updateBy is threaded through unchanged.
96
+ expect(onUpdate.updateBy.query["_id"].toString()).toBe(OV1.toString());
97
+ });
98
+
99
+ /*
100
+ * ----------------------------------------------------------------------- *
101
+ * (2) onUpdateSuccess refreshes BOTH the old (A) and the new (A2) users.
102
+ * ----------------------------------------------------------------------- *
103
+ */
104
+ test("onUpdateSuccess refreshes rosters for BOTH the old (A) and new (A2) override users", async () => {
105
+ silenceLoggerError();
106
+
107
+ // The post-update row for OV1 now points at the NEW user A2.
108
+ jest
109
+ .spyOn(OnCallDutyPolicyUserOverrideService, "findOneById")
110
+ .mockResolvedValue({ projectId: P1, overrideUserId: USER_A2 } as any);
111
+
112
+ const refresh: jest.SpyInstance = jest
113
+ .spyOn(OnCallDutyPolicyScheduleService, "refreshRostersForUserInProject")
114
+ .mockResolvedValue(undefined);
115
+
116
+ const onUpdate: any = {
117
+ updateBy: fakeUpdateBy(),
118
+ // OLD user captured by onBeforeUpdate.
119
+ carryForward: [{ projectId: P1, overrideUserId: USER_A }],
120
+ };
121
+
122
+ await (OnCallDutyPolicyUserOverrideService as any).onUpdateSuccess(
123
+ onUpdate,
124
+ [OV1],
125
+ );
126
+
127
+ const userIds: Array<string> = refreshedUserIds(refresh);
128
+
129
+ // Both the vacated user A and the substitute A2 have their rosters recomputed.
130
+ expect(userIds).toContain(USER_A.toString());
131
+ expect(userIds).toContain(USER_A2.toString());
132
+ expect(refresh).toHaveBeenCalledTimes(2);
133
+
134
+ // Every refresh is scoped to the correct project.
135
+ for (const call of refresh.mock.calls) {
136
+ const arg: { projectId: ObjectID; userId: ObjectID } = call[0] as {
137
+ projectId: ObjectID;
138
+ userId: ObjectID;
139
+ };
140
+ expect(arg.projectId.toString()).toBe(P1.toString());
141
+ }
142
+ });
143
+
144
+ /*
145
+ * ----------------------------------------------------------------------- *
146
+ * (3) DEDUP: an unchanged override user (A -> A) is refreshed exactly once.
147
+ * ----------------------------------------------------------------------- *
148
+ */
149
+ test("onUpdateSuccess dedupes: when old == new (A -> A) the roster refresh runs exactly ONCE", async () => {
150
+ silenceLoggerError();
151
+
152
+ // Post-update row still points at A (only e.g. the times changed).
153
+ jest
154
+ .spyOn(OnCallDutyPolicyUserOverrideService, "findOneById")
155
+ .mockResolvedValue({ projectId: P1, overrideUserId: USER_A } as any);
156
+
157
+ const refresh: jest.SpyInstance = jest
158
+ .spyOn(OnCallDutyPolicyScheduleService, "refreshRostersForUserInProject")
159
+ .mockResolvedValue(undefined);
160
+
161
+ const onUpdate: any = {
162
+ updateBy: fakeUpdateBy(),
163
+ carryForward: [{ projectId: P1, overrideUserId: USER_A }],
164
+ };
165
+
166
+ await (OnCallDutyPolicyUserOverrideService as any).onUpdateSuccess(
167
+ onUpdate,
168
+ [OV1],
169
+ );
170
+
171
+ // Same {p1, A} from both the old and new side: refreshed a single time.
172
+ expect(refresh).toHaveBeenCalledTimes(1);
173
+ expect(refreshedUserIds(refresh)).toEqual([USER_A.toString()]);
174
+ });
175
+
176
+ /*
177
+ * ----------------------------------------------------------------------- *
178
+ * (4) onBeforeUpdate is best-effort: a findBy failure yields empty
179
+ * carryForward and does NOT reject the update path.
180
+ * ----------------------------------------------------------------------- *
181
+ */
182
+ test("onBeforeUpdate is resilient: if findBy throws, it returns an empty carryForward and does not reject", async () => {
183
+ silenceLoggerError();
184
+
185
+ jest
186
+ .spyOn(OnCallDutyPolicyUserOverrideService, "findBy")
187
+ .mockRejectedValue(new Error("db unavailable"));
188
+
189
+ const onUpdate: any = await (
190
+ OnCallDutyPolicyUserOverrideService as any
191
+ ).onBeforeUpdate(fakeUpdateBy());
192
+
193
+ expect(Array.isArray(onUpdate.carryForward)).toBe(true);
194
+ expect(onUpdate.carryForward).toHaveLength(0);
195
+ // The updateBy is still threaded through so the CRUD update proceeds.
196
+ expect(onUpdate.updateBy).toBeTruthy();
197
+ });
198
+ });
@@ -0,0 +1,495 @@
1
+ import UserOnCallLogService from "../../../Server/Services/UserOnCallLogService";
2
+ import Model from "../../../Models/DatabaseModels/UserOnCallLog";
3
+ import ObjectID from "../../../Types/ObjectID";
4
+ import logger from "../../../Server/Utils/Logger";
5
+ import { JSONObject } from "../../../Types/JSON";
6
+ import { describe, expect, test, afterEach, beforeEach } from "@jest/globals";
7
+
8
+ /*
9
+ * UserOnCallLogService.claimNotificationRuleExecution is the atomic guard added
10
+ * for audit finding F7. Overlapping ExecutePendingExecutions runs (a slow tick,
11
+ * a stalled-job re-delivery, or a burst release across worker replicas) could
12
+ * both observe executedNotificationRules WITHOUT a given delayed rule's key,
13
+ * both write it, and both page the responder for a single escalation.
14
+ *
15
+ * The fix collapses the read-modify-write into ONE conditional UPDATE that only
16
+ * sets executedNotificationRules[ruleId] when the key is absent and RETURNs the
17
+ * affected row, so exactly one concurrent run wins (RETURNING a row => claimed)
18
+ * and every loser skips (no row => already executed).
19
+ *
20
+ * These tests mock the TypeORM manager (no Postgres) and the two inherited
21
+ * persistence helpers (findOneById / updateOneById) to lock in:
22
+ * (a) atomic success (a row returned) => TRUE, and NO read-modify-write;
23
+ * (b) atomic no-op (no row returned) => FALSE, and NO read-modify-write;
24
+ * (c) the emitted SQL is a single UPDATE carrying the `NOT (... jsonb ? key)`
25
+ * existence guard and a RETURNING clause (so the claim is atomic);
26
+ * (d) FAIL-SAFE: if the atomic statement throws, it degrades to the previous
27
+ * non-atomic findOneById + updateOneById marking so a page is never
28
+ * dropped (a rare duplicate page beats a missed page for on-call).
29
+ */
30
+
31
+ type QueryCall = [string, Array<unknown>];
32
+
33
+ // Stand-in for a persisted UserOnCallLog row returned by the fallback read.
34
+ function fakeLog(
35
+ id: ObjectID,
36
+ executedNotificationRules: JSONObject | undefined,
37
+ ): Model {
38
+ return {
39
+ id,
40
+ executedNotificationRules,
41
+ } as unknown as Model;
42
+ }
43
+
44
+ // Mock this.getRepository().manager.query with the supplied jest mock.
45
+ function mockManagerQuery(query: jest.Mock): void {
46
+ jest.spyOn(UserOnCallLogService, "getRepository").mockReturnValue({
47
+ manager: {
48
+ query,
49
+ },
50
+ } as any);
51
+ }
52
+
53
+ describe("UserOnCallLogService.claimNotificationRuleExecution", () => {
54
+ beforeEach(() => {
55
+ // Keep the fallback's logger.error noise out of the test output.
56
+ jest.spyOn(logger, "error").mockImplementation((): void => {
57
+ return undefined;
58
+ });
59
+ });
60
+
61
+ afterEach(() => {
62
+ jest.restoreAllMocks();
63
+ });
64
+
65
+ /*
66
+ * ----------------------------------------------------------------------- *
67
+ * (a) Atomic success: a row was returned => THIS call claimed the rule.
68
+ * -----------------------------------------------------------------------
69
+ */
70
+
71
+ test("returns TRUE when the atomic UPDATE returns a row (claim won)", async () => {
72
+ const query: jest.Mock = jest.fn().mockResolvedValue([{ _id: "x" }]);
73
+ mockManagerQuery(query);
74
+
75
+ const findOneById: jest.SpyInstance = jest.spyOn(
76
+ UserOnCallLogService,
77
+ "findOneById",
78
+ );
79
+ const updateOneById: jest.SpyInstance = jest.spyOn(
80
+ UserOnCallLogService,
81
+ "updateOneById",
82
+ );
83
+
84
+ const claimed: boolean =
85
+ await UserOnCallLogService.claimNotificationRuleExecution({
86
+ userOnCallLogId: ObjectID.generate(),
87
+ userNotificationRuleId: ObjectID.generate(),
88
+ });
89
+
90
+ expect(claimed).toBe(true);
91
+ // Exactly one statement, and it truly was atomic: no read-modify-write.
92
+ expect(query).toHaveBeenCalledTimes(1);
93
+ expect(findOneById).not.toHaveBeenCalled();
94
+ expect(updateOneById).not.toHaveBeenCalled();
95
+ });
96
+
97
+ test("returns TRUE even when the driver returns multiple RETURNING rows", async () => {
98
+ const query: jest.Mock = jest
99
+ .fn()
100
+ .mockResolvedValue([{ _id: "a" }, { _id: "b" }]);
101
+ mockManagerQuery(query);
102
+
103
+ const claimed: boolean =
104
+ await UserOnCallLogService.claimNotificationRuleExecution({
105
+ userOnCallLogId: ObjectID.generate(),
106
+ userNotificationRuleId: ObjectID.generate(),
107
+ });
108
+
109
+ expect(claimed).toBe(true);
110
+ });
111
+
112
+ /*
113
+ * ----------------------------------------------------------------------- *
114
+ * (b) Atomic no-op: guard suppressed the write => already executed.
115
+ * -----------------------------------------------------------------------
116
+ */
117
+
118
+ test("returns FALSE when the atomic UPDATE returns no rows (already executed)", async () => {
119
+ const query: jest.Mock = jest.fn().mockResolvedValue([]);
120
+ mockManagerQuery(query);
121
+
122
+ const findOneById: jest.SpyInstance = jest.spyOn(
123
+ UserOnCallLogService,
124
+ "findOneById",
125
+ );
126
+ const updateOneById: jest.SpyInstance = jest.spyOn(
127
+ UserOnCallLogService,
128
+ "updateOneById",
129
+ );
130
+
131
+ const claimed: boolean =
132
+ await UserOnCallLogService.claimNotificationRuleExecution({
133
+ userOnCallLogId: ObjectID.generate(),
134
+ userNotificationRuleId: ObjectID.generate(),
135
+ });
136
+
137
+ expect(claimed).toBe(false);
138
+ expect(query).toHaveBeenCalledTimes(1);
139
+ // A suppressed write is a decisive "someone else won" — never fall back.
140
+ expect(findOneById).not.toHaveBeenCalled();
141
+ expect(updateOneById).not.toHaveBeenCalled();
142
+ });
143
+
144
+ test.each<[string, unknown]>([
145
+ ["undefined", undefined],
146
+ ["null", null],
147
+ ["a non-array object", {}],
148
+ ])(
149
+ "returns FALSE (no claim) when the driver resolves %s instead of a rows array",
150
+ async (_label: string, resolved: unknown) => {
151
+ const query: jest.Mock = jest.fn().mockResolvedValue(resolved);
152
+ mockManagerQuery(query);
153
+
154
+ const updateOneById: jest.SpyInstance = jest.spyOn(
155
+ UserOnCallLogService,
156
+ "updateOneById",
157
+ );
158
+
159
+ /*
160
+ * NOTE: current behavior — the Array.isArray guard treats any
161
+ * non-array (or empty) result as "not claimed"; it resolves (does not
162
+ * reject), so the fail-safe fallback is NOT engaged here.
163
+ */
164
+ const claimed: boolean =
165
+ await UserOnCallLogService.claimNotificationRuleExecution({
166
+ userOnCallLogId: ObjectID.generate(),
167
+ userNotificationRuleId: ObjectID.generate(),
168
+ });
169
+
170
+ expect(claimed).toBe(false);
171
+ expect(updateOneById).not.toHaveBeenCalled();
172
+ },
173
+ );
174
+
175
+ /*
176
+ * ----------------------------------------------------------------------- *
177
+ * (c) The emitted SQL is a single, atomic, guarded UPDATE.
178
+ * -----------------------------------------------------------------------
179
+ */
180
+
181
+ test("emits a single conditional UPDATE with jsonb existence guard and RETURNING", async () => {
182
+ const query: jest.Mock = jest.fn().mockResolvedValue([{ _id: "x" }]);
183
+ mockManagerQuery(query);
184
+
185
+ const logId: ObjectID = ObjectID.generate();
186
+ const ruleId: ObjectID = ObjectID.generate();
187
+
188
+ await UserOnCallLogService.claimNotificationRuleExecution({
189
+ userOnCallLogId: logId,
190
+ userNotificationRuleId: ruleId,
191
+ });
192
+
193
+ expect(query).toHaveBeenCalledTimes(1);
194
+ const [sql, params] = query.mock.calls[0] as QueryCall;
195
+
196
+ // Exactly one statement, and it is an UPDATE against the right table.
197
+ expect(sql.trim().startsWith(`UPDATE "UserOnCallLog"`)).toBe(true);
198
+ expect(sql.match(/UPDATE/g)).toHaveLength(1);
199
+ // No separate read — the claim must not be a SELECT-then-UPDATE.
200
+ expect(sql).not.toContain("SELECT");
201
+ expect(sql).not.toContain("INSERT");
202
+ expect(sql).not.toContain("DELETE");
203
+
204
+ // The atomicity-critical guard: only write when the key is ABSENT.
205
+ expect(sql).toContain("NOT (");
206
+ // jsonb `?` existence operator against the rule key parameter.
207
+ expect(sql).toMatch(/jsonb\s*\?\s*\$2::text/);
208
+ // jsonb_set writes the key idempotently (create-if-missing).
209
+ expect(sql).toContain("jsonb_set");
210
+ // RETURNING lets the caller detect whether THIS statement claimed the row.
211
+ expect(sql).toContain("RETURNING");
212
+ expect(sql).toContain(`"_id"`);
213
+
214
+ // Bound parameters: [logId, ruleKey, nowIso]; no raw interpolation.
215
+ expect(params).toHaveLength(3);
216
+ expect(params[0]).toBe(logId.toString());
217
+ expect(params[1]).toBe(ruleId.toString());
218
+ expect(typeof params[2]).toBe("string");
219
+ // The third parameter is an ISO timestamp string.
220
+ expect(Number.isNaN(Date.parse(params[2] as string))).toBe(false);
221
+ expect(params[2]).toBe(new Date(params[2] as string).toISOString());
222
+ });
223
+
224
+ test("the guard parameter ($2) is reused for both jsonb_set path and existence check", async () => {
225
+ const query: jest.Mock = jest.fn().mockResolvedValue([{ _id: "x" }]);
226
+ mockManagerQuery(query);
227
+
228
+ await UserOnCallLogService.claimNotificationRuleExecution({
229
+ userOnCallLogId: ObjectID.generate(),
230
+ userNotificationRuleId: ObjectID.generate(),
231
+ });
232
+
233
+ const [sql] = query.mock.calls[0] as QueryCall;
234
+ /*
235
+ * The same $2 key drives the write path (ARRAY[$2::text]) and the guard,
236
+ * so what we test for absence is exactly what we write.
237
+ */
238
+ expect(sql).toContain("ARRAY[$2::text]");
239
+ expect(sql).toContain("$2::text)");
240
+ // The log id is matched by uuid-cast primary key.
241
+ expect(sql).toContain(`"_id" = $1::uuid`);
242
+ });
243
+
244
+ /*
245
+ * ----------------------------------------------------------------------- *
246
+ * (d) FAIL-SAFE fallback when the atomic statement throws.
247
+ * -----------------------------------------------------------------------
248
+ */
249
+
250
+ test("falls back to findOneById + updateOneById and returns TRUE when rule not yet marked (empty object)", async () => {
251
+ const query: jest.Mock = jest
252
+ .fn()
253
+ .mockRejectedValue(new Error("jsonb operator unavailable"));
254
+ mockManagerQuery(query);
255
+
256
+ const logId: ObjectID = ObjectID.generate();
257
+ const ruleId: ObjectID = ObjectID.generate();
258
+
259
+ const findOneById: jest.SpyInstance = jest
260
+ .spyOn(UserOnCallLogService, "findOneById")
261
+ .mockResolvedValue(fakeLog(logId, {}));
262
+ const updateOneById: jest.SpyInstance = jest
263
+ .spyOn(UserOnCallLogService, "updateOneById")
264
+ .mockImplementation((): Promise<void> => {
265
+ return Promise.resolve();
266
+ });
267
+
268
+ const claimed: boolean =
269
+ await UserOnCallLogService.claimNotificationRuleExecution({
270
+ userOnCallLogId: logId,
271
+ userNotificationRuleId: ruleId,
272
+ });
273
+
274
+ expect(claimed).toBe(true);
275
+ expect(query).toHaveBeenCalledTimes(1);
276
+ expect(findOneById).toHaveBeenCalledTimes(1);
277
+
278
+ // Fallback reads the right row and selects the field it needs to inspect.
279
+ const findArg: {
280
+ id: ObjectID;
281
+ select: JSONObject;
282
+ props: { isRoot: boolean };
283
+ } = findOneById.mock.calls[0][0] as {
284
+ id: ObjectID;
285
+ select: JSONObject;
286
+ props: { isRoot: boolean };
287
+ };
288
+ expect(findArg.id.toString()).toBe(logId.toString());
289
+ expect(findArg.select["executedNotificationRules"]).toBe(true);
290
+ expect(findArg.props.isRoot).toBe(true);
291
+
292
+ // And it marks exactly this rule as executed.
293
+ expect(updateOneById).toHaveBeenCalledTimes(1);
294
+ const updateArg: {
295
+ id: ObjectID;
296
+ data: { executedNotificationRules: JSONObject };
297
+ props: { isRoot: boolean };
298
+ } = updateOneById.mock.calls[0][0] as {
299
+ id: ObjectID;
300
+ data: { executedNotificationRules: JSONObject };
301
+ props: { isRoot: boolean };
302
+ };
303
+ expect(updateArg.id.toString()).toBe(logId.toString());
304
+ expect(Object.keys(updateArg.data.executedNotificationRules)).toContain(
305
+ ruleId.toString(),
306
+ );
307
+ expect(updateArg.props.isRoot).toBe(true);
308
+ });
309
+
310
+ test("fallback also claims (TRUE) when executedNotificationRules is undefined", async () => {
311
+ const query: jest.Mock = jest.fn().mockRejectedValue(new Error("boom"));
312
+ mockManagerQuery(query);
313
+
314
+ const logId: ObjectID = ObjectID.generate();
315
+ const ruleId: ObjectID = ObjectID.generate();
316
+
317
+ jest
318
+ .spyOn(UserOnCallLogService, "findOneById")
319
+ .mockResolvedValue(fakeLog(logId, undefined));
320
+ const updateOneById: jest.SpyInstance = jest
321
+ .spyOn(UserOnCallLogService, "updateOneById")
322
+ .mockImplementation((): Promise<void> => {
323
+ return Promise.resolve();
324
+ });
325
+
326
+ const claimed: boolean =
327
+ await UserOnCallLogService.claimNotificationRuleExecution({
328
+ userOnCallLogId: logId,
329
+ userNotificationRuleId: ruleId,
330
+ });
331
+
332
+ expect(claimed).toBe(true);
333
+ expect(updateOneById).toHaveBeenCalledTimes(1);
334
+ const updateArg: { data: { executedNotificationRules: JSONObject } } =
335
+ updateOneById.mock.calls[0][0] as {
336
+ data: { executedNotificationRules: JSONObject };
337
+ };
338
+ expect(Object.keys(updateArg.data.executedNotificationRules)).toEqual([
339
+ ruleId.toString(),
340
+ ]);
341
+ });
342
+
343
+ test("fallback preserves other already-executed rule keys while adding the new one", async () => {
344
+ const query: jest.Mock = jest.fn().mockRejectedValue(new Error("boom"));
345
+ mockManagerQuery(query);
346
+
347
+ const logId: ObjectID = ObjectID.generate();
348
+ const ruleId: ObjectID = ObjectID.generate();
349
+ const otherRuleKey: string = ObjectID.generate().toString();
350
+
351
+ jest.spyOn(UserOnCallLogService, "findOneById").mockResolvedValue(
352
+ fakeLog(logId, {
353
+ [otherRuleKey]: new Date().toISOString(),
354
+ }),
355
+ );
356
+ const updateOneById: jest.SpyInstance = jest
357
+ .spyOn(UserOnCallLogService, "updateOneById")
358
+ .mockImplementation((): Promise<void> => {
359
+ return Promise.resolve();
360
+ });
361
+
362
+ const claimed: boolean =
363
+ await UserOnCallLogService.claimNotificationRuleExecution({
364
+ userOnCallLogId: logId,
365
+ userNotificationRuleId: ruleId,
366
+ });
367
+
368
+ expect(claimed).toBe(true);
369
+ const updateArg: { data: { executedNotificationRules: JSONObject } } =
370
+ updateOneById.mock.calls[0][0] as {
371
+ data: { executedNotificationRules: JSONObject };
372
+ };
373
+ const keys: Array<string> = Object.keys(
374
+ updateArg.data.executedNotificationRules,
375
+ );
376
+ expect(keys).toContain(otherRuleKey);
377
+ expect(keys).toContain(ruleId.toString());
378
+ expect(keys).toHaveLength(2);
379
+ });
380
+
381
+ test("fallback returns FALSE without updating when the rule is ALREADY in executedNotificationRules", async () => {
382
+ const query: jest.Mock = jest.fn().mockRejectedValue(new Error("boom"));
383
+ mockManagerQuery(query);
384
+
385
+ const logId: ObjectID = ObjectID.generate();
386
+ const ruleId: ObjectID = ObjectID.generate();
387
+
388
+ const findOneById: jest.SpyInstance = jest
389
+ .spyOn(UserOnCallLogService, "findOneById")
390
+ .mockResolvedValue(
391
+ fakeLog(logId, {
392
+ [ruleId.toString()]: new Date().toISOString(),
393
+ }),
394
+ );
395
+ const updateOneById: jest.SpyInstance = jest
396
+ .spyOn(UserOnCallLogService, "updateOneById")
397
+ .mockImplementation((): Promise<void> => {
398
+ return Promise.resolve();
399
+ });
400
+
401
+ const claimed: boolean =
402
+ await UserOnCallLogService.claimNotificationRuleExecution({
403
+ userOnCallLogId: logId,
404
+ userNotificationRuleId: ruleId,
405
+ });
406
+
407
+ expect(claimed).toBe(false);
408
+ expect(findOneById).toHaveBeenCalledTimes(1);
409
+ // Already marked => no duplicate write, no duplicate page.
410
+ expect(updateOneById).not.toHaveBeenCalled();
411
+ });
412
+
413
+ test("fallback returns FALSE without updating when the log row is missing (findOneById => null)", async () => {
414
+ const query: jest.Mock = jest.fn().mockRejectedValue(new Error("boom"));
415
+ mockManagerQuery(query);
416
+
417
+ const findOneById: jest.SpyInstance = jest
418
+ .spyOn(UserOnCallLogService, "findOneById")
419
+ .mockResolvedValue(null);
420
+ const updateOneById: jest.SpyInstance = jest
421
+ .spyOn(UserOnCallLogService, "updateOneById")
422
+ .mockImplementation((): Promise<void> => {
423
+ return Promise.resolve();
424
+ });
425
+
426
+ const claimed: boolean =
427
+ await UserOnCallLogService.claimNotificationRuleExecution({
428
+ userOnCallLogId: ObjectID.generate(),
429
+ userNotificationRuleId: ObjectID.generate(),
430
+ });
431
+
432
+ expect(claimed).toBe(false);
433
+ expect(findOneById).toHaveBeenCalledTimes(1);
434
+ expect(updateOneById).not.toHaveBeenCalled();
435
+ });
436
+
437
+ test("fallback engages exactly once (single atomic attempt) and logs the failure", async () => {
438
+ const query: jest.Mock = jest.fn().mockRejectedValue(new Error("boom"));
439
+ mockManagerQuery(query);
440
+
441
+ const loggerError: jest.SpyInstance = jest.spyOn(logger, "error");
442
+
443
+ jest
444
+ .spyOn(UserOnCallLogService, "findOneById")
445
+ .mockResolvedValue(fakeLog(ObjectID.generate(), {}));
446
+ jest
447
+ .spyOn(UserOnCallLogService, "updateOneById")
448
+ .mockImplementation((): Promise<void> => {
449
+ return Promise.resolve();
450
+ });
451
+
452
+ await UserOnCallLogService.claimNotificationRuleExecution({
453
+ userOnCallLogId: ObjectID.generate(),
454
+ userNotificationRuleId: ObjectID.generate(),
455
+ });
456
+
457
+ // The atomic path is attempted once, then the fallback takes over.
458
+ expect(query).toHaveBeenCalledTimes(1);
459
+ expect(loggerError).toHaveBeenCalled();
460
+ });
461
+
462
+ test("distinct rule ids on the same log are claimed independently in the atomic path", async () => {
463
+ /*
464
+ * Model two concurrent-but-different escalation rules: both should claim
465
+ * because the guard is per-key, not per-log.
466
+ */
467
+ const logId: ObjectID = ObjectID.generate();
468
+ const ruleA: ObjectID = ObjectID.generate();
469
+ const ruleB: ObjectID = ObjectID.generate();
470
+
471
+ const query: jest.Mock = jest.fn().mockResolvedValue([{ _id: "x" }]);
472
+ mockManagerQuery(query);
473
+
474
+ const claimedA: boolean =
475
+ await UserOnCallLogService.claimNotificationRuleExecution({
476
+ userOnCallLogId: logId,
477
+ userNotificationRuleId: ruleA,
478
+ });
479
+ const claimedB: boolean =
480
+ await UserOnCallLogService.claimNotificationRuleExecution({
481
+ userOnCallLogId: logId,
482
+ userNotificationRuleId: ruleB,
483
+ });
484
+
485
+ expect(claimedA).toBe(true);
486
+ expect(claimedB).toBe(true);
487
+ expect(query).toHaveBeenCalledTimes(2);
488
+ // Each call bound its own rule key.
489
+ expect((query.mock.calls[0] as QueryCall)[1][1]).toBe(ruleA.toString());
490
+ expect((query.mock.calls[1] as QueryCall)[1][1]).toBe(ruleB.toString());
491
+ // Same log id in both.
492
+ expect((query.mock.calls[0] as QueryCall)[1][0]).toBe(logId.toString());
493
+ expect((query.mock.calls[1] as QueryCall)[1][0]).toBe(logId.toString());
494
+ });
495
+ });