@oneuptime/common 11.3.26 → 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 (241) hide show
  1. package/Models/DatabaseModels/IncidentEpisodeRoleMember.ts +1 -1
  2. package/Models/DatabaseModels/IncidentMember.ts +1 -1
  3. package/Models/DatabaseModels/IncomingCallPolicy.ts +13 -0
  4. package/Models/DatabaseModels/Index.ts +3 -0
  5. package/Models/DatabaseModels/MigrationFailure.ts +170 -0
  6. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +51 -2
  7. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +44 -0
  8. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +6 -0
  9. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.ts +6 -0
  10. package/Models/DatabaseModels/OnCallDutyPolicyTimeLog.ts +1 -1
  11. package/Models/DatabaseModels/OnCallDutyPolicyUserOverride.ts +2 -2
  12. package/Server/API/AlertAPI.ts +1 -1
  13. package/Server/API/IncidentAPI.ts +2 -2
  14. package/Server/API/IncidentEpisodeAPI.ts +1 -1
  15. package/Server/API/LlmProviderAPI.ts +1 -0
  16. package/Server/API/ScheduledMaintenanceAPI.ts +1 -1
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.ts +25 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.ts +81 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.ts +25 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.ts +35 -0
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  22. package/Server/Infrastructure/PostgresDatabase.ts +11 -0
  23. package/Server/Services/AIService.ts +74 -2
  24. package/Server/Services/AnalyticsDatabaseService.ts +10 -1
  25. package/Server/Services/DatabaseService.ts +10 -1
  26. package/Server/Services/IncidentService.ts +1 -1
  27. package/Server/Services/IncomingCallPolicyEscalationRuleService.ts +75 -2
  28. package/Server/Services/IncomingCallPolicyService.ts +42 -1
  29. package/Server/Services/Index.ts +2 -0
  30. package/Server/Services/MigrationFailureService.ts +9 -0
  31. package/Server/Services/MonitorProbeService.ts +33 -7
  32. package/Server/Services/MonitorService.ts +19 -7
  33. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  34. package/Server/Services/OnCallDutyPolicyEscalationRuleService.ts +185 -41
  35. package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +23 -15
  36. package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +39 -0
  37. package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +148 -0
  38. package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +11 -2
  39. package/Server/Services/OnCallDutyPolicyScheduleService.ts +502 -26
  40. package/Server/Services/OnCallDutyPolicyTimeLogService.ts +49 -7
  41. package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +218 -4
  42. package/Server/Services/ProjectCallSMSConfigService.ts +82 -1
  43. package/Server/Services/TeamMemberService.ts +6 -0
  44. package/Server/Services/UserNotificationRuleService.ts +13 -42
  45. package/Server/Services/UserOnCallLogService.ts +90 -0
  46. package/Server/Services/UserOnCallLogTimelineService.ts +43 -1
  47. package/Server/Types/Database/QueryUtil.ts +57 -0
  48. package/Server/Utils/AI/AlertAIContextBuilder.ts +6 -1
  49. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +14 -2
  50. package/Server/Utils/AI/IncidentAIContextBuilder.ts +18 -3
  51. package/Server/Utils/AI/IncidentEpisodeAIContextBuilder.ts +6 -1
  52. package/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.ts +12 -2
  53. package/Server/Utils/AI/Toolbox/IncidentTools.ts +183 -1
  54. package/Server/Utils/AI/Toolbox/Index.ts +4 -1
  55. package/Server/Utils/AI/Toolbox/RecentChangesTools.ts +208 -0
  56. package/Server/Utils/AI/Toolbox/Serializer.ts +42 -0
  57. package/Server/Utils/Database/MigrationFailureLog.ts +240 -0
  58. package/Server/Utils/IncomingCallPhoneNumber.ts +41 -0
  59. package/Server/Utils/LLM/LLMService.ts +110 -11
  60. package/Server/Utils/Telemetry/IoTSnapshotScan.ts +12 -9
  61. package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +12 -9
  62. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +0 -8
  63. package/Tests/Server/Services/OnCallDutyPolicyScheduleLayerUserReorder.test.ts +233 -0
  64. package/Tests/Server/Services/OnCallDutyPolicyTimeLogServiceScoping.test.ts +242 -0
  65. package/Tests/Server/Services/OnCallDutyPolicyUserOverrideEdit.test.ts +198 -0
  66. package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +495 -0
  67. package/Tests/Server/Types/Database/QueryUtil.test.ts +74 -0
  68. package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +78 -0
  69. package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +68 -0
  70. package/Tests/Types/DateExhaustiveTimezone.test.ts +700 -0
  71. package/Tests/Types/DateTimezoneWallClock.test.ts +99 -0
  72. package/Tests/Types/Events/Recurring.test.ts +22 -9
  73. package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixes.test.ts +291 -0
  74. package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixesRound2.test.ts +398 -0
  75. package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeekendGap.test.ts +114 -0
  76. package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeeklyDayShift.test.ts +76 -0
  77. package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyMutation.test.ts +128 -0
  78. package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyProbe.test.ts +203 -0
  79. package/Tests/Types/OnCallDutyPolicy/LayerUtilDiffFuzz.test.ts +322 -0
  80. package/Tests/Types/OnCallDutyPolicy/LayerUtilEdgeCases.test.ts +280 -0
  81. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveCurrentUser.test.ts +863 -0
  82. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveDaily.test.ts +900 -0
  83. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveMultiLayer.test.ts +1104 -0
  84. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +957 -0
  85. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveWeekly.test.ts +1095 -0
  86. package/Tests/Types/OnCallDutyPolicy/LayerUtilInvariants.test.ts +232 -0
  87. package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +227 -0
  88. package/Tests/Types/OnCallDutyPolicy/LayerUtilMonthYearAudit.test.ts +255 -0
  89. package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerAudit.test.ts +388 -0
  90. package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerEdge.test.ts +300 -0
  91. package/Tests/Types/OnCallDutyPolicy/LayerUtilOvernightDSTAudit.test.ts +158 -0
  92. package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapFix.test.ts +253 -0
  93. package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapRepro.test.ts +157 -0
  94. package/Tests/Types/OnCallDutyPolicy/LayerUtilRotationFixes.test.ts +414 -0
  95. package/Tests/Types/OnCallDutyPolicy/LayerUtilTimezone.test.ts +243 -0
  96. package/Tests/Types/OnCallDutyPolicy/LayerUtilWeekendGapRepro.test.ts +112 -0
  97. package/Tests/Types/OnCallDutyPolicy/OverrideLensAudit.test.ts +117 -0
  98. package/Tests/Types/OnCallDutyPolicy/RestrictionTimes.test.ts +8 -6
  99. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesDefaultDayAudit.test.ts +112 -0
  100. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesExhaustive.test.ts +821 -0
  101. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesMutationInvariant.test.ts +886 -0
  102. package/Tests/Types/OnCallDutyPolicy/UserOverrideUtil.test.ts +255 -0
  103. package/Tests/Types/OnCallDutyPolicy/UserOverrideUtilExhaustive.test.ts +1126 -0
  104. package/Types/Date.ts +200 -15
  105. package/Types/OnCallDutyPolicy/Layer.ts +790 -260
  106. package/Types/OnCallDutyPolicy/RestrictionTimes.ts +39 -13
  107. package/Types/OnCallDutyPolicy/UserOverrideUtil.ts +21 -5
  108. package/UI/Components/Events/RecurringFieldElement.tsx +16 -0
  109. package/UI/Components/ModelFormModal/ModelFormModal.tsx +8 -0
  110. package/UI/Components/RadioButtons/BasicRadioButtons.tsx +1 -1
  111. package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js +1 -1
  112. package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js.map +1 -1
  113. package/build/dist/Models/DatabaseModels/IncidentMember.js +1 -1
  114. package/build/dist/Models/DatabaseModels/IncidentMember.js.map +1 -1
  115. package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js +10 -0
  116. package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js.map +1 -1
  117. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  118. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  119. package/build/dist/Models/DatabaseModels/MigrationFailure.js +196 -0
  120. package/build/dist/Models/DatabaseModels/MigrationFailure.js.map +1 -0
  121. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +52 -2
  122. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  123. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +45 -0
  124. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  125. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +6 -0
  126. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
  127. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js +6 -0
  128. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js.map +1 -1
  129. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js +1 -1
  130. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js.map +1 -1
  131. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js +2 -2
  132. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js.map +1 -1
  133. package/build/dist/Server/API/AlertAPI.js +1 -1
  134. package/build/dist/Server/API/IncidentAPI.js +2 -2
  135. package/build/dist/Server/API/IncidentEpisodeAPI.js +1 -1
  136. package/build/dist/Server/API/LlmProviderAPI.js +1 -0
  137. package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
  138. package/build/dist/Server/API/ScheduledMaintenanceAPI.js +1 -1
  139. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js +18 -0
  140. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js.map +1 -0
  141. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js +38 -0
  142. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js.map +1 -0
  143. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js +16 -0
  144. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js.map +1 -0
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js +18 -0
  146. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js.map +1 -0
  147. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  148. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  149. package/build/dist/Server/Infrastructure/PostgresDatabase.js +9 -0
  150. package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
  151. package/build/dist/Server/Services/AIService.js +53 -2
  152. package/build/dist/Server/Services/AIService.js.map +1 -1
  153. package/build/dist/Server/Services/AnalyticsDatabaseService.js +10 -1
  154. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  155. package/build/dist/Server/Services/DatabaseService.js +10 -1
  156. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  157. package/build/dist/Server/Services/IncidentService.js +1 -1
  158. package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js +55 -2
  159. package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js.map +1 -1
  160. package/build/dist/Server/Services/IncomingCallPolicyService.js +40 -0
  161. package/build/dist/Server/Services/IncomingCallPolicyService.js.map +1 -1
  162. package/build/dist/Server/Services/Index.js +2 -0
  163. package/build/dist/Server/Services/Index.js.map +1 -1
  164. package/build/dist/Server/Services/MigrationFailureService.js +9 -0
  165. package/build/dist/Server/Services/MigrationFailureService.js.map +1 -0
  166. package/build/dist/Server/Services/MonitorProbeService.js +33 -7
  167. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  168. package/build/dist/Server/Services/MonitorService.js +12 -5
  169. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  170. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +81 -22
  171. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  172. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js +134 -31
  173. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js.map +1 -1
  174. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +21 -13
  175. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
  176. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +36 -0
  177. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
  178. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +121 -0
  179. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
  180. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +11 -2
  181. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
  182. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +409 -36
  183. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  184. package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js +52 -8
  185. package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js.map +1 -1
  186. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +170 -3
  187. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
  188. package/build/dist/Server/Services/ProjectCallSMSConfigService.js +72 -0
  189. package/build/dist/Server/Services/ProjectCallSMSConfigService.js.map +1 -1
  190. package/build/dist/Server/Services/TeamMemberService.js +6 -0
  191. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  192. package/build/dist/Server/Services/UserNotificationRuleService.js +12 -30
  193. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  194. package/build/dist/Server/Services/UserOnCallLogService.js +75 -0
  195. package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
  196. package/build/dist/Server/Services/UserOnCallLogTimelineService.js +31 -1
  197. package/build/dist/Server/Services/UserOnCallLogTimelineService.js.map +1 -1
  198. package/build/dist/Server/Types/Database/QueryUtil.js +45 -0
  199. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  200. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js +6 -1
  201. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js.map +1 -1
  202. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +13 -2
  203. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
  204. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js +18 -3
  205. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js.map +1 -1
  206. package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js +6 -1
  207. package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js.map +1 -1
  208. package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js +12 -2
  209. package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js.map +1 -1
  210. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +152 -1
  211. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -1
  212. package/build/dist/Server/Utils/AI/Toolbox/Index.js +4 -1
  213. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  214. package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js +165 -0
  215. package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js.map +1 -0
  216. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +40 -0
  217. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -1
  218. package/build/dist/Server/Utils/Database/MigrationFailureLog.js +174 -0
  219. package/build/dist/Server/Utils/Database/MigrationFailureLog.js.map +1 -0
  220. package/build/dist/Server/Utils/IncomingCallPhoneNumber.js +30 -0
  221. package/build/dist/Server/Utils/IncomingCallPhoneNumber.js.map +1 -0
  222. package/build/dist/Server/Utils/LLM/LLMService.js +81 -5
  223. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  224. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +11 -8
  225. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
  226. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +11 -8
  227. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
  228. package/build/dist/Types/Date.js +153 -15
  229. package/build/dist/Types/Date.js.map +1 -1
  230. package/build/dist/Types/OnCallDutyPolicy/Layer.js +602 -171
  231. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  232. package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js +27 -13
  233. package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js.map +1 -1
  234. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js +20 -5
  235. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js.map +1 -1
  236. package/build/dist/UI/Components/Events/RecurringFieldElement.js +13 -0
  237. package/build/dist/UI/Components/Events/RecurringFieldElement.js.map +1 -1
  238. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  239. package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js +1 -1
  240. package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js.map +1 -1
  241. package/package.json +1 -1
@@ -0,0 +1,255 @@
1
+ import LayerUtil, { LayerProps } from "../../../Types/OnCallDutyPolicy/Layer";
2
+ import RestrictionTimes from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
3
+ import Recurring from "../../../Types/Events/Recurring";
4
+ import EventInterval from "../../../Types/Events/EventInterval";
5
+ import PositiveNumber from "../../../Types/PositiveNumber";
6
+ import OneUptimeDate from "../../../Types/Date";
7
+ import User from "../../../Models/DatabaseModels/User";
8
+ import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
9
+ import { describe, expect, test } from "@jest/globals";
10
+
11
+ /*
12
+ * Audit H1: moveHandsOffTimeAfterCurrentEventStartTime used a single MULTIPLIED
13
+ * calendar add from the anchor (addRotationUnits(anchor, k*interval)) to place a
14
+ * rotation boundary. For Month/Year, moment end-of-month-clamps a multiplied add
15
+ * differently than stepping one period at a time — e.g. Jan-31 + 11 months
16
+ * multiplied = Dec-31, but iterating eleven +1-month steps = Dec-28. The
17
+ * current-user counter (countElapsedRotationPeriods) already ITERATES, so the
18
+ * emitted handoff boundary landed off the grid the current user was resolved on:
19
+ * the schedule paged the wrong user and fired the handoff nearly a month early
20
+ * for Month/Year rotations anchored on day 29-31 (or Feb-29 for Year).
21
+ *
22
+ * The invariant these tests lock in: the current user + handoff resolved from a
23
+ * window that opens at "now" must match a full expansion from the layer's anchor
24
+ * (both walk the SAME iterated grid), and Month/Year boundaries must stay on the
25
+ * clamped iterated grid (never re-inflate to day 30/31 after a short month).
26
+ */
27
+
28
+ function user(id: string): User {
29
+ return {
30
+ id: {
31
+ toString: (): string => {
32
+ return id;
33
+ },
34
+ } as any,
35
+ } as User;
36
+ }
37
+
38
+ function rotation(type: EventInterval, count: number): Recurring {
39
+ const r: Recurring = new Recurring();
40
+ r.intervalType = type;
41
+ r.intervalCount = new PositiveNumber(count);
42
+ return r;
43
+ }
44
+
45
+ function expandFrom(
46
+ layer: LayerProps,
47
+ windowStart: Date,
48
+ windowEnd: Date,
49
+ ): Array<CalendarEvent> {
50
+ return new LayerUtil().getEvents({
51
+ ...layer,
52
+ calendarStartDate: windowStart,
53
+ calendarEndDate: windowEnd,
54
+ });
55
+ }
56
+
57
+ // The event that covers `at` in a full expansion from the layer anchor.
58
+ function coveringEvent(
59
+ events: Array<CalendarEvent>,
60
+ at: Date,
61
+ ): CalendarEvent | null {
62
+ for (const event of events) {
63
+ if (
64
+ OneUptimeDate.isOnOrBefore(event.start, at) &&
65
+ OneUptimeDate.isOnOrAfter(event.end, at)
66
+ ) {
67
+ return event;
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+
73
+ describe("LayerUtil audit H1: Month/Year rotation boundary iterated grid", () => {
74
+ test("monthly rotation anchored Jan-31: current user + handoff from 'now' matches full expansion from anchor", () => {
75
+ const anchor: Date = new Date(Date.UTC(2025, 0, 31, 0, 0, 0));
76
+ const now: Date = new Date(Date.UTC(2025, 11, 29, 12, 0, 0));
77
+ const layer: LayerProps = {
78
+ users: [user("A"), user("B")],
79
+ startDateTimeOfLayer: anchor,
80
+ handOffTime: anchor,
81
+ restrictionTimes: RestrictionTimes.getDefault(),
82
+ rotation: rotation(EventInterval.Month, 1),
83
+ timezone: "America/New_York",
84
+ };
85
+
86
+ const fromNow: Array<CalendarEvent> = expandFrom(
87
+ layer,
88
+ now,
89
+ new Date(Date.UTC(2026, 3, 5, 0, 0, 0)),
90
+ );
91
+ const fromAnchor: Array<CalendarEvent> = expandFrom(
92
+ layer,
93
+ anchor,
94
+ new Date(Date.UTC(2026, 3, 5, 0, 0, 0)),
95
+ );
96
+
97
+ const current: CalendarEvent | undefined = fromNow[0];
98
+ const covering: CalendarEvent | null = coveringEvent(fromAnchor, now);
99
+
100
+ expect(current).toBeTruthy();
101
+ expect(covering).toBeTruthy();
102
+
103
+ // Same user on call for "now" whether resolved from now or from the anchor.
104
+ expect(current!.title).toBe(covering!.title);
105
+
106
+ // Same handoff boundary (the crux of the bug — these disagreed before H1).
107
+ expect(new Date(current!.end).toISOString()).toBe(
108
+ new Date(covering!.end).toISOString(),
109
+ );
110
+
111
+ // Regression guard: the handoff is in Jan 2026, NOT the buggy Dec-31 2025.
112
+ const end: Date = new Date(current!.end);
113
+ expect(end.getUTCFullYear()).toBe(2026);
114
+ expect(end.getUTCMonth()).toBe(0); // January
115
+ });
116
+
117
+ test("monthly rotation anchored Jan-31: no emitted boundary re-inflates past day 29 after a short month", () => {
118
+ const anchor: Date = new Date(Date.UTC(2025, 0, 31, 0, 0, 0));
119
+ const layer: LayerProps = {
120
+ users: [user("A"), user("B")],
121
+ startDateTimeOfLayer: anchor,
122
+ handOffTime: anchor,
123
+ restrictionTimes: RestrictionTimes.getDefault(),
124
+ rotation: rotation(EventInterval.Month, 1),
125
+ timezone: "UTC",
126
+ };
127
+
128
+ const events: Array<CalendarEvent> = expandFrom(
129
+ layer,
130
+ anchor,
131
+ new Date(Date.UTC(2026, 11, 1, 0, 0, 0)),
132
+ );
133
+
134
+ /*
135
+ * After Jan-31 the iterated grid drops to Feb-28 and stays on day <= 28
136
+ * (28 in UTC). The buggy multiplied form produced Mar-31, Apr-30, ... —
137
+ * boundary days of 30/31. Assert every internal handoff boundary is on day
138
+ * <= 29 (allowing month-length clamping wiggle), which the multiplied form
139
+ * violated. The final event ends at the clamped calendarEnd, so skip it.
140
+ */
141
+ for (let i: number = 0; i < events.length - 1; i++) {
142
+ const end: Date = new Date(events[i]!.end);
143
+ expect(end.getUTCDate()).toBeLessThanOrEqual(29);
144
+ }
145
+ });
146
+
147
+ test("monthly rotation with intervalCount=3 stays consistent (current == full-expansion covering)", () => {
148
+ const anchor: Date = new Date(Date.UTC(2025, 0, 31, 0, 0, 0));
149
+ const now: Date = new Date(Date.UTC(2026, 0, 15, 6, 0, 0));
150
+ const layer: LayerProps = {
151
+ users: [user("A"), user("B"), user("C")],
152
+ startDateTimeOfLayer: anchor,
153
+ handOffTime: anchor,
154
+ restrictionTimes: RestrictionTimes.getDefault(),
155
+ rotation: rotation(EventInterval.Month, 3),
156
+ timezone: "America/New_York",
157
+ };
158
+
159
+ const fromNow: Array<CalendarEvent> = expandFrom(
160
+ layer,
161
+ now,
162
+ new Date(Date.UTC(2026, 8, 1, 0, 0, 0)),
163
+ );
164
+ const fromAnchor: Array<CalendarEvent> = expandFrom(
165
+ layer,
166
+ anchor,
167
+ new Date(Date.UTC(2026, 8, 1, 0, 0, 0)),
168
+ );
169
+
170
+ const current: CalendarEvent | undefined = fromNow[0];
171
+ const covering: CalendarEvent | null = coveringEvent(fromAnchor, now);
172
+
173
+ expect(current).toBeTruthy();
174
+ expect(covering).toBeTruthy();
175
+ expect(current!.title).toBe(covering!.title);
176
+ expect(new Date(current!.end).toISOString()).toBe(
177
+ new Date(covering!.end).toISOString(),
178
+ );
179
+ });
180
+
181
+ test("yearly rotation anchored Feb-29 (leap): boundary clamps to Feb-28 and stays consistent", () => {
182
+ const anchor: Date = new Date(Date.UTC(2024, 1, 29, 0, 0, 0));
183
+ const now: Date = new Date(Date.UTC(2027, 5, 1, 0, 0, 0));
184
+ const layer: LayerProps = {
185
+ users: [user("A"), user("B")],
186
+ startDateTimeOfLayer: anchor,
187
+ handOffTime: anchor,
188
+ restrictionTimes: RestrictionTimes.getDefault(),
189
+ rotation: rotation(EventInterval.Year, 1),
190
+ timezone: "UTC",
191
+ };
192
+
193
+ const fromNow: Array<CalendarEvent> = expandFrom(
194
+ layer,
195
+ now,
196
+ new Date(Date.UTC(2031, 0, 1, 0, 0, 0)),
197
+ );
198
+ const fromAnchor: Array<CalendarEvent> = expandFrom(
199
+ layer,
200
+ anchor,
201
+ new Date(Date.UTC(2031, 0, 1, 0, 0, 0)),
202
+ );
203
+
204
+ const current: CalendarEvent | undefined = fromNow[0];
205
+ const covering: CalendarEvent | null = coveringEvent(fromAnchor, now);
206
+
207
+ expect(current).toBeTruthy();
208
+ expect(covering).toBeTruthy();
209
+ expect(current!.title).toBe(covering!.title);
210
+ expect(new Date(current!.end).toISOString()).toBe(
211
+ new Date(covering!.end).toISOString(),
212
+ );
213
+
214
+ /*
215
+ * The first post-anchor yearly boundary must be Feb-28 (non-leap), not
216
+ * Feb-29 re-inflated by a multiplied add.
217
+ */
218
+ const firstBoundary: CalendarEvent | undefined = fromAnchor[0];
219
+ expect(firstBoundary).toBeTruthy();
220
+ const end: Date = new Date(firstBoundary!.end);
221
+ expect(end.getUTCMonth()).toBe(1); // February
222
+ expect(end.getUTCDate()).toBe(28);
223
+ });
224
+
225
+ test("Day and Week rotations are unaffected (no calendar clamping) — sanity", () => {
226
+ const anchor: Date = new Date(Date.UTC(2025, 0, 31, 0, 0, 0));
227
+ const now: Date = new Date(Date.UTC(2025, 5, 15, 9, 0, 0));
228
+ const layer: LayerProps = {
229
+ users: [user("A"), user("B")],
230
+ startDateTimeOfLayer: anchor,
231
+ handOffTime: anchor,
232
+ restrictionTimes: RestrictionTimes.getDefault(),
233
+ rotation: rotation(EventInterval.Day, 1),
234
+ timezone: "UTC",
235
+ };
236
+
237
+ const fromNow: Array<CalendarEvent> = expandFrom(
238
+ layer,
239
+ now,
240
+ new Date(Date.UTC(2025, 5, 20, 0, 0, 0)),
241
+ );
242
+ const fromAnchor: Array<CalendarEvent> = expandFrom(
243
+ layer,
244
+ anchor,
245
+ new Date(Date.UTC(2025, 5, 20, 0, 0, 0)),
246
+ );
247
+
248
+ const current: CalendarEvent | undefined = fromNow[0];
249
+ const covering: CalendarEvent | null = coveringEvent(fromAnchor, now);
250
+ expect(current!.title).toBe(covering!.title);
251
+ expect(new Date(current!.end).toISOString()).toBe(
252
+ new Date(covering!.end).toISOString(),
253
+ );
254
+ });
255
+ });
@@ -0,0 +1,388 @@
1
+ import LayerUtil, { LayerProps } from "../../../Types/OnCallDutyPolicy/Layer";
2
+ import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
3
+ import RestrictionTimes, {
4
+ RestrictionType,
5
+ WeeklyResctriction,
6
+ } from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
7
+ import Recurring from "../../../Types/Events/Recurring";
8
+ import OneUptimeDate from "../../../Types/Date";
9
+ import User from "../../../Models/DatabaseModels/User";
10
+ import EventInterval from "../../../Types/Events/EventInterval";
11
+ import DayOfWeek from "../../../Types/Day/DayOfWeek";
12
+
13
+ function user(id: string): User {
14
+ return {
15
+ id: {
16
+ toString: () => {
17
+ return id;
18
+ },
19
+ } as any,
20
+ } as User;
21
+ }
22
+
23
+ function rotation(t: EventInterval, c: number): Recurring {
24
+ return Recurring.fromJSON({
25
+ _type: "Recurring",
26
+ value: {
27
+ intervalType: t,
28
+ intervalCount: { _type: "PositiveNumber", value: c },
29
+ },
30
+ } as any);
31
+ }
32
+
33
+ function noRestriction(): RestrictionTimes {
34
+ const r: RestrictionTimes = new RestrictionTimes();
35
+ r.restictionType = RestrictionType.None;
36
+ r.dayRestrictionTimes = null;
37
+ return r;
38
+ }
39
+
40
+ function dailyRestriction(
41
+ startHour: number,
42
+ endHour: number,
43
+ ): RestrictionTimes {
44
+ const r: RestrictionTimes = new RestrictionTimes();
45
+ r.restictionType = RestrictionType.Daily;
46
+ r.dayRestrictionTimes = {
47
+ startTime: OneUptimeDate.getDateWithCustomTime({
48
+ hours: startHour,
49
+ minutes: 0,
50
+ seconds: 0,
51
+ }),
52
+ endTime: OneUptimeDate.getDateWithCustomTime({
53
+ hours: endHour,
54
+ minutes: 0,
55
+ seconds: 0,
56
+ }),
57
+ };
58
+ return r;
59
+ }
60
+
61
+ function weeklyRestriction(
62
+ windows: Array<{
63
+ startDay: DayOfWeek;
64
+ endDay: DayOfWeek;
65
+ startHour: number;
66
+ endHour: number;
67
+ }>,
68
+ ): RestrictionTimes {
69
+ const r: RestrictionTimes = new RestrictionTimes();
70
+ r.restictionType = RestrictionType.Weekly;
71
+ r.dayRestrictionTimes = null;
72
+ r.weeklyRestrictionTimes = windows.map(
73
+ (w: {
74
+ startDay: DayOfWeek;
75
+ endDay: DayOfWeek;
76
+ startHour: number;
77
+ endHour: number;
78
+ }): WeeklyResctriction => {
79
+ return {
80
+ startDay: w.startDay,
81
+ endDay: w.endDay,
82
+ startTime: OneUptimeDate.getDateWithCustomTime({
83
+ hours: w.startHour,
84
+ minutes: 0,
85
+ seconds: 0,
86
+ }),
87
+ endTime: OneUptimeDate.getDateWithCustomTime({
88
+ hours: w.endHour,
89
+ minutes: 0,
90
+ seconds: 0,
91
+ }),
92
+ };
93
+ },
94
+ );
95
+ return r;
96
+ }
97
+
98
+ // Which user covers instant t in a single event list? half-open [start, end).
99
+ function coveringUser(events: Array<CalendarEvent>, t: Date): string | null {
100
+ for (const e of events) {
101
+ if (e.start.getTime() <= t.getTime() && e.end.getTime() > t.getTime()) {
102
+ return e.title;
103
+ }
104
+ }
105
+ return null;
106
+ }
107
+
108
+ /*
109
+ * Oracle: expected on-call user at instant t across layers, resolved by priority
110
+ * (lowest layer index that covers t wins). Uses each layer's own getEvents.
111
+ */
112
+ function oracleUser(
113
+ perLayerEvents: Array<Array<CalendarEvent>>,
114
+ t: Date,
115
+ ): string | null {
116
+ for (const layerEvents of perLayerEvents) {
117
+ const u: string | null = coveringUser(layerEvents, t);
118
+ if (u !== null) {
119
+ return u;
120
+ }
121
+ }
122
+ return null;
123
+ }
124
+
125
+ // Returns overlapping pairs (real double-coverage, more than the 1s seam).
126
+ function overlappingPairs(
127
+ events: Array<CalendarEvent>,
128
+ ): Array<[CalendarEvent, CalendarEvent]> {
129
+ const pairs: Array<[CalendarEvent, CalendarEvent]> = [];
130
+ for (let i: number = 0; i < events.length; i++) {
131
+ for (let j: number = i + 1; j < events.length; j++) {
132
+ const a: CalendarEvent = events[i]!;
133
+ const b: CalendarEvent = events[j]!;
134
+ // overlap in seconds of [a.start,a.end) ∩ [b.start,b.end)
135
+ const start: number = Math.max(a.start.getTime(), b.start.getTime());
136
+ const end: number = Math.min(a.end.getTime(), b.end.getTime());
137
+ if (end - start > 0) {
138
+ pairs.push([a, b]);
139
+ }
140
+ }
141
+ }
142
+ return pairs;
143
+ }
144
+
145
+ describe("Multi-layer priority merge audit (lens: getMultiLayerEvents / removeOverlappingEvents)", () => {
146
+ const util: LayerUtil = new LayerUtil();
147
+
148
+ /*
149
+ * Runs the merge and compares against the per-layer priority oracle at many
150
+ * instants. Also asserts no real overlap (no double paging).
151
+ */
152
+ function checkMerge(
153
+ layers: Array<LayerProps>,
154
+ calStart: Date,
155
+ calEnd: Date,
156
+ sampleStepMinutes: number = 30,
157
+ ): { mismatches: Array<string>; overlaps: number } {
158
+ const merged: Array<CalendarEvent> = util.getMultiLayerEvents({
159
+ layers: layers,
160
+ calendarStartDate: calStart,
161
+ calendarEndDate: calEnd,
162
+ });
163
+
164
+ const perLayerEvents: Array<Array<CalendarEvent>> = layers.map(
165
+ (layer: LayerProps): Array<CalendarEvent> => {
166
+ return util.getEvents({
167
+ ...layer,
168
+ calendarStartDate: calStart,
169
+ calendarEndDate: calEnd,
170
+ });
171
+ },
172
+ );
173
+
174
+ const mismatches: Array<string> = [];
175
+
176
+ // Sample instants offset by 17s from clean boundaries to avoid 1s seams.
177
+ for (
178
+ let ms: number = calStart.getTime() + 17000;
179
+ ms < calEnd.getTime();
180
+ ms += sampleStepMinutes * 60 * 1000
181
+ ) {
182
+ const t: Date = new Date(ms);
183
+ const expected: string | null = oracleUser(perLayerEvents, t);
184
+ const actual: string | null = coveringUser(merged, t);
185
+ if (expected !== actual) {
186
+ mismatches.push(
187
+ `${OneUptimeDate.toString(t)} expected=${expected} actual=${actual}`,
188
+ );
189
+ }
190
+ }
191
+
192
+ const overlaps: Array<[CalendarEvent, CalendarEvent]> =
193
+ overlappingPairs(merged);
194
+
195
+ return { mismatches: mismatches, overlaps: overlaps.length };
196
+ }
197
+
198
+ test("Scenario A: three 24/7 layers, different rotations -> highest priority always wins, no gaps/overlaps", () => {
199
+ const calStart: Date = new Date(2026, 0, 5, 0, 0, 0); // Mon Jan 5 2026
200
+ const calEnd: Date = new Date(2026, 0, 26, 0, 0, 0); // 3 weeks
201
+
202
+ const layers: Array<LayerProps> = [
203
+ {
204
+ users: [user("A1"), user("A2")],
205
+ startDateTimeOfLayer: calStart,
206
+ restrictionTimes: noRestriction(),
207
+ handOffTime: calStart,
208
+ rotation: rotation(EventInterval.Week, 1),
209
+ },
210
+ {
211
+ users: [user("B1"), user("B2"), user("B3")],
212
+ startDateTimeOfLayer: calStart,
213
+ restrictionTimes: noRestriction(),
214
+ handOffTime: calStart,
215
+ rotation: rotation(EventInterval.Day, 2),
216
+ },
217
+ {
218
+ users: [user("C1")],
219
+ startDateTimeOfLayer: calStart,
220
+ restrictionTimes: noRestriction(),
221
+ handOffTime: calStart,
222
+ rotation: rotation(EventInterval.Week, 1),
223
+ },
224
+ ];
225
+
226
+ const { mismatches, overlaps } = checkMerge(layers, calStart, calEnd);
227
+ // Layer 0 is 24/7, so it always wins; every instant should be A1/A2.
228
+ expect(mismatches).toEqual([]);
229
+ expect(overlaps).toBe(0);
230
+ });
231
+
232
+ test("Scenario B: business-hours primary (Daily 09-17) over 24/7 fallback", () => {
233
+ const calStart: Date = new Date(2026, 0, 5, 0, 0, 0);
234
+ const calEnd: Date = new Date(2026, 0, 19, 0, 0, 0); // 2 weeks
235
+
236
+ const layers: Array<LayerProps> = [
237
+ {
238
+ users: [user("P1"), user("P2")],
239
+ startDateTimeOfLayer: calStart,
240
+ restrictionTimes: dailyRestriction(9, 17),
241
+ handOffTime: new Date(2026, 0, 5, 12, 0, 0),
242
+ rotation: rotation(EventInterval.Day, 1),
243
+ },
244
+ {
245
+ users: [user("F1"), user("F2")],
246
+ startDateTimeOfLayer: calStart,
247
+ restrictionTimes: noRestriction(),
248
+ handOffTime: calStart,
249
+ rotation: rotation(EventInterval.Week, 1),
250
+ },
251
+ ];
252
+
253
+ const { mismatches, overlaps } = checkMerge(layers, calStart, calEnd);
254
+ expect(mismatches).toEqual([]);
255
+ expect(overlaps).toBe(0);
256
+ });
257
+
258
+ test("Scenario C: TWO restricted primaries over a 24/7 fallback (3 layers)", () => {
259
+ const calStart: Date = new Date(2026, 0, 5, 0, 0, 0);
260
+ const calEnd: Date = new Date(2026, 0, 19, 0, 0, 0);
261
+
262
+ const layers: Array<LayerProps> = [
263
+ // Highest: mornings 06-10
264
+ {
265
+ users: [user("M1"), user("M2")],
266
+ startDateTimeOfLayer: calStart,
267
+ restrictionTimes: dailyRestriction(6, 10),
268
+ handOffTime: new Date(2026, 0, 5, 8, 0, 0),
269
+ rotation: rotation(EventInterval.Day, 1),
270
+ },
271
+ // Middle: afternoons 14-18
272
+ {
273
+ users: [user("N1"), user("N2")],
274
+ startDateTimeOfLayer: calStart,
275
+ restrictionTimes: dailyRestriction(14, 18),
276
+ handOffTime: new Date(2026, 0, 5, 16, 0, 0),
277
+ rotation: rotation(EventInterval.Day, 1),
278
+ },
279
+ // Fallback: 24/7
280
+ {
281
+ users: [user("Z1")],
282
+ startDateTimeOfLayer: calStart,
283
+ restrictionTimes: noRestriction(),
284
+ handOffTime: calStart,
285
+ rotation: rotation(EventInterval.Week, 1),
286
+ },
287
+ ];
288
+
289
+ const { mismatches, overlaps } = checkMerge(layers, calStart, calEnd);
290
+ expect(mismatches).toEqual([]);
291
+ expect(overlaps).toBe(0);
292
+ });
293
+
294
+ test("Scenario D: overlapping restricted primaries (both high priority) over fallback", () => {
295
+ const calStart: Date = new Date(2026, 0, 5, 0, 0, 0);
296
+ const calEnd: Date = new Date(2026, 0, 12, 0, 0, 0);
297
+
298
+ const layers: Array<LayerProps> = [
299
+ // Highest: 08-14
300
+ {
301
+ users: [user("H1")],
302
+ startDateTimeOfLayer: calStart,
303
+ restrictionTimes: dailyRestriction(8, 14),
304
+ handOffTime: calStart,
305
+ rotation: rotation(EventInterval.Week, 1),
306
+ },
307
+ // Second: 10-18 (OVERLAPS the first at 10-14)
308
+ {
309
+ users: [user("K1")],
310
+ startDateTimeOfLayer: calStart,
311
+ restrictionTimes: dailyRestriction(10, 18),
312
+ handOffTime: calStart,
313
+ rotation: rotation(EventInterval.Week, 1),
314
+ },
315
+ // Fallback 24/7
316
+ {
317
+ users: [user("Z1")],
318
+ startDateTimeOfLayer: calStart,
319
+ restrictionTimes: noRestriction(),
320
+ handOffTime: calStart,
321
+ rotation: rotation(EventInterval.Week, 1),
322
+ },
323
+ ];
324
+
325
+ const { mismatches, overlaps } = checkMerge(layers, calStart, calEnd);
326
+ expect(mismatches).toEqual([]);
327
+ expect(overlaps).toBe(0);
328
+ });
329
+
330
+ test("Scenario E: weekly-restricted primary (weekdays only) over 24/7 fallback", () => {
331
+ const calStart: Date = new Date(2026, 0, 5, 0, 0, 0); // Mon
332
+ const calEnd: Date = new Date(2026, 0, 26, 0, 0, 0);
333
+
334
+ const layers: Array<LayerProps> = [
335
+ {
336
+ users: [user("W1"), user("W2")],
337
+ startDateTimeOfLayer: calStart,
338
+ restrictionTimes: weeklyRestriction([
339
+ {
340
+ startDay: DayOfWeek.Monday,
341
+ endDay: DayOfWeek.Friday,
342
+ startHour: 9,
343
+ endHour: 17,
344
+ },
345
+ ]),
346
+ handOffTime: new Date(2026, 0, 5, 12, 0, 0),
347
+ rotation: rotation(EventInterval.Day, 1),
348
+ },
349
+ {
350
+ users: [user("F1")],
351
+ startDateTimeOfLayer: calStart,
352
+ restrictionTimes: noRestriction(),
353
+ handOffTime: calStart,
354
+ rotation: rotation(EventInterval.Week, 1),
355
+ },
356
+ ];
357
+
358
+ const { mismatches, overlaps } = checkMerge(layers, calStart, calEnd);
359
+ expect(mismatches).toEqual([]);
360
+ expect(overlaps).toBe(0);
361
+ });
362
+
363
+ test("Scenario F: fast rotation primary (hourly) restricted over fallback", () => {
364
+ const calStart: Date = new Date(2026, 0, 5, 0, 0, 0);
365
+ const calEnd: Date = new Date(2026, 0, 8, 0, 0, 0);
366
+
367
+ const layers: Array<LayerProps> = [
368
+ {
369
+ users: [user("A"), user("B"), user("C")],
370
+ startDateTimeOfLayer: calStart,
371
+ restrictionTimes: dailyRestriction(9, 17),
372
+ handOffTime: new Date(2026, 0, 5, 9, 0, 0),
373
+ rotation: rotation(EventInterval.Hour, 1),
374
+ },
375
+ {
376
+ users: [user("Z")],
377
+ startDateTimeOfLayer: calStart,
378
+ restrictionTimes: noRestriction(),
379
+ handOffTime: calStart,
380
+ rotation: rotation(EventInterval.Week, 1),
381
+ },
382
+ ];
383
+
384
+ const { mismatches, overlaps } = checkMerge(layers, calStart, calEnd, 7);
385
+ expect(mismatches).toEqual([]);
386
+ expect(overlaps).toBe(0);
387
+ });
388
+ });