@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,243 @@
1
+ import LayerUtil 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 User from "../../../Models/DatabaseModels/User";
9
+ import EventInterval from "../../../Types/Events/EventInterval";
10
+ import DayOfWeek from "../../../Types/Day/DayOfWeek";
11
+ import moment from "moment-timezone";
12
+
13
+ /*
14
+ * HIGH-5 (timezone) + LOW-2 (DST). These tests construct restriction times and
15
+ * verify results entirely in an explicit IANA zone via moment.tz, so they hold
16
+ * regardless of the machine/CI local timezone — which is exactly the bug:
17
+ * restriction wall-clock must resolve in the SCHEDULE's zone, not the server's.
18
+ */
19
+
20
+ function user(id: string): User {
21
+ return {
22
+ id: {
23
+ toString: () => {
24
+ return id;
25
+ },
26
+ } as any,
27
+ } as User;
28
+ }
29
+
30
+ function rotation(
31
+ intervalType: EventInterval,
32
+ intervalCount: number,
33
+ ): Recurring {
34
+ return Recurring.fromJSON({
35
+ _type: "Recurring",
36
+ value: {
37
+ intervalType: intervalType,
38
+ intervalCount: { _type: "PositiveNumber", value: intervalCount },
39
+ },
40
+ } as any);
41
+ }
42
+
43
+ // An absolute instant that is `iso` wall-clock in `tz`.
44
+ function tzInstant(iso: string, tz: string): Date {
45
+ return moment.tz(iso, tz).toDate();
46
+ }
47
+
48
+ // Wall-clock "HH:mm" of an instant AS SEEN IN `tz`.
49
+ function hhmm(d: Date, tz: string): string {
50
+ return moment.tz(d, tz).format("HH:mm");
51
+ }
52
+
53
+ describe("LayerUtil HIGH-5: daily restriction resolves in the schedule timezone", () => {
54
+ const tz: string = "America/New_York";
55
+
56
+ test("09:00-17:00 restriction resolves to 09:00-17:00 in the schedule zone", () => {
57
+ const util: LayerUtil = new LayerUtil();
58
+
59
+ const layerStart: Date = tzInstant("2026-02-16 00:00", tz); // Mon (no DST)
60
+ const calStart: Date = layerStart;
61
+ const calEnd: Date = tzInstant("2026-02-19 00:00", tz); // +3 days
62
+
63
+ const restrictionTimes: RestrictionTimes = new RestrictionTimes();
64
+ restrictionTimes.restictionType = RestrictionType.Daily;
65
+ restrictionTimes.dayRestrictionTimes = {
66
+ startTime: tzInstant("2026-02-16 09:00", tz),
67
+ endTime: tzInstant("2026-02-16 17:00", tz),
68
+ };
69
+
70
+ const events: Array<CalendarEvent> = util.getEvents({
71
+ users: [user("u1")],
72
+ startDateTimeOfLayer: layerStart,
73
+ handOffTime: layerStart,
74
+ restrictionTimes: restrictionTimes,
75
+ rotation: rotation(EventInterval.Day, 1),
76
+ timezone: tz,
77
+ calendarStartDate: calStart,
78
+ calendarEndDate: calEnd,
79
+ });
80
+
81
+ // One 09:00-17:00 window per day, in New York wall-clock.
82
+ const windows: Array<CalendarEvent> = events.filter((e: CalendarEvent) => {
83
+ return hhmm(e.start, tz) === "09:00";
84
+ });
85
+ expect(windows.length).toBeGreaterThanOrEqual(3);
86
+ for (const w of windows) {
87
+ expect(hhmm(w.start, tz)).toBe("09:00");
88
+ expect(hhmm(w.end, tz)).toBe("17:00");
89
+ }
90
+ });
91
+
92
+ test("a schedule authored + resolved in Kolkata resolves to its own wall-clock", () => {
93
+ const util: LayerUtil = new LayerUtil();
94
+ const tzIST: string = "Asia/Kolkata"; // UTC+5:30, no DST
95
+
96
+ /*
97
+ * Author and resolve entirely in Kolkata, with the layer start aligned to
98
+ * local midnight (as a real schedule created in that zone would be).
99
+ */
100
+ const layerStart: Date = tzInstant("2026-02-16 00:00", tzIST);
101
+ const restrictionTimes: RestrictionTimes = new RestrictionTimes();
102
+ restrictionTimes.restictionType = RestrictionType.Daily;
103
+ restrictionTimes.dayRestrictionTimes = {
104
+ startTime: tzInstant("2026-02-16 09:30", tzIST),
105
+ endTime: tzInstant("2026-02-16 18:30", tzIST),
106
+ };
107
+
108
+ const events: Array<CalendarEvent> = util.getEvents({
109
+ users: [user("u1")],
110
+ startDateTimeOfLayer: layerStart,
111
+ handOffTime: layerStart,
112
+ restrictionTimes: restrictionTimes,
113
+ rotation: rotation(EventInterval.Day, 1),
114
+ timezone: tzIST,
115
+ calendarStartDate: layerStart,
116
+ calendarEndDate: tzInstant("2026-02-19 00:00", tzIST),
117
+ });
118
+
119
+ const windows: Array<CalendarEvent> = events.filter((e: CalendarEvent) => {
120
+ return hhmm(e.start, tzIST) === "09:30";
121
+ });
122
+ expect(windows.length).toBeGreaterThanOrEqual(2);
123
+ for (const w of windows) {
124
+ expect(hhmm(w.end, tzIST)).toBe("18:30");
125
+ }
126
+ });
127
+ });
128
+
129
+ describe("LayerUtil LOW-2: DST correctness across spring-forward", () => {
130
+ const tz: string = "America/New_York";
131
+
132
+ test("09:00-17:00 restriction stays 09:00-17:00 local across the DST spring-forward day", () => {
133
+ const util: LayerUtil = new LayerUtil();
134
+
135
+ // US spring-forward 2026 is Sun Mar 8. Cover Sat Mar 7 -> Tue Mar 10.
136
+ const layerStart: Date = tzInstant("2026-03-07 00:00", tz);
137
+ const restrictionTimes: RestrictionTimes = new RestrictionTimes();
138
+ restrictionTimes.restictionType = RestrictionType.Daily;
139
+ restrictionTimes.dayRestrictionTimes = {
140
+ startTime: tzInstant("2026-03-07 09:00", tz),
141
+ endTime: tzInstant("2026-03-07 17:00", tz),
142
+ };
143
+
144
+ const events: Array<CalendarEvent> = util.getEvents({
145
+ users: [user("u1")],
146
+ startDateTimeOfLayer: layerStart,
147
+ handOffTime: layerStart,
148
+ restrictionTimes: restrictionTimes,
149
+ rotation: rotation(EventInterval.Day, 1),
150
+ timezone: tz,
151
+ calendarStartDate: layerStart,
152
+ calendarEndDate: tzInstant("2026-03-10 00:00", tz),
153
+ });
154
+
155
+ const windows: Array<CalendarEvent> = events.filter((e: CalendarEvent) => {
156
+ return hhmm(e.start, tz) === "09:00";
157
+ });
158
+ // Sat, Sun (DST day), Mon — all keep 09:00-17:00 local despite the lost hour.
159
+ expect(windows.length).toBeGreaterThanOrEqual(3);
160
+ for (const w of windows) {
161
+ expect(hhmm(w.start, tz)).toBe("09:00");
162
+ expect(hhmm(w.end, tz)).toBe("17:00");
163
+ }
164
+ });
165
+ });
166
+
167
+ describe("LayerUtil HIGH-5: weekly restriction resolves day-of-week in the schedule zone", () => {
168
+ const tz: string = "America/New_York";
169
+
170
+ test("Mon 09:00 -> Wed 17:00 weekly restriction covers Tue midday, not Sat", () => {
171
+ const util: LayerUtil = new LayerUtil();
172
+
173
+ const monday: Date = tzInstant("2026-02-16 00:00", tz); // Monday
174
+ const weekly: WeeklyResctriction = {
175
+ startDay: DayOfWeek.Monday,
176
+ endDay: DayOfWeek.Wednesday,
177
+ startTime: tzInstant("2026-02-16 09:00", tz), // Monday 09:00 NY
178
+ endTime: tzInstant("2026-02-18 17:00", tz), // Wednesday 17:00 NY
179
+ };
180
+ const restrictionTimes: RestrictionTimes = new RestrictionTimes();
181
+ restrictionTimes.restictionType = RestrictionType.Weekly;
182
+ restrictionTimes.weeklyRestrictionTimes = [weekly];
183
+
184
+ const events: Array<CalendarEvent> = util.getEvents({
185
+ users: [user("u1")],
186
+ startDateTimeOfLayer: monday,
187
+ handOffTime: tzInstant("2026-02-23 00:00", tz),
188
+ restrictionTimes: restrictionTimes,
189
+ rotation: rotation(EventInterval.Week, 1),
190
+ timezone: tz,
191
+ calendarStartDate: monday,
192
+ calendarEndDate: tzInstant("2026-02-23 00:00", tz),
193
+ });
194
+
195
+ const covered: (t: Date) => boolean = (t: Date): boolean => {
196
+ return events.some((e: CalendarEvent) => {
197
+ return (
198
+ t.getTime() >= e.start.getTime() && t.getTime() < e.end.getTime()
199
+ );
200
+ });
201
+ };
202
+
203
+ // Tue 12:00 NY is inside Mon 09:00 -> Wed 17:00.
204
+ expect(covered(tzInstant("2026-02-17 12:00", tz))).toBe(true);
205
+ // Sat 12:00 NY is not.
206
+ expect(covered(tzInstant("2026-02-21 12:00", tz))).toBe(false);
207
+ });
208
+ });
209
+
210
+ describe("LayerUtil HIGH-5 backward compatibility: no timezone == legacy local behavior", () => {
211
+ test("omitting timezone yields identical results to before (local reconstruction)", () => {
212
+ const util: LayerUtil = new LayerUtil();
213
+ // Author restriction in machine-local time via native Date.
214
+ const layerStart: Date = new Date(2026, 1, 16, 0, 0, 0); // local midnight
215
+ const restrictionTimes: RestrictionTimes = new RestrictionTimes();
216
+ restrictionTimes.restictionType = RestrictionType.Daily;
217
+ restrictionTimes.dayRestrictionTimes = {
218
+ startTime: new Date(2026, 1, 16, 9, 0, 0),
219
+ endTime: new Date(2026, 1, 16, 17, 0, 0),
220
+ };
221
+
222
+ const events: Array<CalendarEvent> = util.getEvents({
223
+ users: [user("u1")],
224
+ startDateTimeOfLayer: layerStart,
225
+ handOffTime: layerStart,
226
+ restrictionTimes: restrictionTimes,
227
+ rotation: rotation(EventInterval.Day, 1),
228
+ // timezone intentionally omitted
229
+ calendarStartDate: layerStart,
230
+ calendarEndDate: new Date(2026, 1, 19, 0, 0, 0),
231
+ });
232
+
233
+ const windows: Array<CalendarEvent> = events.filter((e: CalendarEvent) => {
234
+ return e.start.getHours() === 9;
235
+ });
236
+ expect(windows.length).toBeGreaterThanOrEqual(2);
237
+ for (const w of windows) {
238
+ // Local wall-clock hours preserved exactly as legacy behavior.
239
+ expect(w.start.getHours()).toBe(9);
240
+ expect(w.end.getHours()).toBe(17);
241
+ }
242
+ });
243
+ });
@@ -0,0 +1,112 @@
1
+ /**
2
+ * REPRO (weekly granularity of the post-window-gap off-by-one):
3
+ * weekly rotation [A,B] with a Mon-Fri weekly restriction, resolved on the
4
+ * WEEKEND, attributes next week to the WRONG user.
5
+ *
6
+ * Layer start Mon 2025-01-06 00:00 UTC, weekly rotation, restriction active
7
+ * Mon 00:00 -> Sat 00:00 (i.e. Mon-Fri).
8
+ * Full expansion (calendar): week0 (Jan6-10)=A, week1 (Jan13-17)=B ...
9
+ * Windowed asked on Sat 2025-01-11 12:00 : next week=A (WRONG, should be B).
10
+ */
11
+ import LayerUtil, { LayerProps } from "../../../Types/OnCallDutyPolicy/Layer";
12
+ import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
13
+ import RestrictionTimes, {
14
+ RestrictionType,
15
+ WeeklyResctriction,
16
+ } from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
17
+ import Recurring from "../../../Types/Events/Recurring";
18
+ import OneUptimeDate from "../../../Types/Date";
19
+ import User from "../../../Models/DatabaseModels/User";
20
+ import EventInterval from "../../../Types/Events/EventInterval";
21
+ import PositiveNumber from "../../../Types/PositiveNumber";
22
+ import DayOfWeek from "../../../Types/Day/DayOfWeek";
23
+
24
+ function user(id: string): User {
25
+ return {
26
+ id: {
27
+ toString: (): string => {
28
+ return id;
29
+ },
30
+ } as any,
31
+ } as User;
32
+ }
33
+
34
+ function makeLayer(): LayerProps {
35
+ const rot: Recurring = new Recurring();
36
+ rot.intervalType = EventInterval.Week;
37
+ rot.intervalCount = new PositiveNumber(1);
38
+ const start: Date = OneUptimeDate.fromString("2025-01-06T00:00:00.000Z"); // Monday
39
+
40
+ // Active Mon 00:00 -> Sat 00:00 (Mon-Fri coverage). Non-wrapping window.
41
+ const weekly: WeeklyResctriction = {
42
+ startDay: DayOfWeek.Monday,
43
+ endDay: DayOfWeek.Saturday,
44
+ startTime: OneUptimeDate.fromString("2025-01-06T00:00:00.000Z"), // Monday
45
+ endTime: OneUptimeDate.fromString("2025-01-11T00:00:00.000Z"), // Saturday
46
+ };
47
+ const r: RestrictionTimes = new RestrictionTimes();
48
+ r.restictionType = RestrictionType.Weekly;
49
+ r.weeklyRestrictionTimes = [weekly];
50
+
51
+ return {
52
+ users: [user("A"), user("B")],
53
+ startDateTimeOfLayer: start,
54
+ restrictionTimes: r,
55
+ handOffTime: Recurring.getNextDateInterval(start, rot),
56
+ rotation: rot,
57
+ timezone: undefined,
58
+ };
59
+ }
60
+
61
+ function fmt(e: CalendarEvent): string {
62
+ return `${e.title} [${e.start.toISOString()} -> ${e.end.toISOString()}]`;
63
+ }
64
+
65
+ describe("Weekly rotation + Mon-Fri restriction: weekend query off-by-one", () => {
66
+ test("week1 is B in calendar but a Saturday query says next week=A", () => {
67
+ const layer: LayerProps = makeLayer();
68
+ const util: LayerUtil = new LayerUtil();
69
+
70
+ const full: Array<CalendarEvent> = util.getEvents({
71
+ ...layer,
72
+ calendarStartDate: layer.startDateTimeOfLayer,
73
+ calendarEndDate: OneUptimeDate.fromString("2025-01-25T00:00:00.000Z"),
74
+ });
75
+
76
+ // Who covers Wed Jan 15 (inside week1 Mon-Fri) in the calendar?
77
+ const jan15: Date = OneUptimeDate.fromString("2025-01-15T12:00:00.000Z");
78
+ const week1Cover: CalendarEvent | undefined = full.find(
79
+ (e: CalendarEvent) => {
80
+ return (
81
+ OneUptimeDate.isOnOrAfter(jan15, e.start) &&
82
+ OneUptimeDate.isBefore(jan15, e.end)
83
+ );
84
+ },
85
+ );
86
+
87
+ // Ask on Saturday Jan 11 12:00 (weekend gap of week0).
88
+ const askAt: Date = OneUptimeDate.fromString("2025-01-11T12:00:00.000Z");
89
+ const windowed: Array<CalendarEvent> = util.getEvents(
90
+ {
91
+ ...layer,
92
+ calendarStartDate: askAt,
93
+ calendarEndDate: OneUptimeDate.addRemoveDays(askAt, 14),
94
+ },
95
+ { getNumberOfEvents: 1 },
96
+ );
97
+
98
+ // eslint-disable-next-line no-console
99
+ console.log("FULL:", full.map(fmt));
100
+ // eslint-disable-next-line no-console
101
+ console.log("week1 covered by (full):", week1Cover && fmt(week1Cover));
102
+ // eslint-disable-next-line no-console
103
+ console.log(
104
+ "WINDOWED next @Sat Jan11 12:00:",
105
+ windowed[0] && fmt(windowed[0]),
106
+ );
107
+
108
+ expect(week1Cover?.title).toBe("B");
109
+ // Windowed next on-call should be week1 = B; it is A -> BUG.
110
+ expect(windowed[0]?.title).toBe("B");
111
+ });
112
+ });
@@ -0,0 +1,117 @@
1
+ import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
2
+ import UserOverrideUtil, {
3
+ UserOverrideRecord,
4
+ } from "../../../Types/OnCallDutyPolicy/UserOverrideUtil";
5
+
6
+ function at(h: number, m: number = 0, s: number = 0, ms: number = 0): Date {
7
+ return new Date(2026, 0, 1, h, m, s, ms);
8
+ }
9
+
10
+ function event(
11
+ title: string,
12
+ startHour: number,
13
+ endHour: number,
14
+ ): CalendarEvent {
15
+ return {
16
+ id: 1,
17
+ title,
18
+ allDay: false,
19
+ start: at(startHour),
20
+ end: at(endHour),
21
+ };
22
+ }
23
+
24
+ function resolvedAt(events: Array<CalendarEvent>, t: Date): string | null {
25
+ for (const e of events) {
26
+ if (t.getTime() >= e.start.getTime() && t.getTime() < e.end.getTime()) {
27
+ return e.title;
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+
33
+ describe("Override lens audit", () => {
34
+ test("REPRO: applyOverridesToEvents is non-transitive (roster stores B for chain A->B, B->C)", () => {
35
+ // User A is on-call. A->B (A on vacation, route to B). B->C (B also busy, route to C).
36
+ const base: CalendarEvent = event("A", 8, 14);
37
+ const aToB: UserOverrideRecord = {
38
+ overrideUserId: "A",
39
+ routeAlertsToUserId: "B",
40
+ startsAt: at(9),
41
+ endsAt: at(13),
42
+ onCallDutyPolicyId: null,
43
+ };
44
+ const bToC: UserOverrideRecord = {
45
+ overrideUserId: "B",
46
+ routeAlertsToUserId: "C",
47
+ startsAt: at(9),
48
+ endsAt: at(13),
49
+ onCallDutyPolicyId: null,
50
+ };
51
+ const result: Array<CalendarEvent> =
52
+ UserOverrideUtil.applyOverridesToEvents({
53
+ events: [base],
54
+ overrides: [aToB, bToC],
55
+ });
56
+ // Non-transitive: schedule resolution (roster + dashboard) reports B, NOT C.
57
+ expect(resolvedAt(result, at(11))).toBe("B");
58
+ /*
59
+ * The escalation paging path would then run getRouteAlertToUserId("B"),
60
+ * find the B->C override, and page C -> divergence from the roster (B).
61
+ */
62
+ });
63
+
64
+ test("REPRO: swap pair A->B and B->A resolves to B (roster), escalation would route B->A", () => {
65
+ const base: CalendarEvent = event("A", 8, 14);
66
+ const aToB: UserOverrideRecord = {
67
+ overrideUserId: "A",
68
+ routeAlertsToUserId: "B",
69
+ startsAt: at(9),
70
+ endsAt: at(13),
71
+ onCallDutyPolicyId: null,
72
+ };
73
+ const bToA: UserOverrideRecord = {
74
+ overrideUserId: "B",
75
+ routeAlertsToUserId: "A",
76
+ startsAt: at(9),
77
+ endsAt: at(13),
78
+ onCallDutyPolicyId: null,
79
+ };
80
+ const result: Array<CalendarEvent> =
81
+ UserOverrideUtil.applyOverridesToEvents({
82
+ events: [base],
83
+ overrides: [aToB, bToA],
84
+ });
85
+ // Roster resolves to B. getRouteAlertToUserId("B") would map B->A and page A.
86
+ expect(resolvedAt(result, at(11))).toBe("B");
87
+ });
88
+
89
+ test("EDGE: self-override A->A blocks a legit overlapping override A->B", () => {
90
+ const base: CalendarEvent = event("A", 8, 14);
91
+ const selfOverride: UserOverrideRecord = {
92
+ overrideUserId: "A",
93
+ routeAlertsToUserId: "A", // self, no-op substitution
94
+ startsAt: at(10),
95
+ endsAt: at(12),
96
+ onCallDutyPolicyId: null,
97
+ };
98
+ const aToB: UserOverrideRecord = {
99
+ overrideUserId: "A",
100
+ routeAlertsToUserId: "B",
101
+ startsAt: at(9),
102
+ endsAt: at(13),
103
+ onCallDutyPolicyId: null,
104
+ };
105
+ const result: Array<CalendarEvent> =
106
+ UserOverrideUtil.applyOverridesToEvents({
107
+ events: [base],
108
+ overrides: [selfOverride, aToB],
109
+ });
110
+ /*
111
+ * At 11:00 the real override A->B is expected, but the self-override marked
112
+ * [10,12] first, so it is skipped and A stays on-call.
113
+ */
114
+ // eslint-disable-next-line no-console
115
+ console.log("self-override case @11:00 =>", resolvedAt(result, at(11)));
116
+ });
117
+ });
@@ -209,11 +209,13 @@ describe("RestrictionTimes", () => {
209
209
  expect(restored.dayRestrictionTimes!.endTime).toBeDefined();
210
210
  });
211
211
 
212
- test("falls back to an empty object when weeklyRestrictionTimes is missing", () => {
212
+ test("falls back to an empty array when weeklyRestrictionTimes is missing", () => {
213
213
  /*
214
- * The source uses `(data["weeklyRestrictionTimes"] ...) || {}`, so when the
215
- * value object omits weeklyRestrictionTimes the fallback is an empty object
216
- * (NOT an empty array). This documents that quirky behavior precisely.
214
+ * When the value object omits weeklyRestrictionTimes, fromJSON must fall
215
+ * back to an empty ARRAY so the array-type contract holds. A previous
216
+ * `|| {}` fallback produced a non-array `{}` that slipped past
217
+ * Layer.getEventsByWeeklyRestriction's length guard and threw
218
+ * "not iterable" during weekly expansion (audit F20).
217
219
  */
218
220
  const restored: RestrictionTimes = RestrictionTimes.fromJSON({
219
221
  _type: ObjectType.RestrictionTimes,
@@ -223,8 +225,8 @@ describe("RestrictionTimes", () => {
223
225
  },
224
226
  });
225
227
 
226
- expect(restored.weeklyRestrictionTimes).toEqual({});
227
- expect(Array.isArray(restored.weeklyRestrictionTimes)).toBe(false);
228
+ expect(restored.weeklyRestrictionTimes).toEqual([]);
229
+ expect(Array.isArray(restored.weeklyRestrictionTimes)).toBe(true);
228
230
  });
229
231
 
230
232
  test("preserves a provided weeklyRestrictionTimes array on fromJSON", () => {
@@ -0,0 +1,112 @@
1
+ import RestrictionTimes, {
2
+ RestrictionType,
3
+ WeeklyResctriction,
4
+ } from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
5
+ import LayerUtil, { LayerProps } from "../../../Types/OnCallDutyPolicy/Layer";
6
+ import Recurring from "../../../Types/Events/Recurring";
7
+ import EventInterval from "../../../Types/Events/EventInterval";
8
+ import PositiveNumber from "../../../Types/PositiveNumber";
9
+ import OneUptimeDate from "../../../Types/Date";
10
+ import DayOfWeek from "../../../Types/Day/DayOfWeek";
11
+ import User from "../../../Models/DatabaseModels/User";
12
+ import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
13
+ import { describe, expect, test } from "@jest/globals";
14
+
15
+ /*
16
+ * Audit M4: getDefaultWeeklyRestrictionTIme() built startTime/endTime with
17
+ * getDateWithCustomTime(), which stamps TODAY's date. The rotation engine derives
18
+ * the ENFORCED weekday purely from the timestamp (getDayOfWeek(startTime)), not
19
+ * from the startDay/endDay enum the dropdown displays — so an untouched default
20
+ * enforced coverage on TODAY's weekday while still showing "Sunday -> Monday".
21
+ * The fix anchors the default timestamps onto the named weekday so the enforced
22
+ * day matches the displayed day.
23
+ */
24
+
25
+ function user(id: string): User {
26
+ return {
27
+ id: {
28
+ toString: (): string => {
29
+ return id;
30
+ },
31
+ } as any,
32
+ } as User;
33
+ }
34
+
35
+ describe("RestrictionTimes audit M4: default weekly restriction weekday", () => {
36
+ test("default startTime falls on Sunday and endTime on Monday (matches the enum)", () => {
37
+ const def: WeeklyResctriction =
38
+ RestrictionTimes.getDefaultWeeklyRestrictionTIme();
39
+
40
+ expect(def.startDay).toBe(DayOfWeek.Sunday);
41
+ expect(def.endDay).toBe(DayOfWeek.Monday);
42
+
43
+ /*
44
+ * The TIMESTAMP's weekday — what the engine actually enforces — matches the
45
+ * enum. Before the fix this was TODAY's weekday.
46
+ */
47
+ expect(OneUptimeDate.getDayOfWeek(def.startTime)).toBe(DayOfWeek.Sunday);
48
+ expect(OneUptimeDate.getDayOfWeek(def.endTime)).toBe(DayOfWeek.Monday);
49
+ });
50
+
51
+ test("default preserves the intended times of day (00:00 start, 01:00 end)", () => {
52
+ const def: WeeklyResctriction =
53
+ RestrictionTimes.getDefaultWeeklyRestrictionTIme();
54
+
55
+ expect(new Date(def.startTime).getHours()).toBe(0);
56
+ expect(new Date(def.endTime).getHours()).toBe(1);
57
+ });
58
+
59
+ test("engine enforces the default weekly restriction on Sunday, not today", () => {
60
+ /*
61
+ * Wednesday 2025-06-04 — the layer/window start deliberately falls on a
62
+ * non-Sunday to prove enforcement follows the restriction weekday, not the
63
+ * start day.
64
+ */
65
+ const anchor: Date = new Date(Date.UTC(2025, 5, 4, 0, 0, 0));
66
+
67
+ const rt: RestrictionTimes = new RestrictionTimes();
68
+ rt.restictionType = RestrictionType.Weekly;
69
+ rt.weeklyRestrictionTimes = [
70
+ RestrictionTimes.getDefaultWeeklyRestrictionTIme(),
71
+ ];
72
+
73
+ const r: Recurring = new Recurring();
74
+ r.intervalType = EventInterval.Week;
75
+ r.intervalCount = new PositiveNumber(1);
76
+
77
+ /*
78
+ * Expand in the SAME zone the Common default was built in (server-local, i.e.
79
+ * timezone undefined). getDefaultWeeklyRestrictionTIme lands the timestamp on
80
+ * Sunday in server-local time; enforcing and reading the weekday in that same
81
+ * zone keeps the assertion independent of the CI machine's timezone (the
82
+ * frontend reconciles the default to the schedule zone separately).
83
+ */
84
+ const layer: LayerProps = {
85
+ users: [user("A")],
86
+ startDateTimeOfLayer: anchor,
87
+ handOffTime: anchor,
88
+ restrictionTimes: rt,
89
+ rotation: r,
90
+ timezone: undefined,
91
+ };
92
+
93
+ const events: Array<CalendarEvent> = new LayerUtil().getEvents({
94
+ ...layer,
95
+ calendarStartDate: anchor,
96
+ calendarEndDate: new Date(Date.UTC(2025, 5, 18, 0, 0, 0)),
97
+ });
98
+
99
+ expect(events.length).toBeGreaterThan(0);
100
+
101
+ /*
102
+ * Every emitted covered segment must start on Sunday (the restriction window
103
+ * is Sunday 00:00 -> Monday 01:00). Before the fix the default landed on the
104
+ * current weekday, so coverage appeared mid-week instead. Monday is allowed
105
+ * only as the continuation edge; no segment may start Tue-Sat.
106
+ */
107
+ for (const e of events) {
108
+ const startDow: DayOfWeek = OneUptimeDate.getDayOfWeek(new Date(e.start));
109
+ expect([DayOfWeek.Sunday, DayOfWeek.Monday]).toContain(startDow);
110
+ }
111
+ });
112
+ });