@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,398 @@
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
+ /*
14
+ * Regression tests for the on-call audit fixes (round 2): F0, F2, F3, F4, F5, F8.
15
+ * Layer props set an explicit `timezone` so date math is independent of the
16
+ * machine's local timezone.
17
+ */
18
+
19
+ function u(id: string): User {
20
+ return {
21
+ id: {
22
+ toString: () => {
23
+ return id;
24
+ },
25
+ },
26
+ } as unknown as User;
27
+ }
28
+
29
+ function d(iso: string): Date {
30
+ return OneUptimeDate.fromString(iso);
31
+ }
32
+
33
+ function rot(intervalType: EventInterval, count: number): Recurring {
34
+ return Recurring.fromJSON({
35
+ _type: "Recurring",
36
+ value: {
37
+ intervalType: intervalType,
38
+ intervalCount: { _type: "PositiveNumber", value: count },
39
+ },
40
+ } as any);
41
+ }
42
+
43
+ function noRestriction(): RestrictionTimes {
44
+ const r: RestrictionTimes = new RestrictionTimes();
45
+ r.restictionType = RestrictionType.None;
46
+ r.dayRestrictionTimes = null;
47
+ return r;
48
+ }
49
+
50
+ /*
51
+ * Daily restriction whose From/To are given as absolute instants (so their
52
+ * wall-clock is unambiguous under the layer's timezone).
53
+ */
54
+ function dailyRestrictionFromInstants(
55
+ startInstant: string,
56
+ endInstant: string,
57
+ ): RestrictionTimes {
58
+ const r: RestrictionTimes = new RestrictionTimes();
59
+ r.restictionType = RestrictionType.Daily;
60
+ r.dayRestrictionTimes = {
61
+ startTime: d(startInstant),
62
+ endTime: d(endInstant),
63
+ };
64
+ return r;
65
+ }
66
+
67
+ function coveringEvent(
68
+ events: Array<CalendarEvent>,
69
+ at: Date,
70
+ ): CalendarEvent | undefined {
71
+ return events.find((e: CalendarEvent) => {
72
+ return (
73
+ OneUptimeDate.isOnOrBefore(e.start, at) &&
74
+ OneUptimeDate.isOnOrAfter(e.end, at)
75
+ );
76
+ });
77
+ }
78
+
79
+ describe("LayerUtil audit fixes round 2", () => {
80
+ describe("F0 - month/year rotation current-user near month-end", () => {
81
+ test("monthly rotation anchored on Jan 31 pages the correct user on Mar 30", () => {
82
+ const util: LayerUtil = new LayerUtil();
83
+ const users: Array<User> = [u("A"), u("B"), u("C"), u("D"), u("E")];
84
+ const base: LayerProps = {
85
+ users,
86
+ startDateTimeOfLayer: d("2024-01-01T10:00:00.000Z"),
87
+ handOffTime: d("2024-01-31T10:00:00.000Z"),
88
+ restrictionTimes: noRestriction(),
89
+ rotation: rot(EventInterval.Month, 1),
90
+ timezone: "UTC",
91
+ };
92
+
93
+ const target: Date = d("2024-03-30T12:00:00.000Z");
94
+
95
+ // Ground truth: expand from the layer start and find who covers the target.
96
+ const full: Array<CalendarEvent> = util.getEvents({
97
+ ...base,
98
+ calendarStartDate: d("2024-01-01T10:00:00.000Z"),
99
+ calendarEndDate: d("2024-05-01T00:00:00.000Z"),
100
+ });
101
+ const groundTruth: CalendarEvent | undefined = coveringEvent(
102
+ full,
103
+ target,
104
+ );
105
+ // Jan31 -> Feb29 -> Mar29 -> Apr29 : on Mar 30, D (index 3) is on call.
106
+ expect(groundTruth?.title).toBe("D");
107
+
108
+ /*
109
+ * The live "who is on call now" path resolves via a window that STARTS at
110
+ * the target instant; it must agree with the full-expansion ground truth.
111
+ */
112
+ const nowWindow: Array<CalendarEvent> = util.getEvents({
113
+ ...base,
114
+ calendarStartDate: target,
115
+ calendarEndDate: d("2024-03-30T13:00:00.000Z"),
116
+ });
117
+ expect(nowWindow[0]?.title).toBe("D");
118
+ });
119
+
120
+ test("yearly rotation anchored on Feb 29 stays consistent across leap boundary", () => {
121
+ const util: LayerUtil = new LayerUtil();
122
+ const users: Array<User> = [u("A"), u("B"), u("C")];
123
+ const base: LayerProps = {
124
+ users,
125
+ startDateTimeOfLayer: d("2024-01-01T00:00:00.000Z"),
126
+ handOffTime: d("2024-02-29T09:00:00.000Z"),
127
+ restrictionTimes: noRestriction(),
128
+ rotation: rot(EventInterval.Year, 1),
129
+ timezone: "UTC",
130
+ };
131
+
132
+ const target: Date = d("2027-06-01T00:00:00.000Z");
133
+
134
+ const full: Array<CalendarEvent> = util.getEvents({
135
+ ...base,
136
+ calendarStartDate: d("2024-01-01T00:00:00.000Z"),
137
+ calendarEndDate: d("2028-01-01T00:00:00.000Z"),
138
+ });
139
+ const groundTruth: CalendarEvent | undefined = coveringEvent(
140
+ full,
141
+ target,
142
+ );
143
+ expect(groundTruth).toBeDefined();
144
+
145
+ const nowWindow: Array<CalendarEvent> = util.getEvents({
146
+ ...base,
147
+ calendarStartDate: target,
148
+ calendarEndDate: d("2027-06-01T01:00:00.000Z"),
149
+ });
150
+ expect(nowWindow[0]?.title).toBe(groundTruth?.title);
151
+ });
152
+ });
153
+
154
+ describe("F2 - long rotation period + daily restriction keeps full coverage", () => {
155
+ test("quarterly rotation with a business-hours daily restriction covers days past 50", () => {
156
+ const util: LayerUtil = new LayerUtil();
157
+ const layer: LayerProps = {
158
+ users: [u("A")],
159
+ startDateTimeOfLayer: d("2026-01-01T00:00:00.000Z"),
160
+ handOffTime: d("2026-01-01T00:00:00.000Z"),
161
+ // 09:00 -> 17:00 UTC daily restriction.
162
+ restrictionTimes: dailyRestrictionFromInstants(
163
+ "2026-01-01T09:00:00.000Z",
164
+ "2026-01-01T17:00:00.000Z",
165
+ ),
166
+ rotation: rot(EventInterval.Month, 3), // quarterly -> one ~91-day event
167
+ timezone: "UTC",
168
+ };
169
+
170
+ const events: Array<CalendarEvent> = util.getEvents({
171
+ ...layer,
172
+ calendarStartDate: d("2026-01-01T00:00:00.000Z"),
173
+ calendarEndDate: d("2026-04-01T00:00:00.000Z"),
174
+ });
175
+
176
+ /*
177
+ * There must be a real 09:00-17:00 coverage window in mid-March (well past
178
+ * the old 50-day cap). Before the fix, coverage stopped around Feb 19.
179
+ */
180
+ const midMarch: Date = d("2026-03-16T12:00:00.000Z");
181
+ const covering: CalendarEvent | undefined = coveringEvent(
182
+ events,
183
+ midMarch,
184
+ );
185
+ expect(covering).toBeDefined();
186
+ expect(covering?.title).toBe("A");
187
+ });
188
+ });
189
+
190
+ describe("F3 - weekly wrap-around does not double-cover", () => {
191
+ test("Fri->Mon weekly restriction over a 2-week rotation has no overlapping events", () => {
192
+ const util: LayerUtil = new LayerUtil();
193
+
194
+ // Fri 20:00 -> Mon 08:00 UTC weekend window. 2026-01-09 is a Friday.
195
+ const weekly: WeeklyResctriction = {
196
+ startDay: DayOfWeek.Friday,
197
+ endDay: DayOfWeek.Monday,
198
+ startTime: d("2026-01-09T20:00:00.000Z"),
199
+ endTime: d("2026-01-05T08:00:00.000Z"),
200
+ };
201
+ const restrictionTimes: RestrictionTimes = new RestrictionTimes();
202
+ restrictionTimes.restictionType = RestrictionType.Weekly;
203
+ restrictionTimes.weeklyRestrictionTimes = [weekly];
204
+
205
+ const layer: LayerProps = {
206
+ users: [u("A"), u("B")],
207
+ startDateTimeOfLayer: d("2026-01-05T00:00:00.000Z"),
208
+ handOffTime: d("2026-01-05T00:00:00.000Z"),
209
+ restrictionTimes,
210
+ rotation: rot(EventInterval.Week, 2),
211
+ timezone: "UTC",
212
+ };
213
+
214
+ const events: Array<CalendarEvent> = util.getEvents({
215
+ ...layer,
216
+ calendarStartDate: d("2026-01-05T00:00:00.000Z"),
217
+ calendarEndDate: d("2026-02-02T00:00:00.000Z"),
218
+ });
219
+
220
+ // No two events may overlap (the old head/main tiling duplicated Sun->Mon).
221
+ for (let i: number = 0; i < events.length; i++) {
222
+ for (let j: number = i + 1; j < events.length; j++) {
223
+ const a: CalendarEvent = events[i]!;
224
+ const b: CalendarEvent = events[j]!;
225
+ const overlap: boolean =
226
+ OneUptimeDate.isBefore(a.start, b.end) &&
227
+ OneUptimeDate.isBefore(b.start, a.end);
228
+ expect(overlap).toBe(false);
229
+ }
230
+ }
231
+ });
232
+ });
233
+
234
+ describe("F8 - long restriction gap does not truncate to empty", () => {
235
+ test("hourly rotation with a Monday-only weekly window still finds upcoming coverage >100h out", () => {
236
+ const util: LayerUtil = new LayerUtil();
237
+
238
+ // Active Mondays only: Mon 00:00 -> Tue 00:00 UTC. 2026-01-05 is a Monday.
239
+ const weekly: WeeklyResctriction = {
240
+ startDay: DayOfWeek.Monday,
241
+ endDay: DayOfWeek.Tuesday,
242
+ startTime: d("2026-01-05T00:00:00.000Z"),
243
+ endTime: d("2026-01-06T00:00:00.000Z"),
244
+ };
245
+ const restrictionTimes: RestrictionTimes = new RestrictionTimes();
246
+ restrictionTimes.restictionType = RestrictionType.Weekly;
247
+ restrictionTimes.weeklyRestrictionTimes = [weekly];
248
+
249
+ const layer: LayerProps = {
250
+ users: [u("A"), u("B")],
251
+ startDateTimeOfLayer: d("2026-01-01T00:00:00.000Z"),
252
+ handOffTime: d("2026-01-01T00:00:00.000Z"),
253
+ restrictionTimes,
254
+ rotation: rot(EventInterval.Hour, 1),
255
+ timezone: "UTC",
256
+ };
257
+
258
+ /*
259
+ * "now" is Tuesday 01:00, just after the Monday window ended. The next
260
+ * covered window is next Monday (~143h away) — more than the old 100-period
261
+ * cap, which returned [] (schedule reporting nobody on-call).
262
+ */
263
+ const calendarStart: Date = d("2026-01-06T01:00:00.000Z");
264
+ const events: Array<CalendarEvent> = util.getEvents({
265
+ ...layer,
266
+ calendarStartDate: calendarStart,
267
+ calendarEndDate: d("2027-01-06T00:00:00.000Z"),
268
+ });
269
+
270
+ expect(events.length).toBeGreaterThan(0);
271
+ // The first coverage is next Monday, > 100 hours after calendarStart.
272
+ const hundredHoursOut: Date = OneUptimeDate.addRemoveHours(
273
+ calendarStart,
274
+ 100,
275
+ );
276
+ expect(OneUptimeDate.isAfter(events[0]!.start, hundredHoursOut)).toBe(
277
+ true,
278
+ );
279
+ });
280
+ });
281
+
282
+ describe("Follow-up - daily restriction covers later days when the event starts after the window", () => {
283
+ test("weekly rotation with a 20:00 handoff and 09:00-17:00 daily restriction covers days 2..7", () => {
284
+ const util: LayerUtil = new LayerUtil();
285
+
286
+ /*
287
+ * 2026-01-05 is a Monday. Handoff/start at 20:00 — AFTER that day's
288
+ * 09:00-17:00 restriction window.
289
+ */
290
+ const layer: LayerProps = {
291
+ users: [u("A")],
292
+ startDateTimeOfLayer: d("2026-01-05T20:00:00.000Z"),
293
+ handOffTime: d("2026-01-05T20:00:00.000Z"),
294
+ restrictionTimes: dailyRestrictionFromInstants(
295
+ "2026-01-05T09:00:00.000Z",
296
+ "2026-01-05T17:00:00.000Z",
297
+ ),
298
+ rotation: rot(EventInterval.Week, 1),
299
+ timezone: "UTC",
300
+ };
301
+
302
+ const events: Array<CalendarEvent> = util.getEvents({
303
+ ...layer,
304
+ calendarStartDate: d("2026-01-05T20:00:00.000Z"),
305
+ calendarEndDate: d("2026-01-12T20:00:00.000Z"),
306
+ });
307
+
308
+ // Before the fix this returned ZERO events (the whole week uncovered).
309
+ expect(events.length).toBeGreaterThan(0);
310
+
311
+ // Tuesday (day 2) noon is covered by that day's 09:00-17:00 window.
312
+ const tuesdayNoon: Date = d("2026-01-06T12:00:00.000Z");
313
+ expect(coveringEvent(events, tuesdayNoon)?.title).toBe("A");
314
+
315
+ /*
316
+ * The first night (Monday 22:00, after the 20:00 start, outside the
317
+ * restriction) is correctly NOT covered.
318
+ */
319
+ const mondayNight: Date = d("2026-01-05T22:00:00.000Z");
320
+ expect(coveringEvent(events, mondayNight)).toBeUndefined();
321
+ });
322
+ });
323
+
324
+ describe("F4 - rotation handoff preserves schedule wall-clock across DST", () => {
325
+ test("daily 09:00 America/New_York handoff stays at 09:00 local after spring-forward", () => {
326
+ const util: LayerUtil = new LayerUtil();
327
+ const layer: LayerProps = {
328
+ users: [u("A"), u("B")],
329
+ // 2026-03-05 09:00 America/New_York (EST, UTC-5) = 14:00 UTC.
330
+ startDateTimeOfLayer: d("2026-03-05T14:00:00.000Z"),
331
+ handOffTime: d("2026-03-05T14:00:00.000Z"),
332
+ restrictionTimes: noRestriction(),
333
+ rotation: rot(EventInterval.Day, 1),
334
+ timezone: "America/New_York",
335
+ };
336
+
337
+ const events: Array<CalendarEvent> = util.getEvents({
338
+ ...layer,
339
+ calendarStartDate: d("2026-03-05T14:00:00.000Z"),
340
+ calendarEndDate: d("2026-03-12T00:00:00.000Z"),
341
+ });
342
+
343
+ /*
344
+ * US spring-forward is 2026-03-08. On March 10 (EDT, UTC-4), 09:00 local is
345
+ * 13:00 UTC. Before the fix, absolute +24h stepping drifted it to 14:00 UTC.
346
+ */
347
+ const march10Afternoon: Date = d("2026-03-10T18:00:00.000Z");
348
+ const covering: CalendarEvent | undefined = coveringEvent(
349
+ events,
350
+ march10Afternoon,
351
+ );
352
+ expect(covering).toBeDefined();
353
+ expect(new Date(covering!.start).getUTCHours()).toBe(13);
354
+ });
355
+ });
356
+
357
+ describe("F5 - daily restriction preserves schedule wall-clock across DST", () => {
358
+ test("09:00-17:00 America/New_York restriction stays 09:00-17:00 local after spring-forward", () => {
359
+ const util: LayerUtil = new LayerUtil();
360
+
361
+ const layer: LayerProps = {
362
+ users: [u("A")],
363
+ /*
364
+ * 2026-03-05 00:00 America/New_York (EST) = 05:00 UTC — a NY-day-aligned
365
+ * handoff, as real weekly schedules use.
366
+ */
367
+ startDateTimeOfLayer: d("2026-03-05T05:00:00.000Z"),
368
+ handOffTime: d("2026-03-05T05:00:00.000Z"),
369
+ // 09:00 EST and 17:00 EST reference instants -> 09:00/17:00 New York.
370
+ restrictionTimes: dailyRestrictionFromInstants(
371
+ "2026-03-05T14:00:00.000Z",
372
+ "2026-03-05T22:00:00.000Z",
373
+ ),
374
+ rotation: rot(EventInterval.Week, 1), // one 7-day event spanning DST
375
+ timezone: "America/New_York",
376
+ };
377
+
378
+ const events: Array<CalendarEvent> = util.getEvents({
379
+ ...layer,
380
+ calendarStartDate: d("2026-03-05T05:00:00.000Z"),
381
+ calendarEndDate: d("2026-03-12T05:00:00.000Z"),
382
+ });
383
+
384
+ /*
385
+ * The March 10 window (EDT) must be 09:00-17:00 local = 13:00-21:00 UTC.
386
+ * A drifted window would be 10:00-18:00 local (14:00-22:00 UTC), leaving
387
+ * 13:30 UTC uncovered.
388
+ */
389
+ const march10Morning: Date = d("2026-03-10T13:30:00.000Z"); // 09:30 EDT
390
+ const covering: CalendarEvent | undefined = coveringEvent(
391
+ events,
392
+ march10Morning,
393
+ );
394
+ expect(covering).toBeDefined();
395
+ expect(new Date(covering!.start).getUTCHours()).toBe(13);
396
+ });
397
+ });
398
+ });
@@ -0,0 +1,114 @@
1
+ /**
2
+ * REGRESSION (audit F8): weekly wrap-around (weekend) restriction close must not
3
+ * drift across a DST transition.
4
+ *
5
+ * getWeeklyRestrictionTimesForWeek builds the wrap-around "main" segment end as
6
+ * a +7-day step. If that step is taken in the process zone instead of the
7
+ * schedule zone, then when the server/process zone (e.g. UTC) differs from the
8
+ * schedule zone across a DST transition, the weekend window's Monday-morning
9
+ * close drifts by the DST offset — over-covering (extra-hour paging of the
10
+ * weekend user) or, on fall-back, opening a coverage gap.
11
+ *
12
+ * These assertions are expressed in schedule-zone wall-clock via moment-timezone
13
+ * so they hold regardless of the process TZ the test runs under (run under
14
+ * TZ=UTC to exercise the exact server-vs-schedule divergence the bug needs).
15
+ */
16
+ import LayerUtil from "../../../Types/OnCallDutyPolicy/Layer";
17
+ import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
18
+ import RestrictionTimes, {
19
+ RestrictionType,
20
+ WeeklyResctriction,
21
+ } from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
22
+ import Recurring from "../../../Types/Events/Recurring";
23
+ import User from "../../../Models/DatabaseModels/User";
24
+ import EventInterval from "../../../Types/Events/EventInterval";
25
+ import DayOfWeek from "../../../Types/Day/DayOfWeek";
26
+ import moment from "moment-timezone";
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(
39
+ intervalType: EventInterval,
40
+ intervalCount: number,
41
+ ): Recurring {
42
+ return Recurring.fromJSON({
43
+ _type: "Recurring",
44
+ value: {
45
+ intervalType: intervalType,
46
+ intervalCount: { _type: "PositiveNumber", value: intervalCount },
47
+ },
48
+ } as any);
49
+ }
50
+
51
+ // An absolute instant that is `iso` wall-clock in `tz`.
52
+ function tzInstant(iso: string, tz: string): Date {
53
+ return moment.tz(iso, tz).toDate();
54
+ }
55
+
56
+ const NY: string = "America/New_York";
57
+
58
+ function weekendLayerEvents(): Array<CalendarEvent> {
59
+ const util: LayerUtil = new LayerUtil();
60
+
61
+ // Layer starts Sun Mar 1 2026 00:00 NY; monthly single-user rotation.
62
+ const layerStart: Date = tzInstant("2026-03-01 00:00", NY);
63
+
64
+ // Weekend wrap-around restriction: Fri 20:00 -> Mon 08:00 (NY wall-clock).
65
+ const weekly: WeeklyResctriction = {
66
+ startDay: DayOfWeek.Friday,
67
+ endDay: DayOfWeek.Monday,
68
+ startTime: tzInstant("2026-01-02 20:00", NY), // a Friday 20:00 NY
69
+ endTime: tzInstant("2026-01-05 08:00", NY), // a Monday 08:00 NY
70
+ };
71
+ const restrictionTimes: RestrictionTimes = new RestrictionTimes();
72
+ restrictionTimes.restictionType = RestrictionType.Weekly;
73
+ restrictionTimes.weeklyRestrictionTimes = [weekly];
74
+
75
+ return util.getEvents({
76
+ users: [user("weekend-user")],
77
+ startDateTimeOfLayer: layerStart,
78
+ handOffTime: tzInstant("2026-04-01 00:00", NY),
79
+ restrictionTimes: restrictionTimes,
80
+ rotation: rotation(EventInterval.Month, 1),
81
+ timezone: NY,
82
+ calendarStartDate: layerStart,
83
+ calendarEndDate: tzInstant("2026-04-01 00:00", NY),
84
+ });
85
+ }
86
+
87
+ describe("F8: weekend wrap-around restriction close is stable across DST", () => {
88
+ const events: Array<CalendarEvent> = weekendLayerEvents();
89
+
90
+ const covered: (t: Date) => boolean = (t: Date): boolean => {
91
+ return events.some((e: CalendarEvent) => {
92
+ return t.getTime() >= e.start.getTime() && t.getTime() < e.end.getTime();
93
+ });
94
+ };
95
+
96
+ /*
97
+ * US spring-forward 2026 is Sunday March 8; the weekend that opens Fri Mar 6
98
+ * 20:00 closes Mon Mar 9 08:00 NY.
99
+ */
100
+ test("inside the weekend window is covered (Fri 20:30 and Mon 07:30 NY)", () => {
101
+ expect(covered(tzInstant("2026-03-06 20:30", NY))).toBe(true);
102
+ expect(covered(tzInstant("2026-03-09 07:30", NY))).toBe(true);
103
+ });
104
+
105
+ test("Mon 2026-03-09 08:30 NY is NOT covered (authored close is 08:00, no DST drift)", () => {
106
+ expect(covered(tzInstant("2026-03-09 08:30", NY))).toBe(false);
107
+ });
108
+
109
+ test("a non-DST weekend closes at the same 08:00 NY wall-clock", () => {
110
+ // Weekend opening Fri Mar 20 20:00 closes Mon Mar 23 08:00 NY (no DST that week).
111
+ expect(covered(tzInstant("2026-03-23 07:30", NY))).toBe(true);
112
+ expect(covered(tzInstant("2026-03-23 08:30", NY))).toBe(false);
113
+ });
114
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * REGRESSION (audit F9): OneUptimeDate.moveDateToTheDayOfWeek must keep its
3
+ * timezone on the final day-shift, so a weekly restriction's day-of-week
4
+ * boundary keeps its authored wall-clock across a DST transition.
5
+ *
6
+ * The first two steps (keepTimeButMoveDay, getDayOfWeek) are timezone-aware; the
7
+ * final addRemoveDays shift must be too, otherwise a shift that straddles a DST
8
+ * instant in the schedule zone (but not the process zone) drifts the resolved
9
+ * boundary by the DST offset.
10
+ *
11
+ * Assertions are in schedule-zone wall-clock (moment-timezone) so they hold
12
+ * regardless of the process TZ (run under TZ=UTC to model a UTC server).
13
+ */
14
+ import LayerUtil from "../../../Types/OnCallDutyPolicy/Layer";
15
+ import RestrictionTimes, {
16
+ RestrictionType,
17
+ WeeklyResctriction,
18
+ } from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
19
+ import OneUptimeDate from "../../../Types/Date";
20
+ import DayOfWeek from "../../../Types/Day/DayOfWeek";
21
+ import moment from "moment-timezone";
22
+
23
+ function hhmm(d: Date, tz: string): string {
24
+ return moment.tz(d, tz).format("HH:mm");
25
+ }
26
+
27
+ const NY: string = "America/New_York";
28
+
29
+ describe("F9: moveDateToTheDayOfWeek preserves wall-clock across DST", () => {
30
+ it("moves a Sunday 01:00 NY restriction into a spring-forward week keeping 01:00", () => {
31
+ /*
32
+ * Authored Sunday 01:00 NY, moved into the week containing the 2026-03-08
33
+ * spring-forward. The day-shift crosses the DST instant; wall-clock must hold.
34
+ */
35
+ const restriction: Date = moment.tz("2026-01-04 01:00", NY).toDate(); // a Sunday 01:00
36
+ const eventStartTime: Date = moment.tz("2026-03-11 12:00", NY).toDate(); // Wednesday of DST week
37
+
38
+ const moved: Date = OneUptimeDate.moveDateToTheDayOfWeek(
39
+ restriction,
40
+ eventStartTime,
41
+ OneUptimeDate.getDayOfWeek(restriction, NY), // Sunday
42
+ NY,
43
+ );
44
+
45
+ expect(hhmm(moved, NY)).toBe("01:00");
46
+ });
47
+
48
+ it("END-TO-END: Sunday 01:00-05:00 weekly restriction keeps its wall-clock in a DST week", () => {
49
+ const util: LayerUtil = new LayerUtil();
50
+ (util as any).timezone = NY;
51
+
52
+ const wr: WeeklyResctriction = {
53
+ startTime: moment.tz("2026-01-04 01:00", NY).toDate(), // Sunday 01:00 NY
54
+ startDay: DayOfWeek.Sunday,
55
+ endTime: moment.tz("2026-01-04 05:00", NY).toDate(), // Sunday 05:00 NY
56
+ endDay: DayOfWeek.Sunday,
57
+ } as any;
58
+
59
+ const rt: RestrictionTimes = new RestrictionTimes();
60
+ rt.restictionType = RestrictionType.Weekly;
61
+ rt.weeklyRestrictionTimes = [wr];
62
+
63
+ // Resolve on Wednesday of the week containing the 2026-03-08 spring-forward.
64
+ const windows: Array<{ startTime: Date; endTime: Date }> =
65
+ util.getWeeklyRestrictionTimesForWeek({
66
+ eventStartTime: moment.tz("2026-03-11 12:00", NY).toDate(),
67
+ eventEndTime: moment.tz("2026-03-14 23:59", NY).toDate(),
68
+ restrictionTimes: rt,
69
+ });
70
+
71
+ expect(windows.length).toBeGreaterThan(0);
72
+ const w0: { startTime: Date; endTime: Date } = windows[0]!;
73
+ expect(hhmm(w0.startTime, NY)).toBe("01:00");
74
+ expect(hhmm(w0.endTime, NY)).toBe("05:00");
75
+ });
76
+ });