@oneuptime/common 11.3.26 → 11.4.0

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 (298) hide show
  1. package/Models/DatabaseModels/AIRun.ts +50 -0
  2. package/Models/DatabaseModels/IncidentEpisodeRoleMember.ts +1 -1
  3. package/Models/DatabaseModels/IncidentMember.ts +1 -1
  4. package/Models/DatabaseModels/IncomingCallPolicy.ts +13 -0
  5. package/Models/DatabaseModels/Index.ts +3 -0
  6. package/Models/DatabaseModels/MigrationFailure.ts +170 -0
  7. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +51 -2
  8. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +44 -0
  9. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +6 -0
  10. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.ts +6 -0
  11. package/Models/DatabaseModels/OnCallDutyPolicyTimeLog.ts +1 -1
  12. package/Models/DatabaseModels/OnCallDutyPolicyUserOverride.ts +2 -2
  13. package/Models/DatabaseModels/Project.ts +60 -0
  14. package/Server/API/AIInvestigationAPI.ts +148 -0
  15. package/Server/API/AlertAPI.ts +1 -1
  16. package/Server/API/IncidentAPI.ts +2 -2
  17. package/Server/API/IncidentEpisodeAPI.ts +1 -1
  18. package/Server/API/LlmProviderAPI.ts +1 -0
  19. package/Server/API/ScheduledMaintenanceAPI.ts +1 -1
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.ts +25 -0
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.ts +81 -0
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.ts +25 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.ts +35 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.ts +19 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.ts +33 -0
  26. package/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.ts +19 -0
  27. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -0
  28. package/Server/Infrastructure/PostgresDatabase.ts +11 -0
  29. package/Server/Services/AIService.ts +74 -2
  30. package/Server/Services/AlertService.ts +25 -0
  31. package/Server/Services/AnalyticsDatabaseService.ts +10 -1
  32. package/Server/Services/DatabaseService.ts +10 -1
  33. package/Server/Services/IncidentService.ts +26 -1
  34. package/Server/Services/IncidentStateTimelineService.ts +19 -0
  35. package/Server/Services/IncomingCallPolicyEscalationRuleService.ts +75 -2
  36. package/Server/Services/IncomingCallPolicyService.ts +42 -1
  37. package/Server/Services/Index.ts +2 -0
  38. package/Server/Services/MigrationFailureService.ts +9 -0
  39. package/Server/Services/MonitorProbeService.ts +33 -7
  40. package/Server/Services/MonitorService.ts +19 -7
  41. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  42. package/Server/Services/OnCallDutyPolicyEscalationRuleService.ts +185 -41
  43. package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +23 -15
  44. package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +39 -0
  45. package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +148 -0
  46. package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +11 -2
  47. package/Server/Services/OnCallDutyPolicyScheduleService.ts +502 -26
  48. package/Server/Services/OnCallDutyPolicyTimeLogService.ts +49 -7
  49. package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +218 -4
  50. package/Server/Services/ProjectCallSMSConfigService.ts +82 -1
  51. package/Server/Services/TeamMemberService.ts +6 -0
  52. package/Server/Services/UserNotificationRuleService.ts +13 -42
  53. package/Server/Services/UserOnCallLogService.ts +90 -0
  54. package/Server/Services/UserOnCallLogTimelineService.ts +43 -1
  55. package/Server/Types/Database/QueryUtil.ts +57 -0
  56. package/Server/Utils/AI/AlertAIContextBuilder.ts +6 -1
  57. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +14 -2
  58. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +119 -11
  59. package/Server/Utils/AI/IncidentAIContextBuilder.ts +18 -3
  60. package/Server/Utils/AI/IncidentEpisodeAIContextBuilder.ts +6 -1
  61. package/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.ts +12 -2
  62. package/Server/Utils/AI/Sentinel/AlertInvestigationRunner.ts +141 -0
  63. package/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.ts +178 -0
  64. package/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.ts +105 -0
  65. package/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.ts +405 -0
  66. package/Server/Utils/AI/Sentinel/SentinelMemory.ts +167 -0
  67. package/Server/Utils/AI/Toolbox/IncidentTools.ts +183 -1
  68. package/Server/Utils/AI/Toolbox/Index.ts +15 -1
  69. package/Server/Utils/AI/Toolbox/RecentChangesTools.ts +208 -0
  70. package/Server/Utils/AI/Toolbox/SentinelActionTools.ts +577 -0
  71. package/Server/Utils/AI/Toolbox/Serializer.ts +42 -0
  72. package/Server/Utils/Database/MigrationFailureLog.ts +240 -0
  73. package/Server/Utils/IncomingCallPhoneNumber.ts +41 -0
  74. package/Server/Utils/LLM/LLMService.ts +110 -11
  75. package/Server/Utils/Telemetry/IoTSnapshotScan.ts +12 -9
  76. package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +12 -9
  77. package/Tests/Server/API/BaseAPI.test.ts +1 -0
  78. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +0 -8
  79. package/Tests/Server/Services/OnCallDutyPolicyScheduleLayerUserReorder.test.ts +233 -0
  80. package/Tests/Server/Services/OnCallDutyPolicyTimeLogServiceScoping.test.ts +242 -0
  81. package/Tests/Server/Services/OnCallDutyPolicyUserOverrideEdit.test.ts +198 -0
  82. package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +495 -0
  83. package/Tests/Server/Types/Database/QueryUtil.test.ts +74 -0
  84. package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +78 -0
  85. package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +68 -0
  86. package/Tests/Types/DateExhaustiveTimezone.test.ts +700 -0
  87. package/Tests/Types/DateTimezoneWallClock.test.ts +99 -0
  88. package/Tests/Types/Events/Recurring.test.ts +22 -9
  89. package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixes.test.ts +291 -0
  90. package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixesRound2.test.ts +398 -0
  91. package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeekendGap.test.ts +114 -0
  92. package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeeklyDayShift.test.ts +76 -0
  93. package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyMutation.test.ts +128 -0
  94. package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyProbe.test.ts +203 -0
  95. package/Tests/Types/OnCallDutyPolicy/LayerUtilDiffFuzz.test.ts +322 -0
  96. package/Tests/Types/OnCallDutyPolicy/LayerUtilEdgeCases.test.ts +280 -0
  97. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveCurrentUser.test.ts +863 -0
  98. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveDaily.test.ts +900 -0
  99. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveMultiLayer.test.ts +1104 -0
  100. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +957 -0
  101. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveWeekly.test.ts +1095 -0
  102. package/Tests/Types/OnCallDutyPolicy/LayerUtilInvariants.test.ts +232 -0
  103. package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +227 -0
  104. package/Tests/Types/OnCallDutyPolicy/LayerUtilMonthYearAudit.test.ts +255 -0
  105. package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerAudit.test.ts +388 -0
  106. package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerEdge.test.ts +300 -0
  107. package/Tests/Types/OnCallDutyPolicy/LayerUtilOvernightDSTAudit.test.ts +158 -0
  108. package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapFix.test.ts +253 -0
  109. package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapRepro.test.ts +157 -0
  110. package/Tests/Types/OnCallDutyPolicy/LayerUtilRotationFixes.test.ts +414 -0
  111. package/Tests/Types/OnCallDutyPolicy/LayerUtilTimezone.test.ts +243 -0
  112. package/Tests/Types/OnCallDutyPolicy/LayerUtilWeekendGapRepro.test.ts +112 -0
  113. package/Tests/Types/OnCallDutyPolicy/OverrideLensAudit.test.ts +117 -0
  114. package/Tests/Types/OnCallDutyPolicy/RestrictionTimes.test.ts +8 -6
  115. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesDefaultDayAudit.test.ts +112 -0
  116. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesExhaustive.test.ts +821 -0
  117. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesMutationInvariant.test.ts +886 -0
  118. package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtil.test.ts +260 -0
  119. package/Tests/Types/OnCallDutyPolicy/UserOverrideUtil.test.ts +255 -0
  120. package/Tests/Types/OnCallDutyPolicy/UserOverrideUtilExhaustive.test.ts +1126 -0
  121. package/Types/Date.ts +246 -15
  122. package/Types/OnCallDutyPolicy/Layer.ts +790 -260
  123. package/Types/OnCallDutyPolicy/RestrictionTimes.ts +39 -13
  124. package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +257 -0
  125. package/Types/OnCallDutyPolicy/UserOverrideUtil.ts +21 -5
  126. package/UI/Components/Calendar/Calendar.tsx +8 -2
  127. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +5 -5
  128. package/UI/Components/Events/RecurringFieldElement.tsx +16 -0
  129. package/UI/Components/ModelFormModal/ModelFormModal.tsx +8 -0
  130. package/UI/Components/RadioButtons/BasicRadioButtons.tsx +1 -1
  131. package/build/dist/Models/DatabaseModels/AIRun.js +52 -0
  132. package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
  133. package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js +1 -1
  134. package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js.map +1 -1
  135. package/build/dist/Models/DatabaseModels/IncidentMember.js +1 -1
  136. package/build/dist/Models/DatabaseModels/IncidentMember.js.map +1 -1
  137. package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js +10 -0
  138. package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js.map +1 -1
  139. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  140. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  141. package/build/dist/Models/DatabaseModels/MigrationFailure.js +196 -0
  142. package/build/dist/Models/DatabaseModels/MigrationFailure.js.map +1 -0
  143. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +52 -2
  144. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  145. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +45 -0
  146. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  147. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +6 -0
  148. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
  149. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js +6 -0
  150. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js.map +1 -1
  151. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js +1 -1
  152. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js.map +1 -1
  153. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js +2 -2
  154. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js.map +1 -1
  155. package/build/dist/Models/DatabaseModels/Project.js +62 -0
  156. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  157. package/build/dist/Server/API/AIInvestigationAPI.js +107 -0
  158. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -0
  159. package/build/dist/Server/API/AlertAPI.js +1 -1
  160. package/build/dist/Server/API/IncidentAPI.js +2 -2
  161. package/build/dist/Server/API/IncidentEpisodeAPI.js +1 -1
  162. package/build/dist/Server/API/LlmProviderAPI.js +1 -0
  163. package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
  164. package/build/dist/Server/API/ScheduledMaintenanceAPI.js +1 -1
  165. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js +18 -0
  166. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js.map +1 -0
  167. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js +38 -0
  168. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js.map +1 -0
  169. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js +16 -0
  170. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js.map +1 -0
  171. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js +18 -0
  172. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js.map +1 -0
  173. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.js +12 -0
  174. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.js.map +1 -0
  175. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.js +18 -0
  176. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.js.map +1 -0
  177. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.js +12 -0
  178. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.js.map +1 -0
  179. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -0
  180. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  181. package/build/dist/Server/Infrastructure/PostgresDatabase.js +9 -0
  182. package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
  183. package/build/dist/Server/Services/AIService.js +53 -2
  184. package/build/dist/Server/Services/AIService.js.map +1 -1
  185. package/build/dist/Server/Services/AlertService.js +24 -0
  186. package/build/dist/Server/Services/AlertService.js.map +1 -1
  187. package/build/dist/Server/Services/AnalyticsDatabaseService.js +10 -1
  188. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  189. package/build/dist/Server/Services/DatabaseService.js +10 -1
  190. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  191. package/build/dist/Server/Services/IncidentService.js +25 -1
  192. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  193. package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
  194. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  195. package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js +55 -2
  196. package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js.map +1 -1
  197. package/build/dist/Server/Services/IncomingCallPolicyService.js +40 -0
  198. package/build/dist/Server/Services/IncomingCallPolicyService.js.map +1 -1
  199. package/build/dist/Server/Services/Index.js +2 -0
  200. package/build/dist/Server/Services/Index.js.map +1 -1
  201. package/build/dist/Server/Services/MigrationFailureService.js +9 -0
  202. package/build/dist/Server/Services/MigrationFailureService.js.map +1 -0
  203. package/build/dist/Server/Services/MonitorProbeService.js +33 -7
  204. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  205. package/build/dist/Server/Services/MonitorService.js +12 -5
  206. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  207. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +81 -22
  208. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  209. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js +134 -31
  210. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js.map +1 -1
  211. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +21 -13
  212. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
  213. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +36 -0
  214. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
  215. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +121 -0
  216. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
  217. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +11 -2
  218. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
  219. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +409 -36
  220. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  221. package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js +52 -8
  222. package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js.map +1 -1
  223. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +170 -3
  224. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
  225. package/build/dist/Server/Services/ProjectCallSMSConfigService.js +72 -0
  226. package/build/dist/Server/Services/ProjectCallSMSConfigService.js.map +1 -1
  227. package/build/dist/Server/Services/TeamMemberService.js +6 -0
  228. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  229. package/build/dist/Server/Services/UserNotificationRuleService.js +12 -30
  230. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  231. package/build/dist/Server/Services/UserOnCallLogService.js +75 -0
  232. package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
  233. package/build/dist/Server/Services/UserOnCallLogTimelineService.js +31 -1
  234. package/build/dist/Server/Services/UserOnCallLogTimelineService.js.map +1 -1
  235. package/build/dist/Server/Types/Database/QueryUtil.js +45 -0
  236. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  237. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js +6 -1
  238. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js.map +1 -1
  239. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +13 -2
  240. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
  241. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +59 -11
  242. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
  243. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js +18 -3
  244. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js.map +1 -1
  245. package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js +6 -1
  246. package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js.map +1 -1
  247. package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js +12 -2
  248. package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js.map +1 -1
  249. package/build/dist/Server/Utils/AI/Sentinel/AlertInvestigationRunner.js +119 -0
  250. package/build/dist/Server/Utils/AI/Sentinel/AlertInvestigationRunner.js.map +1 -0
  251. package/build/dist/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.js +147 -0
  252. package/build/dist/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.js.map +1 -0
  253. package/build/dist/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.js +93 -0
  254. package/build/dist/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.js.map +1 -0
  255. package/build/dist/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.js +311 -0
  256. package/build/dist/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.js.map +1 -0
  257. package/build/dist/Server/Utils/AI/Sentinel/SentinelMemory.js +143 -0
  258. package/build/dist/Server/Utils/AI/Sentinel/SentinelMemory.js.map +1 -0
  259. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +152 -1
  260. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -1
  261. package/build/dist/Server/Utils/AI/Toolbox/Index.js +10 -1
  262. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  263. package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js +165 -0
  264. package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js.map +1 -0
  265. package/build/dist/Server/Utils/AI/Toolbox/SentinelActionTools.js +451 -0
  266. package/build/dist/Server/Utils/AI/Toolbox/SentinelActionTools.js.map +1 -0
  267. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +40 -0
  268. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -1
  269. package/build/dist/Server/Utils/Database/MigrationFailureLog.js +174 -0
  270. package/build/dist/Server/Utils/Database/MigrationFailureLog.js.map +1 -0
  271. package/build/dist/Server/Utils/IncomingCallPhoneNumber.js +30 -0
  272. package/build/dist/Server/Utils/IncomingCallPhoneNumber.js.map +1 -0
  273. package/build/dist/Server/Utils/LLM/LLMService.js +81 -5
  274. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  275. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +11 -8
  276. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
  277. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +11 -8
  278. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
  279. package/build/dist/Types/Date.js +185 -15
  280. package/build/dist/Types/Date.js.map +1 -1
  281. package/build/dist/Types/OnCallDutyPolicy/Layer.js +602 -171
  282. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  283. package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js +27 -13
  284. package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js.map +1 -1
  285. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +159 -0
  286. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -0
  287. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js +20 -5
  288. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js.map +1 -1
  289. package/build/dist/UI/Components/Calendar/Calendar.js +2 -2
  290. package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
  291. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
  292. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  293. package/build/dist/UI/Components/Events/RecurringFieldElement.js +13 -0
  294. package/build/dist/UI/Components/Events/RecurringFieldElement.js.map +1 -1
  295. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  296. package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js +1 -1
  297. package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js.map +1 -1
  298. package/package.json +1 -1
@@ -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
+ });
@@ -0,0 +1,74 @@
1
+ import QueryUtil from "../../../../Server/Types/Database/QueryUtil";
2
+ import TeamMember from "../../../../Models/DatabaseModels/TeamMember";
3
+ import Search from "../../../../Types/BaseDatabase/Search";
4
+ import ObjectID from "../../../../Types/ObjectID";
5
+ import { FindOperator } from "typeorm";
6
+
7
+ /*
8
+ * serializeQuery historically only visited top-level query keys. A search
9
+ * operator nested under a single-entity relation (e.g. filtering TeamMember by
10
+ * `user.name`) therefore reached TypeORM as a raw Search instance, which
11
+ * TypeORM silently degrades to an exact equality (`user.name = 'foo'`) instead
12
+ * of an ILIKE. That is what made the Owner / user picker "search" appear to do
13
+ * nothing once a project had more than a page of members. These tests lock in
14
+ * the recursion that converts nested relation operators into real SQL.
15
+ */
16
+ describe("QueryUtil.serializeQuery — nested relation operators", () => {
17
+ it("converts a Search nested under a single-entity relation into a raw ILIKE", () => {
18
+ const query: Record<string, unknown> = {
19
+ projectId: ObjectID.generate(),
20
+ user: { name: new Search("alice") },
21
+ };
22
+
23
+ const result: Record<string, any> = QueryUtil.serializeQuery(
24
+ TeamMember,
25
+ query as any,
26
+ ) as unknown as Record<string, any>;
27
+
28
+ /*
29
+ * The nested Search must become a TypeORM Raw ILIKE, never survive as a
30
+ * Search instance.
31
+ */
32
+ expect(result["user"].name).toBeInstanceOf(FindOperator);
33
+ expect(result["user"].name).not.toBeInstanceOf(Search);
34
+ expect(result["user"].name.type).toBe("raw");
35
+ // QueryHelper.search wraps the (lower-cased) term as an ILIKE parameter.
36
+ expect(Object.values(result["user"].name.objectLiteralParameters)).toEqual([
37
+ "%alice%",
38
+ ]);
39
+ });
40
+
41
+ it("also converts a Search on a relation's email column", () => {
42
+ const query: Record<string, unknown> = {
43
+ projectId: ObjectID.generate(),
44
+ user: { email: new Search("alice@example.com") },
45
+ };
46
+
47
+ const result: Record<string, any> = QueryUtil.serializeQuery(
48
+ TeamMember,
49
+ query as any,
50
+ ) as unknown as Record<string, any>;
51
+
52
+ expect(result["user"].email).toBeInstanceOf(FindOperator);
53
+ expect(result["user"].email.type).toBe("raw");
54
+ expect(Object.values(result["user"].email.objectLiteralParameters)).toEqual(
55
+ ["%alice@example.com%"],
56
+ );
57
+ });
58
+
59
+ it("leaves a plain id equality nested under a relation untouched", () => {
60
+ const userId: ObjectID = ObjectID.generate();
61
+ const query: Record<string, unknown> = {
62
+ projectId: ObjectID.generate(),
63
+ user: { _id: userId.toString() },
64
+ };
65
+
66
+ const result: Record<string, any> = QueryUtil.serializeQuery(
67
+ TeamMember,
68
+ query as any,
69
+ ) as unknown as Record<string, any>;
70
+
71
+ // Plain id equality must NOT be rewritten into an ILIKE — it stays a value.
72
+ expect(result["user"]._id).toBe(userId.toString());
73
+ });
74
+ });
@@ -306,3 +306,81 @@ describe("LLMService — OpenAI-compatible (generic, e.g. vLLM)", () => {
306
306
  ).rejects.toThrow("Model Name is required");
307
307
  });
308
308
  });
309
+
310
+ describe("LLMService — prompt caching", () => {
311
+ test("Anthropic marks system and the last tool with an ephemeral cache breakpoint", async () => {
312
+ const spy: PostSpy = mockPostResponse({
313
+ content: [{ type: "text", text: "hi" }],
314
+ stop_reason: "end_turn",
315
+ usage: {
316
+ input_tokens: 12,
317
+ output_tokens: 4,
318
+ cache_read_input_tokens: 100,
319
+ cache_creation_input_tokens: 8,
320
+ },
321
+ });
322
+
323
+ const response: LLMCompletionResponse = await LLMService.getCompletion({
324
+ llmProviderConfig: { llmType: LlmType.Anthropic, apiKey: "test-key" },
325
+ messages: [
326
+ { role: "system", content: "be helpful" },
327
+ { role: "user", content: "hi" },
328
+ ],
329
+ tools: [
330
+ {
331
+ name: "a",
332
+ description: "a",
333
+ inputSchema: { type: "object", properties: {} },
334
+ },
335
+ {
336
+ name: "b",
337
+ description: "b",
338
+ inputSchema: { type: "object", properties: {} },
339
+ },
340
+ ],
341
+ });
342
+
343
+ const requestBody: JSONObject = (
344
+ spy.mock.calls[0]![0] as { data: JSONObject }
345
+ ).data;
346
+
347
+ // The system prompt is sent as a content-block array with a cache breakpoint.
348
+ const systemBlocks: Array<JSONObject> = requestBody[
349
+ "system"
350
+ ] as Array<JSONObject>;
351
+ expect(Array.isArray(systemBlocks)).toBe(true);
352
+ expect(systemBlocks[0]!["cache_control"]).toEqual({ type: "ephemeral" });
353
+
354
+ // Only the last tool carries the breakpoint (it caches the whole block).
355
+ const tools: Array<JSONObject> = requestBody["tools"] as Array<JSONObject>;
356
+ expect(tools[tools.length - 1]!["cache_control"]).toEqual({
357
+ type: "ephemeral",
358
+ });
359
+ expect(tools[0]!["cache_control"]).toBeUndefined();
360
+
361
+ // totalTokens folds in cached + cache-creation input tokens (12+100+8+4).
362
+ expect(response.usage!.totalTokens).toBe(124);
363
+ expect(response.usage!.cachedInputTokens).toBe(100);
364
+ expect(response.usage!.cacheCreationTokens).toBe(8);
365
+ });
366
+
367
+ test("OpenAI surfaces cached prompt tokens from prompt_tokens_details", async () => {
368
+ mockPostResponse({
369
+ choices: [{ message: { content: "ok" } }],
370
+ usage: {
371
+ prompt_tokens: 50,
372
+ completion_tokens: 5,
373
+ total_tokens: 55,
374
+ prompt_tokens_details: { cached_tokens: 40 },
375
+ },
376
+ });
377
+
378
+ const response: LLMCompletionResponse = await LLMService.getCompletion({
379
+ llmProviderConfig: { llmType: LlmType.OpenAI, apiKey: "test-key" },
380
+ messages: [{ role: "user", content: "hi" }],
381
+ });
382
+
383
+ expect(response.usage!.cachedInputTokens).toBe(40);
384
+ expect(response.usage!.totalTokens).toBe(55);
385
+ });
386
+ });