@oneuptime/common 12.0.28 → 12.0.30

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 (367) hide show
  1. package/Models/DatabaseModels/DashboardDomain.ts +43 -0
  2. package/Models/DatabaseModels/Index.ts +11 -0
  3. package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
  4. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +43 -0
  5. package/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +662 -0
  6. package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +584 -0
  7. package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
  8. package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +546 -0
  9. package/Models/DatabaseModels/UserOnCallShiftReminder.ts +231 -0
  10. package/Models/DatabaseModels/UserOnCallShiftReminderLog.ts +331 -0
  11. package/Server/API/DashboardDomainAPI.ts +74 -0
  12. package/Server/API/MicrosoftTeamsAPI.ts +81 -180
  13. package/Server/API/OnCallCalendarAPI.ts +2194 -0
  14. package/Server/API/StatusPageDomainAPI.ts +74 -0
  15. package/Server/EnvironmentConfig.ts +138 -0
  16. package/Server/Infrastructure/OnCallCalendarFeedCache.ts +1050 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.ts +253 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  21. package/Server/Middleware/OnCallCalendarFeedRateLimit.ts +487 -0
  22. package/Server/Middleware/ProjectAuthorization.ts +6 -1
  23. package/Server/Services/DashboardDomainService.ts +131 -0
  24. package/Server/Services/Index.ts +12 -0
  25. package/Server/Services/LogPipelineProcessorService.ts +88 -0
  26. package/Server/Services/MonitorTemplateService.ts +70 -11
  27. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +32 -0
  28. package/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.ts +260 -0
  29. package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +70 -3
  30. package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +138 -3
  31. package/Server/Services/OnCallDutyPolicyScheduleService.ts +1465 -134
  32. package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +167 -31
  33. package/Server/Services/ProjectOnCallCalendarFeedService.ts +192 -0
  34. package/Server/Services/StatusPageDomainService.ts +163 -3
  35. package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
  36. package/Server/Services/TeamMemberService.ts +489 -1
  37. package/Server/Services/TeamService.ts +50 -0
  38. package/Server/Services/UserNotificationSettingService.ts +44 -1
  39. package/Server/Services/UserOnCallCalendarFeedService.ts +159 -0
  40. package/Server/Services/UserOnCallShiftReminderLogService.ts +151 -0
  41. package/Server/Services/UserOnCallShiftReminderService.ts +112 -0
  42. package/Server/Types/Domain.ts +17 -0
  43. package/Server/Utils/APIKey/AccessPermission.ts +24 -0
  44. package/Server/Utils/Greenlock/Greenlock.ts +156 -58
  45. package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
  46. package/Server/Utils/OnCall/CalendarFeedToken.ts +241 -0
  47. package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1488 -0
  48. package/Server/Utils/OnCall/OnCallCalendarFeedUrls.ts +230 -0
  49. package/Server/Utils/OnCall/OnCallShiftChangeListeners.ts +227 -0
  50. package/Server/Utils/OnCall/OnCallShiftMaterializer.ts +1127 -0
  51. package/Server/Utils/OnCall/OnCallShiftReminderListener.ts +174 -0
  52. package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +2584 -0
  53. package/Server/Utils/Response.ts +134 -0
  54. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
  55. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
  56. package/Server/Utils/StartServer.ts +10 -0
  57. package/Server/Utils/Telemetry/AppMetrics.ts +51 -0
  58. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
  59. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
  60. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
  61. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
  62. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
  63. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
  64. package/Tests/App/Dashboard/OnCallCalendarFeedLinks.test.tsx +229 -0
  65. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +489 -0
  66. package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +149 -0
  67. package/Tests/App/Dashboard/OnCallCalendarFeedUtil.test.ts +855 -0
  68. package/Tests/App/Dashboard/OnCallPersonalCalendarFeed.test.tsx +1209 -0
  69. package/Tests/App/Dashboard/OnCallSharedCalendarFeedCard.test.tsx +1019 -0
  70. package/Tests/App/Dashboard/OnCallShiftRemindersAndUpcomingShifts.test.tsx +655 -0
  71. package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +73 -0
  72. package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
  73. package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
  74. package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
  75. package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
  76. package/Tests/Models/OnCallCalendarFeedModels.test.ts +642 -0
  77. package/Tests/Models/OnCallDutyPolicyScheduleCalendarFeed.test.ts +288 -0
  78. package/Tests/Models/OnCallDutyPolicyScheduleShiftConfigVersion.test.ts +88 -0
  79. package/Tests/Models/UserOnCallShiftReminderModels.test.ts +368 -0
  80. package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
  81. package/Tests/Server/API/Helpers.ts +8 -0
  82. package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
  83. package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
  84. package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
  85. package/Tests/Server/API/OnCallCalendarAPI.test.ts +4461 -0
  86. package/Tests/Server/EnvironmentConfigOnCallCalendarFeed.test.ts +357 -0
  87. package/Tests/Server/Infrastructure/OnCallCalendarFeedCache.test.ts +1589 -0
  88. package/Tests/Server/Infrastructure/Postgres/AddOnCallCalendarFeedsMigration.test.ts +369 -0
  89. package/Tests/Server/Middleware/OnCallCalendarFeedRateLimit.test.ts +1365 -0
  90. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
  91. package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
  92. package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
  93. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +70 -0
  94. package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
  95. package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
  96. package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
  97. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
  98. package/Tests/Server/Services/OnCallCalendarFeedServices.test.ts +1103 -0
  99. package/Tests/Server/Services/OnCallDutyPolicyScheduleResolver.test.ts +1037 -0
  100. package/Tests/Server/Services/OnCallShiftConfigPropagation.test.ts +1270 -0
  101. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
  102. package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
  103. package/Tests/Server/Services/TeamDeleteMemberCleanup.test.ts +156 -0
  104. package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +755 -0
  105. package/Tests/Server/Services/UserNotificationSettingShiftReminderDefaults.test.ts +267 -0
  106. package/Tests/Server/Services/UserOnCallShiftReminderServices.test.ts +583 -0
  107. package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
  108. package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
  109. package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +254 -0
  110. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
  111. package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
  112. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
  113. package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
  114. package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
  115. package/Tests/Server/Utils/OnCall/CalendarFeedToken.test.ts +467 -0
  116. package/Tests/Server/Utils/OnCall/OnCallCalendarFeedRenderer.test.ts +2142 -0
  117. package/Tests/Server/Utils/OnCall/OnCallCalendarFeedUrls.test.ts +278 -0
  118. package/Tests/Server/Utils/OnCall/OnCallResolverTestHarness.ts +440 -0
  119. package/Tests/Server/Utils/OnCall/OnCallShiftChangeListeners.test.ts +314 -0
  120. package/Tests/Server/Utils/OnCall/OnCallShiftMaterializer.test.ts +1502 -0
  121. package/Tests/Server/Utils/OnCall/OnCallShiftReminderListener.test.ts +491 -0
  122. package/Tests/Server/Utils/OnCall/OnCallShiftReminderRunner.test.ts +3088 -0
  123. package/Tests/Server/Utils/OnCall/OnCallShiftReminderTestHarness.ts +910 -0
  124. package/Tests/Server/Utils/ResponseSendCalendarResponse.test.ts +561 -0
  125. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
  126. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
  127. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
  128. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
  129. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
  130. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
  131. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
  132. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
  133. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
  134. package/Tests/Server/Utils/StartServerEncryptionSecretWarning.test.ts +64 -0
  135. package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +29 -0
  136. package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
  137. package/Tests/Types/API/HTTPResponse.test.ts +175 -0
  138. package/Tests/Types/API/Route.test.ts +36 -4
  139. package/Tests/Types/Calendar/ICalendar.test.ts +488 -0
  140. package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
  141. package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
  142. package/Tests/Types/DateLocalTimeString.test.ts +259 -0
  143. package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
  144. package/Tests/Types/NotificationSetting/NotificationSettingEventType.test.ts +53 -0
  145. package/Tests/Types/OnCallDutyPolicy/CalendarFeedTestFixtures.ts +310 -0
  146. package/Tests/Types/OnCallDutyPolicy/CalendarFeedWindow.test.ts +205 -0
  147. package/Tests/Types/OnCallDutyPolicy/LayerUtilLayerMetaAndIterationCap.test.ts +458 -0
  148. package/Tests/Types/OnCallDutyPolicy/MaterializedShift.test.ts +197 -0
  149. package/Tests/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.test.ts +1986 -0
  150. package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtilGroupKey.test.ts +678 -0
  151. package/Tests/Types/OnCallDutyPolicy/ShiftSeamUtil.test.ts +417 -0
  152. package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
  153. package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
  154. package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
  155. package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
  156. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
  157. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
  158. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
  159. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
  160. package/Tests/UI/Utils/Navigation.test.ts +82 -0
  161. package/Tests/Utils/Array.test.ts +100 -0
  162. package/Tests/Utils/CertificateReissue.test.ts +225 -0
  163. package/Tests/Utils/Grok/Grok.test.ts +492 -0
  164. package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
  165. package/Types/API/Route.ts +20 -5
  166. package/Types/Calendar/ICalendar.ts +442 -0
  167. package/Types/Date.ts +143 -13
  168. package/Types/Email/EmailTemplateType.ts +2 -0
  169. package/Types/NotificationSetting/NotificationSettingEventType.ts +4 -0
  170. package/Types/OnCallDutyPolicy/CalendarFeedWindow.ts +140 -0
  171. package/Types/OnCallDutyPolicy/Layer.ts +190 -35
  172. package/Types/OnCallDutyPolicy/MaterializedShift.ts +274 -0
  173. package/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.ts +1415 -0
  174. package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +159 -4
  175. package/Types/OnCallDutyPolicy/ShiftSeamUtil.ts +106 -0
  176. package/Types/Rum/SessionReplay.ts +62 -1
  177. package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
  178. package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
  179. package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
  180. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
  181. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
  182. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
  183. package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
  184. package/UI/Utils/Navigation.ts +42 -0
  185. package/Utils/Array.ts +37 -0
  186. package/Utils/CertificateReissue.ts +128 -0
  187. package/Utils/Grok/Grok.ts +461 -0
  188. package/Utils/Grok/GrokPatterns.ts +118 -0
  189. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
  190. package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
  191. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  192. package/build/dist/Models/DatabaseModels/Index.js +11 -0
  193. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  194. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
  195. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  196. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +44 -0
  197. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  198. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +699 -0
  199. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -0
  200. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +620 -0
  201. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -0
  202. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
  203. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  204. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +585 -0
  205. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -0
  206. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js +247 -0
  207. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js.map +1 -0
  208. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js +355 -0
  209. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js.map +1 -0
  210. package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
  211. package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
  212. package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
  213. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  214. package/build/dist/Server/API/OnCallCalendarAPI.js +1371 -0
  215. package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -0
  216. package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
  217. package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
  218. package/build/dist/Server/EnvironmentConfig.js +102 -0
  219. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  220. package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js +663 -0
  221. package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js.map +1 -0
  222. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
  223. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
  224. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
  225. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
  226. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js +103 -0
  227. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js.map +1 -0
  228. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  229. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  230. package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js +342 -0
  231. package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js.map +1 -0
  232. package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
  233. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  234. package/build/dist/Server/Services/DashboardDomainService.js +104 -0
  235. package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
  236. package/build/dist/Server/Services/Index.js +12 -0
  237. package/build/dist/Server/Services/Index.js.map +1 -1
  238. package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
  239. package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
  240. package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
  241. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  242. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +20 -0
  243. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  244. package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js +219 -0
  245. package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js.map +1 -0
  246. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +47 -3
  247. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
  248. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +112 -3
  249. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
  250. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +955 -9
  251. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  252. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +93 -6
  253. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
  254. package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js +157 -0
  255. package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js.map +1 -0
  256. package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
  257. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  258. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
  259. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
  260. package/build/dist/Server/Services/TeamMemberService.js +396 -1
  261. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  262. package/build/dist/Server/Services/TeamService.js +43 -0
  263. package/build/dist/Server/Services/TeamService.js.map +1 -1
  264. package/build/dist/Server/Services/UserNotificationSettingService.js +26 -1
  265. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  266. package/build/dist/Server/Services/UserOnCallCalendarFeedService.js +140 -0
  267. package/build/dist/Server/Services/UserOnCallCalendarFeedService.js.map +1 -0
  268. package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js +116 -0
  269. package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js.map +1 -0
  270. package/build/dist/Server/Services/UserOnCallShiftReminderService.js +92 -0
  271. package/build/dist/Server/Services/UserOnCallShiftReminderService.js.map +1 -0
  272. package/build/dist/Server/Types/Domain.js +16 -0
  273. package/build/dist/Server/Types/Domain.js.map +1 -1
  274. package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
  275. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  276. package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
  277. package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
  278. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
  279. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
  280. package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js +161 -0
  281. package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js.map +1 -0
  282. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js +983 -0
  283. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js.map +1 -0
  284. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js +155 -0
  285. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js.map +1 -0
  286. package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js +145 -0
  287. package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js.map +1 -0
  288. package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js +752 -0
  289. package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js.map +1 -0
  290. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js +92 -0
  291. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js.map +1 -0
  292. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1786 -0
  293. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -0
  294. package/build/dist/Server/Utils/Response.js +97 -0
  295. package/build/dist/Server/Utils/Response.js.map +1 -1
  296. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
  297. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  298. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
  299. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  300. package/build/dist/Server/Utils/StartServer.js +9 -1
  301. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  302. package/build/dist/Server/Utils/Telemetry/AppMetrics.js +41 -0
  303. package/build/dist/Server/Utils/Telemetry/AppMetrics.js.map +1 -1
  304. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
  305. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  306. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
  307. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  308. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
  309. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
  310. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
  311. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  312. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
  313. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  314. package/build/dist/Types/API/Route.js +16 -5
  315. package/build/dist/Types/API/Route.js.map +1 -1
  316. package/build/dist/Types/Calendar/ICalendar.js +290 -0
  317. package/build/dist/Types/Calendar/ICalendar.js.map +1 -0
  318. package/build/dist/Types/Date.js +115 -13
  319. package/build/dist/Types/Date.js.map +1 -1
  320. package/build/dist/Types/Email/EmailTemplateType.js +2 -0
  321. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  322. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
  323. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
  324. package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js +97 -0
  325. package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js.map +1 -0
  326. package/build/dist/Types/OnCallDutyPolicy/Layer.js +113 -19
  327. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  328. package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js +134 -0
  329. package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js.map +1 -0
  330. package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js +800 -0
  331. package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js.map +1 -0
  332. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +97 -3
  333. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
  334. package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js +77 -0
  335. package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js.map +1 -0
  336. package/build/dist/Types/Rum/SessionReplay.js +38 -0
  337. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  338. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
  339. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
  340. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
  341. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  342. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
  343. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
  344. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
  345. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  346. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
  347. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  348. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
  349. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  350. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
  351. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
  352. package/build/dist/UI/Utils/Navigation.js +30 -0
  353. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  354. package/build/dist/Utils/Array.js +25 -0
  355. package/build/dist/Utils/Array.js.map +1 -1
  356. package/build/dist/Utils/CertificateReissue.js +97 -0
  357. package/build/dist/Utils/CertificateReissue.js.map +1 -0
  358. package/build/dist/Utils/Grok/Grok.js +324 -0
  359. package/build/dist/Utils/Grok/Grok.js.map +1 -0
  360. package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
  361. package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
  362. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
  363. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  364. package/package.json +1 -1
  365. package/UI/Utils/JsonWebToken.ts +0 -14
  366. package/build/dist/UI/Utils/JsonWebToken.js +0 -10
  367. package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
@@ -0,0 +1,1037 @@
1
+ import OnCallDutyPolicyScheduleService, {
2
+ ResolvedPolicyVariant,
3
+ ResolvedShiftSegments,
4
+ } from "../../../Server/Services/OnCallDutyPolicyScheduleService";
5
+ import OnCallDutyPolicyUserOverrideService from "../../../Server/Services/OnCallDutyPolicyUserOverrideService";
6
+ import logger from "../../../Server/Utils/Logger";
7
+ import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
8
+ import BadDataException from "../../../Types/Exception/BadDataException";
9
+ import EventInterval from "../../../Types/Events/EventInterval";
10
+ import { MaterializedShiftPolicy } from "../../../Types/OnCallDutyPolicy/MaterializedShift";
11
+ import UserOverrideUtil, {
12
+ UserOverrideRecord,
13
+ } from "../../../Types/OnCallDutyPolicy/UserOverrideUtil";
14
+ import {
15
+ at,
16
+ dailyRestriction,
17
+ rotation,
18
+ } from "../../Types/OnCallDutyPolicy/CalendarFeedTestFixtures";
19
+ import {
20
+ FakeDb,
21
+ emptyDb,
22
+ installFakeDb,
23
+ makeAttachment,
24
+ makeLayer,
25
+ makeLayerUser,
26
+ makeOverride,
27
+ makeSchedule,
28
+ oid,
29
+ } from "../Utils/OnCall/OnCallResolverTestHarness";
30
+ import {
31
+ afterEach,
32
+ beforeEach,
33
+ describe,
34
+ expect,
35
+ jest,
36
+ test,
37
+ } from "@jest/globals";
38
+
39
+ /*
40
+ * getResolvedShiftSegments / getResolvedShiftSegmentsForSchedules: the
41
+ * source of truth behind the calendar feeds. Runs the REAL service code
42
+ * (layer props builder, window widening, engine, override selection, policy
43
+ * variants, lastModifiedAt) against an in-memory fake of the tables it reads.
44
+ */
45
+
46
+ const UTC: string = "UTC";
47
+ const PROJECT: string = "project-1";
48
+ const OTHER_PROJECT: string = "project-2";
49
+ const SCHEDULE: string = "schedule-1";
50
+ const OTHER_SCHEDULE: string = "schedule-2";
51
+
52
+ const WINDOW_START: Date = at("2026-03-02T00:00:00Z");
53
+ const WINDOW_END: Date = at("2026-03-05T00:00:00Z");
54
+
55
+ // A daily A/B rotation handing off at 09:00 UTC, running since New Year.
56
+ function dailyLayer(
57
+ db: FakeDb,
58
+ data?: {
59
+ scheduleId?: string;
60
+ projectId?: string;
61
+ layerId?: string;
62
+ users?: Array<string>;
63
+ order?: number;
64
+ restricted?: boolean;
65
+ updatedAt?: Date;
66
+ },
67
+ ): void {
68
+ const scheduleId: string = data?.scheduleId ?? SCHEDULE;
69
+ const projectId: string = data?.projectId ?? PROJECT;
70
+ const layerId: string = data?.layerId ?? "layer-1";
71
+ const users: Array<string> = data?.users ?? ["A", "B"];
72
+
73
+ db.layers.push(
74
+ makeLayer({
75
+ id: layerId,
76
+ scheduleId,
77
+ projectId,
78
+ name: "Primary",
79
+ order: data?.order ?? 1,
80
+ startsAt: at("2026-01-01T00:00:00Z"),
81
+ handOffTime: at("2026-01-01T09:00:00Z"),
82
+ rotation: rotation(EventInterval.Day, 1),
83
+ restrictionTimes: data?.restricted
84
+ ? dailyRestriction("09:00", "17:00", UTC)
85
+ : undefined,
86
+ updatedAt: data?.updatedAt,
87
+ }),
88
+ );
89
+
90
+ users.forEach((userId: string, index: number) => {
91
+ db.layerUsers.push(
92
+ makeLayerUser({
93
+ id: `${layerId}-user-${userId}`,
94
+ scheduleId,
95
+ layerId,
96
+ projectId,
97
+ userId,
98
+ order: index + 1,
99
+ }),
100
+ );
101
+ });
102
+ }
103
+
104
+ function baseDb(): FakeDb {
105
+ const db: FakeDb = emptyDb();
106
+ db.schedules.push(
107
+ makeSchedule({
108
+ id: SCHEDULE,
109
+ projectId: PROJECT,
110
+ name: "Payments",
111
+ timezone: UTC,
112
+ shiftConfigVersion: 7,
113
+ }),
114
+ );
115
+ dailyLayer(db);
116
+ return db;
117
+ }
118
+
119
+ function coveringSegment(
120
+ segments: Array<CalendarEvent>,
121
+ instant: Date,
122
+ ): CalendarEvent | undefined {
123
+ return segments.find((segment: CalendarEvent) => {
124
+ return (
125
+ segment.start.getTime() <= instant.getTime() &&
126
+ segment.end.getTime() > instant.getTime()
127
+ );
128
+ });
129
+ }
130
+
131
+ async function resolve(
132
+ db: FakeDb,
133
+ options?: { maxSimulationIterations?: number; scheduleId?: string },
134
+ ): Promise<ResolvedShiftSegments> {
135
+ installFakeDb(db);
136
+
137
+ const resolved: ResolvedShiftSegments | null =
138
+ await OnCallDutyPolicyScheduleService.getResolvedShiftSegments({
139
+ scheduleId: oid(options?.scheduleId ?? SCHEDULE),
140
+ windowStart: WINDOW_START,
141
+ windowEnd: WINDOW_END,
142
+ maxSimulationIterations: options?.maxSimulationIterations,
143
+ });
144
+
145
+ expect(resolved).not.toBeNull();
146
+ return resolved!;
147
+ }
148
+
149
+ describe("OnCallDutyPolicyScheduleService shift resolver", () => {
150
+ beforeEach(() => {
151
+ jest.spyOn(logger, "error").mockImplementation((): void => {
152
+ return undefined;
153
+ });
154
+ });
155
+
156
+ afterEach(() => {
157
+ jest.restoreAllMocks();
158
+ });
159
+
160
+ describe("window handling", () => {
161
+ test("returns only segments overlapping the window, UNCLIPPED at both ends", async () => {
162
+ const resolved: ResolvedShiftSegments = await resolve(baseDb());
163
+
164
+ expect(resolved.segments.length).toBeGreaterThan(0);
165
+
166
+ for (const segment of resolved.segments) {
167
+ expect(segment.start.getTime()).toBeLessThan(WINDOW_END.getTime());
168
+ expect(segment.end.getTime()).toBeGreaterThan(WINDOW_START.getTime());
169
+ }
170
+
171
+ // The shift covering the window start began at the previous 09:00 handoff.
172
+ const first: CalendarEvent = resolved.segments[0]!;
173
+ expect(first.start.getTime()).toBeLessThan(WINDOW_START.getTime());
174
+ expect(first.start.toISOString()).toMatch(/^2026-03-01T09:00:0[01]/);
175
+
176
+ // The shift covering the window end runs to the next 09:00 handoff.
177
+ const last: CalendarEvent =
178
+ resolved.segments[resolved.segments.length - 1]!;
179
+ expect(last.end.getTime()).toBeGreaterThan(WINDOW_END.getTime());
180
+ expect(last.end.toISOString()).toMatch(/^2026-03-05T09:00:00/);
181
+ });
182
+
183
+ test("segments alternate between the layer users at each 09:00 handoff", async () => {
184
+ const resolved: ResolvedShiftSegments = await resolve(baseDb());
185
+
186
+ const titles: Array<string> = resolved.segments.map(
187
+ (segment: CalendarEvent) => {
188
+ return segment.title;
189
+ },
190
+ );
191
+
192
+ expect(titles.length).toBe(4); // 03-01 09:00 .. 03-05 09:00 = four daily shifts
193
+ for (let i: number = 1; i < titles.length; i++) {
194
+ expect(titles[i]).not.toBe(titles[i - 1]);
195
+ expect(["A", "B"]).toContain(titles[i]);
196
+ }
197
+ });
198
+
199
+ test("rejects an inverted window", async () => {
200
+ installFakeDb(baseDb());
201
+
202
+ await expect(
203
+ OnCallDutyPolicyScheduleService.getResolvedShiftSegments({
204
+ scheduleId: oid(SCHEDULE),
205
+ windowStart: WINDOW_END,
206
+ windowEnd: WINDOW_START,
207
+ }),
208
+ ).rejects.toBeInstanceOf(BadDataException);
209
+ });
210
+
211
+ test("a schedule with no layers resolves to no segments and no variants", async () => {
212
+ const db: FakeDb = emptyDb();
213
+ db.schedules.push(
214
+ makeSchedule({ id: SCHEDULE, projectId: PROJECT, timezone: UTC }),
215
+ );
216
+
217
+ const resolved: ResolvedShiftSegments = await resolve(db);
218
+
219
+ expect(resolved.segments).toEqual([]);
220
+ expect(resolved.policyVariants).toEqual([]);
221
+ expect(resolved.layerProps).toEqual([]);
222
+ expect(resolved.truncated).toBe(false);
223
+ });
224
+
225
+ test("an unknown schedule resolves to null (and is skipped by the batch)", async () => {
226
+ installFakeDb(baseDb());
227
+
228
+ const single: ResolvedShiftSegments | null =
229
+ await OnCallDutyPolicyScheduleService.getResolvedShiftSegments({
230
+ scheduleId: oid("missing"),
231
+ windowStart: WINDOW_START,
232
+ windowEnd: WINDOW_END,
233
+ });
234
+ expect(single).toBeNull();
235
+
236
+ const batch: Array<ResolvedShiftSegments> =
237
+ await OnCallDutyPolicyScheduleService.getResolvedShiftSegmentsForSchedules(
238
+ {
239
+ scheduleIds: [oid("missing"), oid(SCHEDULE)],
240
+ windowStart: WINDOW_START,
241
+ windowEnd: WINDOW_END,
242
+ },
243
+ );
244
+ expect(batch).toHaveLength(1);
245
+ expect(batch[0]!.schedule.id).toBe(SCHEDULE);
246
+ });
247
+
248
+ test("an empty id list resolves to an empty batch without touching the database", async () => {
249
+ installFakeDb(baseDb());
250
+
251
+ const batch: Array<ResolvedShiftSegments> =
252
+ await OnCallDutyPolicyScheduleService.getResolvedShiftSegmentsForSchedules(
253
+ {
254
+ scheduleIds: [],
255
+ windowStart: WINDOW_START,
256
+ windowEnd: WINDOW_END,
257
+ },
258
+ );
259
+
260
+ expect(batch).toEqual([]);
261
+ expect(OnCallDutyPolicyScheduleService.findBy).not.toHaveBeenCalled();
262
+ });
263
+ });
264
+
265
+ describe("schedule info and layer props", () => {
266
+ test("carries the schedule columns and stamps layer id/name on the layer props and events", async () => {
267
+ const resolved: ResolvedShiftSegments = await resolve(baseDb());
268
+
269
+ expect(resolved.schedule).toEqual({
270
+ id: SCHEDULE,
271
+ name: "Payments",
272
+ timezone: UTC,
273
+ projectId: PROJECT,
274
+ shiftConfigVersion: 7,
275
+ });
276
+
277
+ expect(resolved.layerProps).toHaveLength(1);
278
+ expect(resolved.layerProps[0]!.layerId).toBe("layer-1");
279
+ expect(resolved.layerProps[0]!.layerName).toBe("Primary");
280
+ expect(resolved.layerProps[0]!.timezone).toBe(UTC);
281
+ expect(resolved.scheduleUserIds).toEqual(["A", "B"]);
282
+
283
+ for (const segment of resolved.segments) {
284
+ expect((segment as unknown as Record<string, unknown>)["layerId"]).toBe(
285
+ "layer-1",
286
+ );
287
+ expect(
288
+ (segment as unknown as Record<string, unknown>)["layerName"],
289
+ ).toBe("Primary");
290
+ }
291
+ });
292
+
293
+ test("omits the timezone key for a legacy schedule without one", async () => {
294
+ const db: FakeDb = baseDb();
295
+ db.schedules[0]!["timezone"] = undefined;
296
+
297
+ const resolved: ResolvedShiftSegments = await resolve(db);
298
+
299
+ expect("timezone" in resolved.schedule).toBe(false);
300
+ expect(resolved.layerProps[0]!.timezone).toBeUndefined();
301
+ });
302
+
303
+ test("a non-numeric shiftConfigVersion reads as 0", async () => {
304
+ const db: FakeDb = baseDb();
305
+ db.schedules[0]!["shiftConfigVersion"] = undefined;
306
+
307
+ const resolved: ResolvedShiftSegments = await resolve(db);
308
+ expect(resolved.schedule.shiftConfigVersion).toBe(0);
309
+ });
310
+ });
311
+
312
+ describe("attached policies", () => {
313
+ test("dedupes (policy, rule) rows and orders them", async () => {
314
+ const db: FakeDb = baseDb();
315
+ db.attachments.push(
316
+ makeAttachment({
317
+ id: "att-1",
318
+ scheduleId: SCHEDULE,
319
+ projectId: PROJECT,
320
+ policyId: "P1",
321
+ policyName: "Payments Policy",
322
+ ruleId: "R2",
323
+ ruleName: "Secondary",
324
+ ruleOrder: 2,
325
+ }),
326
+ makeAttachment({
327
+ id: "att-2",
328
+ scheduleId: SCHEDULE,
329
+ projectId: PROJECT,
330
+ policyId: "P1",
331
+ policyName: "Payments Policy",
332
+ ruleId: "R1",
333
+ ruleName: "Primary",
334
+ ruleOrder: 1,
335
+ }),
336
+ // Duplicate row for the same (policy, rule): the join has no unique index.
337
+ makeAttachment({
338
+ id: "att-3",
339
+ scheduleId: SCHEDULE,
340
+ projectId: PROJECT,
341
+ policyId: "P1",
342
+ policyName: "Payments Policy",
343
+ ruleId: "R1",
344
+ ruleName: "Primary",
345
+ ruleOrder: 1,
346
+ }),
347
+ );
348
+
349
+ const resolved: ResolvedShiftSegments = await resolve(db);
350
+
351
+ expect(resolved.attachedPolicies).toEqual([
352
+ {
353
+ policyId: "P1",
354
+ policyName: "Payments Policy",
355
+ ruleId: "R1",
356
+ ruleName: "Primary",
357
+ ruleOrder: 1,
358
+ },
359
+ {
360
+ policyId: "P1",
361
+ policyName: "Payments Policy",
362
+ ruleId: "R2",
363
+ ruleName: "Secondary",
364
+ ruleOrder: 2,
365
+ },
366
+ ]);
367
+ });
368
+
369
+ test("attachments of other schedules are not mixed in", async () => {
370
+ const db: FakeDb = baseDb();
371
+ db.schedules.push(
372
+ makeSchedule({ id: OTHER_SCHEDULE, projectId: PROJECT, timezone: UTC }),
373
+ );
374
+ db.attachments.push(
375
+ makeAttachment({
376
+ id: "att-other",
377
+ scheduleId: OTHER_SCHEDULE,
378
+ projectId: PROJECT,
379
+ policyId: "P9",
380
+ ruleId: "R9",
381
+ }),
382
+ );
383
+
384
+ const resolved: ResolvedShiftSegments = await resolve(db);
385
+ expect(resolved.attachedPolicies).toEqual([]);
386
+ });
387
+ });
388
+
389
+ describe("overrides in the roster's policy context", () => {
390
+ const OVERRIDE_START: Date = at("2026-03-03T11:00:00Z");
391
+ const OVERRIDE_END: Date = at("2026-03-03T13:00:00Z");
392
+
393
+ function whoIsOnAt(
394
+ resolved: ResolvedShiftSegments,
395
+ instant: Date,
396
+ ): string | undefined {
397
+ return coveringSegment(resolved.segments, instant)?.title;
398
+ }
399
+
400
+ test("a GLOBAL override substitutes the user inside its window and stamps override meta", async () => {
401
+ const db: FakeDb = baseDb();
402
+ const onCallAtNoon: string = whoIsOnAt(
403
+ await resolve(db),
404
+ at("2026-03-03T12:00:00Z"),
405
+ )!;
406
+
407
+ db.overrides.push(
408
+ makeOverride({
409
+ id: "ov-1",
410
+ projectId: PROJECT,
411
+ overrideUserId: onCallAtNoon,
412
+ routeAlertsToUserId: "C",
413
+ startsAt: OVERRIDE_START,
414
+ endsAt: OVERRIDE_END,
415
+ }),
416
+ );
417
+
418
+ const resolved: ResolvedShiftSegments = await resolve(db);
419
+
420
+ expect(whoIsOnAt(resolved, at("2026-03-03T12:00:00Z"))).toBe("C");
421
+ expect(whoIsOnAt(resolved, at("2026-03-03T10:00:00Z"))).toBe(
422
+ onCallAtNoon,
423
+ );
424
+ expect(whoIsOnAt(resolved, at("2026-03-03T14:00:00Z"))).toBe(
425
+ onCallAtNoon,
426
+ );
427
+
428
+ const substituted: CalendarEvent = coveringSegment(
429
+ resolved.segments,
430
+ at("2026-03-03T12:00:00Z"),
431
+ )!;
432
+ expect(substituted.start).toEqual(OVERRIDE_START);
433
+ expect(substituted.end).toEqual(OVERRIDE_END);
434
+ expect(UserOverrideUtil.getOverrideMeta(substituted)).toMatchObject({
435
+ originalUserId: onCallAtNoon,
436
+ overrideUserId: "C",
437
+ });
438
+
439
+ // The override took part in the resolution.
440
+ expect(resolved.overrides).toHaveLength(1);
441
+ expect(resolved.overrides[0]).toMatchObject({
442
+ overrideUserId: onCallAtNoon,
443
+ routeAlertsToUserId: "C",
444
+ onCallDutyPolicyId: null,
445
+ });
446
+ });
447
+
448
+ test("an override for a NON-member is ignored", async () => {
449
+ const db: FakeDb = baseDb();
450
+ db.overrides.push(
451
+ makeOverride({
452
+ id: "ov-stranger",
453
+ projectId: PROJECT,
454
+ overrideUserId: "Z",
455
+ routeAlertsToUserId: "C",
456
+ startsAt: OVERRIDE_START,
457
+ endsAt: OVERRIDE_END,
458
+ }),
459
+ );
460
+
461
+ const resolved: ResolvedShiftSegments = await resolve(db);
462
+
463
+ expect(whoIsOnAt(resolved, at("2026-03-03T12:00:00Z"))).not.toBe("C");
464
+ expect(resolved.overrides).toEqual([]);
465
+ });
466
+
467
+ test("an override in ANOTHER project is ignored even for a member", async () => {
468
+ const db: FakeDb = baseDb();
469
+ db.overrides.push(
470
+ makeOverride({
471
+ id: "ov-other-project",
472
+ projectId: OTHER_PROJECT,
473
+ overrideUserId: "A",
474
+ routeAlertsToUserId: "C",
475
+ startsAt: OVERRIDE_START,
476
+ endsAt: OVERRIDE_END,
477
+ }),
478
+ makeOverride({
479
+ id: "ov-other-project-2",
480
+ projectId: OTHER_PROJECT,
481
+ overrideUserId: "B",
482
+ routeAlertsToUserId: "C",
483
+ startsAt: OVERRIDE_START,
484
+ endsAt: OVERRIDE_END,
485
+ }),
486
+ );
487
+
488
+ const resolved: ResolvedShiftSegments = await resolve(db);
489
+
490
+ expect(whoIsOnAt(resolved, at("2026-03-03T12:00:00Z"))).not.toBe("C");
491
+ expect(resolved.overrides).toEqual([]);
492
+ });
493
+
494
+ test("an override outside the (widened) window does not participate", async () => {
495
+ const db: FakeDb = baseDb();
496
+ db.overrides.push(
497
+ makeOverride({
498
+ id: "ov-far",
499
+ projectId: PROJECT,
500
+ overrideUserId: "A",
501
+ routeAlertsToUserId: "C",
502
+ startsAt: at("2026-06-01T00:00:00Z"),
503
+ endsAt: at("2026-06-02T00:00:00Z"),
504
+ }),
505
+ );
506
+
507
+ const resolved: ResolvedShiftSegments = await resolve(db);
508
+ expect(resolved.overrides).toEqual([]);
509
+ });
510
+
511
+ test("with exactly ONE attached policy, that policy's scoped overrides apply (the roster rule)", async () => {
512
+ const db: FakeDb = baseDb();
513
+ db.attachments.push(
514
+ makeAttachment({
515
+ id: "att-1",
516
+ scheduleId: SCHEDULE,
517
+ projectId: PROJECT,
518
+ policyId: "P1",
519
+ ruleId: "R1",
520
+ }),
521
+ );
522
+ const onCallAtNoon: string = whoIsOnAt(
523
+ await resolve(db),
524
+ at("2026-03-03T12:00:00Z"),
525
+ )!;
526
+
527
+ db.overrides.push(
528
+ makeOverride({
529
+ id: "ov-scoped",
530
+ projectId: PROJECT,
531
+ overrideUserId: onCallAtNoon,
532
+ routeAlertsToUserId: "D",
533
+ startsAt: OVERRIDE_START,
534
+ endsAt: OVERRIDE_END,
535
+ policyId: "P1",
536
+ }),
537
+ );
538
+
539
+ const resolved: ResolvedShiftSegments = await resolve(db);
540
+
541
+ expect(whoIsOnAt(resolved, at("2026-03-03T12:00:00Z"))).toBe("D");
542
+ expect(resolved.policyVariants).toEqual([]);
543
+ expect(resolved.overrides[0]!.onCallDutyPolicyId).toBe("P1");
544
+ });
545
+
546
+ test("a scoped override for a policy the schedule is NOT attached to is ignored", async () => {
547
+ const db: FakeDb = baseDb();
548
+ db.attachments.push(
549
+ makeAttachment({
550
+ id: "att-1",
551
+ scheduleId: SCHEDULE,
552
+ projectId: PROJECT,
553
+ policyId: "P1",
554
+ ruleId: "R1",
555
+ }),
556
+ );
557
+ db.overrides.push(
558
+ makeOverride({
559
+ id: "ov-scoped-other",
560
+ projectId: PROJECT,
561
+ overrideUserId: "A",
562
+ routeAlertsToUserId: "D",
563
+ startsAt: OVERRIDE_START,
564
+ endsAt: OVERRIDE_END,
565
+ policyId: "P2",
566
+ }),
567
+ makeOverride({
568
+ id: "ov-scoped-other-b",
569
+ projectId: PROJECT,
570
+ overrideUserId: "B",
571
+ routeAlertsToUserId: "D",
572
+ startsAt: OVERRIDE_START,
573
+ endsAt: OVERRIDE_END,
574
+ policyId: "P2",
575
+ }),
576
+ );
577
+
578
+ const resolved: ResolvedShiftSegments = await resolve(db);
579
+
580
+ expect(whoIsOnAt(resolved, at("2026-03-03T12:00:00Z"))).not.toBe("D");
581
+ expect(resolved.policyVariants).toEqual([]);
582
+ expect(resolved.overrides).toEqual([]);
583
+ });
584
+
585
+ test("with ZERO attached policies, scoped overrides never apply", async () => {
586
+ const db: FakeDb = baseDb();
587
+ db.overrides.push(
588
+ makeOverride({
589
+ id: "ov-scoped",
590
+ projectId: PROJECT,
591
+ overrideUserId: "A",
592
+ routeAlertsToUserId: "D",
593
+ startsAt: OVERRIDE_START,
594
+ endsAt: OVERRIDE_END,
595
+ policyId: "P1",
596
+ }),
597
+ makeOverride({
598
+ id: "ov-scoped-b",
599
+ projectId: PROJECT,
600
+ overrideUserId: "B",
601
+ routeAlertsToUserId: "D",
602
+ startsAt: OVERRIDE_START,
603
+ endsAt: OVERRIDE_END,
604
+ policyId: "P1",
605
+ }),
606
+ );
607
+
608
+ const resolved: ResolvedShiftSegments = await resolve(db);
609
+ expect(whoIsOnAt(resolved, at("2026-03-03T12:00:00Z"))).not.toBe("D");
610
+ expect(resolved.policyVariants).toEqual([]);
611
+ });
612
+ });
613
+
614
+ describe("policy variants (two or more attached policies)", () => {
615
+ const OVERRIDE_START: Date = at("2026-03-03T09:00:00Z");
616
+ const OVERRIDE_END: Date = at("2026-03-04T09:00:00Z");
617
+
618
+ function twoPolicyDb(): FakeDb {
619
+ const db: FakeDb = baseDb();
620
+ db.attachments.push(
621
+ makeAttachment({
622
+ id: "att-1",
623
+ scheduleId: SCHEDULE,
624
+ projectId: PROJECT,
625
+ policyId: "P1",
626
+ policyName: "Payments",
627
+ ruleId: "R1",
628
+ }),
629
+ makeAttachment({
630
+ id: "att-2",
631
+ scheduleId: SCHEDULE,
632
+ projectId: PROJECT,
633
+ policyId: "P2",
634
+ policyName: "Checkout",
635
+ ruleId: "R2",
636
+ }),
637
+ );
638
+ return db;
639
+ }
640
+
641
+ test("no scoped override overlapping the window => no variants, base is global-only", async () => {
642
+ const db: FakeDb = twoPolicyDb();
643
+
644
+ const resolved: ResolvedShiftSegments = await resolve(db);
645
+
646
+ expect(resolved.policyVariants).toEqual([]);
647
+ expect(
648
+ resolved.attachedPolicies.map((p: MaterializedShiftPolicy) => {
649
+ return p.policyId;
650
+ }),
651
+ ).toEqual(["P2", "P1"]);
652
+ });
653
+
654
+ test("a scoped override for a member yields a variant for THAT policy only, base stays global", async () => {
655
+ const db: FakeDb = twoPolicyDb();
656
+ const baseline: ResolvedShiftSegments = await resolve(db);
657
+ const onCall: string = coveringSegment(
658
+ baseline.segments,
659
+ at("2026-03-03T12:00:00Z"),
660
+ )!.title;
661
+
662
+ db.overrides.push(
663
+ makeOverride({
664
+ id: "ov-p1",
665
+ projectId: PROJECT,
666
+ overrideUserId: onCall,
667
+ routeAlertsToUserId: "E",
668
+ startsAt: OVERRIDE_START,
669
+ endsAt: OVERRIDE_END,
670
+ policyId: "P1",
671
+ }),
672
+ );
673
+
674
+ const resolved: ResolvedShiftSegments = await resolve(db);
675
+
676
+ // Base (global-only) still names the rostered person.
677
+ expect(
678
+ coveringSegment(resolved.segments, at("2026-03-03T12:00:00Z"))!.title,
679
+ ).toBe(onCall);
680
+
681
+ expect(resolved.policyVariants).toHaveLength(1);
682
+ const variant: ResolvedPolicyVariant = resolved.policyVariants[0]!;
683
+ expect(variant.policyId).toBe("P1");
684
+ expect(variant.policyName).toBe("Payments");
685
+ expect(
686
+ coveringSegment(variant.segments, at("2026-03-03T12:00:00Z"))!.title,
687
+ ).toBe("E");
688
+
689
+ // The variant's segments are window-filtered too.
690
+ for (const segment of variant.segments) {
691
+ expect(segment.start.getTime()).toBeLessThan(WINDOW_END.getTime());
692
+ expect(segment.end.getTime()).toBeGreaterThan(WINDOW_START.getTime());
693
+ }
694
+
695
+ // Both the scoped override is recorded as participating, with its scope.
696
+ expect(
697
+ resolved.overrides.filter((o: UserOverrideRecord) => {
698
+ return o.onCallDutyPolicyId === "P1";
699
+ }),
700
+ ).toHaveLength(1);
701
+ });
702
+
703
+ test("global overrides apply inside every variant as well", async () => {
704
+ const db: FakeDb = twoPolicyDb();
705
+ const baseline: ResolvedShiftSegments = await resolve(db);
706
+ const onCallDay3: string = coveringSegment(
707
+ baseline.segments,
708
+ at("2026-03-03T12:00:00Z"),
709
+ )!.title;
710
+ const onCallDay4: string = coveringSegment(
711
+ baseline.segments,
712
+ at("2026-03-04T12:00:00Z"),
713
+ )!.title;
714
+
715
+ db.overrides.push(
716
+ makeOverride({
717
+ id: "ov-global",
718
+ projectId: PROJECT,
719
+ overrideUserId: onCallDay4,
720
+ routeAlertsToUserId: "G",
721
+ startsAt: at("2026-03-04T10:00:00Z"),
722
+ endsAt: at("2026-03-04T12:30:00Z"),
723
+ }),
724
+ makeOverride({
725
+ id: "ov-p2",
726
+ projectId: PROJECT,
727
+ overrideUserId: onCallDay3,
728
+ routeAlertsToUserId: "E",
729
+ startsAt: OVERRIDE_START,
730
+ endsAt: OVERRIDE_END,
731
+ policyId: "P2",
732
+ }),
733
+ );
734
+
735
+ const resolved: ResolvedShiftSegments = await resolve(db);
736
+
737
+ const variant: ResolvedPolicyVariant = resolved.policyVariants.find(
738
+ (v: ResolvedPolicyVariant) => {
739
+ return v.policyId === "P2";
740
+ },
741
+ )!;
742
+ expect(variant).toBeDefined();
743
+ expect(
744
+ coveringSegment(variant.segments, at("2026-03-03T12:00:00Z"))!.title,
745
+ ).toBe("E");
746
+ expect(
747
+ coveringSegment(variant.segments, at("2026-03-04T11:00:00Z"))!.title,
748
+ ).toBe("G");
749
+ expect(
750
+ coveringSegment(resolved.segments, at("2026-03-04T11:00:00Z"))!.title,
751
+ ).toBe("G");
752
+ });
753
+
754
+ test("scoped overrides for a non-member do not create a variant", async () => {
755
+ const db: FakeDb = twoPolicyDb();
756
+ db.overrides.push(
757
+ makeOverride({
758
+ id: "ov-p1-stranger",
759
+ projectId: PROJECT,
760
+ overrideUserId: "Z",
761
+ routeAlertsToUserId: "E",
762
+ startsAt: OVERRIDE_START,
763
+ endsAt: OVERRIDE_END,
764
+ policyId: "P1",
765
+ }),
766
+ );
767
+
768
+ const resolved: ResolvedShiftSegments = await resolve(db);
769
+ expect(resolved.policyVariants).toEqual([]);
770
+ });
771
+ });
772
+
773
+ describe("lastModifiedAt", () => {
774
+ test("is the max updatedAt over layers, layer users, attachments and participating overrides — never the schedule row", async () => {
775
+ const db: FakeDb = emptyDb();
776
+ db.schedules.push(
777
+ makeSchedule({
778
+ id: SCHEDULE,
779
+ projectId: PROJECT,
780
+ timezone: UTC,
781
+ updatedAt: at("2026-12-31T00:00:00Z"), // roster refresh noise
782
+ }),
783
+ );
784
+ dailyLayer(db, { updatedAt: at("2026-08-01T00:00:00Z") });
785
+ db.layerUsers[0]!["updatedAt"] = at("2026-08-05T00:00:00Z");
786
+ db.attachments.push(
787
+ makeAttachment({
788
+ id: "att-1",
789
+ scheduleId: SCHEDULE,
790
+ projectId: PROJECT,
791
+ policyId: "P1",
792
+ ruleId: "R1",
793
+ updatedAt: at("2026-08-03T00:00:00Z"),
794
+ }),
795
+ );
796
+ db.overrides.push(
797
+ makeOverride({
798
+ id: "ov-1",
799
+ projectId: PROJECT,
800
+ overrideUserId: "A",
801
+ routeAlertsToUserId: "C",
802
+ startsAt: at("2026-03-03T11:00:00Z"),
803
+ endsAt: at("2026-03-03T13:00:00Z"),
804
+ updatedAt: at("2026-08-10T00:00:00Z"),
805
+ }),
806
+ );
807
+
808
+ const resolved: ResolvedShiftSegments = await resolve(db);
809
+ expect(resolved.lastModifiedAt.toISOString()).toBe(
810
+ "2026-08-10T00:00:00.000Z",
811
+ );
812
+ });
813
+
814
+ test("an override that does not participate does not count", async () => {
815
+ const db: FakeDb = baseDb();
816
+ db.overrides.push(
817
+ makeOverride({
818
+ id: "ov-far",
819
+ projectId: PROJECT,
820
+ overrideUserId: "A",
821
+ routeAlertsToUserId: "C",
822
+ startsAt: at("2027-01-01T00:00:00Z"),
823
+ endsAt: at("2027-01-02T00:00:00Z"),
824
+ updatedAt: at("2027-06-01T00:00:00Z"),
825
+ }),
826
+ );
827
+
828
+ const resolved: ResolvedShiftSegments = await resolve(db);
829
+ expect(resolved.lastModifiedAt.toISOString()).toBe(
830
+ "2026-08-01T10:00:00.000Z",
831
+ );
832
+ });
833
+
834
+ test("falls back to the schedule's createdAt when nothing is configured", async () => {
835
+ const db: FakeDb = emptyDb();
836
+ db.schedules.push(
837
+ makeSchedule({
838
+ id: SCHEDULE,
839
+ projectId: PROJECT,
840
+ createdAt: at("2026-02-02T02:02:02Z"),
841
+ }),
842
+ );
843
+
844
+ const resolved: ResolvedShiftSegments = await resolve(db);
845
+ expect(resolved.lastModifiedAt.toISOString()).toBe(
846
+ "2026-02-02T02:02:02.000Z",
847
+ );
848
+ });
849
+
850
+ test("is identical across two resolutions of an unchanged configuration", async () => {
851
+ const first: ResolvedShiftSegments = await resolve(baseDb());
852
+ jest.restoreAllMocks();
853
+ jest.spyOn(logger, "error").mockImplementation((): void => {
854
+ return undefined;
855
+ });
856
+ const second: ResolvedShiftSegments = await resolve(baseDb());
857
+
858
+ expect(first.lastModifiedAt).toEqual(second.lastModifiedAt);
859
+ expect(
860
+ first.segments.map((s: CalendarEvent) => {
861
+ return [s.title, s.start, s.end];
862
+ }),
863
+ ).toEqual(
864
+ second.segments.map((s: CalendarEvent) => {
865
+ return [s.title, s.start, s.end];
866
+ }),
867
+ );
868
+ });
869
+ });
870
+
871
+ describe("truncation", () => {
872
+ test("propagates the engine's truncated flag when the iteration cap is hit", async () => {
873
+ const db: FakeDb = emptyDb();
874
+ db.schedules.push(
875
+ makeSchedule({ id: SCHEDULE, projectId: PROJECT, timezone: UTC }),
876
+ );
877
+ /*
878
+ * A restricted hourly rotation running for years: the pre-window
879
+ * simulation needs tens of thousands of periods.
880
+ */
881
+ db.layers.push(
882
+ makeLayer({
883
+ id: "layer-old",
884
+ scheduleId: SCHEDULE,
885
+ projectId: PROJECT,
886
+ startsAt: at("2020-01-01T00:00:00Z"),
887
+ handOffTime: at("2020-01-01T09:00:00Z"),
888
+ rotation: rotation(EventInterval.Hour, 1),
889
+ restrictionTimes: dailyRestriction("09:00", "17:00", UTC),
890
+ }),
891
+ );
892
+ db.layerUsers.push(
893
+ makeLayerUser({
894
+ id: "lu-a",
895
+ scheduleId: SCHEDULE,
896
+ layerId: "layer-old",
897
+ projectId: PROJECT,
898
+ userId: "A",
899
+ }),
900
+ );
901
+
902
+ const resolved: ResolvedShiftSegments = await resolve(db, {
903
+ maxSimulationIterations: 10,
904
+ });
905
+
906
+ expect(resolved.truncated).toBe(true);
907
+ });
908
+
909
+ test("is false for an ordinary schedule", async () => {
910
+ const resolved: ResolvedShiftSegments = await resolve(baseDb(), {
911
+ maxSimulationIterations: 100000,
912
+ });
913
+ expect(resolved.truncated).toBe(false);
914
+ expect(resolved.segments.length).toBeGreaterThan(0);
915
+ });
916
+ });
917
+
918
+ describe("batched resolution", () => {
919
+ test("resolves several schedules across projects with one override query, scoping overrides per project", async () => {
920
+ const db: FakeDb = baseDb();
921
+ db.schedules.push(
922
+ makeSchedule({
923
+ id: OTHER_SCHEDULE,
924
+ projectId: OTHER_PROJECT,
925
+ name: "Checkout",
926
+ timezone: UTC,
927
+ }),
928
+ );
929
+ dailyLayer(db, {
930
+ scheduleId: OTHER_SCHEDULE,
931
+ projectId: OTHER_PROJECT,
932
+ layerId: "layer-2",
933
+ users: ["A", "X"],
934
+ });
935
+ // A is a member of both schedules; this override lives in project 2 only.
936
+ db.overrides.push(
937
+ makeOverride({
938
+ id: "ov-p2",
939
+ projectId: OTHER_PROJECT,
940
+ overrideUserId: "A",
941
+ routeAlertsToUserId: "C",
942
+ startsAt: at("2026-03-01T00:00:00Z"),
943
+ endsAt: at("2026-03-06T00:00:00Z"),
944
+ }),
945
+ makeOverride({
946
+ id: "ov-p2-x",
947
+ projectId: OTHER_PROJECT,
948
+ overrideUserId: "X",
949
+ routeAlertsToUserId: "C",
950
+ startsAt: at("2026-03-01T00:00:00Z"),
951
+ endsAt: at("2026-03-06T00:00:00Z"),
952
+ }),
953
+ );
954
+
955
+ installFakeDb(db);
956
+
957
+ const batch: Array<ResolvedShiftSegments> =
958
+ await OnCallDutyPolicyScheduleService.getResolvedShiftSegmentsForSchedules(
959
+ {
960
+ scheduleIds: [oid(SCHEDULE), oid(OTHER_SCHEDULE), oid(SCHEDULE)],
961
+ windowStart: WINDOW_START,
962
+ windowEnd: WINDOW_END,
963
+ },
964
+ );
965
+
966
+ expect(
967
+ batch.map((r: ResolvedShiftSegments) => {
968
+ return r.schedule.id;
969
+ }),
970
+ ).toEqual([SCHEDULE, OTHER_SCHEDULE]);
971
+ expect(OnCallDutyPolicyUserOverrideService.findBy).toHaveBeenCalledTimes(
972
+ 1,
973
+ );
974
+
975
+ const [first, second] = batch as [
976
+ ResolvedShiftSegments,
977
+ ResolvedShiftSegments,
978
+ ];
979
+
980
+ // Project 1: nobody substituted.
981
+ expect(first.overrides).toEqual([]);
982
+ for (const segment of first.segments) {
983
+ expect(["A", "B"]).toContain(segment.title);
984
+ }
985
+
986
+ // Project 2: everyone substituted by C for the whole window.
987
+ expect(second.overrides).toHaveLength(2);
988
+ for (const segment of second.segments) {
989
+ expect(segment.title).toBe("C");
990
+ }
991
+ });
992
+
993
+ test("each schedule reports its own truncated flag and layer props", async () => {
994
+ const db: FakeDb = baseDb();
995
+ db.schedules.push(
996
+ makeSchedule({ id: OTHER_SCHEDULE, projectId: PROJECT, timezone: UTC }),
997
+ );
998
+ db.layers.push(
999
+ makeLayer({
1000
+ id: "layer-old",
1001
+ scheduleId: OTHER_SCHEDULE,
1002
+ projectId: PROJECT,
1003
+ startsAt: at("2020-01-01T00:00:00Z"),
1004
+ handOffTime: at("2020-01-01T09:00:00Z"),
1005
+ rotation: rotation(EventInterval.Hour, 1),
1006
+ restrictionTimes: dailyRestriction("09:00", "17:00", UTC),
1007
+ }),
1008
+ );
1009
+ db.layerUsers.push(
1010
+ makeLayerUser({
1011
+ id: "lu-old",
1012
+ scheduleId: OTHER_SCHEDULE,
1013
+ layerId: "layer-old",
1014
+ projectId: PROJECT,
1015
+ userId: "Q",
1016
+ }),
1017
+ );
1018
+
1019
+ installFakeDb(db);
1020
+
1021
+ const batch: Array<ResolvedShiftSegments> =
1022
+ await OnCallDutyPolicyScheduleService.getResolvedShiftSegmentsForSchedules(
1023
+ {
1024
+ scheduleIds: [oid(SCHEDULE), oid(OTHER_SCHEDULE)],
1025
+ windowStart: WINDOW_START,
1026
+ windowEnd: WINDOW_END,
1027
+ maxSimulationIterations: 10,
1028
+ },
1029
+ );
1030
+
1031
+ expect(batch[0]!.truncated).toBe(false);
1032
+ expect(batch[0]!.segments.length).toBeGreaterThan(0);
1033
+ expect(batch[1]!.truncated).toBe(true);
1034
+ expect(batch[1]!.layerProps[0]!.layerId).toBe("layer-old");
1035
+ });
1036
+ });
1037
+ });