@oneuptime/common 11.3.25 → 11.3.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (397) hide show
  1. package/Models/DatabaseModels/AIConversation.ts +322 -0
  2. package/Models/DatabaseModels/AIConversationMessage.ts +495 -0
  3. package/Models/DatabaseModels/AIRun.ts +584 -0
  4. package/Models/DatabaseModels/AIRunEvent.ts +443 -0
  5. package/Models/DatabaseModels/IncidentEpisodeRoleMember.ts +1 -1
  6. package/Models/DatabaseModels/IncidentMember.ts +1 -1
  7. package/Models/DatabaseModels/IncomingCallPolicy.ts +13 -0
  8. package/Models/DatabaseModels/Index.ts +11 -0
  9. package/Models/DatabaseModels/LlmLog.ts +26 -0
  10. package/Models/DatabaseModels/LlmProvider.ts +2 -2
  11. package/Models/DatabaseModels/MigrationFailure.ts +170 -0
  12. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +51 -2
  13. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +44 -0
  14. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +6 -0
  15. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.ts +6 -0
  16. package/Models/DatabaseModels/OnCallDutyPolicyTimeLog.ts +1 -1
  17. package/Models/DatabaseModels/OnCallDutyPolicyUserOverride.ts +2 -2
  18. package/Server/API/AIChatAPI.ts +693 -0
  19. package/Server/API/AlertAPI.ts +1 -1
  20. package/Server/API/IncidentAPI.ts +2 -2
  21. package/Server/API/IncidentEpisodeAPI.ts +1 -1
  22. package/Server/API/LlmProviderAPI.ts +170 -0
  23. package/Server/API/MicrosoftTeamsAPI.ts +5 -0
  24. package/Server/API/ScheduledMaintenanceAPI.ts +1 -1
  25. package/Server/API/SlackAPI.ts +663 -0
  26. package/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.ts +245 -0
  27. package/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.ts +36 -0
  28. package/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.ts +46 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.ts +119 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.ts +25 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.ts +81 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.ts +25 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.ts +35 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +16 -0
  35. package/Server/Infrastructure/PostgresDatabase.ts +11 -0
  36. package/Server/Services/AIConversationMessageService.ts +39 -0
  37. package/Server/Services/AIConversationService.ts +63 -0
  38. package/Server/Services/AIRunEventService.ts +39 -0
  39. package/Server/Services/AIRunService.ts +39 -0
  40. package/Server/Services/AIService.ts +127 -33
  41. package/Server/Services/AnalyticsDatabaseService.ts +10 -1
  42. package/Server/Services/DatabaseService.ts +29 -1
  43. package/Server/Services/IncidentService.ts +1 -1
  44. package/Server/Services/IncomingCallPolicyEscalationRuleService.ts +75 -2
  45. package/Server/Services/IncomingCallPolicyService.ts +42 -1
  46. package/Server/Services/Index.ts +2 -0
  47. package/Server/Services/LlmProviderService.ts +110 -0
  48. package/Server/Services/MigrationFailureService.ts +9 -0
  49. package/Server/Services/MonitorProbeService.ts +33 -7
  50. package/Server/Services/MonitorService.ts +19 -7
  51. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  52. package/Server/Services/OnCallDutyPolicyEscalationRuleService.ts +185 -41
  53. package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +23 -15
  54. package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +39 -0
  55. package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +148 -0
  56. package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +11 -2
  57. package/Server/Services/OnCallDutyPolicyScheduleService.ts +502 -26
  58. package/Server/Services/OnCallDutyPolicyTimeLogService.ts +49 -7
  59. package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +218 -4
  60. package/Server/Services/ProjectCallSMSConfigService.ts +82 -1
  61. package/Server/Services/ProjectService.ts +24 -0
  62. package/Server/Services/TeamMemberService.ts +6 -0
  63. package/Server/Services/UserNotificationRuleService.ts +13 -42
  64. package/Server/Services/UserOnCallLogService.ts +90 -0
  65. package/Server/Services/UserOnCallLogTimelineService.ts +43 -1
  66. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +74 -9
  67. package/Server/Types/Database/QueryUtil.ts +57 -0
  68. package/Server/Utils/AI/AIChatPrivacyFilter.ts +28 -0
  69. package/Server/Utils/AI/AlertAIContextBuilder.ts +6 -1
  70. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +1066 -0
  71. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +239 -0
  72. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +51 -0
  73. package/Server/Utils/AI/IncidentAIContextBuilder.ts +18 -3
  74. package/Server/Utils/AI/IncidentEpisodeAIContextBuilder.ts +6 -1
  75. package/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.ts +12 -2
  76. package/Server/Utils/AI/Toolbox/AlertTools.ts +201 -0
  77. package/Server/Utils/AI/Toolbox/AlertWriteTools.ts +174 -0
  78. package/Server/Utils/AI/Toolbox/ContextTools.ts +189 -0
  79. package/Server/Utils/AI/Toolbox/ExceptionTools.ts +149 -0
  80. package/Server/Utils/AI/Toolbox/IncidentTools.ts +386 -0
  81. package/Server/Utils/AI/Toolbox/IncidentWriteTools.ts +350 -0
  82. package/Server/Utils/AI/Toolbox/Index.ts +231 -0
  83. package/Server/Utils/AI/Toolbox/LogTools.ts +339 -0
  84. package/Server/Utils/AI/Toolbox/MetricTools.ts +179 -0
  85. package/Server/Utils/AI/Toolbox/MonitorTools.ts +193 -0
  86. package/Server/Utils/AI/Toolbox/RecentChangesTools.ts +208 -0
  87. package/Server/Utils/AI/Toolbox/Serializer.ts +299 -0
  88. package/Server/Utils/AI/Toolbox/ToolTypes.ts +249 -0
  89. package/Server/Utils/AI/Toolbox/TraceTools.ts +402 -0
  90. package/Server/Utils/AI/Toolbox/WidgetBuilder.ts +189 -0
  91. package/Server/Utils/Database/MigrationFailureLog.ts +240 -0
  92. package/Server/Utils/IncomingCallPhoneNumber.ts +41 -0
  93. package/Server/Utils/LLM/LLMService.ts +605 -96
  94. package/Server/Utils/Telemetry/IoTSnapshotScan.ts +12 -9
  95. package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +12 -9
  96. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +512 -19
  97. package/Server/Utils/Workspace/Slack/Slack.ts +80 -0
  98. package/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
  99. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +0 -8
  100. package/Tests/Server/Services/OnCallDutyPolicyScheduleLayerUserReorder.test.ts +233 -0
  101. package/Tests/Server/Services/OnCallDutyPolicyTimeLogServiceScoping.test.ts +242 -0
  102. package/Tests/Server/Services/OnCallDutyPolicyUserOverrideEdit.test.ts +198 -0
  103. package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +495 -0
  104. package/Tests/Server/Types/Database/QueryUtil.test.ts +74 -0
  105. package/Tests/Server/Utils/AI/AIChatModelACL.test.ts +42 -0
  106. package/Tests/Server/Utils/AI/ChatAgentHelpers.test.ts +89 -0
  107. package/Tests/Server/Utils/AI/LLMServiceBaseUrl.test.ts +139 -0
  108. package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +386 -0
  109. package/Tests/Server/Utils/AI/ToolArgsGetTimeRange.test.ts +62 -0
  110. package/Tests/Server/Utils/AI/ToolArgsScopeServiceIds.test.ts +79 -0
  111. package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +223 -0
  112. package/Tests/Types/Billing/SubscriptionStatus.test.ts +127 -0
  113. package/Tests/Types/DateExhaustiveTimezone.test.ts +700 -0
  114. package/Tests/Types/DateTimezoneWallClock.test.ts +99 -0
  115. package/Tests/Types/Events/Recurring.test.ts +22 -9
  116. package/Tests/Types/Metrics/RecordingRuleDefinition.test.ts +213 -0
  117. package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixes.test.ts +291 -0
  118. package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixesRound2.test.ts +398 -0
  119. package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeekendGap.test.ts +114 -0
  120. package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeeklyDayShift.test.ts +76 -0
  121. package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyMutation.test.ts +128 -0
  122. package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyProbe.test.ts +203 -0
  123. package/Tests/Types/OnCallDutyPolicy/LayerUtilDiffFuzz.test.ts +322 -0
  124. package/Tests/Types/OnCallDutyPolicy/LayerUtilEdgeCases.test.ts +280 -0
  125. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveCurrentUser.test.ts +863 -0
  126. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveDaily.test.ts +900 -0
  127. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveMultiLayer.test.ts +1104 -0
  128. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +957 -0
  129. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveWeekly.test.ts +1095 -0
  130. package/Tests/Types/OnCallDutyPolicy/LayerUtilInvariants.test.ts +232 -0
  131. package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +227 -0
  132. package/Tests/Types/OnCallDutyPolicy/LayerUtilMonthYearAudit.test.ts +255 -0
  133. package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerAudit.test.ts +388 -0
  134. package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerEdge.test.ts +300 -0
  135. package/Tests/Types/OnCallDutyPolicy/LayerUtilOvernightDSTAudit.test.ts +158 -0
  136. package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapFix.test.ts +253 -0
  137. package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapRepro.test.ts +157 -0
  138. package/Tests/Types/OnCallDutyPolicy/LayerUtilRotationFixes.test.ts +414 -0
  139. package/Tests/Types/OnCallDutyPolicy/LayerUtilTimezone.test.ts +243 -0
  140. package/Tests/Types/OnCallDutyPolicy/LayerUtilWeekendGapRepro.test.ts +112 -0
  141. package/Tests/Types/OnCallDutyPolicy/OverrideLensAudit.test.ts +117 -0
  142. package/Tests/Types/OnCallDutyPolicy/RestrictionTimes.test.ts +8 -6
  143. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesDefaultDayAudit.test.ts +112 -0
  144. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesExhaustive.test.ts +821 -0
  145. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesMutationInvariant.test.ts +886 -0
  146. package/Tests/Types/OnCallDutyPolicy/UserOverrideUtil.test.ts +255 -0
  147. package/Tests/Types/OnCallDutyPolicy/UserOverrideUtilExhaustive.test.ts +1126 -0
  148. package/Types/AI/AIChatMessageRole.ts +6 -0
  149. package/Types/AI/AIChatMessageStatus.ts +32 -0
  150. package/Types/AI/AIChatPermissionMode.ts +69 -0
  151. package/Types/AI/AIChatTypes.ts +231 -0
  152. package/Types/AI/AIRunEventType.ts +17 -0
  153. package/Types/AI/AIRunStatus.ts +28 -0
  154. package/Types/AI/AIRunType.ts +6 -0
  155. package/Types/Date.ts +200 -15
  156. package/Types/LLM/LlmType.ts +6 -0
  157. package/Types/OnCallDutyPolicy/Layer.ts +790 -260
  158. package/Types/OnCallDutyPolicy/RestrictionTimes.ts +39 -13
  159. package/Types/OnCallDutyPolicy/UserOverrideUtil.ts +21 -5
  160. package/UI/Components/Events/RecurringFieldElement.tsx +16 -0
  161. package/UI/Components/Header/HeaderIconDropdownButton.tsx +22 -12
  162. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +35 -3
  163. package/UI/Components/ModelFormModal/ModelFormModal.tsx +8 -0
  164. package/UI/Components/Page/Page.tsx +14 -0
  165. package/UI/Components/RadioButtons/BasicRadioButtons.tsx +1 -1
  166. package/UI/Utils/LlmTypeDropdownOptions.ts +40 -0
  167. package/UI/Utils/TestLLMProvider.ts +59 -0
  168. package/build/dist/Models/DatabaseModels/AIConversation.js +345 -0
  169. package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -0
  170. package/build/dist/Models/DatabaseModels/AIConversationMessage.js +521 -0
  171. package/build/dist/Models/DatabaseModels/AIConversationMessage.js.map +1 -0
  172. package/build/dist/Models/DatabaseModels/AIRun.js +619 -0
  173. package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -0
  174. package/build/dist/Models/DatabaseModels/AIRunEvent.js +469 -0
  175. package/build/dist/Models/DatabaseModels/AIRunEvent.js.map +1 -0
  176. package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js +1 -1
  177. package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js.map +1 -1
  178. package/build/dist/Models/DatabaseModels/IncidentMember.js +1 -1
  179. package/build/dist/Models/DatabaseModels/IncidentMember.js.map +1 -1
  180. package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js +10 -0
  181. package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js.map +1 -1
  182. package/build/dist/Models/DatabaseModels/Index.js +10 -0
  183. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  184. package/build/dist/Models/DatabaseModels/LlmLog.js +28 -0
  185. package/build/dist/Models/DatabaseModels/LlmLog.js.map +1 -1
  186. package/build/dist/Models/DatabaseModels/LlmProvider.js +2 -2
  187. package/build/dist/Models/DatabaseModels/LlmProvider.js.map +1 -1
  188. package/build/dist/Models/DatabaseModels/MigrationFailure.js +196 -0
  189. package/build/dist/Models/DatabaseModels/MigrationFailure.js.map +1 -0
  190. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +52 -2
  191. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  192. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +45 -0
  193. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  194. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +6 -0
  195. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
  196. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js +6 -0
  197. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js.map +1 -1
  198. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js +1 -1
  199. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js.map +1 -1
  200. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js +2 -2
  201. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js.map +1 -1
  202. package/build/dist/Server/API/AIChatAPI.js +498 -0
  203. package/build/dist/Server/API/AIChatAPI.js.map +1 -0
  204. package/build/dist/Server/API/AlertAPI.js +1 -1
  205. package/build/dist/Server/API/IncidentAPI.js +2 -2
  206. package/build/dist/Server/API/IncidentEpisodeAPI.js +1 -1
  207. package/build/dist/Server/API/LlmProviderAPI.js +108 -1
  208. package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
  209. package/build/dist/Server/API/MicrosoftTeamsAPI.js +4 -0
  210. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  211. package/build/dist/Server/API/ScheduledMaintenanceAPI.js +1 -1
  212. package/build/dist/Server/API/SlackAPI.js +442 -0
  213. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  214. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js +96 -0
  215. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js.map +1 -0
  216. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js +25 -0
  217. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js.map +1 -0
  218. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js +31 -0
  219. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js.map +1 -0
  220. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js +46 -0
  221. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js.map +1 -0
  222. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js +18 -0
  223. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js.map +1 -0
  224. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js +38 -0
  225. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js.map +1 -0
  226. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js +16 -0
  227. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js.map +1 -0
  228. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js +18 -0
  229. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js.map +1 -0
  230. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +16 -0
  231. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  232. package/build/dist/Server/Infrastructure/PostgresDatabase.js +9 -0
  233. package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
  234. package/build/dist/Server/Services/AIConversationMessageService.js +34 -0
  235. package/build/dist/Server/Services/AIConversationMessageService.js.map +1 -0
  236. package/build/dist/Server/Services/AIConversationService.js +42 -0
  237. package/build/dist/Server/Services/AIConversationService.js.map +1 -0
  238. package/build/dist/Server/Services/AIRunEventService.js +34 -0
  239. package/build/dist/Server/Services/AIRunEventService.js.map +1 -0
  240. package/build/dist/Server/Services/AIRunService.js +34 -0
  241. package/build/dist/Server/Services/AIRunService.js.map +1 -0
  242. package/build/dist/Server/Services/AIService.js +84 -25
  243. package/build/dist/Server/Services/AIService.js.map +1 -1
  244. package/build/dist/Server/Services/AnalyticsDatabaseService.js +10 -1
  245. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  246. package/build/dist/Server/Services/DatabaseService.js +20 -1
  247. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  248. package/build/dist/Server/Services/IncidentService.js +1 -1
  249. package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js +55 -2
  250. package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js.map +1 -1
  251. package/build/dist/Server/Services/IncomingCallPolicyService.js +40 -0
  252. package/build/dist/Server/Services/IncomingCallPolicyService.js.map +1 -1
  253. package/build/dist/Server/Services/Index.js +2 -0
  254. package/build/dist/Server/Services/Index.js.map +1 -1
  255. package/build/dist/Server/Services/LlmProviderService.js +108 -0
  256. package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
  257. package/build/dist/Server/Services/MigrationFailureService.js +9 -0
  258. package/build/dist/Server/Services/MigrationFailureService.js.map +1 -0
  259. package/build/dist/Server/Services/MonitorProbeService.js +33 -7
  260. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  261. package/build/dist/Server/Services/MonitorService.js +12 -5
  262. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  263. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +81 -22
  264. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  265. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js +134 -31
  266. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js.map +1 -1
  267. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +21 -13
  268. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
  269. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +36 -0
  270. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
  271. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +121 -0
  272. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
  273. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +11 -2
  274. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
  275. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +409 -36
  276. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  277. package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js +52 -8
  278. package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js.map +1 -1
  279. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +170 -3
  280. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
  281. package/build/dist/Server/Services/ProjectCallSMSConfigService.js +72 -0
  282. package/build/dist/Server/Services/ProjectCallSMSConfigService.js.map +1 -1
  283. package/build/dist/Server/Services/ProjectService.js +25 -0
  284. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  285. package/build/dist/Server/Services/TeamMemberService.js +6 -0
  286. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  287. package/build/dist/Server/Services/UserNotificationRuleService.js +12 -30
  288. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  289. package/build/dist/Server/Services/UserOnCallLogService.js +75 -0
  290. package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
  291. package/build/dist/Server/Services/UserOnCallLogTimelineService.js +31 -1
  292. package/build/dist/Server/Services/UserOnCallLogTimelineService.js.map +1 -1
  293. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +50 -7
  294. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  295. package/build/dist/Server/Types/Database/QueryUtil.js +45 -0
  296. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  297. package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js +18 -0
  298. package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js.map +1 -0
  299. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js +6 -1
  300. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js.map +1 -1
  301. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +756 -0
  302. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -0
  303. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +165 -0
  304. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -0
  305. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +44 -0
  306. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -0
  307. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js +18 -3
  308. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js.map +1 -1
  309. package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js +6 -1
  310. package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js.map +1 -1
  311. package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js +12 -2
  312. package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js.map +1 -1
  313. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +167 -0
  314. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -0
  315. package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js +136 -0
  316. package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js.map +1 -0
  317. package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js +141 -0
  318. package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js.map +1 -0
  319. package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js +117 -0
  320. package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js.map +1 -0
  321. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +318 -0
  322. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -0
  323. package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js +280 -0
  324. package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js.map +1 -0
  325. package/build/dist/Server/Utils/AI/Toolbox/Index.js +153 -0
  326. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -0
  327. package/build/dist/Server/Utils/AI/Toolbox/LogTools.js +246 -0
  328. package/build/dist/Server/Utils/AI/Toolbox/LogTools.js.map +1 -0
  329. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +120 -0
  330. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -0
  331. package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js +158 -0
  332. package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js.map +1 -0
  333. package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js +165 -0
  334. package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js.map +1 -0
  335. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +228 -0
  336. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -0
  337. package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js +142 -0
  338. package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js.map +1 -0
  339. package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js +309 -0
  340. package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js.map +1 -0
  341. package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js +120 -0
  342. package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js.map +1 -0
  343. package/build/dist/Server/Utils/Database/MigrationFailureLog.js +174 -0
  344. package/build/dist/Server/Utils/Database/MigrationFailureLog.js.map +1 -0
  345. package/build/dist/Server/Utils/IncomingCallPhoneNumber.js +30 -0
  346. package/build/dist/Server/Utils/IncomingCallPhoneNumber.js.map +1 -0
  347. package/build/dist/Server/Utils/LLM/LLMService.js +452 -92
  348. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  349. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +11 -8
  350. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
  351. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +11 -8
  352. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
  353. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +378 -15
  354. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  355. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +59 -0
  356. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  357. package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
  358. package/build/dist/Types/AI/AIChatMessageRole.js +7 -0
  359. package/build/dist/Types/AI/AIChatMessageRole.js.map +1 -0
  360. package/build/dist/Types/AI/AIChatMessageStatus.js +27 -0
  361. package/build/dist/Types/AI/AIChatMessageStatus.js.map +1 -0
  362. package/build/dist/Types/AI/AIChatPermissionMode.js +53 -0
  363. package/build/dist/Types/AI/AIChatPermissionMode.js.map +1 -0
  364. package/build/dist/Types/AI/AIChatTypes.js +74 -0
  365. package/build/dist/Types/AI/AIChatTypes.js.map +1 -0
  366. package/build/dist/Types/AI/AIRunEventType.js +18 -0
  367. package/build/dist/Types/AI/AIRunEventType.js.map +1 -0
  368. package/build/dist/Types/AI/AIRunStatus.js +26 -0
  369. package/build/dist/Types/AI/AIRunStatus.js.map +1 -0
  370. package/build/dist/Types/AI/AIRunType.js +7 -0
  371. package/build/dist/Types/AI/AIRunType.js.map +1 -0
  372. package/build/dist/Types/Date.js +153 -15
  373. package/build/dist/Types/Date.js.map +1 -1
  374. package/build/dist/Types/LLM/LlmType.js +6 -0
  375. package/build/dist/Types/LLM/LlmType.js.map +1 -1
  376. package/build/dist/Types/OnCallDutyPolicy/Layer.js +602 -171
  377. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  378. package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js +27 -13
  379. package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js.map +1 -1
  380. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js +20 -5
  381. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js.map +1 -1
  382. package/build/dist/UI/Components/Events/RecurringFieldElement.js +13 -0
  383. package/build/dist/UI/Components/Events/RecurringFieldElement.js.map +1 -1
  384. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +10 -5
  385. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  386. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +18 -4
  387. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  388. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  389. package/build/dist/UI/Components/Page/Page.js +3 -1
  390. package/build/dist/UI/Components/Page/Page.js.map +1 -1
  391. package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js +1 -1
  392. package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js.map +1 -1
  393. package/build/dist/UI/Utils/LlmTypeDropdownOptions.js +38 -0
  394. package/build/dist/UI/Utils/LlmTypeDropdownOptions.js.map +1 -0
  395. package/build/dist/UI/Utils/TestLLMProvider.js +37 -0
  396. package/build/dist/UI/Utils/TestLLMProvider.js.map +1 -0
  397. package/package.json +1 -1
@@ -0,0 +1,863 @@
1
+ /**
2
+ * EXHAUSTIVE differential: the WINDOWED live current/next resolution must agree
3
+ * with a FULL expansion from layer start, across a broad config matrix.
4
+ *
5
+ * The live "who is on call now / next" path never expands from the layer's
6
+ * birthday; it opens a calendar window at the CURRENT instant and asks for the
7
+ * first (getNumberOfEvents:1) or first-two (getNumberOfEvents:2) events. That
8
+ * paging path is exercised two ways here:
9
+ * - LayerUtil.getEvents(now .. now+horizon, {getNumberOfEvents}) (per-layer)
10
+ * - LayerUtil.getMultiLayerEvents(now .. now+small, {getNumberOfEvents}) (roster)
11
+ *
12
+ * ORACLE: a full expansion from the layer start over a generous horizon is the
13
+ * ground truth. For any instant `at`, the on-call user resolved by the windowed
14
+ * path must equal the title of the EARLIEST full-expansion event whose end is
15
+ * after `at` — i.e. the event that COVERS `at` when `at` is inside a coverage
16
+ * window, or the NEXT covered event when `at` falls in a restriction gap. The
17
+ * first two windowed titles must match the first two such full events.
18
+ *
19
+ * The matrix is intervals x rotationCounts x userCounts x
20
+ * {no restriction, daily 09-17, weekly Mon-Fri} x tz {local, NY, Kolkata}.
21
+ *
22
+ * Sampling: instants are the MIDPOINTS of real full-expansion events (guaranteed
23
+ * strictly interior => off-boundary) and the MIDPOINTS of real restriction gaps
24
+ * (> 5 minutes, so the +/-1s rotation-boundary stitch is never sampled — that
25
+ * stitch is a known display artifact, not a paging bug, per the task).
26
+ *
27
+ * The suite is fully differential, so every assertion holds regardless of the
28
+ * process timezone; it is intended to be run under the default zone AND under
29
+ * TZ=UTC to exercise server-vs-schedule divergence.
30
+ */
31
+ import LayerUtil, { LayerProps } from "../../../Types/OnCallDutyPolicy/Layer";
32
+ import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
33
+ import RestrictionTimes, {
34
+ RestrictionType,
35
+ WeeklyResctriction,
36
+ } from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
37
+ import Recurring from "../../../Types/Events/Recurring";
38
+ import OneUptimeDate from "../../../Types/Date";
39
+ import User from "../../../Models/DatabaseModels/User";
40
+ import EventInterval from "../../../Types/Events/EventInterval";
41
+ import PositiveNumber from "../../../Types/PositiveNumber";
42
+ import DayOfWeek from "../../../Types/Day/DayOfWeek";
43
+
44
+ function user(id: string): User {
45
+ return {
46
+ id: {
47
+ toString: (): string => {
48
+ return id;
49
+ },
50
+ } as any,
51
+ } as User;
52
+ }
53
+
54
+ function noRestriction(): RestrictionTimes {
55
+ const r: RestrictionTimes = new RestrictionTimes();
56
+ r.restictionType = RestrictionType.None;
57
+ r.dayRestrictionTimes = null;
58
+ return r;
59
+ }
60
+
61
+ function dailyRestriction(
62
+ startHour: number,
63
+ endHour: number,
64
+ ): RestrictionTimes {
65
+ const r: RestrictionTimes = new RestrictionTimes();
66
+ r.restictionType = RestrictionType.Daily;
67
+ r.dayRestrictionTimes = {
68
+ startTime: OneUptimeDate.getDateWithCustomTime({
69
+ hours: startHour,
70
+ minutes: 0,
71
+ seconds: 0,
72
+ }),
73
+ endTime: OneUptimeDate.getDateWithCustomTime({
74
+ hours: endHour,
75
+ minutes: 0,
76
+ seconds: 0,
77
+ }),
78
+ };
79
+ return r;
80
+ }
81
+
82
+ /**
83
+ * Weekly "Monday through Friday" coverage: a single non-wrapping weekly window
84
+ * from Monday 00:00 to Saturday 00:00, leaving Saturday & Sunday as a gap. The
85
+ * anchor dates below are a real Monday / Saturday; getWeeklyRestrictionTimesForWeek
86
+ * re-derives the day-of-week (in the schedule zone) and moves the window into the
87
+ * event's week, so the calendar dates are only day-of-week anchors.
88
+ */
89
+ function weeklyMondayToFriday(): RestrictionTimes {
90
+ const weekly: WeeklyResctriction = {
91
+ startDay: DayOfWeek.Monday,
92
+ endDay: DayOfWeek.Saturday,
93
+ startTime: OneUptimeDate.fromString("2025-01-06T00:00:00.000Z"), // Monday
94
+ endTime: OneUptimeDate.fromString("2025-01-11T00:00:00.000Z"), // Saturday
95
+ };
96
+ const r: RestrictionTimes = new RestrictionTimes();
97
+ r.restictionType = RestrictionType.Weekly;
98
+ r.weeklyRestrictionTimes = [weekly];
99
+ return r;
100
+ }
101
+
102
+ type RestrictionKind = "none" | "daily09-17" | "weeklyMonFri";
103
+
104
+ function buildRestriction(kind: RestrictionKind): RestrictionTimes {
105
+ switch (kind) {
106
+ case "none":
107
+ return noRestriction();
108
+ case "daily09-17":
109
+ return dailyRestriction(9, 17);
110
+ case "weeklyMonFri":
111
+ return weeklyMondayToFriday();
112
+ default:
113
+ return noRestriction();
114
+ }
115
+ }
116
+
117
+ interface Config {
118
+ name: string;
119
+ intervalType: EventInterval;
120
+ intervalCount: number;
121
+ userIds: string[];
122
+ restrictionKind: RestrictionKind;
123
+ timezone: string | undefined;
124
+ start: Date;
125
+ }
126
+
127
+ function rotationOf(c: Config): Recurring {
128
+ const rot: Recurring = new Recurring();
129
+ rot.intervalType = c.intervalType;
130
+ rot.intervalCount = new PositiveNumber(c.intervalCount);
131
+ return rot;
132
+ }
133
+
134
+ function layerOf(c: Config): LayerProps {
135
+ const rot: Recurring = rotationOf(c);
136
+ const handoff: Date = Recurring.getNextDateInterval(c.start, rot);
137
+ return {
138
+ users: c.userIds.map(user),
139
+ startDateTimeOfLayer: c.start,
140
+ restrictionTimes: buildRestriction(c.restrictionKind),
141
+ handOffTime: handoff,
142
+ rotation: rot,
143
+ timezone: c.timezone,
144
+ };
145
+ }
146
+
147
+ // ---- horizons / windows -----------------------------------------------------
148
+
149
+ /*
150
+ * Ground-truth full-expansion horizon (days) — long enough to expose several
151
+ * rotation periods AND several coverage cycles (incl. a weekend) for sampling,
152
+ * while keeping the number of generated events modest.
153
+ */
154
+ const GROUND_HORIZON_DAYS: Record<string, number> = {
155
+ [EventInterval.Hour]: 7,
156
+ [EventInterval.Day]: 24,
157
+ [EventInterval.Week]: 63,
158
+ [EventInterval.Month]: 135,
159
+ [EventInterval.Year]: 1100,
160
+ };
161
+
162
+ /*
163
+ * Windowed getEvents end (days): huge, but getNumberOfEvents forces an early
164
+ * return after 1-2 events, so this is O(a couple of periods), not the window.
165
+ */
166
+ const WINDOWED_END_DAYS: number = 400;
167
+
168
+ /*
169
+ * getMultiLayerEvents window (days): expands fully (no per-layer cap), so keep it
170
+ * small. The FIRST covered/next event is always within ~2 days (daily overnight
171
+ * gap <=16h, weekend gap <=2d, coverage otherwise immediate), so 5 days always
172
+ * contains it for every restriction in this matrix.
173
+ */
174
+ const MULTI_WINDOW_DAYS: number = 5;
175
+
176
+ /*
177
+ * Only sample substantial restriction gaps (daily overnight ~16h, weekend ~2d).
178
+ * This excludes the tiny +/-1s rotation-boundary stitch AND leaves room for the
179
+ * off-center nudge below to stay inside the gap.
180
+ */
181
+ const GAP_THRESHOLD_MS: number = 2 * 60 * 60 * 1000;
182
+
183
+ /*
184
+ * Push a gap sample OFF the gap's geometric centre and off any whole-hour
185
+ * instant. A symmetric weekend gap's exact midpoint frequently lands on a clean
186
+ * rotation boundary (e.g. midnight for a day rotation), where the +/-1s stitch
187
+ * makes "next" ambiguous — the boundary artifact the task says to avoid. A prime
188
+ * 41m37s offset lands the sample at HH:41:37, clear of the HH:00:00 wall-clock
189
+ * rotation boundaries this matrix produces.
190
+ */
191
+ const GAP_NUDGE_MS: number = 41 * 60 * 1000 + 37 * 1000;
192
+
193
+ const MAX_COVERED_SAMPLES: number = 3;
194
+ const MAX_GAP_SAMPLES: number = 2;
195
+
196
+ // A Monday 00:00 UTC.
197
+ const FIXED_START_JAN: Date = OneUptimeDate.fromString(
198
+ "2025-01-06T00:00:00.000Z",
199
+ );
200
+
201
+ // ---- expansion helpers ------------------------------------------------------
202
+
203
+ function fullExpand(layer: LayerProps, from: Date, to: Date): CalendarEvent[] {
204
+ const util: LayerUtil = new LayerUtil();
205
+ return util.getEvents({
206
+ ...layer,
207
+ calendarStartDate: from,
208
+ calendarEndDate: to,
209
+ });
210
+ }
211
+
212
+ function windowedTitles(layer: LayerProps, at: Date, n: number): string[] {
213
+ const util: LayerUtil = new LayerUtil();
214
+ const events: CalendarEvent[] = util.getEvents(
215
+ {
216
+ ...layer,
217
+ calendarStartDate: at,
218
+ calendarEndDate: OneUptimeDate.addRemoveDays(at, WINDOWED_END_DAYS),
219
+ },
220
+ { getNumberOfEvents: n },
221
+ );
222
+ return events.map((e: CalendarEvent) => {
223
+ return e.title;
224
+ });
225
+ }
226
+
227
+ function multiLayerFirstTitle(layer: LayerProps, at: Date): string | null {
228
+ const util: LayerUtil = new LayerUtil();
229
+ const events: CalendarEvent[] = util.getMultiLayerEvents(
230
+ {
231
+ layers: [layer],
232
+ calendarStartDate: at,
233
+ calendarEndDate: OneUptimeDate.addRemoveDays(at, MULTI_WINDOW_DAYS),
234
+ },
235
+ { getNumberOfEvents: 1 },
236
+ );
237
+ return events[0]?.title ?? null;
238
+ }
239
+
240
+ /*
241
+ * Earliest-first full events whose end is strictly after `at`. For a covered
242
+ * instant this begins with the covering event; for a gap instant it begins with
243
+ * the next covered event.
244
+ */
245
+ function relevantEventsAfter(full: CalendarEvent[], at: Date): CalendarEvent[] {
246
+ const t: number = at.getTime();
247
+ return full
248
+ .filter((e: CalendarEvent) => {
249
+ return e.end.getTime() > t;
250
+ })
251
+ .sort((a: CalendarEvent, b: CalendarEvent) => {
252
+ return a.start.getTime() - b.start.getTime();
253
+ });
254
+ }
255
+
256
+ function durationMs(e: CalendarEvent): number {
257
+ return e.end.getTime() - e.start.getTime();
258
+ }
259
+
260
+ /*
261
+ * Median positive event duration — the yardstick for "significant" coverage.
262
+ * Rotation boundaries that fall a DST-hour inside a restriction-window edge leave
263
+ * a sub-window "sliver" of the previous user hugging the boundary (a variant of
264
+ * the +/-1s rotation-boundary stitch). The current-on-call resolution is correct
265
+ * inside such slivers, but the SECOND-in-line ("next after next") can stitch to
266
+ * the boundary artifact. We therefore only compare the 2nd windowed event when
267
+ * both oracle events involved are significant (>= a fraction of the median), i.e.
268
+ * not boundary slivers — matching the task's guidance to avoid boundary instants.
269
+ */
270
+ function medianDurationMs(events: CalendarEvent[]): number {
271
+ const ds: number[] = events
272
+ .map((e: CalendarEvent) => {
273
+ return durationMs(e);
274
+ })
275
+ .filter((d: number) => {
276
+ return d > 0;
277
+ })
278
+ .sort((a: number, b: number) => {
279
+ return a - b;
280
+ });
281
+ if (ds.length === 0) {
282
+ return 0;
283
+ }
284
+ const mid: number = Math.floor(ds.length / 2);
285
+ if (ds.length % 2 === 1) {
286
+ return ds[mid] as number;
287
+ }
288
+ return Math.floor(((ds[mid - 1] as number) + (ds[mid] as number)) / 2);
289
+ }
290
+
291
+ // Spread up to `maxPicks` distinct indices across [0, n).
292
+ function pickSpread(n: number, maxPicks: number): number[] {
293
+ if (n <= 0) {
294
+ return [];
295
+ }
296
+ if (n <= maxPicks) {
297
+ return Array.from({ length: n }, (_: unknown, i: number) => {
298
+ return i;
299
+ });
300
+ }
301
+ const picks: Set<number> = new Set<number>();
302
+ for (let j: number = 0; j < maxPicks; j++) {
303
+ const idx: number = Math.floor(((j + 0.5) / maxPicks) * n);
304
+ picks.add(Math.min(n - 1, idx));
305
+ }
306
+ return Array.from(picks).sort((a: number, b: number) => {
307
+ return a - b;
308
+ });
309
+ }
310
+
311
+ interface Sample {
312
+ at: Date;
313
+ kind: "covered" | "gap";
314
+ }
315
+
316
+ /*
317
+ * Build off-boundary sample instants from a full expansion: event midpoints
318
+ * (covered) and large-gap midpoints (gap).
319
+ */
320
+ function buildSamples(full: CalendarEvent[]): Sample[] {
321
+ const samples: Sample[] = [];
322
+
323
+ // Covered samples: interior midpoints of a spread of events.
324
+ const coveredIdx: number[] = pickSpread(full.length, MAX_COVERED_SAMPLES);
325
+ for (const i of coveredIdx) {
326
+ const e: CalendarEvent | undefined = full[i];
327
+ if (!e) {
328
+ continue;
329
+ }
330
+ const s: number = e.start.getTime();
331
+ const en: number = e.end.getTime();
332
+ if (en - s < 2000) {
333
+ continue; // too short to have a safely-interior midpoint
334
+ }
335
+ samples.push({ at: new Date(Math.floor((s + en) / 2)), kind: "covered" });
336
+ }
337
+
338
+ // Gap samples: off-centre instants inside substantial gaps between events.
339
+ const gapMidpoints: Date[] = [];
340
+ for (let i: number = 0; i < full.length - 1; i++) {
341
+ const cur: CalendarEvent | undefined = full[i];
342
+ const next: CalendarEvent | undefined = full[i + 1];
343
+ if (!cur || !next) {
344
+ continue;
345
+ }
346
+ const gapStart: number = cur.end.getTime();
347
+ const gapEnd: number = next.start.getTime();
348
+ if (gapEnd - gapStart > GAP_THRESHOLD_MS) {
349
+ const centre: number = Math.floor((gapStart + gapEnd) / 2);
350
+ let at: number = centre + GAP_NUDGE_MS;
351
+ if (at >= gapEnd) {
352
+ at = centre - GAP_NUDGE_MS; // stay strictly inside (gap always >= 2h)
353
+ }
354
+ gapMidpoints.push(new Date(at));
355
+ }
356
+ }
357
+ const gapIdx: number[] = pickSpread(gapMidpoints.length, MAX_GAP_SAMPLES);
358
+ for (const i of gapIdx) {
359
+ const at: Date | undefined = gapMidpoints[i];
360
+ if (at) {
361
+ samples.push({ at, kind: "gap" });
362
+ }
363
+ }
364
+
365
+ return samples;
366
+ }
367
+
368
+ /*
369
+ * Core differential check for one config; returns a list of human-readable
370
+ * mismatch descriptions (empty => all windowed paths agree with the oracle).
371
+ */
372
+ function checkConfig(c: Config): string[] {
373
+ const layer: LayerProps = layerOf(c);
374
+ const groundToDays: number = GROUND_HORIZON_DAYS[c.intervalType] ?? 60;
375
+ const full: CalendarEvent[] = fullExpand(
376
+ layer,
377
+ c.start,
378
+ OneUptimeDate.addRemoveDays(c.start, groundToDays),
379
+ );
380
+
381
+ const mismatches: string[] = [];
382
+
383
+ if (full.length === 0) {
384
+ mismatches.push("full expansion produced ZERO events (unexpected)");
385
+ return mismatches;
386
+ }
387
+
388
+ const sigMs: number = 0.35 * medianDurationMs(full);
389
+ const samples: Sample[] = buildSamples(full);
390
+
391
+ for (const sample of samples) {
392
+ const at: Date = sample.at;
393
+ const relevant: CalendarEvent[] = relevantEventsAfter(full, at);
394
+
395
+ if (relevant.length === 0) {
396
+ // Interior samples always have a relevant full event; guard anyway.
397
+ continue;
398
+ }
399
+
400
+ const expected0: string = relevant[0]!.title;
401
+ const iso: string = at.toISOString();
402
+
403
+ // (1) windowed getNumberOfEvents:1 — the "who is on call now / next" primitive.
404
+ const w1: string[] = windowedTitles(layer, at, 1);
405
+ if (w1.length === 0) {
406
+ mismatches.push(
407
+ `${sample.kind} at=${iso} windowed(1) returned NOTHING; expected=${expected0}`,
408
+ );
409
+ } else if (w1[0] !== expected0) {
410
+ mismatches.push(
411
+ `${sample.kind} at=${iso} windowed(1)[0]=${w1[0]} expected=${expected0}`,
412
+ );
413
+ }
414
+
415
+ // (2) windowed getNumberOfEvents:2 — first two on the roster.
416
+ const w2: string[] = windowedTitles(layer, at, 2);
417
+ if (w2.length === 0) {
418
+ mismatches.push(
419
+ `${sample.kind} at=${iso} windowed(2) returned NOTHING; expected=${expected0}`,
420
+ );
421
+ } else if (w2[0] !== expected0) {
422
+ mismatches.push(
423
+ `${sample.kind} at=${iso} windowed(2)[0]=${w2[0]} expected=${expected0}`,
424
+ );
425
+ }
426
+
427
+ /*
428
+ * (2b) Second-in-line — only where both oracle events are significant (not
429
+ * boundary slivers), so the comparison is off-boundary as the task requires.
430
+ */
431
+ const canCheckNext: boolean =
432
+ relevant.length >= 2 &&
433
+ durationMs(relevant[0]!) >= sigMs &&
434
+ durationMs(relevant[1]!) >= sigMs;
435
+ if (canCheckNext) {
436
+ const expected1: string = relevant[1]!.title;
437
+ if (w2.length < 2) {
438
+ mismatches.push(
439
+ `${sample.kind} at=${iso} windowed(2) had <2 events but oracle 2nd=${expected1}`,
440
+ );
441
+ } else if (w2[1] !== expected1) {
442
+ mismatches.push(
443
+ `${sample.kind} at=${iso} windowed(2)[1]=${w2[1]} expected=${expected1}`,
444
+ );
445
+ }
446
+ }
447
+
448
+ // (3) getMultiLayerEvents — the actual live roster path.
449
+ const m1: string | null = multiLayerFirstTitle(layer, at);
450
+ if (m1 === null) {
451
+ mismatches.push(
452
+ `${sample.kind} at=${iso} multiLayer returned NOTHING; expected=${expected0}`,
453
+ );
454
+ } else if (m1 !== expected0) {
455
+ mismatches.push(
456
+ `${sample.kind} at=${iso} multiLayer[0]=${m1} expected=${expected0}`,
457
+ );
458
+ }
459
+ }
460
+
461
+ return mismatches;
462
+ }
463
+
464
+ // ---- matrix -----------------------------------------------------------------
465
+
466
+ const INTERVALS: EventInterval[] = [
467
+ EventInterval.Hour,
468
+ EventInterval.Day,
469
+ EventInterval.Week,
470
+ EventInterval.Month,
471
+ ];
472
+ const COUNTS: number[] = [1, 2, 3];
473
+ // Multi-user rotation sets (single-user is covered separately below).
474
+ const USER_SETS: string[][] = [
475
+ ["A", "B"],
476
+ ["A", "B", "C"],
477
+ ];
478
+ const RESTRICTIONS: RestrictionKind[] = ["none", "daily09-17", "weeklyMonFri"];
479
+ const TIMEZONES: (string | undefined)[] = [
480
+ undefined,
481
+ "America/New_York",
482
+ "Asia/Kolkata",
483
+ ];
484
+
485
+ function buildMatrix(): Config[] {
486
+ const configs: Config[] = [];
487
+ for (const it of INTERVALS) {
488
+ for (const count of COUNTS) {
489
+ for (const users of USER_SETS) {
490
+ for (const kind of RESTRICTIONS) {
491
+ for (const tz of TIMEZONES) {
492
+ configs.push({
493
+ name: `${count}x${it} users=${users.length} ${kind} tz=${tz ?? "local"}`,
494
+ intervalType: it,
495
+ intervalCount: count,
496
+ userIds: users,
497
+ restrictionKind: kind,
498
+ timezone: tz,
499
+ start: FIXED_START_JAN,
500
+ });
501
+ }
502
+ }
503
+ }
504
+ }
505
+ }
506
+ return configs;
507
+ }
508
+
509
+ /*
510
+ * REGRESSION: intervalCount >= 2 rotation-boundary overshoot.
511
+ *
512
+ * moveHandsOffTimeAfterCurrentEventStartTime used to align the next handoff via
513
+ * ceil(getUnitsInclusive / interval) * interval, which OVERSHOT by a full
514
+ * rotation period for query instants sitting in the last partial period before a
515
+ * boundary — and a DST offset shift could push the inclusive unit count across
516
+ * an even/odd threshold, triggering it. The first windowed period then spanned
517
+ * TWO rotations and resolved the wrong current/next on-call user.
518
+ *
519
+ * Config that reproduced it: an every-2-days rotation [A,B,C] with a weekly
520
+ * restriction, America/New_York, sampled in the ~1h before a Saturday-evening
521
+ * 2-day boundary during the week after US fall-back. The calendar's next covered
522
+ * shift is B; the windowed resolution used to return A.
523
+ */
524
+ describe("REGRESSION: intervalCount>=2 boundary overshoot resolves the correct next user", () => {
525
+ it("every-2-days + weekly restriction, gap query just before a post-DST boundary", () => {
526
+ const c: Config = {
527
+ name: "reg-2xday-weekly",
528
+ intervalType: EventInterval.Day,
529
+ intervalCount: 2,
530
+ userIds: ["A", "B", "C"],
531
+ restrictionKind: "weeklyMonFri",
532
+ timezone: "America/New_York",
533
+ start: OneUptimeDate.fromString("2025-10-27T00:00:00.000Z"),
534
+ };
535
+ const layer: LayerProps = layerOf(c);
536
+ const util: LayerUtil = new LayerUtil();
537
+ const full: CalendarEvent[] = fullExpand(
538
+ layer,
539
+ c.start,
540
+ OneUptimeDate.fromString("2025-11-25T00:00:00.000Z"),
541
+ );
542
+
543
+ /*
544
+ * Probe every 20 minutes across the Nov 14-18 weekend; the windowed "next
545
+ * covered user" must always equal the full-expansion oracle (cover, else the
546
+ * next event after the instant). Before the fix, Sat 19:00-19:40 EST
547
+ * diverged (windowed A vs oracle B).
548
+ */
549
+ const startProbe: Date = OneUptimeDate.fromString(
550
+ "2025-11-14T18:00:00.000Z",
551
+ );
552
+ const mismatches: string[] = [];
553
+ for (let i: number = 0; i < 72 * 3; i++) {
554
+ const at: Date = OneUptimeDate.addRemoveMinutes(startProbe, i * 20);
555
+ let cover: CalendarEvent | null = null;
556
+ let next: CalendarEvent | null = null;
557
+ for (const e of full) {
558
+ if (
559
+ OneUptimeDate.isOnOrAfter(at, e.start) &&
560
+ OneUptimeDate.isBefore(at, e.end)
561
+ ) {
562
+ cover = e;
563
+ }
564
+ if (OneUptimeDate.isAfter(e.start, at)) {
565
+ if (!next || OneUptimeDate.isBefore(e.start, next.start)) {
566
+ next = e;
567
+ }
568
+ }
569
+ }
570
+ const oracle: string | null = cover
571
+ ? cover.title
572
+ : next
573
+ ? next.title
574
+ : null;
575
+ const windowed: string | null =
576
+ util.getEvents(
577
+ {
578
+ ...layer,
579
+ calendarStartDate: at,
580
+ calendarEndDate: OneUptimeDate.addRemoveDays(at, 20),
581
+ },
582
+ { getNumberOfEvents: 1 },
583
+ )[0]?.title ?? null;
584
+ if (windowed !== oracle) {
585
+ mismatches.push(
586
+ `${at.toISOString()} windowed=${windowed} oracle=${oracle}`,
587
+ );
588
+ }
589
+ }
590
+ expect(mismatches).toEqual([]);
591
+ });
592
+
593
+ it("does not overshoot the boundary for a range of every-N-day/hour rotations", () => {
594
+ /*
595
+ * Broad guard: for several interval counts, resolving anywhere inside a
596
+ * period yields a first event whose END is the NEXT on-grid boundary (never
597
+ * one period further out).
598
+ */
599
+ const util: LayerUtil = new LayerUtil();
600
+ const start: Date = OneUptimeDate.fromString("2025-01-06T00:00:00.000Z");
601
+ for (const [it, count] of [
602
+ [EventInterval.Day, 2],
603
+ [EventInterval.Day, 3],
604
+ [EventInterval.Hour, 2],
605
+ [EventInterval.Hour, 3],
606
+ [EventInterval.Week, 2],
607
+ ] as Array<[EventInterval, number]>) {
608
+ const layer: LayerProps = layerOf({
609
+ name: `guard-${count}x${it}`,
610
+ intervalType: it,
611
+ intervalCount: count,
612
+ userIds: ["A", "B", "C"],
613
+ restrictionKind: "none",
614
+ timezone: "America/New_York",
615
+ start,
616
+ });
617
+ const full: CalendarEvent[] = fullExpand(
618
+ layer,
619
+ start,
620
+ OneUptimeDate.addRemoveDays(start, 40),
621
+ );
622
+ /*
623
+ * For each full event, sampling 3 points inside it must resolve the same
624
+ * user as the full expansion (current user correct, no overshoot).
625
+ */
626
+ for (const e of full.slice(1, 12)) {
627
+ const mid: Date = new Date((e.start.getTime() + e.end.getTime()) / 2);
628
+ const windowed: string | null =
629
+ util.getEvents(
630
+ {
631
+ ...layer,
632
+ calendarStartDate: mid,
633
+ calendarEndDate: OneUptimeDate.addRemoveDays(mid, 40),
634
+ },
635
+ { getNumberOfEvents: 1 },
636
+ )[0]?.title ?? null;
637
+ expect(windowed).toBe(e.title);
638
+ }
639
+ }
640
+ });
641
+ });
642
+
643
+ describe("Windowed current/next resolution == full expansion (broad matrix)", () => {
644
+ const configs: Config[] = buildMatrix();
645
+
646
+ for (const c of configs) {
647
+ it(`${c.name}`, () => {
648
+ const mismatches: string[] = checkConfig(c);
649
+ expect({ config: c.name, mismatches }).toEqual({
650
+ config: c.name,
651
+ mismatches: [],
652
+ });
653
+ });
654
+ }
655
+ });
656
+
657
+ /*
658
+ * Single-user layers: no rotation ever occurs (always the same person), across
659
+ * every interval x restriction x tz. The windowed/live paths must still agree
660
+ * with the full expansion (coverage windows, gaps, and boundaries all resolve to
661
+ * the one user).
662
+ */
663
+ describe("Single-user layers: windowed == full (every interval/restriction/tz)", () => {
664
+ const configs: Config[] = [];
665
+ for (const it of INTERVALS) {
666
+ for (const kind of RESTRICTIONS) {
667
+ for (const tz of TIMEZONES) {
668
+ configs.push({
669
+ name: `1x${it} users=1 ${kind} tz=${tz ?? "local"}`,
670
+ intervalType: it,
671
+ intervalCount: 1,
672
+ userIds: ["A"],
673
+ restrictionKind: kind,
674
+ timezone: tz,
675
+ start: FIXED_START_JAN,
676
+ });
677
+ }
678
+ }
679
+ }
680
+
681
+ for (const c of configs) {
682
+ it(`${c.name}`, () => {
683
+ const mismatches: string[] = checkConfig(c);
684
+ expect({ config: c.name, mismatches }).toEqual({
685
+ config: c.name,
686
+ mismatches: [],
687
+ });
688
+ });
689
+ }
690
+ });
691
+
692
+ /*
693
+ * DST-spanning starts stress server-vs-schedule wall-clock divergence. New York
694
+ * springs forward 2025-03-09 and falls back 2025-11-02; Kolkata never shifts.
695
+ * These are especially meaningful under TZ=UTC (server zone != schedule zone).
696
+ */
697
+ function buildDstMatrix(): Config[] {
698
+ const dstStarts: { label: string; start: Date }[] = [
699
+ {
700
+ label: "spring-forward-week",
701
+ start: OneUptimeDate.fromString("2025-03-03T00:00:00.000Z"), // Monday before US DST
702
+ },
703
+ {
704
+ label: "fall-back-week",
705
+ start: OneUptimeDate.fromString("2025-10-27T00:00:00.000Z"), // Monday before US fall-back
706
+ },
707
+ ];
708
+ const dstIntervals: EventInterval[] = [EventInterval.Day, EventInterval.Week];
709
+ const dstRestrictions: RestrictionKind[] = ["daily09-17", "weeklyMonFri"];
710
+ const dstTimezones: (string | undefined)[] = [
711
+ "America/New_York",
712
+ "Asia/Kolkata",
713
+ ];
714
+
715
+ const configs: Config[] = [];
716
+ for (const s of dstStarts) {
717
+ for (const intervalType of dstIntervals) {
718
+ for (const count of [1, 2]) {
719
+ for (const kind of dstRestrictions) {
720
+ for (const tz of dstTimezones) {
721
+ configs.push({
722
+ name: `${s.label} ${count}x${intervalType} ${kind} tz=${tz ?? "local"}`,
723
+ intervalType,
724
+ intervalCount: count,
725
+ userIds: ["A", "B", "C"],
726
+ restrictionKind: kind,
727
+ timezone: tz,
728
+ start: s.start,
729
+ });
730
+ }
731
+ }
732
+ }
733
+ }
734
+ }
735
+ return configs;
736
+ }
737
+
738
+ describe("Windowed == full across DST transitions (schedule tz vs server)", () => {
739
+ const configs: Config[] = buildDstMatrix();
740
+
741
+ for (const c of configs) {
742
+ it(`${c.name}`, () => {
743
+ const mismatches: string[] = checkConfig(c);
744
+ expect({ config: c.name, mismatches }).toEqual({
745
+ config: c.name,
746
+ mismatches: [],
747
+ });
748
+ });
749
+ }
750
+ });
751
+
752
+ /*
753
+ * A handful of CONCRETE anchors (absolute expected users) so the suite pins real
754
+ * behavior, not merely windowed==full self-consistency. These are independently
755
+ * hand-derivable from the rotation/restriction definition.
756
+ */
757
+ describe("Concrete anchors (absolute expected on-call user)", () => {
758
+ const MON_JAN6: Date = FIXED_START_JAN;
759
+
760
+ function dailyRotationLayer(
761
+ users: string[],
762
+ intervalCount: number,
763
+ restrictionKind: RestrictionKind,
764
+ ): LayerProps {
765
+ return layerOf({
766
+ name: "anchor",
767
+ intervalType: EventInterval.Day,
768
+ intervalCount,
769
+ userIds: users,
770
+ restrictionKind,
771
+ timezone: undefined,
772
+ start: MON_JAN6,
773
+ });
774
+ }
775
+
776
+ function firstTitleAt(layer: LayerProps, at: Date): string | null {
777
+ return windowedTitles(layer, at, 1)[0] ?? null;
778
+ }
779
+
780
+ test("unrestricted x1 daily [A,B] rotates A,B,A,B on successive days (noon samples)", () => {
781
+ const layer: LayerProps = dailyRotationLayer(["A", "B"], 1, "none");
782
+ const expectedByDay: string[] = ["A", "B", "A", "B", "A", "B"];
783
+ for (let day: number = 0; day < expectedByDay.length; day++) {
784
+ const noon: Date = OneUptimeDate.addRemoveHours(
785
+ OneUptimeDate.addRemoveDays(MON_JAN6, day),
786
+ 12,
787
+ );
788
+ expect(firstTitleAt(layer, noon)).toBe(expectedByDay[day]);
789
+ expect(multiLayerFirstTitle(layer, noon)).toBe(expectedByDay[day]);
790
+ }
791
+ });
792
+
793
+ test("unrestricted x2 daily [A,B,C] holds each user for two days then advances", () => {
794
+ // Periods: [Jan6,Jan8)=A, [Jan8,Jan10)=B, [Jan10,Jan12)=C, [Jan12,Jan14)=A ...
795
+ const layer: LayerProps = dailyRotationLayer(["A", "B", "C"], 2, "none");
796
+ const cases: { day: number; who: string }[] = [
797
+ { day: 0, who: "A" },
798
+ { day: 1, who: "A" },
799
+ { day: 2, who: "B" },
800
+ { day: 3, who: "B" },
801
+ { day: 4, who: "C" },
802
+ { day: 5, who: "C" },
803
+ { day: 6, who: "A" },
804
+ { day: 7, who: "A" },
805
+ ];
806
+ for (const cse of cases) {
807
+ const noon: Date = OneUptimeDate.addRemoveHours(
808
+ OneUptimeDate.addRemoveDays(MON_JAN6, cse.day),
809
+ 12,
810
+ );
811
+ expect(firstTitleAt(layer, noon)).toBe(cse.who);
812
+ }
813
+ });
814
+
815
+ test("daily 09-17 x1 [A,B]: an evening gap resolves the NEXT day's user (F2)", () => {
816
+ const layer: LayerProps = dailyRotationLayer(["A", "B"], 1, "daily09-17");
817
+ // Jan6=A(9-17), Jan7=B, Jan8=A ... Evening (20:00) gap => next day's user.
818
+ const nextByEveningDay: string[] = ["B", "A", "B", "A", "B"];
819
+ for (let day: number = 0; day < nextByEveningDay.length; day++) {
820
+ const evening: Date = OneUptimeDate.addRemoveHours(
821
+ OneUptimeDate.addRemoveDays(MON_JAN6, day),
822
+ 20,
823
+ );
824
+ expect(firstTitleAt(layer, evening)).toBe(nextByEveningDay[day]);
825
+ expect(multiLayerFirstTitle(layer, evening)).toBe(nextByEveningDay[day]);
826
+ }
827
+ // And inside a covered window the CURRENT user is that day's user.
828
+ expect(
829
+ firstTitleAt(
830
+ layer,
831
+ OneUptimeDate.addRemoveHours(
832
+ OneUptimeDate.addRemoveDays(MON_JAN6, 1),
833
+ 12,
834
+ ),
835
+ ),
836
+ ).toBe("B");
837
+ });
838
+
839
+ test("weekly Mon-Fri x1 [A,B]: weekend gap resolves next Monday's rotated user", () => {
840
+ const layer: LayerProps = layerOf({
841
+ name: "anchor-weekly",
842
+ intervalType: EventInterval.Week,
843
+ intervalCount: 1,
844
+ userIds: ["A", "B"],
845
+ restrictionKind: "weeklyMonFri",
846
+ timezone: undefined,
847
+ start: MON_JAN6,
848
+ });
849
+ /*
850
+ * Week rotation: week1 (Jan6-)=A, week2 (Jan13-)=B, week3=A ...
851
+ * Saturday of week1 (Jan11 12:00) is a weekend gap => next Monday Jan13 = B.
852
+ */
853
+ const week1Saturday: Date = OneUptimeDate.fromString(
854
+ "2025-01-11T12:00:00.000Z",
855
+ );
856
+ expect(firstTitleAt(layer, week1Saturday)).toBe("B");
857
+ expect(multiLayerFirstTitle(layer, week1Saturday)).toBe("B");
858
+
859
+ // Mid-week1 (Wed Jan8 12:00) is covered => current user A.
860
+ const week1Wed: Date = OneUptimeDate.fromString("2025-01-08T12:00:00.000Z");
861
+ expect(firstTitleAt(layer, week1Wed)).toBe("A");
862
+ });
863
+ });