@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,957 @@
1
+ /**
2
+ * EXHAUSTIVE unrestricted-rotation correctness matrix for LayerUtil.getEvents
3
+ * and LayerUtil.getMultiLayerEvents.
4
+ *
5
+ * Covers the full cross product:
6
+ * intervalType {Hour, Day, Week, Month, Year}
7
+ * x intervalCount {1, 2, 3}
8
+ * x users {1, 2, 3, 5}
9
+ * x calendar windows {starting AT the layer start, starting PARTWAY through}
10
+ * x timezone {local (undefined), America/New_York, Asia/Kolkata}
11
+ *
12
+ * Invariants asserted per config:
13
+ * (a) 24/7 CONTIGUITY - each event.start == previous.end + exactly 1 second
14
+ * (no gaps, no overlaps, every event has positive length), over a
15
+ * multi-period horizon.
16
+ * (b) ROTATION ORDER - users cycle A,B,C,... with no skips and no repeats
17
+ * (the user index advances by exactly one every period, modulo the roster
18
+ * size), and an at-layer-start expansion begins with the first user.
19
+ * (c) FAIRNESS - shift counts across the roster differ by at most one over
20
+ * many periods, and every user takes a turn.
21
+ * (d) WINDOWED == FULL EXPANSION - the live "who is on call at `at`" resolution
22
+ * (getEvents / getMultiLayerEvents with getNumberOfEvents:1 starting the
23
+ * window at `at`) agrees with the covering event of a full expansion from
24
+ * the layer start, at many OFF-boundary instants; the windowed event is
25
+ * clamped to `at` and runs to the true period boundary.
26
+ * (e) EDGE CASES - single user (always that user), empty users (no events),
27
+ * calendar end before layer start (no events), and grid-aligned degenerate
28
+ * handoffs (handoff == layer start, handoff before layer start) resolving
29
+ * to the exact same rotation as the natural handoff.
30
+ *
31
+ * A dedicated block exercises America/New_York across US spring-forward and
32
+ * fall-back so the schedule-zone (wall-clock) stepping diverges from the
33
+ * process zone (run this file additionally under TZ=UTC).
34
+ *
35
+ * Everything is internally consistent (windowed vs full both flow through the
36
+ * same engine with the same zone), so the timezone/DST assertions hold under
37
+ * any process TZ. Ground-truth boundaries are always read back from the engine
38
+ * rather than recomputed, so the tests never depend on a hand-rolled boundary
39
+ * model.
40
+ */
41
+ import LayerUtil, { LayerProps } from "../../../Types/OnCallDutyPolicy/Layer";
42
+ import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
43
+ import RestrictionTimes, {
44
+ RestrictionType,
45
+ } from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
46
+ import Recurring from "../../../Types/Events/Recurring";
47
+ import OneUptimeDate from "../../../Types/Date";
48
+ import User from "../../../Models/DatabaseModels/User";
49
+ import EventInterval from "../../../Types/Events/EventInterval";
50
+ import PositiveNumber from "../../../Types/PositiveNumber";
51
+ import moment from "moment-timezone";
52
+
53
+ // Documented User mock pattern (id.toString() returns the title used by events).
54
+ function user(id: string): User {
55
+ return {
56
+ id: {
57
+ toString: (): string => {
58
+ return id;
59
+ },
60
+ } as any,
61
+ } as User;
62
+ }
63
+
64
+ function noRestriction(): RestrictionTimes {
65
+ const r: RestrictionTimes = new RestrictionTimes();
66
+ r.restictionType = RestrictionType.None;
67
+ r.dayRestrictionTimes = null;
68
+ return r;
69
+ }
70
+
71
+ const NY: string = "America/New_York";
72
+ const KOLKATA: string = "Asia/Kolkata";
73
+
74
+ // A Monday 00:00 UTC, far from any DST transition for Day/Week horizons.
75
+ const FIXED_START: Date = OneUptimeDate.fromString("2025-01-06T00:00:00.000Z");
76
+
77
+ interface Cfg {
78
+ name: string;
79
+ intervalType: EventInterval;
80
+ count: number;
81
+ userIds: string[];
82
+ timezone: string | undefined;
83
+ start: Date;
84
+ }
85
+
86
+ /*
87
+ * Calendar-unit stepping that mirrors the engine (Hour absolute; others honor
88
+ * the schedule zone across DST, exactly like moveHandsOffTimeAfterCurrentEventStartTime).
89
+ */
90
+ function addUnits(
91
+ date: Date,
92
+ units: number,
93
+ intervalType: EventInterval,
94
+ tz: string | undefined,
95
+ ): Date {
96
+ switch (intervalType) {
97
+ case EventInterval.Hour:
98
+ return OneUptimeDate.addRemoveHours(date, units);
99
+ case EventInterval.Day:
100
+ return OneUptimeDate.addRemoveDays(date, units, tz);
101
+ case EventInterval.Week:
102
+ return OneUptimeDate.addRemoveWeeks(date, units, tz);
103
+ case EventInterval.Month:
104
+ return OneUptimeDate.addRemoveMonths(date, units, tz);
105
+ case EventInterval.Year:
106
+ return OneUptimeDate.addRemoveYears(date, units, tz);
107
+ default:
108
+ return OneUptimeDate.addRemoveDays(date, units, tz);
109
+ }
110
+ }
111
+
112
+ function rotationOf(cfg: Cfg): Recurring {
113
+ const rot: Recurring = new Recurring();
114
+ rot.intervalType = cfg.intervalType;
115
+ rot.intervalCount = new PositiveNumber(cfg.count);
116
+ return rot;
117
+ }
118
+
119
+ function layerOf(cfg: Cfg, handoff?: Date): LayerProps {
120
+ const rot: Recurring = rotationOf(cfg);
121
+ return {
122
+ users: cfg.userIds.map(user),
123
+ startDateTimeOfLayer: cfg.start,
124
+ restrictionTimes: noRestriction(),
125
+ handOffTime: handoff ?? Recurring.getNextDateInterval(cfg.start, rot),
126
+ rotation: rot,
127
+ timezone: cfg.timezone,
128
+ };
129
+ }
130
+
131
+ // Target number of full-period events to expand per interval (bounds horizon).
132
+ const NUM_FULL: Record<EventInterval, number> = {
133
+ [EventInterval.Hour]: 26,
134
+ [EventInterval.Day]: 26,
135
+ [EventInterval.Week]: 20,
136
+ [EventInterval.Month]: 16,
137
+ [EventInterval.Year]: 12,
138
+ };
139
+
140
+ /*
141
+ * Expand `numFull` full-period events from the layer start. The horizon end is
142
+ * pushed several periods past the target so the returned slice is entirely
143
+ * full periods whose end timestamps are REAL engine boundaries (the trailing
144
+ * artifact event that getEvents can emit when the window end lands on a
145
+ * boundary, if any, falls beyond the slice and is discarded).
146
+ */
147
+ function expandFull(cfg: Cfg, numFull: number): CalendarEvent[] {
148
+ const layer: LayerProps = layerOf(cfg);
149
+ const util: LayerUtil = new LayerUtil();
150
+ const horizonEnd: Date = addUnits(
151
+ cfg.start,
152
+ (numFull + 6) * cfg.count,
153
+ cfg.intervalType,
154
+ cfg.timezone,
155
+ );
156
+ const events: CalendarEvent[] = util.getEvents({
157
+ ...layer,
158
+ calendarStartDate: cfg.start,
159
+ calendarEndDate: horizonEnd,
160
+ });
161
+ return events.slice(0, numFull);
162
+ }
163
+
164
+ function titleIndex(cfg: Cfg, title: string): number {
165
+ return cfg.userIds.indexOf(title);
166
+ }
167
+
168
+ // An interior (strictly off-boundary) instant of an event, at fraction f in (0,1).
169
+ function interiorInstant(e: CalendarEvent, f: number): Date {
170
+ const span: number = e.end.getTime() - e.start.getTime();
171
+ return new Date(e.start.getTime() + Math.floor(f * span));
172
+ }
173
+
174
+ // ---- invariant checkers (return human-readable problems; empty == pass) ----
175
+
176
+ function contiguityProblems(events: CalendarEvent[]): string[] {
177
+ const problems: string[] = [];
178
+ for (let i: number = 0; i < events.length; i++) {
179
+ const e: CalendarEvent = events[i]!;
180
+ if (!OneUptimeDate.isAfter(e.end, e.start)) {
181
+ problems.push(
182
+ `event ${i} non-positive length ${e.start.toISOString()}->${e.end.toISOString()}`,
183
+ );
184
+ }
185
+ if (i > 0) {
186
+ const prev: CalendarEvent = events[i - 1]!;
187
+ const gapSeconds: number = OneUptimeDate.getSecondsBetweenTwoDates(
188
+ prev.end,
189
+ e.start,
190
+ );
191
+ // exactly 1s handoff gap => no overlap (would be <=0) and no gap (>1).
192
+ if (gapSeconds !== 1) {
193
+ problems.push(
194
+ `event ${i} handoff gap ${gapSeconds}s (expected 1) prevEnd=${prev.end.toISOString()} start=${e.start.toISOString()}`,
195
+ );
196
+ }
197
+ }
198
+ }
199
+ return problems;
200
+ }
201
+
202
+ function orderProblems(
203
+ events: CalendarEvent[],
204
+ cfg: Cfg,
205
+ expectedFirstIndex: number | undefined,
206
+ ): string[] {
207
+ const problems: string[] = [];
208
+ const k: number = cfg.userIds.length;
209
+ if (events.length === 0) {
210
+ return problems;
211
+ }
212
+ const idx0: number = titleIndex(cfg, events[0]!.title);
213
+ if (idx0 < 0) {
214
+ problems.push(`event 0 unknown title ${events[0]!.title}`);
215
+ return problems;
216
+ }
217
+ if (expectedFirstIndex !== undefined && idx0 !== expectedFirstIndex) {
218
+ problems.push(`first user index ${idx0} != expected ${expectedFirstIndex}`);
219
+ }
220
+ for (let i: number = 1; i < events.length; i++) {
221
+ const idx: number = titleIndex(cfg, events[i]!.title);
222
+ const expected: number = (idx0 + i) % k;
223
+ if (idx !== expected) {
224
+ problems.push(
225
+ `event ${i} user index ${idx} != expected ${expected} (title ${events[i]!.title})`,
226
+ );
227
+ }
228
+ }
229
+ return problems;
230
+ }
231
+
232
+ function fairnessProblems(events: CalendarEvent[], cfg: Cfg): string[] {
233
+ const problems: string[] = [];
234
+ const counts: Map<string, number> = new Map<string, number>();
235
+ for (const e of events) {
236
+ counts.set(e.title, (counts.get(e.title) || 0) + 1);
237
+ if (titleIndex(cfg, e.title) < 0) {
238
+ problems.push(`unexpected on-call title ${e.title}`);
239
+ }
240
+ }
241
+ const values: number[] = Array.from(counts.values());
242
+ if (values.length > 0) {
243
+ const max: number = Math.max(...values);
244
+ const min: number = Math.min(...values);
245
+ if (max - min > 1) {
246
+ problems.push(`shift-count spread ${min}..${max} exceeds 1`);
247
+ }
248
+ }
249
+ // Over a horizon >= roster size, every user must take a turn.
250
+ if (events.length >= cfg.userIds.length) {
251
+ for (const id of cfg.userIds) {
252
+ if (!counts.has(id)) {
253
+ problems.push(`user ${id} never on-call over ${events.length} periods`);
254
+ }
255
+ }
256
+ }
257
+ return problems;
258
+ }
259
+
260
+ // ---------------------------- matrix ----------------------------
261
+
262
+ const INTERVALS: EventInterval[] = [
263
+ EventInterval.Hour,
264
+ EventInterval.Day,
265
+ EventInterval.Week,
266
+ EventInterval.Month,
267
+ EventInterval.Year,
268
+ ];
269
+ const COUNTS: number[] = [1, 2, 3];
270
+ const USER_SETS: string[][] = [
271
+ ["A"],
272
+ ["A", "B"],
273
+ ["A", "B", "C"],
274
+ ["A", "B", "C", "D", "E"],
275
+ ];
276
+ const TIMEZONES: (string | undefined)[] = [undefined, NY, KOLKATA];
277
+
278
+ function buildMatrix(): Cfg[] {
279
+ const cfgs: Cfg[] = [];
280
+ for (const it of INTERVALS) {
281
+ for (const count of COUNTS) {
282
+ for (const users of USER_SETS) {
283
+ for (const tz of TIMEZONES) {
284
+ cfgs.push({
285
+ name: `${count}x${it} users=${users.length} tz=${tz ?? "local"}`,
286
+ intervalType: it,
287
+ count,
288
+ userIds: users,
289
+ timezone: tz,
290
+ start: FIXED_START,
291
+ });
292
+ }
293
+ }
294
+ }
295
+ }
296
+ return cfgs;
297
+ }
298
+
299
+ const MATRIX: Cfg[] = buildMatrix();
300
+
301
+ /*
302
+ * ================================================================
303
+ * (a)(b)(c) contiguity + rotation order + fairness, window AT layer start.
304
+ * ================================================================
305
+ */
306
+ describe("Unrestricted rotation: 24/7 contiguity + order + fairness (window AT layer start)", () => {
307
+ for (const cfg of MATRIX) {
308
+ it(`${cfg.name}`, () => {
309
+ const numFull: number = NUM_FULL[cfg.intervalType];
310
+ const events: CalendarEvent[] = expandFull(cfg, numFull);
311
+
312
+ // Sanity: we actually got the requested number of full events.
313
+ expect(events.length).toBe(numFull);
314
+
315
+ // (a) contiguity / positive length / no gaps or overlaps.
316
+ expect({ cfg: cfg.name, problems: contiguityProblems(events) }).toEqual({
317
+ cfg: cfg.name,
318
+ problems: [],
319
+ });
320
+
321
+ // (b) rotation order begins at the first user and never skips/repeats.
322
+ expect({
323
+ cfg: cfg.name,
324
+ problems: orderProblems(events, cfg, 0),
325
+ }).toEqual({ cfg: cfg.name, problems: [] });
326
+
327
+ // (c) fairness across the roster.
328
+ expect({
329
+ cfg: cfg.name,
330
+ problems: fairnessProblems(events, cfg),
331
+ }).toEqual({ cfg: cfg.name, problems: [] });
332
+
333
+ // The layer start owns the first period.
334
+ expect(events[0]!.start.getTime()).toBe(cfg.start.getTime());
335
+ // Single-user rotations are always that user.
336
+ if (cfg.userIds.length === 1) {
337
+ expect(
338
+ events.every((e: CalendarEvent) => {
339
+ return e.title === "A";
340
+ }),
341
+ ).toBe(true);
342
+ }
343
+ });
344
+ }
345
+ });
346
+
347
+ /*
348
+ * ================================================================
349
+ * (a)(b) contiguity + consecutive order for a window that starts PARTWAY
350
+ * through a period (the windowed/live path). First event is clamped to the
351
+ * window start and carries the correct in-progress user.
352
+ * ================================================================
353
+ */
354
+ describe("Unrestricted rotation: contiguity + order for a PARTWAY (windowed) calendar start", () => {
355
+ for (const cfg of MATRIX) {
356
+ it(`${cfg.name}`, () => {
357
+ const numFull: number = NUM_FULL[cfg.intervalType];
358
+ const full: CalendarEvent[] = expandFull(cfg, numFull);
359
+
360
+ // Start partway through period p; span M subsequent periods.
361
+ const p: number = Math.min(3, numFull - 8);
362
+ const startIdx: number = p < 0 ? 0 : p;
363
+ const spanPeriods: number = Math.min(12, numFull - startIdx - 2);
364
+
365
+ const windowStart: Date = interiorInstant(full[startIdx]!, 0.4);
366
+ const windowEnd: Date = interiorInstant(
367
+ full[startIdx + spanPeriods]!,
368
+ 0.6,
369
+ );
370
+
371
+ const util: LayerUtil = new LayerUtil();
372
+ const events: CalendarEvent[] = util.getEvents({
373
+ ...layerOf(cfg),
374
+ calendarStartDate: windowStart,
375
+ calendarEndDate: windowEnd,
376
+ });
377
+
378
+ expect(events.length).toBeGreaterThan(0);
379
+
380
+ // First event is clamped to the window start and is the in-progress user.
381
+ expect(events[0]!.start.getTime()).toBe(windowStart.getTime());
382
+ expect(events[0]!.title).toBe(full[startIdx]!.title);
383
+ // ...and runs to that period's true boundary.
384
+ expect(events[0]!.end.getTime()).toBe(full[startIdx]!.end.getTime());
385
+ // Last event is clamped to the window end.
386
+ expect(events[events.length - 1]!.end.getTime()).toBe(
387
+ windowEnd.getTime(),
388
+ );
389
+
390
+ // (a) contiguity.
391
+ expect({ cfg: cfg.name, problems: contiguityProblems(events) }).toEqual({
392
+ cfg: cfg.name,
393
+ problems: [],
394
+ });
395
+
396
+ /*
397
+ * (b) consecutive rotation advancement (no skips/repeats), anchored on the
398
+ * in-progress user (partway start need not begin at index 0).
399
+ */
400
+ const expectedFirst: number = titleIndex(cfg, full[startIdx]!.title);
401
+ expect({
402
+ cfg: cfg.name,
403
+ problems: orderProblems(events, cfg, expectedFirst),
404
+ }).toEqual({ cfg: cfg.name, problems: [] });
405
+ });
406
+ }
407
+ });
408
+
409
+ /*
410
+ * ================================================================
411
+ * (d) windowed resolution == full-expansion cover at many OFF-boundary instants,
412
+ * through BOTH getEvents and getMultiLayerEvents.
413
+ * ================================================================
414
+ */
415
+ function sampleIndices(n: number): number[] {
416
+ const raw: number[] = [0, 1, Math.floor(n / 2), n - 2, n - 1];
417
+ const seen: Set<number> = new Set<number>();
418
+ const out: number[] = [];
419
+ for (const i of raw) {
420
+ if (i >= 0 && i < n && !seen.has(i)) {
421
+ seen.add(i);
422
+ out.push(i);
423
+ }
424
+ }
425
+ return out;
426
+ }
427
+
428
+ describe("Unrestricted rotation: windowed current-user == full-expansion cover (OFF-boundary)", () => {
429
+ // Two off-boundary probes per sampled period (early and late in the period).
430
+ const FRACTIONS: number[] = [0.35, 0.72];
431
+
432
+ for (const cfg of MATRIX) {
433
+ it(`${cfg.name}`, () => {
434
+ const numFull: number = NUM_FULL[cfg.intervalType];
435
+ const full: CalendarEvent[] = expandFull(cfg, numFull);
436
+ const util: LayerUtil = new LayerUtil();
437
+ const problems: string[] = [];
438
+ const indices: number[] = sampleIndices(numFull);
439
+
440
+ for (let s: number = 0; s < indices.length; s++) {
441
+ const idx: number = indices[s]!;
442
+ const cover: CalendarEvent = full[idx]!;
443
+ for (const f of FRACTIONS) {
444
+ const at: Date = interiorInstant(cover, f);
445
+ // strictly interior guard.
446
+ if (
447
+ !OneUptimeDate.isAfter(at, cover.start) ||
448
+ !OneUptimeDate.isBefore(at, cover.end)
449
+ ) {
450
+ continue;
451
+ }
452
+ const windowEnd: Date = addUnits(
453
+ at,
454
+ 4 * cfg.count,
455
+ cfg.intervalType,
456
+ cfg.timezone,
457
+ );
458
+
459
+ // getEvents windowed - the live "who is on call at `at`" path.
460
+ const windowed: CalendarEvent[] = util.getEvents(
461
+ {
462
+ ...layerOf(cfg),
463
+ calendarStartDate: at,
464
+ calendarEndDate: windowEnd,
465
+ },
466
+ { getNumberOfEvents: 1 },
467
+ );
468
+ if (windowed.length !== 1) {
469
+ problems.push(
470
+ `at=${at.toISOString()} getEvents returned ${windowed.length}`,
471
+ );
472
+ continue;
473
+ }
474
+ if (windowed[0]!.title !== cover.title) {
475
+ problems.push(
476
+ `at=${at.toISOString()} getEvents title ${windowed[0]!.title} != cover ${cover.title}`,
477
+ );
478
+ }
479
+ if (windowed[0]!.start.getTime() !== at.getTime()) {
480
+ problems.push(
481
+ `at=${at.toISOString()} getEvents start not clamped to instant (${windowed[0]!.start.toISOString()})`,
482
+ );
483
+ }
484
+ if (windowed[0]!.end.getTime() !== cover.end.getTime()) {
485
+ problems.push(
486
+ `at=${at.toISOString()} getEvents end ${windowed[0]!.end.toISOString()} != boundary ${cover.end.toISOString()}`,
487
+ );
488
+ }
489
+
490
+ /*
491
+ * getMultiLayerEvents windowed (single layer) must agree on the user.
492
+ * Checked once per config (first probe) to keep runtime bounded; the
493
+ * dedicated parity/windowed blocks below cover getMultiLayerEvents more.
494
+ */
495
+ if (s === 0 && f === FRACTIONS[0]) {
496
+ const multi: CalendarEvent[] = util.getMultiLayerEvents(
497
+ {
498
+ layers: [layerOf(cfg)],
499
+ calendarStartDate: at,
500
+ calendarEndDate: windowEnd,
501
+ },
502
+ { getNumberOfEvents: 1 },
503
+ );
504
+ if (multi.length !== 1 || multi[0]!.title !== cover.title) {
505
+ problems.push(
506
+ `at=${at.toISOString()} getMultiLayerEvents title ${multi[0]?.title} != cover ${cover.title}`,
507
+ );
508
+ }
509
+ }
510
+ }
511
+ }
512
+
513
+ expect({ cfg: cfg.name, problems }).toEqual({
514
+ cfg: cfg.name,
515
+ problems: [],
516
+ });
517
+ });
518
+ }
519
+ });
520
+
521
+ /*
522
+ * ================================================================
523
+ * getMultiLayerEvents structural parity + priority shadowing.
524
+ * ================================================================
525
+ */
526
+ describe("getMultiLayerEvents: single-layer parity with getEvents (interior window)", () => {
527
+ const parityCfgs: Cfg[] = MATRIX.filter((c: Cfg) => {
528
+ return c.timezone === undefined && c.count <= 2;
529
+ });
530
+
531
+ for (const cfg of parityCfgs) {
532
+ it(`${cfg.name}`, () => {
533
+ const numFull: number = NUM_FULL[cfg.intervalType];
534
+ const full: CalendarEvent[] = expandFull(cfg, numFull);
535
+ /*
536
+ * interior window: [start, interior of a later period] => no boundary-aligned
537
+ * trailing artifact, so getEvents and getMultiLayerEvents must match exactly.
538
+ */
539
+ const endIdx: number = Math.min(10, numFull - 1);
540
+ const calEnd: Date = interiorInstant(full[endIdx]!, 0.5);
541
+ const util: LayerUtil = new LayerUtil();
542
+
543
+ const single: CalendarEvent[] = util.getEvents({
544
+ ...layerOf(cfg),
545
+ calendarStartDate: cfg.start,
546
+ calendarEndDate: calEnd,
547
+ });
548
+ const multi: CalendarEvent[] = util.getMultiLayerEvents({
549
+ layers: [layerOf(cfg)],
550
+ calendarStartDate: cfg.start,
551
+ calendarEndDate: calEnd,
552
+ });
553
+
554
+ expect(multi.length).toBe(single.length);
555
+ for (let i: number = 0; i < single.length; i++) {
556
+ expect(multi[i]!.title).toBe(single[i]!.title);
557
+ expect(multi[i]!.start.getTime()).toBe(single[i]!.start.getTime());
558
+ expect(multi[i]!.end.getTime()).toBe(single[i]!.end.getTime());
559
+ }
560
+ // And the merged output is itself contiguous.
561
+ expect(contiguityProblems(multi)).toEqual([]);
562
+ });
563
+ }
564
+ });
565
+
566
+ describe("getMultiLayerEvents: a 24/7 higher-priority layer fully shadows a lower-priority layer", () => {
567
+ it("Day x1 primary [A,B] over fallback [X,Y,Z] => only primary users, contiguous", () => {
568
+ const primary: Cfg = {
569
+ name: "primary",
570
+ intervalType: EventInterval.Day,
571
+ count: 1,
572
+ userIds: ["A", "B"],
573
+ timezone: undefined,
574
+ start: FIXED_START,
575
+ };
576
+ const fallback: Cfg = {
577
+ name: "fallback",
578
+ intervalType: EventInterval.Day,
579
+ count: 1,
580
+ userIds: ["X", "Y", "Z"],
581
+ timezone: undefined,
582
+ start: FIXED_START,
583
+ };
584
+
585
+ const full: CalendarEvent[] = expandFull(primary, 12);
586
+ const calEnd: Date = interiorInstant(full[10]!, 0.5);
587
+ const util: LayerUtil = new LayerUtil();
588
+
589
+ const merged: CalendarEvent[] = util.getMultiLayerEvents({
590
+ layers: [layerOf(primary), layerOf(fallback)],
591
+ calendarStartDate: FIXED_START,
592
+ calendarEndDate: calEnd,
593
+ });
594
+ const primaryOnly: CalendarEvent[] = util.getMultiLayerEvents({
595
+ layers: [layerOf(primary)],
596
+ calendarStartDate: FIXED_START,
597
+ calendarEndDate: calEnd,
598
+ });
599
+
600
+ // Fallback is completely shadowed by the 24/7 primary.
601
+ for (const e of merged) {
602
+ expect(["A", "B"]).toContain(e.title);
603
+ }
604
+ // Merged == primary alone.
605
+ expect(merged.length).toBe(primaryOnly.length);
606
+ for (let i: number = 0; i < merged.length; i++) {
607
+ expect(merged[i]!.title).toBe(primaryOnly[i]!.title);
608
+ expect(merged[i]!.start.getTime()).toBe(primaryOnly[i]!.start.getTime());
609
+ expect(merged[i]!.end.getTime()).toBe(primaryOnly[i]!.end.getTime());
610
+ }
611
+ expect(contiguityProblems(merged)).toEqual([]);
612
+ expect(orderProblems(merged, primary, 0)).toEqual([]);
613
+ });
614
+ });
615
+
616
+ /*
617
+ * ================================================================
618
+ * (e) edge cases.
619
+ * ================================================================
620
+ */
621
+ describe("Edge cases", () => {
622
+ it("empty users => no events", () => {
623
+ const cfg: Cfg = {
624
+ name: "empty",
625
+ intervalType: EventInterval.Day,
626
+ count: 1,
627
+ userIds: [],
628
+ timezone: undefined,
629
+ start: FIXED_START,
630
+ };
631
+ const util: LayerUtil = new LayerUtil();
632
+ const events: CalendarEvent[] = util.getEvents({
633
+ ...layerOf(cfg),
634
+ calendarStartDate: FIXED_START,
635
+ calendarEndDate: OneUptimeDate.addRemoveDays(FIXED_START, 10),
636
+ });
637
+ expect(events).toEqual([]);
638
+ const multi: CalendarEvent[] = util.getMultiLayerEvents({
639
+ layers: [layerOf(cfg)],
640
+ calendarStartDate: FIXED_START,
641
+ calendarEndDate: OneUptimeDate.addRemoveDays(FIXED_START, 10),
642
+ });
643
+ expect(multi).toEqual([]);
644
+ });
645
+
646
+ it("calendar end strictly before layer start => no events", () => {
647
+ for (const it of INTERVALS) {
648
+ const cfg: Cfg = {
649
+ name: `calEndBefore-${it}`,
650
+ intervalType: it,
651
+ count: 1,
652
+ userIds: ["A", "B"],
653
+ timezone: undefined,
654
+ start: FIXED_START,
655
+ };
656
+ const util: LayerUtil = new LayerUtil();
657
+ const events: CalendarEvent[] = util.getEvents({
658
+ ...layerOf(cfg),
659
+ calendarStartDate: OneUptimeDate.addRemoveDays(FIXED_START, -30),
660
+ calendarEndDate: OneUptimeDate.addRemoveDays(FIXED_START, -5),
661
+ });
662
+ expect(events).toEqual([]);
663
+ }
664
+ });
665
+
666
+ it("calendar window entirely before layer start (both ends before) => no events", () => {
667
+ const cfg: Cfg = {
668
+ name: "before",
669
+ intervalType: EventInterval.Hour,
670
+ count: 2,
671
+ userIds: ["A", "B", "C"],
672
+ timezone: undefined,
673
+ start: FIXED_START,
674
+ };
675
+ const util: LayerUtil = new LayerUtil();
676
+ const events: CalendarEvent[] = util.getEvents({
677
+ ...layerOf(cfg),
678
+ calendarStartDate: OneUptimeDate.addRemoveHours(FIXED_START, -50),
679
+ calendarEndDate: OneUptimeDate.addRemoveHours(FIXED_START, -1),
680
+ });
681
+ expect(events).toEqual([]);
682
+ });
683
+
684
+ it("single user => always that user across every interval/count", () => {
685
+ for (const it of INTERVALS) {
686
+ for (const count of COUNTS) {
687
+ const cfg: Cfg = {
688
+ name: `single-${count}x${it}`,
689
+ intervalType: it,
690
+ count,
691
+ userIds: ["SOLO"],
692
+ timezone: undefined,
693
+ start: FIXED_START,
694
+ };
695
+ const events: CalendarEvent[] = expandFull(cfg, 10);
696
+ expect(events.length).toBe(10);
697
+ expect(
698
+ events.every((e: CalendarEvent) => {
699
+ return e.title === "SOLO";
700
+ }),
701
+ ).toBe(true);
702
+ expect(contiguityProblems(events)).toEqual([]);
703
+ }
704
+ }
705
+ });
706
+
707
+ it("grid-aligned degenerate handoffs (== start, and before start) equal the natural rotation", () => {
708
+ const intervals: EventInterval[] = [
709
+ EventInterval.Hour,
710
+ EventInterval.Day,
711
+ EventInterval.Week,
712
+ EventInterval.Month,
713
+ ];
714
+ for (const it of intervals) {
715
+ for (const count of [1, 2]) {
716
+ const cfg: Cfg = {
717
+ name: `${count}x${it}`,
718
+ intervalType: it,
719
+ count,
720
+ userIds: ["A", "B", "C"],
721
+ timezone: undefined,
722
+ start: FIXED_START,
723
+ };
724
+ // interior end derived from the natural expansion (no boundary artifact).
725
+ const naturalFull: CalendarEvent[] = expandFull(cfg, 10);
726
+ const calEnd: Date = interiorInstant(naturalFull[8]!, 0.5);
727
+ const util: LayerUtil = new LayerUtil();
728
+
729
+ const natural: CalendarEvent[] = util.getEvents({
730
+ ...layerOf(cfg),
731
+ calendarStartDate: cfg.start,
732
+ calendarEndDate: calEnd,
733
+ });
734
+
735
+ /*
736
+ * handoff == layer start (k=0), and grid-aligned handoffs before the
737
+ * layer start (k = -1, -2, -3 periods) must produce the SAME rotation.
738
+ */
739
+ for (const k of [0, 1, 2, 3]) {
740
+ const handoff: Date = addUnits(
741
+ cfg.start,
742
+ -k * count,
743
+ it,
744
+ cfg.timezone,
745
+ );
746
+ const events: CalendarEvent[] = util.getEvents({
747
+ ...layerOf(cfg, handoff),
748
+ calendarStartDate: cfg.start,
749
+ calendarEndDate: calEnd,
750
+ });
751
+ expect(events.length).toBe(natural.length);
752
+ for (let i: number = 0; i < natural.length; i++) {
753
+ expect(events[i]!.title).toBe(natural[i]!.title);
754
+ expect(events[i]!.start.getTime()).toBe(
755
+ natural[i]!.start.getTime(),
756
+ );
757
+ expect(events[i]!.end.getTime()).toBe(natural[i]!.end.getTime());
758
+ }
759
+ }
760
+ }
761
+ }
762
+ });
763
+
764
+ it("NOTE: current behavior - getEvents emits a trailing negative-length event when the window end lands exactly on a rotation boundary; getMultiLayerEvents filters it out", () => {
765
+ const cfg: Cfg = {
766
+ name: "boundary",
767
+ intervalType: EventInterval.Day,
768
+ count: 1,
769
+ userIds: ["A", "B"],
770
+ timezone: undefined,
771
+ start: FIXED_START,
772
+ };
773
+ const util: LayerUtil = new LayerUtil();
774
+ // 3 days == exactly a rotation boundary for a daily x1 rotation.
775
+ const calEnd: Date = OneUptimeDate.addRemoveDays(FIXED_START, 3);
776
+
777
+ const raw: CalendarEvent[] = util.getEvents({
778
+ ...layerOf(cfg),
779
+ calendarStartDate: FIXED_START,
780
+ calendarEndDate: calEnd,
781
+ });
782
+ const last: CalendarEvent = raw[raw.length - 1]!;
783
+ /*
784
+ * NOTE: current behavior - the final event is inverted (end 1s before start).
785
+ * This is an internal artifact; both production consumers go through
786
+ * getMultiLayerEvents, which removes it (see assertion below). Not a
787
+ * user-observable defect, so it is documented rather than reported.
788
+ */
789
+ expect(OneUptimeDate.isBefore(last.end, last.start)).toBe(true);
790
+
791
+ const multi: CalendarEvent[] = util.getMultiLayerEvents({
792
+ layers: [layerOf(cfg)],
793
+ calendarStartDate: FIXED_START,
794
+ calendarEndDate: calEnd,
795
+ });
796
+ // No inverted / zero-length events survive the merge.
797
+ for (const e of multi) {
798
+ expect(OneUptimeDate.isAfter(e.end, e.start)).toBe(true);
799
+ }
800
+ expect(contiguityProblems(multi)).toEqual([]);
801
+ });
802
+ });
803
+
804
+ /*
805
+ * ================================================================
806
+ * DST / server-vs-schedule divergence (America/New_York). Run also under TZ=UTC.
807
+ * ================================================================
808
+ */
809
+ describe("America/New_York rotation across DST transitions (schedule-zone wall-clock preserved)", () => {
810
+ // 2025-03-08 00:00 New York (EST) - one day before US spring-forward.
811
+ const SPRING_START: Date = moment.tz("2025-03-08 00:00", NY).toDate();
812
+ // 2025-11-01 00:00 New York (EDT) - one day before US fall-back.
813
+ const FALL_START: Date = moment.tz("2025-11-01 00:00", NY).toDate();
814
+
815
+ const scenarios: {
816
+ label: string;
817
+ start: Date;
818
+ it: EventInterval;
819
+ count: number;
820
+ }[] = [
821
+ {
822
+ label: "spring daily x1",
823
+ start: SPRING_START,
824
+ it: EventInterval.Day,
825
+ count: 1,
826
+ },
827
+ {
828
+ label: "spring daily x2",
829
+ start: SPRING_START,
830
+ it: EventInterval.Day,
831
+ count: 2,
832
+ },
833
+ {
834
+ label: "spring weekly x1",
835
+ start: SPRING_START,
836
+ it: EventInterval.Week,
837
+ count: 1,
838
+ },
839
+ {
840
+ label: "spring hourly x1",
841
+ start: SPRING_START,
842
+ it: EventInterval.Hour,
843
+ count: 1,
844
+ },
845
+ {
846
+ label: "fall daily x1",
847
+ start: FALL_START,
848
+ it: EventInterval.Day,
849
+ count: 1,
850
+ },
851
+ {
852
+ label: "fall daily x2",
853
+ start: FALL_START,
854
+ it: EventInterval.Day,
855
+ count: 2,
856
+ },
857
+ {
858
+ label: "fall weekly x1",
859
+ start: FALL_START,
860
+ it: EventInterval.Week,
861
+ count: 1,
862
+ },
863
+ {
864
+ label: "fall hourly x1",
865
+ start: FALL_START,
866
+ it: EventInterval.Hour,
867
+ count: 1,
868
+ },
869
+ ];
870
+
871
+ for (const s of scenarios) {
872
+ it(`${s.label}: contiguous, ordered, and windowed==cover across the transition`, () => {
873
+ const cfg: Cfg = {
874
+ name: s.label,
875
+ intervalType: s.it,
876
+ count: s.count,
877
+ userIds: ["A", "B", "C"],
878
+ timezone: NY,
879
+ start: s.start,
880
+ };
881
+ const numFull: number = 12;
882
+ const full: CalendarEvent[] = expandFull(cfg, numFull);
883
+
884
+ expect(full.length).toBe(numFull);
885
+ // Contiguity holds structurally even across the 23h/25h DST day.
886
+ expect(contiguityProblems(full)).toEqual([]);
887
+ // Rotation order is unaffected by DST.
888
+ expect(orderProblems(full, cfg, 0)).toEqual([]);
889
+ expect(fairnessProblems(full, cfg)).toEqual([]);
890
+
891
+ if (s.it === EventInterval.Day && s.count === 1) {
892
+ /*
893
+ * Prove tz-aware (wall-clock) stepping is in effect rather than fixed
894
+ * 24h arithmetic: the daily rotation period that spans the DST
895
+ * transition has a different absolute length (23h spring / 25h fall)
896
+ * than a normal 24h period. (We do NOT assert the boundary lands on a
897
+ * specific NY wall-clock time: the FIRST handoff is computed zone-naive
898
+ * by Recurring.getNextDateInterval, so the boundary grid may be offset
899
+ * by the DST hour - that is the documented server-vs-schedule behavior.)
900
+ */
901
+ const daySpans: Set<number> = new Set<number>();
902
+ for (const e of full) {
903
+ daySpans.add(OneUptimeDate.getSecondsBetweenTwoDates(e.start, e.end));
904
+ }
905
+ expect(daySpans.size).toBeGreaterThan(1);
906
+ // Every period is within +/- 1h of a nominal 24h day.
907
+ for (const span of Array.from(daySpans)) {
908
+ expect(Math.abs(span - 86400)).toBeLessThanOrEqual(3600 + 1);
909
+ }
910
+ }
911
+
912
+ // Windowed==cover across the transition (OFF-boundary instants).
913
+ const util: LayerUtil = new LayerUtil();
914
+ const problems: string[] = [];
915
+ for (let idx: number = 0; idx < full.length; idx++) {
916
+ const cover: CalendarEvent = full[idx]!;
917
+ for (const f of [0.25, 0.75]) {
918
+ const at: Date = interiorInstant(cover, f);
919
+ if (
920
+ !OneUptimeDate.isAfter(at, cover.start) ||
921
+ !OneUptimeDate.isBefore(at, cover.end)
922
+ ) {
923
+ continue;
924
+ }
925
+ const windowEnd: Date = addUnits(
926
+ at,
927
+ 4 * cfg.count,
928
+ cfg.intervalType,
929
+ cfg.timezone,
930
+ );
931
+ const windowed: CalendarEvent[] = util.getEvents(
932
+ {
933
+ ...layerOf(cfg),
934
+ calendarStartDate: at,
935
+ calendarEndDate: windowEnd,
936
+ },
937
+ { getNumberOfEvents: 1 },
938
+ );
939
+ if (windowed[0]?.title !== cover.title) {
940
+ problems.push(
941
+ `at=${at.toISOString()} title ${windowed[0]?.title} != ${cover.title}`,
942
+ );
943
+ }
944
+ if (windowed[0]!.end.getTime() !== cover.end.getTime()) {
945
+ problems.push(
946
+ `at=${at.toISOString()} end mismatch ${windowed[0]!.end.toISOString()} vs ${cover.end.toISOString()}`,
947
+ );
948
+ }
949
+ }
950
+ }
951
+ expect({ label: s.label, problems }).toEqual({
952
+ label: s.label,
953
+ problems: [],
954
+ });
955
+ });
956
+ }
957
+ });