@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,233 @@
1
+ import OnCallDutyPolicyScheduleLayerUserService from "../../../Server/Services/OnCallDutyPolicyScheduleLayerUserService";
2
+ import Model from "../../../Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser";
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
+ * OnCallDutyPolicyScheduleLayerUserService.onBeforeUpdate re-sequences the
9
+ * `order` of the other rows in a schedule layer when a user is dragged to a new
10
+ * position. Audit finding L3: the "moving down" branch (newOrder > currentOrder)
11
+ * decremented EVERY row with `order <= newOrder`, including rows ABOVE the moved
12
+ * user. Dragging a user downward therefore drove the top row's order to 0 (and
13
+ * negative after repeated down-drags) and opened a gap at 1, breaking the
14
+ * 1-based contiguous invariant that create-default (count+1) and delete
15
+ * re-sequencing depend on.
16
+ *
17
+ * The fix adds the missing lower bound `resource.order > currentOrder`, so only
18
+ * rows strictly between the old and new position shift — mirroring the
19
+ * double-bounded "moving up" branch (`order >= newOrder && order < currentOrder`).
20
+ *
21
+ * These tests stub the three persistence helpers onBeforeUpdate calls
22
+ * (findOneBy -> moved row's current order + layer id; findBy -> every row in the
23
+ * layer; updateOneBy -> the per-row shift) and CAPTURE each updateOneBy so we can
24
+ * assert exactly which rows moved and to what order. No Postgres involved.
25
+ */
26
+
27
+ const service: any = OnCallDutyPolicyScheduleLayerUserService as any;
28
+
29
+ // A schedule layer id shared by every row in these single-layer scenarios.
30
+ const LAYER_ID: ObjectID = new ObjectID("layer1");
31
+
32
+ const USER_A: ObjectID = new ObjectID("A");
33
+ const USER_B: ObjectID = new ObjectID("B");
34
+ const USER_C: ObjectID = new ObjectID("C");
35
+
36
+ // A captured updateOneBy shift: which row and what order it was set to.
37
+ interface CapturedShift {
38
+ id: string;
39
+ order: number;
40
+ }
41
+
42
+ // Build a fake persisted row for the layer.
43
+ function row(id: ObjectID, order: number): Model {
44
+ return {
45
+ _id: id,
46
+ order,
47
+ onCallDutyPolicyScheduleLayerId: LAYER_ID,
48
+ } as unknown as Model;
49
+ }
50
+
51
+ // Build the UpdateBy passed to onBeforeUpdate for moving `movedId` to `newOrder`.
52
+ function updateBy(movedId: ObjectID, newOrder: number): any {
53
+ return {
54
+ data: { order: newOrder },
55
+ query: { _id: movedId },
56
+ props: {},
57
+ } as any;
58
+ }
59
+
60
+ /*
61
+ * Stub findOneBy (moved row lookup), findBy (all rows in the layer) and
62
+ * updateOneBy (record the shift). Returns the array captured shifts are pushed
63
+ * into so each test can inspect exactly what moved.
64
+ */
65
+ function stubService(
66
+ movedRow: Model,
67
+ layerRows: Array<Model>,
68
+ ): Array<CapturedShift> {
69
+ const captured: Array<CapturedShift> = [];
70
+
71
+ jest.spyOn(service, "findOneBy").mockResolvedValue(movedRow as never);
72
+
73
+ jest.spyOn(service, "findBy").mockResolvedValue(layerRows as never);
74
+
75
+ jest
76
+ .spyOn(service, "updateOneBy")
77
+ .mockImplementation((...args: Array<unknown>): Promise<void> => {
78
+ const updateArg: {
79
+ query: { _id: ObjectID };
80
+ data: { order: number };
81
+ } = args[0] as {
82
+ query: { _id: ObjectID };
83
+ data: { order: number };
84
+ };
85
+ captured.push({
86
+ id: updateArg.query._id.toString(),
87
+ order: updateArg.data.order,
88
+ });
89
+ return Promise.resolve();
90
+ });
91
+
92
+ return captured;
93
+ }
94
+
95
+ describe("OnCallDutyPolicyScheduleLayerUserService.onBeforeUpdate reorder", () => {
96
+ afterEach(() => {
97
+ jest.restoreAllMocks();
98
+ });
99
+
100
+ /*
101
+ * ----------------------------------------------------------------------- *
102
+ * 1. MOVE DOWN B: order 2 -> 3. Only C (3 -> 2) shifts; A is untouched.
103
+ * -----------------------------------------------------------------------
104
+ */
105
+ test("moving B down (2 -> 3) decrements ONLY C and never touches A", async () => {
106
+ const captured: Array<CapturedShift> = stubService(row(USER_B, 2), [
107
+ row(USER_A, 1),
108
+ row(USER_B, 2),
109
+ row(USER_C, 3),
110
+ ]);
111
+
112
+ await service.onBeforeUpdate(updateBy(USER_B, 3));
113
+
114
+ // Exactly one row shifted, and it was C going from 3 down to 2.
115
+ expect(captured).toHaveLength(1);
116
+ expect(captured[0]).toEqual({ id: USER_C.toString(), order: 2 });
117
+
118
+ // A (the top row) must never be decremented — the regression.
119
+ const aShifts: Array<CapturedShift> = captured.filter(
120
+ (c: CapturedShift): boolean => {
121
+ return c.id === USER_A.toString();
122
+ },
123
+ );
124
+ expect(aShifts).toHaveLength(0);
125
+
126
+ /*
127
+ * After the shift, with the outer update setting B = 3, the resulting layer
128
+ * is contiguous {A:1, C:2, B:3}.
129
+ */
130
+ const finalOrders: Record<string, number> = {
131
+ [USER_A.toString()]: 1, // untouched
132
+ [USER_C.toString()]: 2, // shifted
133
+ [USER_B.toString()]: 3, // set by the outer update
134
+ };
135
+ expect(finalOrders[USER_A.toString()]).toBe(1);
136
+ expect(finalOrders[USER_C.toString()]).toBe(2);
137
+ expect(finalOrders[USER_B.toString()]).toBe(3);
138
+ });
139
+
140
+ /*
141
+ * ----------------------------------------------------------------------- *
142
+ * 2. MOVE UP C: order 3 -> 1. A (1 -> 2) and B (2 -> 3) increment; C untouched.
143
+ * -----------------------------------------------------------------------
144
+ */
145
+ test("moving C up (3 -> 1) increments ONLY A and B", async () => {
146
+ const captured: Array<CapturedShift> = stubService(row(USER_C, 3), [
147
+ row(USER_A, 1),
148
+ row(USER_B, 2),
149
+ row(USER_C, 3),
150
+ ]);
151
+
152
+ await service.onBeforeUpdate(updateBy(USER_C, 1));
153
+
154
+ expect(captured).toHaveLength(2);
155
+
156
+ const byId: Record<string, number> = {};
157
+ for (const shift of captured) {
158
+ byId[shift.id] = shift.order;
159
+ }
160
+
161
+ expect(byId[USER_A.toString()]).toBe(2);
162
+ expect(byId[USER_B.toString()]).toBe(3);
163
+ // C itself is not part of the shift loop; the outer update sets it to 1.
164
+ expect(byId[USER_C.toString()]).toBeUndefined();
165
+
166
+ // Contiguous {C:1, A:2, B:3}.
167
+ const finalOrders: Record<string, number> = {
168
+ [USER_C.toString()]: 1,
169
+ [USER_A.toString()]: 2,
170
+ [USER_B.toString()]: 3,
171
+ };
172
+ expect(Object.values(finalOrders).sort()).toEqual([1, 2, 3]);
173
+ });
174
+
175
+ /*
176
+ * ----------------------------------------------------------------------- *
177
+ * 3. MOVE DOWN A: order 1 -> 3 (dragging the TOP row all the way down).
178
+ * Only B (2 -> 1) and C (3 -> 2) decrement; A is not touched by the loop.
179
+ * -----------------------------------------------------------------------
180
+ */
181
+ test("moving the top row A down (1 -> 3) decrements ONLY B and C, no row hits 0", async () => {
182
+ const captured: Array<CapturedShift> = stubService(row(USER_A, 1), [
183
+ row(USER_A, 1),
184
+ row(USER_B, 2),
185
+ row(USER_C, 3),
186
+ ]);
187
+
188
+ await service.onBeforeUpdate(updateBy(USER_A, 3));
189
+
190
+ expect(captured).toHaveLength(2);
191
+
192
+ const byId: Record<string, number> = {};
193
+ for (const shift of captured) {
194
+ byId[shift.id] = shift.order;
195
+ }
196
+
197
+ expect(byId[USER_B.toString()]).toBe(1);
198
+ expect(byId[USER_C.toString()]).toBe(2);
199
+ // A is the moved row itself — the shift loop must not decrement it.
200
+ expect(byId[USER_A.toString()]).toBeUndefined();
201
+
202
+ // Nothing collapsed to 0 or negative.
203
+ for (const shift of captured) {
204
+ expect(shift.order).toBeGreaterThan(0);
205
+ }
206
+ });
207
+
208
+ /*
209
+ * ----------------------------------------------------------------------- *
210
+ * 4. Regression: in a move-down, NO captured shift may set an order <= 0.
211
+ * Before the fix, dragging B down drove A's order to 0. This asserts the
212
+ * invariant across the whole move-down code path.
213
+ * -----------------------------------------------------------------------
214
+ */
215
+ test("regression: no move-down shift ever sets order <= 0", async () => {
216
+ const captured: Array<CapturedShift> = stubService(row(USER_B, 2), [
217
+ row(USER_A, 1),
218
+ row(USER_B, 2),
219
+ row(USER_C, 3),
220
+ ]);
221
+
222
+ await service.onBeforeUpdate(updateBy(USER_B, 3));
223
+
224
+ for (const shift of captured) {
225
+ expect(shift.order).toBeGreaterThan(0);
226
+ }
227
+ });
228
+ });
229
+
230
+ // Keep any incidental logger.error noise out of the test output.
231
+ jest.spyOn(logger, "error").mockImplementation((): void => {
232
+ return undefined;
233
+ });
@@ -0,0 +1,242 @@
1
+ import OnCallDutyPolicyTimeLogService from "../../../Server/Services/OnCallDutyPolicyTimeLogService";
2
+ import Model from "../../../Models/DatabaseModels/OnCallDutyPolicyTimeLog";
3
+ import ObjectID from "../../../Types/ObjectID";
4
+ import QueryHelper from "../../../Server/Types/Database/QueryHelper";
5
+ import logger from "../../../Server/Utils/Logger";
6
+ import { describe, expect, test, afterEach } from "@jest/globals";
7
+
8
+ /*
9
+ * OnCallDutyPolicyTimeLogService scopes the team/schedule "context" columns of
10
+ * both its dedup read (startTimeLogForUser) and its close write
11
+ * (endTimeLogForUser) EXPLICITLY. When a context column is absent the query
12
+ * must constrain it to IS NULL rather than omit it.
13
+ *
14
+ * The old `...(x && { x })` OMITTED an absent column, so the column matched ANY
15
+ * value. Two bugs followed:
16
+ * - audit M3: ending a direct-user assignment (no teamId/scheduleId) matched
17
+ * and closed that user's still-open SCHEDULE and TEAM logs too.
18
+ * - audit L4: a direct-user start deduped against an already-open
19
+ * schedule/team log and never created the direct log.
20
+ *
21
+ * These tests mock the inherited DatabaseService persistence helpers
22
+ * (findOneBy / create / updateBy) so no Postgres is needed, and CAPTURE the
23
+ * query object each method is called with to assert the exact IS-NULL vs
24
+ * ObjectID scoping of the teamId and onCallDutyPolicyScheduleId columns.
25
+ */
26
+
27
+ /*
28
+ * The serialized shape of QueryHelper.isNull() — a TypeORM `Raw` FindOperator.
29
+ * It is stable across calls (the SQL closure is dropped by JSON.stringify), so
30
+ * structural equality identifies an IS-NULL constraint on a column.
31
+ */
32
+ const IS_NULL_JSON: string = JSON.stringify(QueryHelper.isNull());
33
+
34
+ // A distinct value that is NOT an IS-NULL operator, for negative assertions.
35
+ function isIsNull(value: unknown): boolean {
36
+ return JSON.stringify(value) === IS_NULL_JSON;
37
+ }
38
+
39
+ // Minimal stand-in for a persisted OnCallDutyPolicyTimeLog row.
40
+ function fakeRow(id: ObjectID): Model {
41
+ return { id } as unknown as Model;
42
+ }
43
+
44
+ // Base args shared by every startTimeLogForUser call in these tests.
45
+ function startArgsBase(): {
46
+ projectId: ObjectID;
47
+ onCallDutyPolicyId: ObjectID;
48
+ onCallDutyPolicyEscalationRuleId: ObjectID;
49
+ userId: ObjectID;
50
+ startsAt: Date;
51
+ } {
52
+ return {
53
+ projectId: new ObjectID("11111111-1111-4111-8111-111111111111"),
54
+ onCallDutyPolicyId: new ObjectID("22222222-2222-4222-8222-222222222222"),
55
+ onCallDutyPolicyEscalationRuleId: new ObjectID(
56
+ "33333333-3333-4333-8333-333333333333",
57
+ ),
58
+ userId: new ObjectID("44444444-4444-4444-8444-444444444444"),
59
+ startsAt: new Date("2026-07-09T00:00:00.000Z"),
60
+ };
61
+ }
62
+
63
+ // Capture the `query` object handed to findOneBy on the next call.
64
+ function spyFindOneBy(resolve: Model | null): jest.SpyInstance {
65
+ return jest
66
+ .spyOn(OnCallDutyPolicyTimeLogService, "findOneBy")
67
+ .mockResolvedValue(resolve as never);
68
+ }
69
+
70
+ // Stub create so startTimeLogForUser can complete when no existing log is found.
71
+ function spyCreate(): jest.SpyInstance {
72
+ return jest
73
+ .spyOn(OnCallDutyPolicyTimeLogService, "create")
74
+ .mockResolvedValue(
75
+ fakeRow(new ObjectID("99999999-9999-4999-8999-999999999999")) as never,
76
+ );
77
+ }
78
+
79
+ // Stub updateBy (returns a row count) for the endTimeLogForUser tests.
80
+ function spyUpdateBy(): jest.SpyInstance {
81
+ return jest
82
+ .spyOn(OnCallDutyPolicyTimeLogService, "updateBy")
83
+ .mockResolvedValue(1 as never);
84
+ }
85
+
86
+ function queryOf(spy: jest.SpyInstance): Record<string, unknown> {
87
+ return (spy.mock.calls[0]![0] as { query: Record<string, unknown> }).query;
88
+ }
89
+
90
+ describe("OnCallDutyPolicyTimeLogService context-column scoping", () => {
91
+ afterEach(() => {
92
+ jest.restoreAllMocks();
93
+ });
94
+
95
+ /*
96
+ * ----------------------------------------------------------------------- *
97
+ * startTimeLogForUser dedup query scoping.
98
+ * ----------------------------------------------------------------------- *
99
+ */
100
+
101
+ test("startTimeLogForUser with NO teamId/scheduleId scopes both columns to IS NULL", async () => {
102
+ const findOneBy: jest.SpyInstance = spyFindOneBy(null);
103
+ const create: jest.SpyInstance = spyCreate();
104
+
105
+ await OnCallDutyPolicyTimeLogService.startTimeLogForUser(startArgsBase());
106
+
107
+ expect(findOneBy).toHaveBeenCalledTimes(1);
108
+ const query: Record<string, unknown> = queryOf(findOneBy);
109
+
110
+ // A direct-user log must only dedup against other direct logs => IS NULL.
111
+ expect(isIsNull(query["teamId"])).toBe(true);
112
+ expect(isIsNull(query["onCallDutyPolicyScheduleId"])).toBe(true);
113
+ // endsAt IS NULL is always part of the open-log scope.
114
+ expect(isIsNull(query["endsAt"])).toBe(true);
115
+
116
+ // Nothing existed => a fresh log row is created.
117
+ expect(create).toHaveBeenCalledTimes(1);
118
+ });
119
+
120
+ test("startTimeLogForUser WITH teamId only: teamId matches the passed ObjectID, schedule IS NULL", async () => {
121
+ const teamId: ObjectID = new ObjectID(
122
+ "55555555-5555-4555-8555-555555555555",
123
+ );
124
+ const findOneBy: jest.SpyInstance = spyFindOneBy(null);
125
+ spyCreate();
126
+
127
+ await OnCallDutyPolicyTimeLogService.startTimeLogForUser({
128
+ ...startArgsBase(),
129
+ teamId,
130
+ });
131
+
132
+ const query: Record<string, unknown> = queryOf(findOneBy);
133
+
134
+ // teamId is the concrete value passed in (not an IS-NULL operator).
135
+ expect(isIsNull(query["teamId"])).toBe(false);
136
+ expect((query["teamId"] as ObjectID).toString()).toBe(teamId.toString());
137
+ // No schedule context => that column is scoped to IS NULL.
138
+ expect(isIsNull(query["onCallDutyPolicyScheduleId"])).toBe(true);
139
+ });
140
+
141
+ test("startTimeLogForUser WITH scheduleId only: schedule matches passed ObjectID, teamId IS NULL", async () => {
142
+ const onCallDutyPolicyScheduleId: ObjectID = new ObjectID(
143
+ "66666666-6666-4666-8666-666666666666",
144
+ );
145
+ const findOneBy: jest.SpyInstance = spyFindOneBy(null);
146
+ spyCreate();
147
+
148
+ await OnCallDutyPolicyTimeLogService.startTimeLogForUser({
149
+ ...startArgsBase(),
150
+ onCallDutyPolicyScheduleId,
151
+ });
152
+
153
+ const query: Record<string, unknown> = queryOf(findOneBy);
154
+
155
+ // Schedule column is the concrete value passed in.
156
+ expect(isIsNull(query["onCallDutyPolicyScheduleId"])).toBe(false);
157
+ expect((query["onCallDutyPolicyScheduleId"] as ObjectID).toString()).toBe(
158
+ onCallDutyPolicyScheduleId.toString(),
159
+ );
160
+ // No team context => that column is scoped to IS NULL.
161
+ expect(isIsNull(query["teamId"])).toBe(true);
162
+ });
163
+
164
+ test("startTimeLogForUser is idempotent: an existing open log is returned and create is NOT called", async () => {
165
+ const existingId: ObjectID = new ObjectID(
166
+ "77777777-7777-4777-8777-777777777777",
167
+ );
168
+ const existing: Model = fakeRow(existingId);
169
+ const findOneBy: jest.SpyInstance = spyFindOneBy(existing);
170
+ const create: jest.SpyInstance = spyCreate();
171
+
172
+ const result: Model =
173
+ await OnCallDutyPolicyTimeLogService.startTimeLogForUser(startArgsBase());
174
+
175
+ expect(findOneBy).toHaveBeenCalledTimes(1);
176
+ // Returns the already-open log verbatim (audit M1 relies on this).
177
+ expect(result).toBe(existing);
178
+ // No duplicate stint is created.
179
+ expect(create).not.toHaveBeenCalled();
180
+ });
181
+
182
+ /*
183
+ * ----------------------------------------------------------------------- *
184
+ * endTimeLogForUser close-query scoping.
185
+ * ----------------------------------------------------------------------- *
186
+ */
187
+
188
+ test("endTimeLogForUser with NO teamId/scheduleId scopes both columns to IS NULL", async () => {
189
+ const updateBy: jest.SpyInstance = spyUpdateBy();
190
+
191
+ await OnCallDutyPolicyTimeLogService.endTimeLogForUser({
192
+ projectId: new ObjectID("11111111-1111-4111-8111-111111111111"),
193
+ onCallDutyPolicyId: new ObjectID("22222222-2222-4222-8222-222222222222"),
194
+ onCallDutyPolicyEscalationRuleId: new ObjectID(
195
+ "33333333-3333-4333-8333-333333333333",
196
+ ),
197
+ userId: new ObjectID("44444444-4444-4444-8444-444444444444"),
198
+ endsAt: new Date("2026-07-09T01:00:00.000Z"),
199
+ });
200
+
201
+ expect(updateBy).toHaveBeenCalledTimes(1);
202
+ const query: Record<string, unknown> = queryOf(updateBy);
203
+
204
+ // Ending a direct-user stint must NOT close schedule/team logs (audit M3).
205
+ expect(isIsNull(query["teamId"])).toBe(true);
206
+ expect(isIsNull(query["onCallDutyPolicyScheduleId"])).toBe(true);
207
+ expect(isIsNull(query["endsAt"])).toBe(true);
208
+ });
209
+
210
+ test("endTimeLogForUser WITH scheduleId scopes onCallDutyPolicyScheduleId to the passed ObjectID", async () => {
211
+ const onCallDutyPolicyScheduleId: ObjectID = new ObjectID(
212
+ "88888888-8888-4888-8888-888888888888",
213
+ );
214
+ const updateBy: jest.SpyInstance = spyUpdateBy();
215
+
216
+ await OnCallDutyPolicyTimeLogService.endTimeLogForUser({
217
+ projectId: new ObjectID("11111111-1111-4111-8111-111111111111"),
218
+ onCallDutyPolicyId: new ObjectID("22222222-2222-4222-8222-222222222222"),
219
+ onCallDutyPolicyEscalationRuleId: new ObjectID(
220
+ "33333333-3333-4333-8333-333333333333",
221
+ ),
222
+ userId: new ObjectID("44444444-4444-4444-8444-444444444444"),
223
+ onCallDutyPolicyScheduleId,
224
+ endsAt: new Date("2026-07-09T01:00:00.000Z"),
225
+ });
226
+
227
+ const query: Record<string, unknown> = queryOf(updateBy);
228
+
229
+ // Only THIS schedule's open stint is closed.
230
+ expect(isIsNull(query["onCallDutyPolicyScheduleId"])).toBe(false);
231
+ expect((query["onCallDutyPolicyScheduleId"] as ObjectID).toString()).toBe(
232
+ onCallDutyPolicyScheduleId.toString(),
233
+ );
234
+ // No team context supplied => team column stays IS NULL.
235
+ expect(isIsNull(query["teamId"])).toBe(true);
236
+ });
237
+ });
238
+
239
+ // Silence any incidental logger.error noise from the service under test.
240
+ jest.spyOn(logger, "error").mockImplementation((): void => {
241
+ return undefined;
242
+ });
@@ -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
+ });