@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,900 @@
1
+ /**
2
+ * EXHAUSTIVE coverage for DAILY-restriction trimming (getEventsByDailyRestriction)
3
+ * and its integration through LayerUtil.getEvents / getMultiLayerEvents.
4
+ *
5
+ * These tests lock in the CORRECT (post-audit-fix) behavior of the daily
6
+ * restriction path across every window shape and rotation combination:
7
+ * - normal same-day windows (09-17, and a matrix of placements)
8
+ * - early-morning windows (00-06)
9
+ * - late windows (18:00-23:59)
10
+ * - windows touching midnight (18:00-00:00)
11
+ * - overnight wrap-around windows (22-06) incl. the FIRST-DAY morning tail
12
+ * - multi-day rotation events (weekly & monthly rotation + a daily restriction)
13
+ * covering every day of the period
14
+ * - rotation periods that start inside / at / after the daily window
15
+ * - the F2 post-window evening-gap resolution returning the correct NEXT user
16
+ * - DST spring-forward & fall-back weeks (America/New_York) where the
17
+ * wall-clock window must stay stable
18
+ * - server-vs-schedule timezone divergence (schedule authored in another zone)
19
+ *
20
+ * Restriction boundaries authored via getDateWithCustomTime carry the run's
21
+ * millisecond component (e.g. 09:00:00.147), so coverage is probed at instants
22
+ * offset a full second from minute boundaries (":01"/":59"), never exactly at
23
+ * HH:MM:00.000. Timezone assertions are expressed in wall-clock via
24
+ * moment-timezone so they hold regardless of the process TZ the suite runs
25
+ * under (run this file additionally with TZ=UTC to exercise the divergence).
26
+ */
27
+ import LayerUtil, { LayerProps } from "../../../Types/OnCallDutyPolicy/Layer";
28
+ import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
29
+ import RestrictionTimes, {
30
+ RestrictionType,
31
+ } from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
32
+ import Recurring from "../../../Types/Events/Recurring";
33
+ import OneUptimeDate from "../../../Types/Date";
34
+ import User from "../../../Models/DatabaseModels/User";
35
+ import EventInterval from "../../../Types/Events/EventInterval";
36
+ import PositiveNumber from "../../../Types/PositiveNumber";
37
+ import StartAndEndTime from "../../../Types/Time/StartAndEndTime";
38
+ import moment from "moment-timezone";
39
+
40
+ /*
41
+ * ---------------------------------------------------------------------------
42
+ * Builders / helpers (mirroring the sibling LayerUtil test files).
43
+ * ---------------------------------------------------------------------------
44
+ */
45
+
46
+ function user(id: string): User {
47
+ return {
48
+ id: {
49
+ toString: (): string => {
50
+ return id;
51
+ },
52
+ } as any,
53
+ } as User;
54
+ }
55
+
56
+ // Local (server-zone) daily restriction authored via wall-clock H:M.
57
+ function dailyLocal(
58
+ sh: number,
59
+ sm: number,
60
+ eh: number,
61
+ em: number,
62
+ ): RestrictionTimes {
63
+ const r: RestrictionTimes = new RestrictionTimes();
64
+ r.restictionType = RestrictionType.Daily;
65
+ r.dayRestrictionTimes = {
66
+ startTime: OneUptimeDate.getDateWithCustomTime({
67
+ hours: sh,
68
+ minutes: sm,
69
+ seconds: 0,
70
+ }),
71
+ endTime: OneUptimeDate.getDateWithCustomTime({
72
+ hours: eh,
73
+ minutes: em,
74
+ seconds: 0,
75
+ }),
76
+ };
77
+ return r;
78
+ }
79
+
80
+ // Timezone-aware daily restriction whose wall-clock is authored IN `tz`.
81
+ function dailyTz(
82
+ startIso: string,
83
+ endIso: string,
84
+ tz: string,
85
+ ): RestrictionTimes {
86
+ const r: RestrictionTimes = new RestrictionTimes();
87
+ r.restictionType = RestrictionType.Daily;
88
+ r.dayRestrictionTimes = {
89
+ startTime: moment.tz(startIso, tz).toDate(),
90
+ endTime: moment.tz(endIso, tz).toDate(),
91
+ };
92
+ return r;
93
+ }
94
+
95
+ function rotationOf(t: EventInterval, c: number): Recurring {
96
+ const r: Recurring = new Recurring();
97
+ r.intervalType = t;
98
+ r.intervalCount = new PositiveNumber(c);
99
+ return r;
100
+ }
101
+
102
+ interface LayerCfg {
103
+ users: string[];
104
+ intervalType: EventInterval;
105
+ intervalCount: number;
106
+ restriction: RestrictionTimes;
107
+ start: Date;
108
+ timezone?: string | undefined;
109
+ }
110
+
111
+ function makeLayer(cfg: LayerCfg): LayerProps {
112
+ const rot: Recurring = rotationOf(cfg.intervalType, cfg.intervalCount);
113
+ return {
114
+ users: cfg.users.map(user),
115
+ startDateTimeOfLayer: cfg.start,
116
+ restrictionTimes: cfg.restriction,
117
+ handOffTime: Recurring.getNextDateInterval(cfg.start, rot),
118
+ rotation: rot,
119
+ timezone: cfg.timezone,
120
+ };
121
+ }
122
+
123
+ function expand(layer: LayerProps, from: Date, to: Date): Array<CalendarEvent> {
124
+ const util: LayerUtil = new LayerUtil();
125
+ return util.getEvents({
126
+ ...layer,
127
+ calendarStartDate: from,
128
+ calendarEndDate: to,
129
+ });
130
+ }
131
+
132
+ // The LIVE "who is on call now / next" path: windowed, getNumberOfEvents:1.
133
+ function windowedNext(layer: LayerProps, at: Date): CalendarEvent | undefined {
134
+ const util: LayerUtil = new LayerUtil();
135
+ return util.getMultiLayerEvents(
136
+ {
137
+ layers: [layer],
138
+ calendarStartDate: at,
139
+ calendarEndDate: OneUptimeDate.addRemoveDays(at, 25),
140
+ },
141
+ { getNumberOfEvents: 1 },
142
+ )[0];
143
+ }
144
+
145
+ // Which user covers instant `t` (start <= t < end). Millisecond-precise.
146
+ function coveringTitle(events: Array<CalendarEvent>, t: Date): string | null {
147
+ for (const e of events) {
148
+ if (e.start.getTime() <= t.getTime() && e.end.getTime() > t.getTime()) {
149
+ return e.title;
150
+ }
151
+ }
152
+ return null;
153
+ }
154
+
155
+ // First event that starts strictly after `t` (ground-truth "next" shift).
156
+ function nextAfter(
157
+ events: Array<CalendarEvent>,
158
+ t: Date,
159
+ ): CalendarEvent | undefined {
160
+ let best: CalendarEvent | undefined = undefined;
161
+ for (const e of events) {
162
+ if (e.start.getTime() > t.getTime()) {
163
+ if (best === undefined || e.start.getTime() < best.start.getTime()) {
164
+ best = e;
165
+ }
166
+ }
167
+ }
168
+ return best;
169
+ }
170
+
171
+ // Local instant Jan (2025) day/H/M/S in the process zone.
172
+ function jan(day: number, h: number, m: number, s: number): Date {
173
+ return new Date(2025, 0, day, h, m, s);
174
+ }
175
+
176
+ const NY: string = "America/New_York";
177
+ const KOLKATA: string = "Asia/Kolkata";
178
+
179
+ function tzInstant(iso: string, tz: string): Date {
180
+ return moment.tz(iso, tz).toDate();
181
+ }
182
+
183
+ function hhmm(d: Date, tz: string): string {
184
+ return moment.tz(d, tz).format("HH:mm");
185
+ }
186
+
187
+ // A Monday, no DST anywhere in early January.
188
+ const MON_JAN6: Date = jan(6, 0, 0, 0);
189
+
190
+ /*
191
+ * ===========================================================================
192
+ * GROUP A - normal same-day window 09:00-17:00 (server-local)
193
+ * ===========================================================================
194
+ */
195
+
196
+ describe("Daily same-day window 09:00-17:00 (local)", () => {
197
+ test("direct trim emits exactly one 09:00-17:00 segment per day over a multi-day event", () => {
198
+ const util: LayerUtil = new LayerUtil();
199
+ const segs: Array<StartAndEndTime> =
200
+ util.trimStartAndEndTimesBasedOnRestrictionTimes({
201
+ eventStartTime: jan(6, 0, 0, 0),
202
+ eventEndTime: jan(9, 0, 0, 0), // 3 full days
203
+ restrictionTimes: dailyLocal(9, 0, 17, 0),
204
+ });
205
+ expect(segs.length).toBe(3);
206
+ for (const s of segs) {
207
+ expect(s.startTime.getHours()).toBe(9);
208
+ expect(s.startTime.getMinutes()).toBe(0);
209
+ expect(s.endTime.getHours()).toBe(17);
210
+ expect(s.endTime.getMinutes()).toBe(0);
211
+ // each segment is strictly inside one calendar day
212
+ expect(s.startTime.getDate()).toBe(s.endTime.getDate());
213
+ }
214
+ });
215
+
216
+ test("daily rotation single user: covered midday, uncovered outside the window each day", () => {
217
+ const layer: LayerProps = makeLayer({
218
+ users: ["A"],
219
+ intervalType: EventInterval.Day,
220
+ intervalCount: 1,
221
+ restriction: dailyLocal(9, 0, 17, 0),
222
+ start: MON_JAN6,
223
+ });
224
+ const ev: Array<CalendarEvent> = expand(layer, MON_JAN6, jan(11, 0, 0, 0));
225
+ for (let d: number = 6; d <= 10; d++) {
226
+ expect(coveringTitle(ev, jan(d, 12, 0, 0))).toBe("A"); // noon covered
227
+ expect(coveringTitle(ev, jan(d, 8, 30, 0))).toBeNull(); // before
228
+ expect(coveringTitle(ev, jan(d, 17, 30, 0))).toBeNull(); // after
229
+ expect(coveringTitle(ev, jan(d, 0, 30, 0))).toBeNull(); // deep night
230
+ }
231
+ });
232
+
233
+ test("boundary instants (second-precise, robust to ms authoring noise)", () => {
234
+ const layer: LayerProps = makeLayer({
235
+ users: ["A"],
236
+ intervalType: EventInterval.Day,
237
+ intervalCount: 1,
238
+ restriction: dailyLocal(9, 0, 17, 0),
239
+ start: MON_JAN6,
240
+ });
241
+ const ev: Array<CalendarEvent> = expand(layer, MON_JAN6, jan(8, 0, 0, 0));
242
+ expect(coveringTitle(ev, jan(6, 8, 59, 59))).toBeNull(); // just before open
243
+ expect(coveringTitle(ev, jan(6, 9, 0, 1))).toBe("A"); // just after open
244
+ expect(coveringTitle(ev, jan(6, 16, 59, 59))).toBe("A"); // just before close
245
+ expect(coveringTitle(ev, jan(6, 17, 0, 1))).toBeNull(); // just after close
246
+ });
247
+
248
+ for (const users of [
249
+ ["A", "B"],
250
+ ["A", "B", "C"],
251
+ ] as string[][]) {
252
+ test(`daily rotation ${users.length} users: correct on-call user at noon each day`, () => {
253
+ const layer: LayerProps = makeLayer({
254
+ users,
255
+ intervalType: EventInterval.Day,
256
+ intervalCount: 1,
257
+ restriction: dailyLocal(9, 0, 17, 0),
258
+ start: MON_JAN6,
259
+ });
260
+ const ev: Array<CalendarEvent> = expand(
261
+ layer,
262
+ MON_JAN6,
263
+ jan(6 + users.length * 2, 0, 0, 0),
264
+ );
265
+ for (let i: number = 0; i < users.length * 2; i++) {
266
+ const expected: string = users[i % users.length] as string;
267
+ expect(coveringTitle(ev, jan(6 + i, 12, 0, 0))).toBe(expected);
268
+ }
269
+ });
270
+ }
271
+ });
272
+
273
+ /*
274
+ * ===========================================================================
275
+ * GROUP B - early-morning same-day window 00:00-06:00
276
+ * ===========================================================================
277
+ */
278
+
279
+ describe("Daily early-morning window 00:00-06:00 (local)", () => {
280
+ test("direct trim emits one 00:00-06:00 segment per day", () => {
281
+ const util: LayerUtil = new LayerUtil();
282
+ const segs: Array<StartAndEndTime> =
283
+ util.trimStartAndEndTimesBasedOnRestrictionTimes({
284
+ eventStartTime: jan(6, 0, 0, 0),
285
+ eventEndTime: jan(9, 0, 0, 0),
286
+ restrictionTimes: dailyLocal(0, 0, 6, 0),
287
+ });
288
+ expect(segs.length).toBe(3);
289
+ for (const s of segs) {
290
+ expect(s.startTime.getHours()).toBe(0);
291
+ expect(s.endTime.getHours()).toBe(6);
292
+ }
293
+ });
294
+
295
+ test("covered in the small hours, uncovered the rest of the day", () => {
296
+ const layer: LayerProps = makeLayer({
297
+ users: ["A"],
298
+ intervalType: EventInterval.Day,
299
+ intervalCount: 1,
300
+ restriction: dailyLocal(0, 0, 6, 0),
301
+ start: MON_JAN6,
302
+ });
303
+ const ev: Array<CalendarEvent> = expand(layer, MON_JAN6, jan(9, 0, 0, 0));
304
+ for (let d: number = 6; d <= 8; d++) {
305
+ expect(coveringTitle(ev, jan(d, 0, 30, 0))).toBe("A");
306
+ expect(coveringTitle(ev, jan(d, 3, 0, 0))).toBe("A");
307
+ expect(coveringTitle(ev, jan(d, 5, 59, 0))).toBe("A");
308
+ expect(coveringTitle(ev, jan(d, 6, 0, 1))).toBeNull();
309
+ expect(coveringTitle(ev, jan(d, 12, 0, 0))).toBeNull();
310
+ expect(coveringTitle(ev, jan(d, 22, 0, 0))).toBeNull();
311
+ }
312
+ });
313
+ });
314
+
315
+ /*
316
+ * ===========================================================================
317
+ * GROUP C - late same-day window 18:00-23:59
318
+ * ===========================================================================
319
+ */
320
+
321
+ describe("Daily late window 18:00-23:59 (local)", () => {
322
+ test("direct trim emits one 18:00-23:59 segment per day", () => {
323
+ const util: LayerUtil = new LayerUtil();
324
+ const segs: Array<StartAndEndTime> =
325
+ util.trimStartAndEndTimesBasedOnRestrictionTimes({
326
+ eventStartTime: jan(6, 0, 0, 0),
327
+ eventEndTime: jan(8, 0, 0, 0),
328
+ restrictionTimes: dailyLocal(18, 0, 23, 59),
329
+ });
330
+ expect(segs.length).toBe(2);
331
+ for (const s of segs) {
332
+ expect(s.startTime.getHours()).toBe(18);
333
+ expect(s.endTime.getHours()).toBe(23);
334
+ expect(s.endTime.getMinutes()).toBe(59);
335
+ }
336
+ });
337
+
338
+ test("covered in the evening, uncovered before 18:00 and after 23:59", () => {
339
+ const layer: LayerProps = makeLayer({
340
+ users: ["A"],
341
+ intervalType: EventInterval.Day,
342
+ intervalCount: 1,
343
+ restriction: dailyLocal(18, 0, 23, 59),
344
+ start: MON_JAN6,
345
+ });
346
+ const ev: Array<CalendarEvent> = expand(layer, MON_JAN6, jan(9, 0, 0, 0));
347
+ for (let d: number = 6; d <= 8; d++) {
348
+ expect(coveringTitle(ev, jan(d, 18, 0, 1))).toBe("A");
349
+ expect(coveringTitle(ev, jan(d, 20, 0, 0))).toBe("A");
350
+ expect(coveringTitle(ev, jan(d, 23, 58, 0))).toBe("A");
351
+ expect(coveringTitle(ev, jan(d, 17, 59, 0))).toBeNull();
352
+ expect(coveringTitle(ev, jan(d, 12, 0, 0))).toBeNull();
353
+ expect(coveringTitle(ev, jan(d, 0, 30, 0))).toBeNull();
354
+ }
355
+ });
356
+ });
357
+
358
+ /*
359
+ * ===========================================================================
360
+ * GROUP D - overnight wrap-around window 22:00-06:00 (incl. first-day morning)
361
+ * ===========================================================================
362
+ */
363
+
364
+ describe("Daily overnight wrap-around window 22:00-06:00 (local)", () => {
365
+ test("first period emits a leading start-of-day morning segment (first-day morning covered)", () => {
366
+ // Period starting at midnight must have its 00:00-06:00 morning tail covered.
367
+ const util: LayerUtil = new LayerUtil();
368
+ const segs: Array<StartAndEndTime> =
369
+ util.trimStartAndEndTimesBasedOnRestrictionTimes({
370
+ eventStartTime: jan(6, 0, 0, 0),
371
+ eventEndTime: jan(7, 0, 0, 0),
372
+ restrictionTimes: dailyLocal(22, 0, 6, 0),
373
+ });
374
+ // Exactly: [Jan6 00:00, Jan6 06:00] morning + [Jan6 22:00, Jan6 23:59:59] night.
375
+ const morning: StartAndEndTime | undefined = segs.find(
376
+ (s: StartAndEndTime) => {
377
+ return s.startTime.getHours() === 0;
378
+ },
379
+ );
380
+ expect(morning).toBeTruthy();
381
+ expect(morning?.startTime.getDate()).toBe(6);
382
+ expect(morning?.endTime.getHours()).toBe(6);
383
+ const night: StartAndEndTime | undefined = segs.find(
384
+ (s: StartAndEndTime) => {
385
+ return s.startTime.getHours() === 22;
386
+ },
387
+ );
388
+ expect(night).toBeTruthy();
389
+ });
390
+
391
+ test("coverage completeness across several days for a midnight-anchored rotation", () => {
392
+ const layer: LayerProps = makeLayer({
393
+ users: ["A"],
394
+ intervalType: EventInterval.Day,
395
+ intervalCount: 1,
396
+ restriction: dailyLocal(22, 0, 6, 0),
397
+ start: MON_JAN6,
398
+ });
399
+ const ev: Array<CalendarEvent> = expand(layer, MON_JAN6, jan(9, 0, 0, 0));
400
+
401
+ // First-day morning (the fix: loop starts one day before the event).
402
+ expect(coveringTitle(ev, jan(6, 0, 0, 1))).toBe("A");
403
+ expect(coveringTitle(ev, jan(6, 3, 0, 0))).toBe("A");
404
+ expect(coveringTitle(ev, jan(6, 5, 59, 0))).toBe("A");
405
+
406
+ // Nights across days.
407
+ for (const d of [6, 7, 8]) {
408
+ expect(coveringTitle(ev, jan(d, 22, 0, 1))).toBe("A");
409
+ expect(coveringTitle(ev, jan(d, 23, 30, 0))).toBe("A");
410
+ }
411
+ // Following mornings.
412
+ for (const d of [7, 8]) {
413
+ expect(coveringTitle(ev, jan(d, 3, 0, 0))).toBe("A");
414
+ }
415
+
416
+ // Daytime gap is uncovered.
417
+ for (const d of [6, 7, 8]) {
418
+ expect(coveringTitle(ev, jan(d, 6, 0, 1))).toBeNull();
419
+ expect(coveringTitle(ev, jan(d, 12, 0, 0))).toBeNull();
420
+ expect(coveringTitle(ev, jan(d, 21, 59, 0))).toBeNull();
421
+ }
422
+ });
423
+
424
+ test("weekly rotation with an overnight restriction covers every night of the week", () => {
425
+ const layer: LayerProps = makeLayer({
426
+ users: ["A"],
427
+ intervalType: EventInterval.Week,
428
+ intervalCount: 1,
429
+ restriction: dailyLocal(22, 0, 6, 0),
430
+ start: MON_JAN6,
431
+ });
432
+ const ev: Array<CalendarEvent> = expand(layer, MON_JAN6, jan(12, 0, 0, 0));
433
+ for (let d: number = 6; d <= 10; d++) {
434
+ expect(coveringTitle(ev, jan(d, 0, 30, 0))).toBe("A"); // morning
435
+ expect(coveringTitle(ev, jan(d, 23, 0, 0))).toBe("A"); // night
436
+ expect(coveringTitle(ev, jan(d, 12, 0, 0))).toBeNull(); // daytime
437
+ }
438
+ });
439
+
440
+ for (const [sh, eh] of [
441
+ [23, 5],
442
+ [20, 8],
443
+ [21, 7],
444
+ ] as Array<[number, number]>) {
445
+ test(`overnight ${sh}:00-${eh}:00 covers the wrap band and excludes the daytime`, () => {
446
+ const layer: LayerProps = makeLayer({
447
+ users: ["A"],
448
+ intervalType: EventInterval.Day,
449
+ intervalCount: 1,
450
+ restriction: dailyLocal(sh, 0, eh, 0),
451
+ start: MON_JAN6,
452
+ });
453
+ const ev: Array<CalendarEvent> = expand(layer, MON_JAN6, jan(9, 0, 0, 0));
454
+ // Deep night and deep early-morning covered; solar midday uncovered.
455
+ expect(coveringTitle(ev, jan(7, 3, 0, 0))).toBe("A");
456
+ expect(coveringTitle(ev, jan(7, sh, 30, 0))).toBe("A");
457
+ expect(coveringTitle(ev, jan(7, 12, 0, 0))).toBeNull();
458
+ // A point strictly between eh and sh (mid-afternoon) is uncovered.
459
+ const mid: number = Math.floor((eh + sh) / 2);
460
+ expect(coveringTitle(ev, jan(7, mid, 30, 0))).toBeNull();
461
+ });
462
+ }
463
+ });
464
+
465
+ /*
466
+ * ===========================================================================
467
+ * GROUP E - window touching midnight (18:00-00:00 == overnight ending at 00:00)
468
+ * ===========================================================================
469
+ */
470
+
471
+ describe("Daily window touching midnight 18:00-00:00 (local)", () => {
472
+ test("covers 18:00 to end-of-day with no phantom morning segment", () => {
473
+ const layer: LayerProps = makeLayer({
474
+ users: ["A"],
475
+ intervalType: EventInterval.Day,
476
+ intervalCount: 1,
477
+ restriction: dailyLocal(18, 0, 0, 0),
478
+ start: MON_JAN6,
479
+ });
480
+ const ev: Array<CalendarEvent> = expand(layer, MON_JAN6, jan(9, 0, 0, 0));
481
+ for (const d of [6, 7, 8]) {
482
+ expect(coveringTitle(ev, jan(d, 18, 0, 1))).toBe("A");
483
+ expect(coveringTitle(ev, jan(d, 23, 59, 30))).toBe("A");
484
+ // No coverage in the morning/day: the wrap "morning" is zero-length.
485
+ expect(coveringTitle(ev, jan(d, 0, 30, 0))).toBeNull();
486
+ expect(coveringTitle(ev, jan(d, 3, 0, 0))).toBeNull();
487
+ expect(coveringTitle(ev, jan(d, 12, 0, 0))).toBeNull();
488
+ expect(coveringTitle(ev, jan(d, 17, 59, 0))).toBeNull();
489
+ }
490
+ });
491
+ });
492
+
493
+ /*
494
+ * ===========================================================================
495
+ * GROUP F - multi-day rotation events (weekly & monthly) with a daily restriction
496
+ * ===========================================================================
497
+ */
498
+
499
+ describe("Multi-day rotation events with a daily 09:00-17:00 restriction", () => {
500
+ test("weekly rotation, 2 users: every day of week 1 is A, every day of week 2 is B", () => {
501
+ const layer: LayerProps = makeLayer({
502
+ users: ["A", "B"],
503
+ intervalType: EventInterval.Week,
504
+ intervalCount: 1,
505
+ restriction: dailyLocal(9, 0, 17, 0),
506
+ start: MON_JAN6,
507
+ });
508
+ const ev: Array<CalendarEvent> = expand(layer, MON_JAN6, jan(20, 0, 0, 0));
509
+ for (let d: number = 0; d < 7; d++) {
510
+ expect(coveringTitle(ev, jan(6 + d, 12, 0, 0))).toBe("A");
511
+ expect(coveringTitle(ev, jan(13 + d, 12, 0, 0))).toBe("B");
512
+ // Off-hours still uncovered even mid-period.
513
+ expect(coveringTitle(ev, jan(6 + d, 8, 30, 0))).toBeNull();
514
+ expect(coveringTitle(ev, jan(6 + d, 18, 0, 0))).toBeNull();
515
+ }
516
+ });
517
+
518
+ test("weekly rotation, 3 users: consecutive weeks are A, B, C", () => {
519
+ const layer: LayerProps = makeLayer({
520
+ users: ["A", "B", "C"],
521
+ intervalType: EventInterval.Week,
522
+ intervalCount: 1,
523
+ restriction: dailyLocal(9, 0, 17, 0),
524
+ start: MON_JAN6,
525
+ });
526
+ const ev: Array<CalendarEvent> = expand(layer, MON_JAN6, jan(27, 0, 0, 0));
527
+ expect(coveringTitle(ev, jan(8, 12, 0, 0))).toBe("A"); // week 1
528
+ expect(coveringTitle(ev, jan(15, 12, 0, 0))).toBe("B"); // week 2
529
+ expect(coveringTitle(ev, jan(22, 12, 0, 0))).toBe("C"); // week 3
530
+ });
531
+
532
+ test("weekly rotation x2 (fortnightly): first two weeks A, next two weeks B", () => {
533
+ const layer: LayerProps = makeLayer({
534
+ users: ["A", "B"],
535
+ intervalType: EventInterval.Week,
536
+ intervalCount: 2,
537
+ restriction: dailyLocal(9, 0, 17, 0),
538
+ start: MON_JAN6,
539
+ });
540
+ const ev: Array<CalendarEvent> = expand(layer, MON_JAN6, jan(34, 0, 0, 0));
541
+ expect(coveringTitle(ev, jan(8, 12, 0, 0))).toBe("A"); // wk1
542
+ expect(coveringTitle(ev, jan(15, 12, 0, 0))).toBe("A"); // wk2
543
+ expect(coveringTitle(ev, jan(22, 12, 0, 0))).toBe("B"); // wk3
544
+ expect(coveringTitle(ev, jan(29, 12, 0, 0))).toBe("B"); // wk4
545
+ });
546
+
547
+ test("monthly rotation, 2 users: Jan is A, Feb is B, Mar is A (mid-month sampling)", () => {
548
+ const layer: LayerProps = makeLayer({
549
+ users: ["A", "B"],
550
+ intervalType: EventInterval.Month,
551
+ intervalCount: 1,
552
+ restriction: dailyLocal(9, 0, 17, 0),
553
+ start: new Date(2026, 0, 1, 0, 0, 0),
554
+ });
555
+ const ev: Array<CalendarEvent> = expand(
556
+ layer,
557
+ new Date(2026, 0, 1, 0, 0, 0),
558
+ new Date(2026, 3, 1, 0, 0, 0),
559
+ );
560
+ expect(coveringTitle(ev, new Date(2026, 0, 15, 12, 0, 0))).toBe("A");
561
+ expect(coveringTitle(ev, new Date(2026, 0, 3, 12, 0, 0))).toBe("A");
562
+ expect(coveringTitle(ev, new Date(2026, 1, 15, 12, 0, 0))).toBe("B");
563
+ expect(coveringTitle(ev, new Date(2026, 2, 15, 12, 0, 0))).toBe("A");
564
+ // Off-hours uncovered inside each month.
565
+ expect(coveringTitle(ev, new Date(2026, 1, 15, 8, 30, 0))).toBeNull();
566
+ expect(coveringTitle(ev, new Date(2026, 1, 15, 18, 0, 0))).toBeNull();
567
+ });
568
+ });
569
+
570
+ /*
571
+ * ===========================================================================
572
+ * GROUP G - rotation periods that start inside / at / after the daily window
573
+ * ===========================================================================
574
+ */
575
+
576
+ describe("Rotation period start relative to the daily window", () => {
577
+ test("start INSIDE the window (12:00): first day trims to [12:00,17:00], last day [09:00,12:00]", () => {
578
+ const layer: LayerProps = makeLayer({
579
+ users: ["A", "B"],
580
+ intervalType: EventInterval.Day,
581
+ intervalCount: 1,
582
+ restriction: dailyLocal(9, 0, 17, 0),
583
+ start: jan(6, 12, 0, 0),
584
+ });
585
+ const ev: Array<CalendarEvent> = expand(
586
+ layer,
587
+ jan(6, 12, 0, 0),
588
+ jan(9, 0, 0, 0),
589
+ );
590
+ // Period 1 (A): partial first afternoon + partial last morning.
591
+ expect(coveringTitle(ev, jan(6, 15, 0, 0))).toBe("A");
592
+ expect(coveringTitle(ev, jan(7, 10, 0, 0))).toBe("A");
593
+ // Period 2 (B) begins at Jan7 12:00.
594
+ expect(coveringTitle(ev, jan(7, 13, 0, 0))).toBe("B");
595
+ // Gaps.
596
+ expect(coveringTitle(ev, jan(6, 18, 0, 0))).toBeNull();
597
+ expect(coveringTitle(ev, jan(7, 8, 0, 0))).toBeNull();
598
+ });
599
+
600
+ test("start AT the window open (09:00): first day covered from 09:00", () => {
601
+ const layer: LayerProps = makeLayer({
602
+ users: ["A", "B"],
603
+ intervalType: EventInterval.Day,
604
+ intervalCount: 1,
605
+ restriction: dailyLocal(9, 0, 17, 0),
606
+ start: jan(6, 9, 0, 0),
607
+ });
608
+ const ev: Array<CalendarEvent> = expand(
609
+ layer,
610
+ jan(6, 9, 0, 0),
611
+ jan(8, 0, 0, 0),
612
+ );
613
+ expect(coveringTitle(ev, jan(6, 9, 0, 30))).toBe("A");
614
+ expect(coveringTitle(ev, jan(6, 16, 59, 0))).toBe("A");
615
+ expect(coveringTitle(ev, jan(6, 17, 0, 1))).toBeNull();
616
+ expect(coveringTitle(ev, jan(7, 12, 0, 0))).toBe("B");
617
+ });
618
+
619
+ test("start AFTER the window close (20:00): first calendar day has no coverage, next day is still this user's turn", () => {
620
+ const layer: LayerProps = makeLayer({
621
+ users: ["A", "B"],
622
+ intervalType: EventInterval.Day,
623
+ intervalCount: 1,
624
+ restriction: dailyLocal(9, 0, 17, 0),
625
+ start: jan(6, 20, 0, 0),
626
+ });
627
+ const ev: Array<CalendarEvent> = expand(
628
+ layer,
629
+ jan(6, 20, 0, 0),
630
+ jan(9, 0, 0, 0),
631
+ );
632
+ // Nothing on Jan6 evening (window already closed).
633
+ expect(coveringTitle(ev, jan(6, 22, 0, 0))).toBeNull();
634
+ // Period 1 (A) still covers its next day (Jan7); period 2 (B) covers Jan8.
635
+ expect(coveringTitle(ev, jan(7, 12, 0, 0))).toBe("A");
636
+ expect(coveringTitle(ev, jan(8, 12, 0, 0))).toBe("B");
637
+ });
638
+ });
639
+
640
+ /*
641
+ * ===========================================================================
642
+ * GROUP H - F2: post-window evening gap resolves the correct NEXT covered user
643
+ * ===========================================================================
644
+ */
645
+
646
+ describe("F2 post-window gap: windowed 'next' matches the full-expansion next shift", () => {
647
+ const counts: number[] = [1, 2, 3];
648
+ const userSets: string[][] = [
649
+ ["A", "B"],
650
+ ["A", "B", "C"],
651
+ ];
652
+
653
+ for (const count of counts) {
654
+ for (const users of userSets) {
655
+ test(`x${count} daily, ${users.length} users, 09-17: evening gaps never go off-by-one over 10 days`, () => {
656
+ const layer: LayerProps = makeLayer({
657
+ users,
658
+ intervalType: EventInterval.Day,
659
+ intervalCount: count,
660
+ restriction: dailyLocal(9, 0, 17, 0),
661
+ start: MON_JAN6,
662
+ });
663
+ const full: Array<CalendarEvent> = expand(
664
+ layer,
665
+ MON_JAN6,
666
+ jan(6 + 14, 0, 0, 0),
667
+ );
668
+ const mismatches: string[] = [];
669
+ for (let d: number = 0; d < 10; d++) {
670
+ const gap: Date = jan(6 + d, 20, 0, 0); // evening, window closed
671
+ const expected: CalendarEvent | undefined = nextAfter(full, gap);
672
+ const got: CalendarEvent | undefined = windowedNext(layer, gap);
673
+ if ((got?.title ?? null) !== (expected?.title ?? null)) {
674
+ mismatches.push(
675
+ `day ${6 + d}: windowed=${got?.title ?? null} expectedNext=${expected?.title ?? null}`,
676
+ );
677
+ }
678
+ }
679
+ expect(mismatches).toEqual([]);
680
+ });
681
+ }
682
+ }
683
+
684
+ test("x1 daily [A,B]: evening gaps resolve to next day's user (B,A,B,A...)", () => {
685
+ const layer: LayerProps = makeLayer({
686
+ users: ["A", "B"],
687
+ intervalType: EventInterval.Day,
688
+ intervalCount: 1,
689
+ restriction: dailyLocal(9, 0, 17, 0),
690
+ start: MON_JAN6,
691
+ });
692
+ expect(windowedNext(layer, jan(6, 20, 0, 0))?.title).toBe("B");
693
+ expect(windowedNext(layer, jan(7, 20, 0, 0))?.title).toBe("A");
694
+ expect(windowedNext(layer, jan(8, 20, 0, 0))?.title).toBe("B");
695
+ expect(windowedNext(layer, jan(9, 20, 0, 0))?.title).toBe("A");
696
+ // And the resolved next event starts the following morning.
697
+ const n: CalendarEvent | undefined = windowedNext(layer, jan(6, 20, 0, 0));
698
+ expect(n?.start.getDate()).toBe(7);
699
+ });
700
+
701
+ test("x2 daily [A,B]: last covered day evening advances, earlier day stays in the period", () => {
702
+ const layer: LayerProps = makeLayer({
703
+ users: ["A", "B"],
704
+ intervalType: EventInterval.Day,
705
+ intervalCount: 2,
706
+ restriction: dailyLocal(9, 0, 17, 0),
707
+ start: MON_JAN6,
708
+ });
709
+ /*
710
+ * Period 1 = Jan6..Jan7 = A; period 2 = Jan8..Jan9 = B.
711
+ * Jan6 evening: still A's period, next covered shift is Jan7 (A).
712
+ */
713
+ expect(windowedNext(layer, jan(6, 20, 0, 0))?.title).toBe("A");
714
+ // Jan7 evening: A's period is over, next covered shift is Jan8 (B).
715
+ expect(windowedNext(layer, jan(7, 20, 0, 0))?.title).toBe("B");
716
+ });
717
+
718
+ test("non-gap resolution: during/before the window returns the CURRENT day's user", () => {
719
+ const layer: LayerProps = makeLayer({
720
+ users: ["A", "B"],
721
+ intervalType: EventInterval.Day,
722
+ intervalCount: 1,
723
+ restriction: dailyLocal(9, 0, 17, 0),
724
+ start: MON_JAN6,
725
+ });
726
+ expect(windowedNext(layer, jan(6, 8, 0, 0))?.title).toBe("A"); // before open
727
+ expect(windowedNext(layer, jan(6, 12, 0, 0))?.title).toBe("A"); // inside
728
+ expect(windowedNext(layer, jan(7, 12, 0, 0))?.title).toBe("B"); // inside next day
729
+ });
730
+ });
731
+
732
+ /*
733
+ * ===========================================================================
734
+ * GROUP I - DST spring-forward & fall-back + server-vs-schedule divergence
735
+ * ===========================================================================
736
+ */
737
+
738
+ describe("DST wall-clock stability (America/New_York, schedule zone)", () => {
739
+ test("09:00-17:00 stays 09:00-17:00 across the spring-forward week (Sun 2026-03-08)", () => {
740
+ const start: Date = tzInstant("2026-03-06 00:00", NY); // Fri
741
+ const layer: LayerProps = {
742
+ users: [user("A")],
743
+ startDateTimeOfLayer: start,
744
+ handOffTime: start,
745
+ restrictionTimes: dailyTz("2026-03-06 09:00", "2026-03-06 17:00", NY),
746
+ rotation: rotationOf(EventInterval.Day, 1),
747
+ timezone: NY,
748
+ };
749
+ const ev: Array<CalendarEvent> = expand(
750
+ layer,
751
+ start,
752
+ tzInstant("2026-03-12 00:00", NY),
753
+ );
754
+ const opens: Array<CalendarEvent> = ev.filter((e: CalendarEvent) => {
755
+ return hhmm(e.start, NY) === "09:00";
756
+ });
757
+ expect(opens.length).toBeGreaterThanOrEqual(5); // Fri..Wed
758
+ for (const w of opens) {
759
+ expect(hhmm(w.start, NY)).toBe("09:00");
760
+ expect(hhmm(w.end, NY)).toBe("17:00");
761
+ }
762
+ // Concrete coverage on the DST-transition Sunday.
763
+ expect(coveringTitle(ev, tzInstant("2026-03-08 12:00", NY))).toBe("A");
764
+ expect(coveringTitle(ev, tzInstant("2026-03-08 08:00", NY))).toBeNull();
765
+ expect(coveringTitle(ev, tzInstant("2026-03-08 18:00", NY))).toBeNull();
766
+ });
767
+
768
+ test("09:00-17:00 stays 09:00-17:00 across the fall-back week (Sun 2026-11-01)", () => {
769
+ const start: Date = tzInstant("2026-10-30 00:00", NY); // Fri
770
+ const layer: LayerProps = {
771
+ users: [user("A")],
772
+ startDateTimeOfLayer: start,
773
+ handOffTime: start,
774
+ restrictionTimes: dailyTz("2026-10-30 09:00", "2026-10-30 17:00", NY),
775
+ rotation: rotationOf(EventInterval.Day, 1),
776
+ timezone: NY,
777
+ };
778
+ const ev: Array<CalendarEvent> = expand(
779
+ layer,
780
+ start,
781
+ tzInstant("2026-11-04 00:00", NY),
782
+ );
783
+ const opens: Array<CalendarEvent> = ev.filter((e: CalendarEvent) => {
784
+ return hhmm(e.start, NY) === "09:00";
785
+ });
786
+ expect(opens.length).toBeGreaterThanOrEqual(5);
787
+ for (const w of opens) {
788
+ expect(hhmm(w.end, NY)).toBe("17:00");
789
+ }
790
+ expect(coveringTitle(ev, tzInstant("2026-11-01 12:00", NY))).toBe("A");
791
+ expect(coveringTitle(ev, tzInstant("2026-11-01 08:00", NY))).toBeNull();
792
+ });
793
+
794
+ test("overnight 22:00-06:00 keeps its wall-clock band across spring-forward", () => {
795
+ const start: Date = tzInstant("2026-03-06 00:00", NY);
796
+ const layer: LayerProps = {
797
+ users: [user("A")],
798
+ startDateTimeOfLayer: start,
799
+ handOffTime: start,
800
+ restrictionTimes: dailyTz("2026-03-06 22:00", "2026-03-06 06:00", NY),
801
+ rotation: rotationOf(EventInterval.Day, 1),
802
+ timezone: NY,
803
+ };
804
+ const ev: Array<CalendarEvent> = expand(
805
+ layer,
806
+ start,
807
+ tzInstant("2026-03-10 00:00", NY),
808
+ );
809
+ // Morning tails close at 06:00 NY, nights open at 22:00 NY.
810
+ const mornings: Array<CalendarEvent> = ev.filter((e: CalendarEvent) => {
811
+ return hhmm(e.start, NY) === "00:00";
812
+ });
813
+ const nights: Array<CalendarEvent> = ev.filter((e: CalendarEvent) => {
814
+ return hhmm(e.start, NY) === "22:00";
815
+ });
816
+ expect(mornings.length).toBeGreaterThanOrEqual(3);
817
+ expect(nights.length).toBeGreaterThanOrEqual(3);
818
+ for (const m of mornings) {
819
+ expect(hhmm(m.end, NY)).toBe("06:00");
820
+ }
821
+ // Coverage on the DST Sunday: 03:00 NY (morning) covered, midday uncovered.
822
+ expect(coveringTitle(ev, tzInstant("2026-03-08 03:00", NY))).toBe("A");
823
+ expect(coveringTitle(ev, tzInstant("2026-03-07 23:00", NY))).toBe("A");
824
+ expect(coveringTitle(ev, tzInstant("2026-03-08 12:00", NY))).toBeNull();
825
+ });
826
+
827
+ test("server-vs-schedule divergence: a Kolkata schedule resolves to its own wall-clock", () => {
828
+ // Authored + resolved in Asia/Kolkata (UTC+5:30, no DST); process zone differs.
829
+ const start: Date = tzInstant("2026-02-16 00:00", KOLKATA);
830
+ const layer: LayerProps = {
831
+ users: [user("A")],
832
+ startDateTimeOfLayer: start,
833
+ handOffTime: start,
834
+ restrictionTimes: dailyTz(
835
+ "2026-02-16 09:30",
836
+ "2026-02-16 18:30",
837
+ KOLKATA,
838
+ ),
839
+ rotation: rotationOf(EventInterval.Day, 1),
840
+ timezone: KOLKATA,
841
+ };
842
+ const ev: Array<CalendarEvent> = expand(
843
+ layer,
844
+ start,
845
+ tzInstant("2026-02-19 00:00", KOLKATA),
846
+ );
847
+ const opens: Array<CalendarEvent> = ev.filter((e: CalendarEvent) => {
848
+ return hhmm(e.start, KOLKATA) === "09:30";
849
+ });
850
+ expect(opens.length).toBeGreaterThanOrEqual(2);
851
+ for (const w of opens) {
852
+ expect(hhmm(w.end, KOLKATA)).toBe("18:30");
853
+ }
854
+ expect(coveringTitle(ev, tzInstant("2026-02-17 12:00", KOLKATA))).toBe("A");
855
+ expect(
856
+ coveringTitle(ev, tzInstant("2026-02-17 08:00", KOLKATA)),
857
+ ).toBeNull();
858
+ expect(
859
+ coveringTitle(ev, tzInstant("2026-02-17 20:00", KOLKATA)),
860
+ ).toBeNull();
861
+ });
862
+ });
863
+
864
+ /*
865
+ * ===========================================================================
866
+ * GROUP J - exhaustive matrix of same-day window placements
867
+ * ===========================================================================
868
+ */
869
+
870
+ describe("Exhaustive same-day window placements (local, daily rotation)", () => {
871
+ const windows: Array<[number, number]> = [
872
+ [1, 5],
873
+ [6, 10],
874
+ [8, 12],
875
+ [9, 17],
876
+ [10, 14],
877
+ [13, 20],
878
+ [19, 23],
879
+ ];
880
+
881
+ for (const [sh, eh] of windows) {
882
+ test(`${sh}:00-${eh}:00 covers its band and excludes the shoulders`, () => {
883
+ const layer: LayerProps = makeLayer({
884
+ users: ["A"],
885
+ intervalType: EventInterval.Day,
886
+ intervalCount: 1,
887
+ restriction: dailyLocal(sh, 0, eh, 0),
888
+ start: MON_JAN6,
889
+ });
890
+ const ev: Array<CalendarEvent> = expand(layer, MON_JAN6, jan(9, 0, 0, 0));
891
+ for (const d of [6, 7, 8]) {
892
+ const mid: number = Math.floor((sh + eh) / 2);
893
+ expect(coveringTitle(ev, jan(d, mid, 30, 0))).toBe("A"); // inside
894
+ expect(coveringTitle(ev, jan(d, sh, 0, 1))).toBe("A"); // just after open
895
+ expect(coveringTitle(ev, jan(d, eh, 0, 1))).toBeNull(); // just after close
896
+ expect(coveringTitle(ev, jan(d, sh - 1, 30, 0))).toBeNull(); // before open
897
+ }
898
+ });
899
+ }
900
+ });