@oneuptime/common 12.0.8 → 12.0.9

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 (769) hide show
  1. package/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +1 -1
  2. package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +1 -1
  3. package/Models/DatabaseModels/AIConversation.ts +32 -0
  4. package/Models/DatabaseModels/AIConversationMessage.ts +41 -0
  5. package/Models/DatabaseModels/AlertEpisodeMember.ts +27 -0
  6. package/Models/DatabaseModels/IncidentEpisodeMember.ts +28 -0
  7. package/Models/DatabaseModels/Index.ts +12 -4
  8. package/Models/DatabaseModels/{TelemetryEntity.ts → InventoryItem.ts} +139 -9
  9. package/Models/DatabaseModels/InventoryItemCustomField.ts +434 -0
  10. package/Models/DatabaseModels/{TelemetryEntityRelationship.ts → InventoryItemRelationship.ts} +8 -8
  11. package/Models/DatabaseModels/NetworkDevice.ts +141 -0
  12. package/Models/DatabaseModels/NetworkDeviceLink.ts +699 -0
  13. package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +467 -0
  14. package/Models/DatabaseModels/NetworkTopologySuppression.ts +429 -0
  15. package/Models/DatabaseModels/OnCallDutyPolicyFeed.ts +9 -0
  16. package/Models/DatabaseModels/Project.ts +78 -0
  17. package/Models/DatabaseModels/UserCall.ts +42 -0
  18. package/Models/DatabaseModels/UserEmail.ts +44 -0
  19. package/Models/DatabaseModels/UserNotificationRule.ts +425 -55
  20. package/Models/DatabaseModels/UserOnCallLogTimeline.ts +24 -2
  21. package/Models/DatabaseModels/UserPush.ts +49 -0
  22. package/Models/DatabaseModels/UserSMS.ts +43 -0
  23. package/Models/DatabaseModels/UserTelegram.ts +59 -0
  24. package/Models/DatabaseModels/UserWebhook.ts +52 -0
  25. package/Models/DatabaseModels/UserWhatsApp.ts +41 -0
  26. package/Models/DatabaseModels/WorkflowVariable.ts +12 -0
  27. package/Server/API/AIChatAPI.ts +315 -1
  28. package/Server/API/DashboardAPI.ts +217 -1
  29. package/Server/API/OnCallReadinessAPI.ts +841 -0
  30. package/Server/API/TeamComplianceAPI.ts +69 -17
  31. package/Server/API/TelemetryAPI.ts +220 -12
  32. package/Server/EnvironmentConfig.ts +52 -0
  33. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +22 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.ts +4 -4
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.ts +5 -5
  36. package/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.ts +35 -0
  37. package/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.ts +82 -0
  38. package/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.ts +91 -0
  39. package/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.ts +47 -0
  40. package/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.ts +255 -0
  41. package/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.ts +107 -0
  42. package/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.ts +90 -0
  43. package/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.ts +39 -0
  44. package/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.ts +33 -0
  45. package/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.ts +59 -0
  46. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +20 -0
  47. package/Server/Infrastructure/Queue.ts +78 -13
  48. package/Server/Middleware/PublicDashboardRateLimit.ts +593 -0
  49. package/Server/Services/AIService.ts +7 -0
  50. package/Server/Services/AlertEpisodeStateTimelineService.ts +29 -0
  51. package/Server/Services/AlertSeverityService.ts +63 -0
  52. package/Server/Services/DashboardService.ts +9 -10
  53. package/Server/Services/DatabaseService.ts +32 -2
  54. package/Server/Services/IncidentEpisodeStateTimelineService.ts +29 -0
  55. package/Server/Services/IncidentSeverityService.ts +76 -0
  56. package/Server/Services/Index.ts +12 -4
  57. package/Server/Services/InventoryItemCustomFieldService.ts +9 -0
  58. package/Server/Services/{TelemetryEntityRelationshipService.ts → InventoryItemRelationshipService.ts} +4 -4
  59. package/Server/Services/{TelemetryEntityService.ts → InventoryItemService.ts} +89 -20
  60. package/Server/Services/LogAggregationService.ts +45 -8
  61. package/Server/Services/MetricAggregationService.ts +121 -0
  62. package/Server/Services/MetricService.ts +7 -7
  63. package/Server/Services/NetworkDeviceLinkRuleService.ts +10 -0
  64. package/Server/Services/NetworkDeviceLinkService.ts +84 -0
  65. package/Server/Services/NetworkDeviceService.ts +140 -0
  66. package/Server/Services/NetworkSiteService.ts +77 -25
  67. package/Server/Services/NetworkTopologySuppressionService.ts +84 -0
  68. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +41 -29
  69. package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +8 -0
  70. package/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.ts +62 -13
  71. package/Server/Services/OnCallDutyPolicyScheduleService.ts +61 -1
  72. package/Server/Services/OnCallNotificationAlertingService.ts +742 -0
  73. package/Server/Services/OnCallReadinessService.ts +2803 -0
  74. package/Server/Services/OnCallSetupReminderService.ts +955 -0
  75. package/Server/Services/ProfileAggregationService.ts +123 -0
  76. package/Server/Services/StatusPageService.ts +9 -10
  77. package/Server/Services/TeamComplianceService.ts +429 -252
  78. package/Server/Services/UserCallService.ts +26 -1
  79. package/Server/Services/UserEmailService.ts +26 -1
  80. package/Server/Services/UserNotificationRuleAdminService.ts +1183 -0
  81. package/Server/Services/UserNotificationRuleService.ts +3812 -333
  82. package/Server/Services/UserOnCallLogService.ts +561 -48
  83. package/Server/Services/UserPushService.ts +29 -0
  84. package/Server/Services/UserService.ts +11 -0
  85. package/Server/Services/UserSmsService.ts +26 -1
  86. package/Server/Services/UserTelegramService.ts +24 -1
  87. package/Server/Services/UserWebhookService.ts +28 -1
  88. package/Server/Services/UserWhatsAppService.ts +24 -1
  89. package/Server/Types/Database/Permissions/BasePermission.ts +19 -0
  90. package/Server/Types/Database/Permissions/CreatePermission.ts +164 -0
  91. package/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.ts +340 -0
  92. package/Server/Types/Database/Permissions/QueryPermission.ts +48 -0
  93. package/Server/Types/Database/Permissions/TenantPermission.ts +8 -1
  94. package/Server/Types/Workflow/Components/API/Delete.ts +1 -1
  95. package/Server/Types/Workflow/Components/API/Get.ts +1 -1
  96. package/Server/Types/Workflow/Components/API/Patch.ts +1 -1
  97. package/Server/Types/Workflow/Components/API/Post.ts +1 -1
  98. package/Server/Types/Workflow/Components/API/Put.ts +1 -1
  99. package/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.ts +29 -5
  100. package/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.ts +18 -10
  101. package/Server/Types/Workflow/Components/BaseModel/ModelArguments.ts +55 -0
  102. package/Server/Types/Workflow/Components/Conditions/IfElse.ts +3 -17
  103. package/Server/Types/Workflow/Components/Email.ts +25 -7
  104. package/Server/Types/Workflow/Components/JavaScript.ts +10 -3
  105. package/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.ts +1 -1
  106. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +643 -48
  107. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +32 -3
  108. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +20 -6
  109. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +7 -0
  110. package/Server/Utils/AI/Toolbox/AIActionTools.ts +2 -2
  111. package/Server/Utils/AI/Toolbox/AIMetaTools.ts +863 -0
  112. package/Server/Utils/AI/Toolbox/AlertTools.ts +177 -15
  113. package/Server/Utils/AI/Toolbox/IncidentTools.ts +191 -10
  114. package/Server/Utils/AI/Toolbox/Index.ts +48 -0
  115. package/Server/Utils/AI/Toolbox/MonitorTools.ts +298 -11
  116. package/Server/Utils/AI/Toolbox/NoteWriteTools.ts +295 -0
  117. package/Server/Utils/AI/Toolbox/OnCallTools.ts +1246 -0
  118. package/Server/Utils/AI/Toolbox/RunbookTools.ts +424 -0
  119. package/Server/Utils/AI/Toolbox/SloTools.ts +456 -0
  120. package/Server/Utils/AI/Toolbox/StatusPageTools.ts +559 -0
  121. package/Server/Utils/AI/Toolbox/TeamTools.ts +327 -0
  122. package/Server/Utils/AI/Toolbox/TimelineTools.ts +615 -0
  123. package/Server/Utils/AI/Toolbox/WorkflowProbeTools.ts +664 -0
  124. package/Server/Utils/ClientIp.ts +221 -0
  125. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +47 -0
  126. package/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.ts +163 -0
  127. package/Server/Utils/Dashboard/PublicDashboardSloWidget.ts +147 -0
  128. package/Server/Utils/Express.ts +12 -17
  129. package/Server/Utils/LLM/LLMService.ts +85 -8
  130. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +204 -10
  131. package/Server/Utils/SSRFProtection.ts +98 -23
  132. package/Server/Utils/StartServer.ts +12 -3
  133. package/Server/Utils/Telemetry/EntityRegistry.ts +205 -18
  134. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +25 -25
  135. package/Server/Utils/Telemetry/TelemetryEntity.ts +160 -52
  136. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +7 -3
  137. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +2146 -0
  138. package/Tests/App/Dashboard/CreateWorkflowModal.test.tsx +561 -0
  139. package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +2470 -0
  140. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +1897 -0
  141. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +3606 -0
  142. package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +784 -0
  143. package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +1119 -0
  144. package/Tests/App/Dashboard/SloWidgetFetching.test.tsx +531 -0
  145. package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +1312 -0
  146. package/Tests/App/Dashboard/UserSettingsSetupChecklistPage.test.tsx +1390 -0
  147. package/Tests/Models/InventoryItemModel.test.ts +174 -0
  148. package/Tests/Models/InventoryItemNaming.test.ts +302 -0
  149. package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +437 -0
  150. package/Tests/Server/API/DashboardPublicRateLimit.test.ts +659 -0
  151. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +18 -0
  152. package/Tests/Server/API/DashboardPublicSloAPI.test.ts +880 -0
  153. package/Tests/Server/API/Helpers.ts +24 -15
  154. package/Tests/Server/API/OnCallReadinessAPI.test.ts +2680 -0
  155. package/Tests/Server/API/OnCallSetupReminderAPI.test.ts +915 -0
  156. package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +533 -0
  157. package/Tests/Server/Infrastructure/Postgres/InventoryItemArchiveMigration.test.ts +213 -0
  158. package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +432 -0
  159. package/Tests/Server/Infrastructure/Queue.test.ts +293 -0
  160. package/Tests/Server/Middleware/PublicDashboardRateLimit.test.ts +1645 -0
  161. package/Tests/Server/Services/AdminRuleEditGuards.test.ts +2848 -0
  162. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +1393 -0
  163. package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +1802 -0
  164. package/Tests/Server/Services/EpisodeStateTimelineNote.test.ts +304 -0
  165. package/Tests/Server/Services/InventoryItemDisplayName.test.ts +339 -0
  166. package/Tests/Server/Services/{TelemetryEntityManualCreate.test.ts → InventoryItemManualCreate.test.ts} +27 -27
  167. package/Tests/Server/Services/IpAllowlistSpoofing.test.ts +450 -0
  168. package/Tests/Server/Services/LogAggregationService.test.ts +235 -1
  169. package/Tests/Server/Services/MetricAggregationService.test.ts +231 -0
  170. package/Tests/Server/Services/MetricEntityMVKeyParity.test.ts +80 -29
  171. package/Tests/Server/Services/MetricServiceAggregate.test.ts +30 -30
  172. package/Tests/Server/Services/NetworkSiteService.test.ts +18 -3
  173. package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +1728 -0
  174. package/Tests/Server/Services/NotificationDeletionImpact.test.ts +2402 -0
  175. package/Tests/Server/Services/OnCallDutyPolicyExecutionLogTimelineGapFeed.test.ts +394 -0
  176. package/Tests/Server/Services/OnCallNotificationFallback.test.ts +1744 -0
  177. package/Tests/Server/Services/OnCallReadinessService.test.ts +4295 -0
  178. package/Tests/Server/Services/OnCallSetupReminder.test.ts +1272 -0
  179. package/Tests/Server/Services/OnCallWeeklyReadinessDigest.test.ts +1021 -0
  180. package/Tests/Server/Services/ProfileAggregationService.test.ts +296 -0
  181. package/Tests/Server/Services/SeverityCreationRuleBackfill.test.ts +1536 -0
  182. package/Tests/Server/Services/SeverityRuleBackfill.test.ts +1818 -0
  183. package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +1845 -0
  184. package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +1394 -0
  185. package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +1166 -0
  186. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +1468 -0
  187. package/Tests/Server/Services/UserOnCallLogNoNotificationRules.test.ts +1457 -0
  188. package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +1546 -0
  189. package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +529 -0
  190. package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +1219 -0
  191. package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +1089 -0
  192. package/Tests/Server/Types/Workflow/Components/ApiComponentErrorPort.test.ts +2 -1
  193. package/Tests/Server/Types/Workflow/Components/BaseModelDatabaseComponents.test.ts +190 -0
  194. package/Tests/Server/Types/Workflow/Components/ChatWebhookComponents.test.ts +44 -14
  195. package/Tests/Server/Types/Workflow/Components/Email.test.ts +151 -0
  196. package/Tests/Server/Types/Workflow/Components/IfElse.test.ts +98 -0
  197. package/Tests/Server/Types/Workflow/Components/JavaScript.test.ts +51 -0
  198. package/Tests/Server/Utils/AI/AIMetaTools.test.ts +586 -0
  199. package/Tests/Server/Utils/AI/AlertMonitorFilters.test.ts +582 -0
  200. package/Tests/Server/Utils/AI/ChatAgentRunner.test.ts +726 -0
  201. package/Tests/Server/Utils/AI/IncidentToolsFilters.test.ts +315 -0
  202. package/Tests/Server/Utils/AI/LLMServiceStopReason.test.ts +314 -0
  203. package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +26 -3
  204. package/Tests/Server/Utils/AI/NoteWriteTools.test.ts +268 -0
  205. package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +169 -0
  206. package/Tests/Server/Utils/AI/OnCallTools.test.ts +664 -0
  207. package/Tests/Server/Utils/AI/RunbookTools.test.ts +325 -0
  208. package/Tests/Server/Utils/AI/SloTools.test.ts +306 -0
  209. package/Tests/Server/Utils/AI/StatusPageTools.test.ts +391 -0
  210. package/Tests/Server/Utils/AI/TeamTools.test.ts +257 -0
  211. package/Tests/Server/Utils/AI/TimelineTools.test.ts +472 -0
  212. package/Tests/Server/Utils/AI/WorkflowProbeTools.test.ts +428 -0
  213. package/Tests/Server/Utils/ClientIp.test.ts +438 -0
  214. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +171 -0
  215. package/Tests/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.test.ts +383 -0
  216. package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +23 -25
  217. package/Tests/Server/Utils/MicrosoftTeamsWebhookUrlValidation.test.ts +6 -0
  218. package/Tests/Server/Utils/Monitor/Criteria/DnssecMonitorCriteria.test.ts +307 -0
  219. package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +468 -0
  220. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorTelemetryDeepLinks.test.ts +460 -0
  221. package/Tests/Server/Utils/ResponseRateLimitStatusCodes.test.ts +137 -0
  222. package/Tests/Server/Utils/SSRFProtectionBypasses.test.ts +40 -8
  223. package/Tests/Server/Utils/SSRFProtectionUserInfo.test.ts +351 -0
  224. package/Tests/Server/Utils/Telemetry/EntityRegistryRetirement.test.ts +531 -0
  225. package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +15 -15
  226. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +356 -39
  227. package/Tests/Types/IP/IP.test.ts +263 -0
  228. package/Tests/Types/IP/IPWhitelist.test.ts +197 -0
  229. package/Tests/Types/IP/IPv6.test.ts +12 -1
  230. package/Tests/Types/Monitor/SnmpOid.test.ts +64 -0
  231. package/Tests/Types/NetworkDevice/NetworkDeviceMonitoringMethod.test.ts +110 -0
  232. package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +106 -0
  233. package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeDifferential.test.ts +511 -0
  234. package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageEndToEnd.test.ts +489 -0
  235. package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageGapTolerance.test.ts +479 -0
  236. package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageState.test.ts +822 -0
  237. package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +6 -6
  238. package/Tests/Types/Workflow/BaseModelComponents.test.ts +429 -0
  239. package/Tests/Types/Workflow/Components/BaseModel.test.ts +225 -0
  240. package/Tests/Types/Workflow/IntegrationCredentialMetadata.test.ts +100 -0
  241. package/Tests/Types/Workflow/TemplateSyntax.test.ts +112 -0
  242. package/Tests/Types/Workflow/Templates.test.ts +916 -101
  243. package/Tests/UI/Components/ActiveFilterChipsOpenRoute.test.tsx +82 -0
  244. package/Tests/UI/Components/ComponentsModal.test.tsx +564 -7
  245. package/Tests/UI/Components/KeyboardShortcut.test.tsx +95 -0
  246. package/Tests/UI/Components/LogDetailsPanelCrossSignal.test.tsx +448 -0
  247. package/Tests/UI/Components/LogsTableCrossLinks.test.tsx +263 -0
  248. package/Tests/UI/Components/PendingProjectInvitations.test.tsx +913 -0
  249. package/Tests/UI/Components/SimpleLogViewer.test.tsx +228 -0
  250. package/Tests/UI/Components/TableRowSelectability.test.tsx +312 -0
  251. package/Tests/UI/Components/Workflow/GraphLint.test.ts +72 -0
  252. package/Tests/UI/Components/Workflow/GraphLintSummary.test.ts +755 -0
  253. package/Tests/UI/Components/Workflow/ModelColumnEditor.test.ts +242 -7
  254. package/Tests/UI/Components/Workflow/ModelColumnEditorServerContract.test.ts +15 -7
  255. package/Tests/UI/Components/Workflow/ModelSchema.test.ts +242 -28
  256. package/Tests/UI/Components/Workflow/RunStatusWatcher.test.ts +59 -1
  257. package/Tests/UI/Components/Workflow/StepTraceViewer.test.tsx +256 -0
  258. package/Tests/UI/Components/Workflow/UseRunWatch.test.tsx +665 -0
  259. package/Tests/UI/Components/Workflow/WorkflowIssuesModal.test.tsx +485 -0
  260. package/Tests/UI/Components/Workflow/WorkflowLogModal.test.tsx +478 -0
  261. package/Tests/UI/Components/Workflow/WorkflowStatusBar.test.tsx +379 -0
  262. package/Tests/UI/EsbuildConfig.test.ts +607 -0
  263. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
  264. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +11 -4
  265. package/Tests/UI/Utils/ModelAPICreateMiscData.test.ts +94 -0
  266. package/Tests/UI/Utils/Platform.test.ts +147 -0
  267. package/Tests/UI/Utils/ProjectInvitationDisplay.test.ts +357 -0
  268. package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +198 -0
  269. package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +487 -0
  270. package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +698 -0
  271. package/Tests/__mocks__/bullmq.js +55 -0
  272. package/Types/AI/AIChatMessageStatus.ts +8 -1
  273. package/Types/AI/AIChatTypes.ts +12 -0
  274. package/Types/Database/AccessControl/OwnerOnlyColumn.ts +88 -0
  275. package/Types/Exception/ExceptionCode.ts +2 -0
  276. package/Types/Exception/ServiceUnavailableException.ts +8 -0
  277. package/Types/Exception/TooManyRequestsException.ts +8 -0
  278. package/Types/IP/IP.ts +93 -47
  279. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +49 -3
  280. package/Types/NetworkDevice/NetworkDeviceMonitoringMethod.ts +55 -0
  281. package/Types/OnCallDutyPolicy/Layer.ts +203 -149
  282. package/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.ts +13 -0
  283. package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +155 -10
  284. package/Types/Permission.ts +193 -0
  285. package/Types/Telemetry/EntityRelationshipType.ts +1 -1
  286. package/Types/Telemetry/EntitySource.ts +1 -1
  287. package/Types/Telemetry/EntityType.ts +1 -1
  288. package/Types/Telemetry/EntityTypeGroups.ts +1 -1
  289. package/Types/Workflow/Components/BaseModel.ts +75 -29
  290. package/Types/Workflow/Components/Discord.ts +1 -0
  291. package/Types/Workflow/Components/Email.ts +12 -3
  292. package/Types/Workflow/Components/JavaScript.ts +7 -0
  293. package/Types/Workflow/Components/MicrosoftTeams.ts +3 -2
  294. package/Types/Workflow/Components/Slack.ts +1 -0
  295. package/Types/Workflow/Components/Telegram.ts +1 -0
  296. package/Types/Workflow/TemplateSyntax.ts +44 -0
  297. package/Types/Workflow/Templates.ts +2097 -45
  298. package/UI/Components/Calendar/Calendar.css +43 -0
  299. package/UI/Components/Calendar/Calendar.tsx +8 -0
  300. package/UI/Components/Card/Card.tsx +2 -2
  301. package/UI/Components/Checkbox/Checkbox.tsx +16 -0
  302. package/UI/Components/Dictionary/Dictionary.tsx +48 -9
  303. package/UI/Components/FormModal/BasicFormModal.tsx +2 -1
  304. package/UI/Components/Header/HeaderIconDropdownButton.tsx +53 -3
  305. package/UI/Components/Input/Input.tsx +1 -0
  306. package/UI/Components/KeyboardShortcut/KeyboardKey.ts +185 -0
  307. package/UI/Components/KeyboardShortcut/KeyboardShortcut.tsx +87 -0
  308. package/UI/Components/LogsViewer/LogsViewer.tsx +28 -0
  309. package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +31 -0
  310. package/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.tsx +18 -18
  311. package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +363 -14
  312. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +11 -0
  313. package/UI/Components/LogsViewer/components/LogsTable.tsx +155 -12
  314. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +29 -0
  315. package/UI/Components/LogsViewer/types.ts +23 -0
  316. package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +9 -2
  317. package/UI/Components/Navbar/NavBarMenuModal.tsx +15 -30
  318. package/UI/Components/ProjectInvitations/PendingProjectInvitations.tsx +442 -0
  319. package/UI/Components/SimpleLogViewer/SimpleLogViewer.tsx +23 -1
  320. package/UI/Components/Table/Table.tsx +49 -16
  321. package/UI/Components/Table/TableBody.tsx +53 -28
  322. package/UI/Components/Table/TableHeader.tsx +13 -0
  323. package/UI/Components/Table/TableRow.tsx +58 -26
  324. package/UI/Components/Workflow/ArgumentsForm.tsx +155 -9
  325. package/UI/Components/Workflow/ComponentReturnValueViewer.tsx +26 -0
  326. package/UI/Components/Workflow/ComponentSettingsModal.tsx +10 -1
  327. package/UI/Components/Workflow/ComponentValuePickerModal.tsx +135 -7
  328. package/UI/Components/Workflow/ComponentsModal.tsx +116 -22
  329. package/UI/Components/Workflow/DocumentationViewer.tsx +59 -9
  330. package/UI/Components/Workflow/GraphLint.ts +42 -5
  331. package/UI/Components/Workflow/GraphLintSummary.ts +390 -0
  332. package/UI/Components/Workflow/ModelColumnEditor.tsx +154 -22
  333. package/UI/Components/Workflow/ModelSchema.ts +115 -33
  334. package/UI/Components/Workflow/RunStatusWatcher.ts +1 -1
  335. package/UI/Components/Workflow/StepTraceViewer.tsx +1 -1
  336. package/UI/Components/Workflow/UseRunWatch.ts +212 -0
  337. package/UI/Components/Workflow/VariableModal.tsx +6 -2
  338. package/UI/Components/Workflow/Workflow.tsx +49 -3
  339. package/UI/Components/Workflow/WorkflowIssuesModal.tsx +255 -0
  340. package/UI/Components/Workflow/WorkflowLogModal.tsx +128 -0
  341. package/UI/Components/Workflow/WorkflowStatusBar.tsx +224 -0
  342. package/UI/Utils/AIChatExport/ConversationMarkdown.ts +10 -0
  343. package/UI/Utils/ModelAPI/ModelAPI.ts +7 -1
  344. package/UI/Utils/Platform.ts +149 -0
  345. package/UI/Utils/ProjectInvitationDisplay.ts +118 -0
  346. package/UI/esbuild-config.js +22 -1
  347. package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +187 -0
  348. package/Utils/Monitor/NetworkTopologyUtil.ts +888 -164
  349. package/Utils/Telemetry/CrossSignalScope.ts +502 -0
  350. package/Utils/Telemetry/EntityKey.ts +5 -5
  351. package/Utils/Telemetry/EntityRelationship.ts +1 -1
  352. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +1 -1
  353. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +1 -1
  354. package/build/dist/Models/DatabaseModels/AIConversation.js +32 -0
  355. package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -1
  356. package/build/dist/Models/DatabaseModels/AIConversationMessage.js +42 -0
  357. package/build/dist/Models/DatabaseModels/AIConversationMessage.js.map +1 -1
  358. package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js +28 -0
  359. package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js.map +1 -1
  360. package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js +29 -0
  361. package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js.map +1 -1
  362. package/build/dist/Models/DatabaseModels/Index.js +12 -4
  363. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  364. package/build/dist/Models/DatabaseModels/{TelemetryEntity.js → InventoryItem.js} +167 -31
  365. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -0
  366. package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js +454 -0
  367. package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js.map +1 -0
  368. package/build/dist/Models/DatabaseModels/{TelemetryEntityRelationship.js → InventoryItemRelationship.js} +27 -27
  369. package/build/dist/Models/DatabaseModels/InventoryItemRelationship.js.map +1 -0
  370. package/build/dist/Models/DatabaseModels/NetworkDevice.js +141 -0
  371. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  372. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +719 -0
  373. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -0
  374. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +475 -0
  375. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -0
  376. package/build/dist/Models/DatabaseModels/NetworkTopologySuppression.js +446 -0
  377. package/build/dist/Models/DatabaseModels/NetworkTopologySuppression.js.map +1 -0
  378. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyFeed.js +9 -0
  379. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyFeed.js.map +1 -1
  380. package/build/dist/Models/DatabaseModels/Project.js +80 -0
  381. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  382. package/build/dist/Models/DatabaseModels/UserCall.js +46 -2
  383. package/build/dist/Models/DatabaseModels/UserCall.js.map +1 -1
  384. package/build/dist/Models/DatabaseModels/UserEmail.js +48 -2
  385. package/build/dist/Models/DatabaseModels/UserEmail.js.map +1 -1
  386. package/build/dist/Models/DatabaseModels/UserNotificationRule.js +424 -55
  387. package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
  388. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +24 -2
  389. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
  390. package/build/dist/Models/DatabaseModels/UserPush.js +51 -1
  391. package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
  392. package/build/dist/Models/DatabaseModels/UserSMS.js +47 -2
  393. package/build/dist/Models/DatabaseModels/UserSMS.js.map +1 -1
  394. package/build/dist/Models/DatabaseModels/UserTelegram.js +65 -3
  395. package/build/dist/Models/DatabaseModels/UserTelegram.js.map +1 -1
  396. package/build/dist/Models/DatabaseModels/UserWebhook.js +56 -2
  397. package/build/dist/Models/DatabaseModels/UserWebhook.js.map +1 -1
  398. package/build/dist/Models/DatabaseModels/UserWhatsApp.js +45 -2
  399. package/build/dist/Models/DatabaseModels/UserWhatsApp.js.map +1 -1
  400. package/build/dist/Models/DatabaseModels/WorkflowVariable.js +12 -0
  401. package/build/dist/Models/DatabaseModels/WorkflowVariable.js.map +1 -1
  402. package/build/dist/Server/API/AIChatAPI.js +237 -1
  403. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  404. package/build/dist/Server/API/DashboardAPI.js +165 -13
  405. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  406. package/build/dist/Server/API/OnCallReadinessAPI.js +599 -0
  407. package/build/dist/Server/API/OnCallReadinessAPI.js.map +1 -0
  408. package/build/dist/Server/API/TeamComplianceAPI.js +68 -9
  409. package/build/dist/Server/API/TeamComplianceAPI.js.map +1 -1
  410. package/build/dist/Server/API/TelemetryAPI.js +129 -18
  411. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  412. package/build/dist/Server/EnvironmentConfig.js +45 -0
  413. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  414. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +22 -0
  415. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  416. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js +4 -4
  417. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.js +18 -0
  418. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.js.map +1 -0
  419. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.js +39 -0
  420. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.js.map +1 -0
  421. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.js +38 -0
  422. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.js.map +1 -0
  423. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.js +22 -0
  424. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.js.map +1 -0
  425. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.js +150 -0
  426. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.js.map +1 -0
  427. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.js +57 -0
  428. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.js.map +1 -0
  429. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.js +69 -0
  430. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.js.map +1 -0
  431. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.js +32 -0
  432. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.js.map +1 -0
  433. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.js +26 -0
  434. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.js.map +1 -0
  435. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.js +48 -0
  436. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.js.map +1 -0
  437. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +20 -0
  438. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  439. package/build/dist/Server/Infrastructure/Queue.js +72 -13
  440. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  441. package/build/dist/Server/Middleware/PublicDashboardRateLimit.js +399 -0
  442. package/build/dist/Server/Middleware/PublicDashboardRateLimit.js.map +1 -0
  443. package/build/dist/Server/Services/AIService.js +1 -0
  444. package/build/dist/Server/Services/AIService.js.map +1 -1
  445. package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js +24 -3
  446. package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js.map +1 -1
  447. package/build/dist/Server/Services/AlertSeverityService.js +54 -0
  448. package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
  449. package/build/dist/Server/Services/DashboardService.js +10 -9
  450. package/build/dist/Server/Services/DashboardService.js.map +1 -1
  451. package/build/dist/Server/Services/DatabaseService.js +24 -2
  452. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  453. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +24 -3
  454. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
  455. package/build/dist/Server/Services/IncidentSeverityService.js +67 -0
  456. package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
  457. package/build/dist/Server/Services/Index.js +12 -4
  458. package/build/dist/Server/Services/Index.js.map +1 -1
  459. package/build/dist/Server/Services/InventoryItemCustomFieldService.js +9 -0
  460. package/build/dist/Server/Services/InventoryItemCustomFieldService.js.map +1 -0
  461. package/build/dist/Server/Services/{TelemetryEntityRelationshipService.js → InventoryItemRelationshipService.js} +6 -6
  462. package/build/dist/Server/Services/InventoryItemRelationshipService.js.map +1 -0
  463. package/build/dist/Server/Services/{TelemetryEntityService.js → InventoryItemService.js} +68 -23
  464. package/build/dist/Server/Services/InventoryItemService.js.map +1 -0
  465. package/build/dist/Server/Services/LogAggregationService.js +27 -8
  466. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  467. package/build/dist/Server/Services/MetricAggregationService.js +80 -0
  468. package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
  469. package/build/dist/Server/Services/MetricService.js +6 -6
  470. package/build/dist/Server/Services/MetricService.js.map +1 -1
  471. package/build/dist/Server/Services/NetworkDeviceLinkRuleService.js +9 -0
  472. package/build/dist/Server/Services/NetworkDeviceLinkRuleService.js.map +1 -0
  473. package/build/dist/Server/Services/NetworkDeviceLinkService.js +71 -0
  474. package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -0
  475. package/build/dist/Server/Services/NetworkDeviceService.js +113 -0
  476. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  477. package/build/dist/Server/Services/NetworkSiteService.js +53 -13
  478. package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
  479. package/build/dist/Server/Services/NetworkTopologySuppressionService.js +85 -0
  480. package/build/dist/Server/Services/NetworkTopologySuppressionService.js.map +1 -0
  481. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +48 -32
  482. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  483. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +8 -0
  484. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
  485. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.js +51 -12
  486. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.js.map +1 -1
  487. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +57 -13
  488. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  489. package/build/dist/Server/Services/OnCallNotificationAlertingService.js +548 -0
  490. package/build/dist/Server/Services/OnCallNotificationAlertingService.js.map +1 -0
  491. package/build/dist/Server/Services/OnCallReadinessService.js +1961 -0
  492. package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -0
  493. package/build/dist/Server/Services/OnCallSetupReminderService.js +738 -0
  494. package/build/dist/Server/Services/OnCallSetupReminderService.js.map +1 -0
  495. package/build/dist/Server/Services/ProfileAggregationService.js +68 -4
  496. package/build/dist/Server/Services/ProfileAggregationService.js.map +1 -1
  497. package/build/dist/Server/Services/StatusPageService.js +11 -10
  498. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  499. package/build/dist/Server/Services/TeamComplianceService.js +312 -160
  500. package/build/dist/Server/Services/TeamComplianceService.js.map +1 -1
  501. package/build/dist/Server/Services/UserCallService.js +24 -1
  502. package/build/dist/Server/Services/UserCallService.js.map +1 -1
  503. package/build/dist/Server/Services/UserEmailService.js +24 -1
  504. package/build/dist/Server/Services/UserEmailService.js.map +1 -1
  505. package/build/dist/Server/Services/UserNotificationRuleAdminService.js +858 -0
  506. package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -0
  507. package/build/dist/Server/Services/UserNotificationRuleService.js +2830 -175
  508. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  509. package/build/dist/Server/Services/UserOnCallLogService.js +488 -43
  510. package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
  511. package/build/dist/Server/Services/UserPushService.js +26 -0
  512. package/build/dist/Server/Services/UserPushService.js.map +1 -1
  513. package/build/dist/Server/Services/UserService.js +10 -0
  514. package/build/dist/Server/Services/UserService.js.map +1 -1
  515. package/build/dist/Server/Services/UserSmsService.js +24 -1
  516. package/build/dist/Server/Services/UserSmsService.js.map +1 -1
  517. package/build/dist/Server/Services/UserTelegramService.js +22 -1
  518. package/build/dist/Server/Services/UserTelegramService.js.map +1 -1
  519. package/build/dist/Server/Services/UserWebhookService.js +25 -1
  520. package/build/dist/Server/Services/UserWebhookService.js.map +1 -1
  521. package/build/dist/Server/Services/UserWhatsAppService.js +22 -1
  522. package/build/dist/Server/Services/UserWhatsAppService.js.map +1 -1
  523. package/build/dist/Server/Types/Database/Permissions/BasePermission.js +12 -1
  524. package/build/dist/Server/Types/Database/Permissions/BasePermission.js.map +1 -1
  525. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +126 -0
  526. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
  527. package/build/dist/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.js +254 -0
  528. package/build/dist/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.js.map +1 -0
  529. package/build/dist/Server/Types/Database/Permissions/QueryPermission.js +47 -2
  530. package/build/dist/Server/Types/Database/Permissions/QueryPermission.js.map +1 -1
  531. package/build/dist/Server/Types/Database/Permissions/TenantPermission.js +7 -0
  532. package/build/dist/Server/Types/Database/Permissions/TenantPermission.js.map +1 -1
  533. package/build/dist/Server/Types/Workflow/Components/API/Delete.js +1 -1
  534. package/build/dist/Server/Types/Workflow/Components/API/Delete.js.map +1 -1
  535. package/build/dist/Server/Types/Workflow/Components/API/Get.js +1 -1
  536. package/build/dist/Server/Types/Workflow/Components/API/Get.js.map +1 -1
  537. package/build/dist/Server/Types/Workflow/Components/API/Patch.js +1 -1
  538. package/build/dist/Server/Types/Workflow/Components/API/Patch.js.map +1 -1
  539. package/build/dist/Server/Types/Workflow/Components/API/Post.js +1 -1
  540. package/build/dist/Server/Types/Workflow/Components/API/Post.js.map +1 -1
  541. package/build/dist/Server/Types/Workflow/Components/API/Put.js +1 -1
  542. package/build/dist/Server/Types/Workflow/Components/API/Put.js.map +1 -1
  543. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js +21 -5
  544. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js.map +1 -1
  545. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js +14 -10
  546. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js.map +1 -1
  547. package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js +31 -0
  548. package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js.map +1 -1
  549. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js +3 -9
  550. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
  551. package/build/dist/Server/Types/Workflow/Components/Email.js +15 -4
  552. package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
  553. package/build/dist/Server/Types/Workflow/Components/JavaScript.js +7 -2
  554. package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
  555. package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js +1 -1
  556. package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js.map +1 -1
  557. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +514 -56
  558. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
  559. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +20 -3
  560. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
  561. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +19 -6
  562. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -1
  563. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +7 -0
  564. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  565. package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js +2 -2
  566. package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js.map +1 -1
  567. package/build/dist/Server/Utils/AI/Toolbox/AIMetaTools.js +692 -0
  568. package/build/dist/Server/Utils/AI/Toolbox/AIMetaTools.js.map +1 -0
  569. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +148 -12
  570. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -1
  571. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +157 -10
  572. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -1
  573. package/build/dist/Server/Utils/AI/Toolbox/Index.js +37 -0
  574. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  575. package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js +259 -14
  576. package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js.map +1 -1
  577. package/build/dist/Server/Utils/AI/Toolbox/NoteWriteTools.js +235 -0
  578. package/build/dist/Server/Utils/AI/Toolbox/NoteWriteTools.js.map +1 -0
  579. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +1000 -0
  580. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -0
  581. package/build/dist/Server/Utils/AI/Toolbox/RunbookTools.js +356 -0
  582. package/build/dist/Server/Utils/AI/Toolbox/RunbookTools.js.map +1 -0
  583. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +394 -0
  584. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -0
  585. package/build/dist/Server/Utils/AI/Toolbox/StatusPageTools.js +465 -0
  586. package/build/dist/Server/Utils/AI/Toolbox/StatusPageTools.js.map +1 -0
  587. package/build/dist/Server/Utils/AI/Toolbox/TeamTools.js +280 -0
  588. package/build/dist/Server/Utils/AI/Toolbox/TeamTools.js.map +1 -0
  589. package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js +527 -0
  590. package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js.map +1 -0
  591. package/build/dist/Server/Utils/AI/Toolbox/WorkflowProbeTools.js +548 -0
  592. package/build/dist/Server/Utils/AI/Toolbox/WorkflowProbeTools.js.map +1 -0
  593. package/build/dist/Server/Utils/ClientIp.js +137 -0
  594. package/build/dist/Server/Utils/ClientIp.js.map +1 -0
  595. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +38 -0
  596. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  597. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.js +89 -0
  598. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.js.map +1 -0
  599. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloWidget.js +77 -0
  600. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloWidget.js.map +1 -0
  601. package/build/dist/Server/Utils/Express.js +12 -12
  602. package/build/dist/Server/Utils/Express.js.map +1 -1
  603. package/build/dist/Server/Utils/LLM/LLMService.js +70 -7
  604. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  605. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +121 -11
  606. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  607. package/build/dist/Server/Utils/SSRFProtection.js +82 -21
  608. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  609. package/build/dist/Server/Utils/StartServer.js +12 -4
  610. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  611. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +165 -18
  612. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  613. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +11 -11
  614. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  615. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +122 -47
  616. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  617. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +7 -3
  618. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  619. package/build/dist/Types/AI/AIChatMessageStatus.js +8 -1
  620. package/build/dist/Types/AI/AIChatMessageStatus.js.map +1 -1
  621. package/build/dist/Types/AI/AIChatTypes.js +12 -0
  622. package/build/dist/Types/AI/AIChatTypes.js.map +1 -1
  623. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +60 -0
  624. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -0
  625. package/build/dist/Types/Exception/ExceptionCode.js +2 -0
  626. package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
  627. package/build/dist/Types/Exception/ServiceUnavailableException.js +8 -0
  628. package/build/dist/Types/Exception/ServiceUnavailableException.js.map +1 -0
  629. package/build/dist/Types/Exception/TooManyRequestsException.js +8 -0
  630. package/build/dist/Types/Exception/TooManyRequestsException.js.map +1 -0
  631. package/build/dist/Types/IP/IP.js +87 -43
  632. package/build/dist/Types/IP/IP.js.map +1 -1
  633. package/build/dist/Types/NetworkDevice/NetworkDeviceMonitoringMethod.js +50 -0
  634. package/build/dist/Types/NetworkDevice/NetworkDeviceMonitoringMethod.js.map +1 -0
  635. package/build/dist/Types/OnCallDutyPolicy/Layer.js +186 -123
  636. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  637. package/build/dist/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.js +13 -0
  638. package/build/dist/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.js.map +1 -1
  639. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +105 -11
  640. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
  641. package/build/dist/Types/Permission.js +174 -0
  642. package/build/dist/Types/Permission.js.map +1 -1
  643. package/build/dist/Types/Telemetry/EntityRelationshipType.js +1 -1
  644. package/build/dist/Types/Telemetry/EntitySource.js +1 -1
  645. package/build/dist/Types/Telemetry/EntityType.js +1 -1
  646. package/build/dist/Types/Telemetry/EntityTypeGroups.js +1 -1
  647. package/build/dist/Types/Telemetry/EntityTypeGroups.js.map +1 -1
  648. package/build/dist/Types/Workflow/Components/BaseModel.js +66 -28
  649. package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
  650. package/build/dist/Types/Workflow/Components/Discord.js +1 -0
  651. package/build/dist/Types/Workflow/Components/Discord.js.map +1 -1
  652. package/build/dist/Types/Workflow/Components/Email.js +12 -3
  653. package/build/dist/Types/Workflow/Components/Email.js.map +1 -1
  654. package/build/dist/Types/Workflow/Components/JavaScript.js +7 -0
  655. package/build/dist/Types/Workflow/Components/JavaScript.js.map +1 -1
  656. package/build/dist/Types/Workflow/Components/MicrosoftTeams.js +3 -2
  657. package/build/dist/Types/Workflow/Components/MicrosoftTeams.js.map +1 -1
  658. package/build/dist/Types/Workflow/Components/Slack.js +1 -0
  659. package/build/dist/Types/Workflow/Components/Slack.js.map +1 -1
  660. package/build/dist/Types/Workflow/Components/Telegram.js +1 -0
  661. package/build/dist/Types/Workflow/Components/Telegram.js.map +1 -1
  662. package/build/dist/Types/Workflow/TemplateSyntax.js +12 -0
  663. package/build/dist/Types/Workflow/TemplateSyntax.js.map +1 -1
  664. package/build/dist/Types/Workflow/Templates.js +1931 -38
  665. package/build/dist/Types/Workflow/Templates.js.map +1 -1
  666. package/build/dist/UI/Components/Calendar/Calendar.js +1 -1
  667. package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
  668. package/build/dist/UI/Components/Checkbox/Checkbox.js +1 -1
  669. package/build/dist/UI/Components/Checkbox/Checkbox.js.map +1 -1
  670. package/build/dist/UI/Components/Dictionary/Dictionary.js +25 -11
  671. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  672. package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
  673. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +27 -4
  674. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  675. package/build/dist/UI/Components/Input/Input.js +1 -0
  676. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  677. package/build/dist/UI/Components/KeyboardShortcut/KeyboardKey.js +163 -0
  678. package/build/dist/UI/Components/KeyboardShortcut/KeyboardKey.js.map +1 -0
  679. package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcut.js +47 -0
  680. package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcut.js.map +1 -0
  681. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +4 -4
  682. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  683. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +11 -1
  684. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -1
  685. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js +10 -8
  686. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js.map +1 -1
  687. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +227 -14
  688. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
  689. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +5 -0
  690. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  691. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +69 -5
  692. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
  693. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +8 -0
  694. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  695. package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
  696. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +9 -2
  697. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
  698. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +8 -21
  699. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  700. package/build/dist/UI/Components/ProjectInvitations/PendingProjectInvitations.js +251 -0
  701. package/build/dist/UI/Components/ProjectInvitations/PendingProjectInvitations.js.map +1 -0
  702. package/build/dist/UI/Components/SimpleLogViewer/SimpleLogViewer.js +9 -2
  703. package/build/dist/UI/Components/SimpleLogViewer/SimpleLogViewer.js.map +1 -1
  704. package/build/dist/UI/Components/Table/Table.js +27 -15
  705. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  706. package/build/dist/UI/Components/Table/TableBody.js +24 -18
  707. package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
  708. package/build/dist/UI/Components/Table/TableHeader.js +9 -1
  709. package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
  710. package/build/dist/UI/Components/Table/TableRow.js +29 -21
  711. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  712. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +130 -12
  713. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  714. package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js +10 -1
  715. package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js.map +1 -1
  716. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +4 -4
  717. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
  718. package/build/dist/UI/Components/Workflow/ComponentValuePickerModal.js +57 -7
  719. package/build/dist/UI/Components/Workflow/ComponentValuePickerModal.js.map +1 -1
  720. package/build/dist/UI/Components/Workflow/ComponentsModal.js +53 -18
  721. package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
  722. package/build/dist/UI/Components/Workflow/DocumentationViewer.js +19 -6
  723. package/build/dist/UI/Components/Workflow/DocumentationViewer.js.map +1 -1
  724. package/build/dist/UI/Components/Workflow/GraphLint.js +33 -4
  725. package/build/dist/UI/Components/Workflow/GraphLint.js.map +1 -1
  726. package/build/dist/UI/Components/Workflow/GraphLintSummary.js +231 -0
  727. package/build/dist/UI/Components/Workflow/GraphLintSummary.js.map +1 -0
  728. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +116 -21
  729. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -1
  730. package/build/dist/UI/Components/Workflow/ModelSchema.js +72 -34
  731. package/build/dist/UI/Components/Workflow/ModelSchema.js.map +1 -1
  732. package/build/dist/UI/Components/Workflow/RunStatusWatcher.js +1 -1
  733. package/build/dist/UI/Components/Workflow/RunStatusWatcher.js.map +1 -1
  734. package/build/dist/UI/Components/Workflow/StepTraceViewer.js +1 -1
  735. package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -1
  736. package/build/dist/UI/Components/Workflow/UseRunWatch.js +123 -0
  737. package/build/dist/UI/Components/Workflow/UseRunWatch.js.map +1 -0
  738. package/build/dist/UI/Components/Workflow/VariableModal.js +3 -2
  739. package/build/dist/UI/Components/Workflow/VariableModal.js.map +1 -1
  740. package/build/dist/UI/Components/Workflow/Workflow.js +41 -1
  741. package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
  742. package/build/dist/UI/Components/Workflow/WorkflowIssuesModal.js +99 -0
  743. package/build/dist/UI/Components/Workflow/WorkflowIssuesModal.js.map +1 -0
  744. package/build/dist/UI/Components/Workflow/WorkflowLogModal.js +56 -0
  745. package/build/dist/UI/Components/Workflow/WorkflowLogModal.js.map +1 -0
  746. package/build/dist/UI/Components/Workflow/WorkflowStatusBar.js +92 -0
  747. package/build/dist/UI/Components/Workflow/WorkflowStatusBar.js.map +1 -0
  748. package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js +9 -0
  749. package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js.map +1 -1
  750. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +1 -1
  751. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
  752. package/build/dist/UI/Utils/Platform.js +118 -0
  753. package/build/dist/UI/Utils/Platform.js.map +1 -0
  754. package/build/dist/UI/Utils/ProjectInvitationDisplay.js +106 -0
  755. package/build/dist/UI/Utils/ProjectInvitationDisplay.js.map +1 -0
  756. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +108 -0
  757. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -0
  758. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +642 -136
  759. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  760. package/build/dist/Utils/Telemetry/CrossSignalScope.js +328 -0
  761. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -0
  762. package/build/dist/Utils/Telemetry/EntityKey.js +5 -5
  763. package/build/dist/Utils/Telemetry/EntityRelationship.js +1 -1
  764. package/jest.config.json +1 -0
  765. package/package.json +1 -1
  766. package/build/dist/Models/DatabaseModels/TelemetryEntity.js.map +0 -1
  767. package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js.map +0 -1
  768. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +0 -1
  769. package/build/dist/Server/Services/TelemetryEntityService.js.map +0 -1
@@ -0,0 +1,1818 @@
1
+ import AlertSeverityService from "../../../Server/Services/AlertSeverityService";
2
+ import IncidentSeverityService from "../../../Server/Services/IncidentSeverityService";
3
+ import UserEmailService from "../../../Server/Services/UserEmailService";
4
+ import UserNotificationRuleService from "../../../Server/Services/UserNotificationRuleService";
5
+ import logger from "../../../Server/Utils/Logger";
6
+ import AlertSeverity from "../../../Models/DatabaseModels/AlertSeverity";
7
+ import IncidentSeverity from "../../../Models/DatabaseModels/IncidentSeverity";
8
+ import UserEmail from "../../../Models/DatabaseModels/UserEmail";
9
+ import UserNotificationRule from "../../../Models/DatabaseModels/UserNotificationRule";
10
+ import LIMIT_MAX from "../../../Types/Database/LimitMax";
11
+ import NotificationRuleType from "../../../Types/NotificationRule/NotificationRuleType";
12
+ import ObjectID from "../../../Types/ObjectID";
13
+ import { EVERY_FIVE_MINUTE } from "../../../Utils/CronTime";
14
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
15
+
16
+ /*
17
+ * GAP A, the worker half.
18
+ *
19
+ * A severity created after a responder joined was a severity that responder had
20
+ * no notification rule for. Default rules are written exactly twice in a
21
+ * responder's life - when they join a project and when they verify a
22
+ * notification method - and both paths iterate the severities that exist AT
23
+ * THAT MOMENT. Nothing revisited the question, so the "Sev4" somebody adds a
24
+ * year in paged precisely nobody: every incident on it counted zero matching
25
+ * rules and dropped into an execution log no one reads.
26
+ *
27
+ * Phase 1 closed that with a worker job,
28
+ * App/FeatureSet/Workers/Jobs/OnCallDutyPolicy/BackfillNotificationRulesForNewSeverities.ts,
29
+ * enqueued by name from IncidentSeverityService.onCreateSuccess and
30
+ * AlertSeverityService.onCreateSuccess and also swept on a five-minute
31
+ * schedule over severities created in the last hour.
32
+ *
33
+ * The neighbouring SeverityCreationRuleBackfill.test.ts covers the ENQUEUE:
34
+ * that creating a severity puts the job on the Worker queue and writes nothing
35
+ * inline. This file covers THE JOB ITSELF, through the three functions it
36
+ * exports for exactly this purpose - findRecentlyCreatedSeverities,
37
+ * buildResponderIntents and backfillSeverity - rather than through RunCron.
38
+ *
39
+ * Four properties are load-bearing, and everything here serves one of them:
40
+ *
41
+ * 1. IT MIRRORS INTENT, IT DOES NOT IMPOSE A DEFAULT. Someone who set
42
+ * "Sev1 -> call me immediately, Sev3 -> email me after fifteen minutes"
43
+ * gets one new rule per distinct (method, delay) pair they already chose
44
+ * for that rule type. Not a hardcoded email-at-zero, which would page
45
+ * instantly a responder who deliberately built a delay into every rule
46
+ * they own; and above all not a phone call for someone who has only ever
47
+ * used email. Mirroring can only ever hand a responder a channel they
48
+ * already opted into. A responder who muted the rule type entirely gets
49
+ * the mute mirrored, not a page.
50
+ *
51
+ * 2. IT IS IDEMPOTENT. The job runs from two routes at once - the by-name
52
+ * enqueue fires seconds after a severity is created, and a scheduled sweep
53
+ * may already be part-way through the same severity - and
54
+ * UserNotificationRule carries no unique index over
55
+ * (project, user, ruleType, severity, method). A duplicated rule is a
56
+ * duplicated page. Two guards are pinned separately below: the per-run
57
+ * snapshot, and the per-row read taken immediately before each write that
58
+ * catches the case where the snapshot was already stale.
59
+ *
60
+ * 3. IT DOES NOT LOAD A PROJECT PER RESPONDER. Reads are one project-wide
61
+ * findAllBy per rule type - which pages in LIMIT_MAX batches under the
62
+ * hood - not one query per responder. A thousand-responder project must
63
+ * not issue a thousand queries per severity.
64
+ *
65
+ * 4. ONE RESPONDER'S FAILURE DOES NOT TAKE THE REST DOWN. A method row that
66
+ * vanishes mid-run is a log line, not an aborted backfill for everybody
67
+ * else in the project.
68
+ *
69
+ * No database is touched. UserNotificationRule is backed by an in-memory table
70
+ * so that rows written by one run are genuinely visible to the next - the
71
+ * idempotence assertions are only worth anything if the reader really reads
72
+ * what the writer really wrote.
73
+ */
74
+
75
+ /*
76
+ * The job registers itself with RunCron at import time, which would otherwise
77
+ * reach for the Worker queue (and therefore Redis) as a side effect of
78
+ * importing this file. Mocked before the job module is imported, both to keep
79
+ * the import inert and to capture the registration itself.
80
+ */
81
+ jest.mock("../../../../App/FeatureSet/Workers/Utils/Cron", () => {
82
+ return {
83
+ __esModule: true,
84
+ default: jest.fn(),
85
+ };
86
+ });
87
+
88
+ // Imported AFTER the mock above so the registration lands in it.
89
+ import RunCron from "../../../../App/FeatureSet/Workers/Utils/Cron";
90
+ import {
91
+ JOB_NAME,
92
+ backfillSeverity,
93
+ buildResponderIntents,
94
+ findRecentlyCreatedSeverities,
95
+ MirroredRule,
96
+ NewSeverity,
97
+ ResponderIntent,
98
+ } from "../../../../App/FeatureSet/Workers/Jobs/OnCallDutyPolicy/BackfillNotificationRulesForNewSeverities";
99
+
100
+ /*
101
+ * The third copy of the job name. IncidentSeverityService and
102
+ * AlertSeverityService each duplicate this string deliberately (Common cannot
103
+ * import from App), and SeverityCreationRuleBackfill.test.ts pins their copies.
104
+ * This is the job's own, so a rename that misses any of the three surfaces.
105
+ */
106
+ const EXPECTED_JOB_NAME: string =
107
+ "OnCallDutyPolicy:BackfillNotificationRulesForNewSeverities";
108
+
109
+ const PROJECT_ID: ObjectID = new ObjectID("project-1");
110
+
111
+ const USER_A: ObjectID = new ObjectID("user-a");
112
+ const USER_B: ObjectID = new ObjectID("user-b");
113
+ const USER_C: ObjectID = new ObjectID("user-c");
114
+
115
+ const EMAIL_A: ObjectID = new ObjectID("user-email-a");
116
+ const EMAIL_A_SECOND: ObjectID = new ObjectID("user-email-a-second");
117
+ const EMAIL_B: ObjectID = new ObjectID("user-email-b");
118
+ const EMAIL_C: ObjectID = new ObjectID("user-email-c");
119
+
120
+ const CALL_A: ObjectID = new ObjectID("user-call-a");
121
+ const SMS_A: ObjectID = new ObjectID("user-sms-a");
122
+ const PUSH_A: ObjectID = new ObjectID("user-push-a");
123
+
124
+ // The severities that existed when the responder configured their rules...
125
+ const SEV_1: ObjectID = new ObjectID("incident-severity-1");
126
+ const SEV_2: ObjectID = new ObjectID("incident-severity-2");
127
+ const SEV_3: ObjectID = new ObjectID("incident-severity-3");
128
+ // ...and the one added a year later, which nothing covered.
129
+ const SEV_NEW: ObjectID = new ObjectID("incident-severity-4-added-later");
130
+
131
+ const ALERT_SEV_1: ObjectID = new ObjectID("alert-severity-1");
132
+ const ALERT_SEV_NEW: ObjectID = new ObjectID("alert-severity-4-added-later");
133
+
134
+ const INCIDENT_RULE_TYPES: Array<NotificationRuleType> = [
135
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
136
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
137
+ ];
138
+
139
+ const ALERT_RULE_TYPES: Array<NotificationRuleType> = [
140
+ NotificationRuleType.ON_CALL_EXECUTED_ALERT,
141
+ NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
142
+ ];
143
+
144
+ type RuleMethodColumn =
145
+ | "userEmailId"
146
+ | "userSmsId"
147
+ | "userCallId"
148
+ | "userPushId"
149
+ | "userWhatsAppId"
150
+ | "userTelegramId"
151
+ | "userWebhookId";
152
+
153
+ type RuleIdColumn =
154
+ | RuleMethodColumn
155
+ | "projectId"
156
+ | "userId"
157
+ | "incidentSeverityId"
158
+ | "alertSeverityId";
159
+
160
+ const METHOD_COLUMNS: Array<RuleMethodColumn> = [
161
+ "userEmailId",
162
+ "userSmsId",
163
+ "userCallId",
164
+ "userPushId",
165
+ "userWhatsAppId",
166
+ "userTelegramId",
167
+ "userWebhookId",
168
+ ];
169
+
170
+ const ID_COLUMNS: Array<RuleIdColumn> = [
171
+ "projectId",
172
+ "userId",
173
+ "incidentSeverityId",
174
+ "alertSeverityId",
175
+ ...METHOD_COLUMNS,
176
+ ];
177
+
178
+ /*
179
+ * ------------------------------------------------------------------------- *
180
+ * An in-memory stand-in for the UserNotificationRule table.
181
+ *
182
+ * Rows are real model instances, because that is what the job reads: it asks
183
+ * rules for `.userId`, `.isOptOut`, `.notifyAfterMinutes` and the seven method
184
+ * foreign keys, and hands `create` a model it built itself.
185
+ * -------------------------------------------------------------------------
186
+ */
187
+
188
+ let ruleStore: Array<UserNotificationRule> = [];
189
+
190
+ // Only the rows THIS run wrote - the whole point of the idempotence assertions.
191
+ let createdRules: Array<UserNotificationRule> = [];
192
+
193
+ /*
194
+ * When set, findAllBy reports this instead of the live table.
195
+ *
196
+ * That is how an OVERLAPPING run is expressed: the job's per-rule-type snapshot
197
+ * is taken once at the top of a run, so a second run that started before the
198
+ * first one wrote anything is a run whose snapshot no longer matches the table.
199
+ * findOneBy always reads the live table, which is precisely the guard under
200
+ * test.
201
+ */
202
+ let frozenSnapshot: Array<UserNotificationRule> | null = null;
203
+
204
+ // The verified email rows the project has, in the order the query returns them.
205
+ let verifiedEmails: Array<{ userId: ObjectID; emailId: ObjectID }> = [];
206
+
207
+ let ruleFindAllBySpy: jest.SpyInstance;
208
+ let ruleFindOneBySpy: jest.SpyInstance;
209
+ let ruleCreateSpy: jest.SpyInstance;
210
+ let userEmailFindAllBySpy: jest.SpyInstance;
211
+ let loggerErrorSpy: jest.SpyInstance;
212
+
213
+ function idOf(value: unknown): string | undefined {
214
+ if (value === undefined || value === null) {
215
+ return undefined;
216
+ }
217
+
218
+ return String(value);
219
+ }
220
+
221
+ interface RuleSpec {
222
+ userId: ObjectID;
223
+ ruleType: NotificationRuleType;
224
+ projectId?: ObjectID | undefined;
225
+ incidentSeverityId?: ObjectID | undefined;
226
+ alertSeverityId?: ObjectID | undefined;
227
+ notifyAfterMinutes?: number | undefined;
228
+ userEmailId?: ObjectID | undefined;
229
+ userSmsId?: ObjectID | undefined;
230
+ userCallId?: ObjectID | undefined;
231
+ userPushId?: ObjectID | undefined;
232
+ isOptOut?: boolean | undefined;
233
+ }
234
+
235
+ let seedCounter: number = 0;
236
+
237
+ /*
238
+ * A rule row as the responder (or an earlier default-rule pass) left it.
239
+ * Columns are only set when the spec names them, so "no severity id at all" and
240
+ * "no delay at all" - both of which really occur in this table - stay
241
+ * expressible.
242
+ */
243
+ function makeRule(spec: RuleSpec): UserNotificationRule {
244
+ seedCounter++;
245
+
246
+ const rule: UserNotificationRule = new UserNotificationRule();
247
+ rule._id = `seeded-rule-${seedCounter}`;
248
+ rule.projectId = spec.projectId || PROJECT_ID;
249
+ rule.userId = spec.userId;
250
+ rule.ruleType = spec.ruleType;
251
+
252
+ if (spec.incidentSeverityId) {
253
+ rule.incidentSeverityId = spec.incidentSeverityId;
254
+ }
255
+
256
+ if (spec.alertSeverityId) {
257
+ rule.alertSeverityId = spec.alertSeverityId;
258
+ }
259
+
260
+ if (spec.notifyAfterMinutes !== undefined) {
261
+ rule.notifyAfterMinutes = spec.notifyAfterMinutes;
262
+ }
263
+
264
+ if (spec.userEmailId) {
265
+ rule.userEmailId = spec.userEmailId;
266
+ }
267
+
268
+ if (spec.userSmsId) {
269
+ rule.userSmsId = spec.userSmsId;
270
+ }
271
+
272
+ if (spec.userCallId) {
273
+ rule.userCallId = spec.userCallId;
274
+ }
275
+
276
+ if (spec.userPushId) {
277
+ rule.userPushId = spec.userPushId;
278
+ }
279
+
280
+ if (spec.isOptOut !== undefined) {
281
+ rule.isOptOut = spec.isOptOut;
282
+ }
283
+
284
+ return rule;
285
+ }
286
+
287
+ function seedRule(spec: RuleSpec): UserNotificationRule {
288
+ const rule: UserNotificationRule = makeRule(spec);
289
+ ruleStore.push(rule);
290
+
291
+ return rule;
292
+ }
293
+
294
+ /*
295
+ * Match a stored row against a service query. A column the query does not carry
296
+ * is not a constraint, which mirrors how the real queries are built: the
297
+ * per-rule-type read names only projectId and ruleType, and the pre-write
298
+ * existence check names exactly one method column.
299
+ */
300
+ function ruleMatchesQuery(
301
+ rule: UserNotificationRule,
302
+ query: Record<string, unknown>,
303
+ ): boolean {
304
+ for (const column of ID_COLUMNS) {
305
+ const expected: string | undefined = idOf(query[column]);
306
+
307
+ if (expected !== undefined && idOf(rule[column]) !== expected) {
308
+ return false;
309
+ }
310
+ }
311
+
312
+ const expectedRuleType: unknown = query["ruleType"];
313
+
314
+ if (expectedRuleType !== undefined && rule.ruleType !== expectedRuleType) {
315
+ return false;
316
+ }
317
+
318
+ const expectedDelay: unknown = query["notifyAfterMinutes"];
319
+
320
+ if (
321
+ expectedDelay !== undefined &&
322
+ rule.notifyAfterMinutes !== expectedDelay
323
+ ) {
324
+ return false;
325
+ }
326
+
327
+ /*
328
+ * Compared as a boolean because an ordinary rule leaves the column undefined
329
+ * rather than false, while the opt-out existence check asks for `true`.
330
+ */
331
+ const expectedOptOut: unknown = query["isOptOut"];
332
+
333
+ if (
334
+ expectedOptOut !== undefined &&
335
+ Boolean(rule.isOptOut) !== expectedOptOut
336
+ ) {
337
+ return false;
338
+ }
339
+
340
+ return true;
341
+ }
342
+
343
+ function matchingRules(
344
+ source: Array<UserNotificationRule>,
345
+ query: Record<string, unknown>,
346
+ ): Array<UserNotificationRule> {
347
+ return source.filter((rule: UserNotificationRule): boolean => {
348
+ return ruleMatchesQuery(rule, query);
349
+ });
350
+ }
351
+
352
+ /* "userCallId:user-call-a", or "none" for an opt-out row. */
353
+ function methodOf(rule: UserNotificationRule): string {
354
+ for (const column of METHOD_COLUMNS) {
355
+ const value: ObjectID | undefined = rule[column];
356
+
357
+ if (value) {
358
+ return `${column}:${value.toString()}`;
359
+ }
360
+ }
361
+
362
+ return "none";
363
+ }
364
+
365
+ /* The whole of what a rule says, in one comparable string: "how@when". */
366
+ function shapeOf(rule: UserNotificationRule): string {
367
+ return `${methodOf(rule)}@${String(rule.notifyAfterMinutes)}`;
368
+ }
369
+
370
+ function createdOfType(
371
+ ruleType: NotificationRuleType,
372
+ ): Array<UserNotificationRule> {
373
+ return createdRules.filter((rule: UserNotificationRule): boolean => {
374
+ return rule.ruleType === ruleType;
375
+ });
376
+ }
377
+
378
+ function shapesCreatedFor(ruleType: NotificationRuleType): Array<string> {
379
+ return createdOfType(ruleType).map((rule: UserNotificationRule): string => {
380
+ return shapeOf(rule);
381
+ });
382
+ }
383
+
384
+ function incidentSeverity(id: ObjectID): NewSeverity {
385
+ return {
386
+ id: id,
387
+ projectId: PROJECT_ID,
388
+ kind: "incident",
389
+ };
390
+ }
391
+
392
+ function alertSeverity(id: ObjectID): NewSeverity {
393
+ return {
394
+ id: id,
395
+ projectId: PROJECT_ID,
396
+ kind: "alert",
397
+ };
398
+ }
399
+
400
+ function incidentSeverityRow(
401
+ id: ObjectID | null,
402
+ projectId: ObjectID | null,
403
+ ): IncidentSeverity {
404
+ const severity: IncidentSeverity = new IncidentSeverity();
405
+
406
+ if (id) {
407
+ severity._id = id.toString();
408
+ }
409
+
410
+ if (projectId) {
411
+ severity.projectId = projectId;
412
+ }
413
+
414
+ return severity;
415
+ }
416
+
417
+ function alertSeverityRow(
418
+ id: ObjectID | null,
419
+ projectId: ObjectID | null,
420
+ ): AlertSeverity {
421
+ const severity: AlertSeverity = new AlertSeverity();
422
+
423
+ if (id) {
424
+ severity._id = id.toString();
425
+ }
426
+
427
+ if (projectId) {
428
+ severity.projectId = projectId;
429
+ }
430
+
431
+ return severity;
432
+ }
433
+
434
+ function userEmailRow(userId: ObjectID, emailId: ObjectID): UserEmail {
435
+ const model: UserEmail = new UserEmail();
436
+ model._id = emailId.toString();
437
+ model.userId = userId;
438
+
439
+ return model;
440
+ }
441
+
442
+ function queryOf(
443
+ spy: jest.SpyInstance,
444
+ callIndex: number,
445
+ ): Record<string, unknown> {
446
+ return (spy.mock.calls[callIndex]![0] as { query: Record<string, unknown> })
447
+ .query;
448
+ }
449
+
450
+ function propsOf(
451
+ spy: jest.SpyInstance,
452
+ callIndex: number,
453
+ ): { isRoot?: boolean | undefined } {
454
+ return (
455
+ spy.mock.calls[callIndex]![0] as {
456
+ props: { isRoot?: boolean | undefined };
457
+ }
458
+ ).props;
459
+ }
460
+
461
+ beforeEach(() => {
462
+ ruleStore = [];
463
+ createdRules = [];
464
+ frozenSnapshot = null;
465
+ verifiedEmails = [];
466
+ seedCounter = 0;
467
+
468
+ ruleFindAllBySpy = jest
469
+ .spyOn(UserNotificationRuleService, "findAllBy")
470
+ .mockImplementation(((data: {
471
+ query: Record<string, unknown>;
472
+ }): Promise<Array<UserNotificationRule>> => {
473
+ return Promise.resolve(
474
+ matchingRules(frozenSnapshot || ruleStore, data.query),
475
+ );
476
+ }) as never);
477
+
478
+ ruleFindOneBySpy = jest
479
+ .spyOn(UserNotificationRuleService, "findOneBy")
480
+ .mockImplementation(((data: {
481
+ query: Record<string, unknown>;
482
+ }): Promise<UserNotificationRule | null> => {
483
+ return Promise.resolve(matchingRules(ruleStore, data.query)[0] || null);
484
+ }) as never);
485
+
486
+ ruleCreateSpy = jest
487
+ .spyOn(UserNotificationRuleService, "create")
488
+ .mockImplementation(((data: {
489
+ data: UserNotificationRule;
490
+ }): Promise<UserNotificationRule> => {
491
+ const rule: UserNotificationRule = data.data;
492
+ rule._id = `created-rule-${createdRules.length + 1}`;
493
+
494
+ ruleStore.push(rule);
495
+ createdRules.push(rule);
496
+
497
+ return Promise.resolve(rule);
498
+ }) as never);
499
+
500
+ userEmailFindAllBySpy = jest
501
+ .spyOn(UserEmailService, "findAllBy")
502
+ .mockImplementation(((): Promise<Array<UserEmail>> => {
503
+ return Promise.resolve(
504
+ verifiedEmails.map(
505
+ (entry: { userId: ObjectID; emailId: ObjectID }): UserEmail => {
506
+ return userEmailRow(entry.userId, entry.emailId);
507
+ },
508
+ ),
509
+ );
510
+ }) as never);
511
+
512
+ // The job logs a failed row rather than throwing; keep the output clean.
513
+ loggerErrorSpy = jest.spyOn(logger, "error").mockImplementation((): void => {
514
+ return undefined;
515
+ });
516
+ });
517
+
518
+ afterEach(() => {
519
+ jest.restoreAllMocks();
520
+ });
521
+
522
+ /*
523
+ * ========================================================================= *
524
+ * The registration itself.
525
+ * =========================================================================
526
+ */
527
+
528
+ describe("the sweep is registered under the name both severity services enqueue", () => {
529
+ test("the job name is the exact string IncidentSeverityService and AlertSeverityService duplicate", () => {
530
+ expect(JOB_NAME).toBe(EXPECTED_JOB_NAME);
531
+ });
532
+
533
+ /*
534
+ * The by-name enqueue is best-effort: if Redis is unreachable when a severity
535
+ * is created, the enqueue is swallowed and coverage depends entirely on this
536
+ * schedule existing. runOnStartup stays false because a worker restart loop
537
+ * must not re-run the sweep on every boot.
538
+ */
539
+ test("it registers a five-minute schedule that does not run on startup", () => {
540
+ const runCronMock: jest.Mock = RunCron as unknown as jest.Mock;
541
+
542
+ expect(runCronMock).toHaveBeenCalledTimes(1);
543
+
544
+ const call: Array<unknown> = runCronMock.mock.calls[0]!;
545
+ expect(call[0]).toBe(EXPECTED_JOB_NAME);
546
+
547
+ const options: { schedule: string; runOnStartup: boolean } = call[1] as {
548
+ schedule: string;
549
+ runOnStartup: boolean;
550
+ };
551
+
552
+ expect(options.schedule).toBe(EVERY_FIVE_MINUTE);
553
+ expect(options.runOnStartup).toBe(false);
554
+ });
555
+ });
556
+
557
+ /*
558
+ * ========================================================================= *
559
+ * findRecentlyCreatedSeverities - the sweep re-derives its own work set.
560
+ *
561
+ * The Worker queue dispatches on job NAME and hands the job function no
562
+ * payload, so the job cannot be told which severity to cover; it has to ask.
563
+ * Asking over a window wider than the schedule is what makes a lost enqueue
564
+ * cost minutes of latency instead of a permanently uncovered severity.
565
+ * =========================================================================
566
+ */
567
+
568
+ describe("findRecentlyCreatedSeverities", () => {
569
+ let incidentFindAllBySpy: jest.SpyInstance;
570
+ let alertFindAllBySpy: jest.SpyInstance;
571
+
572
+ function stubSeverityTables(
573
+ incidentRows: Array<IncidentSeverity>,
574
+ alertRows: Array<AlertSeverity>,
575
+ ): void {
576
+ incidentFindAllBySpy = jest
577
+ .spyOn(IncidentSeverityService, "findAllBy")
578
+ .mockResolvedValue(incidentRows as never);
579
+ alertFindAllBySpy = jest
580
+ .spyOn(AlertSeverityService, "findAllBy")
581
+ .mockResolvedValue(alertRows as never);
582
+ }
583
+
584
+ /*
585
+ * QueryHelper.greaterThanEqualTo builds a TypeORM Raw operator carrying its
586
+ * bound as a bound parameter. Reading it back is the only way to assert the
587
+ * window is a window at all rather than "every severity ever created".
588
+ */
589
+ function lookbackInMinutes(query: Record<string, unknown>): number {
590
+ const operator: { objectLiteralParameters?: Record<string, unknown> } =
591
+ query["createdAt"] as unknown as {
592
+ objectLiteralParameters?: Record<string, unknown>;
593
+ };
594
+
595
+ const parameters: Record<string, unknown> =
596
+ operator.objectLiteralParameters || {};
597
+ const values: Array<unknown> = Object.values(parameters);
598
+
599
+ expect(values).toHaveLength(1);
600
+
601
+ const bound: Date = values[0] as Date;
602
+
603
+ return (Date.now() - bound.getTime()) / (60 * 1000);
604
+ }
605
+
606
+ test("it reads both severity tables, root-privileged, selecting only what it needs", async () => {
607
+ stubSeverityTables([], []);
608
+
609
+ await findRecentlyCreatedSeverities();
610
+
611
+ expect(incidentFindAllBySpy).toHaveBeenCalledTimes(1);
612
+ expect(alertFindAllBySpy).toHaveBeenCalledTimes(1);
613
+
614
+ expect(propsOf(incidentFindAllBySpy, 0).isRoot).toBe(true);
615
+ expect(propsOf(alertFindAllBySpy, 0).isRoot).toBe(true);
616
+
617
+ const select: Record<string, unknown> = (
618
+ incidentFindAllBySpy.mock.calls[0]![0] as {
619
+ select: Record<string, unknown>;
620
+ }
621
+ ).select;
622
+
623
+ expect(select["_id"]).toBe(true);
624
+ expect(select["projectId"]).toBe(true);
625
+ });
626
+
627
+ test("the window is an hour wide - comfortably longer than the five-minute schedule", async () => {
628
+ stubSeverityTables([], []);
629
+
630
+ await findRecentlyCreatedSeverities();
631
+
632
+ expect(lookbackInMinutes(queryOf(incidentFindAllBySpy, 0))).toBeGreaterThan(
633
+ 59,
634
+ );
635
+ expect(lookbackInMinutes(queryOf(incidentFindAllBySpy, 0))).toBeLessThan(
636
+ 61,
637
+ );
638
+ expect(lookbackInMinutes(queryOf(alertFindAllBySpy, 0))).toBeGreaterThan(
639
+ 59,
640
+ );
641
+ expect(lookbackInMinutes(queryOf(alertFindAllBySpy, 0))).toBeLessThan(61);
642
+ });
643
+
644
+ test("a quiet project yields no work at all - severity creation is rare", async () => {
645
+ stubSeverityTables([], []);
646
+
647
+ await expect(findRecentlyCreatedSeverities()).resolves.toEqual([]);
648
+ });
649
+
650
+ /*
651
+ * The kind tag is what tells backfillSeverity which of the two severity
652
+ * columns to write and which pair of rule types to cover, so a row from the
653
+ * wrong table would silently write a rule that can never match.
654
+ */
655
+ test("each severity is tagged with the table it came from", async () => {
656
+ stubSeverityTables(
657
+ [incidentSeverityRow(SEV_NEW, PROJECT_ID)],
658
+ [alertSeverityRow(ALERT_SEV_NEW, PROJECT_ID)],
659
+ );
660
+
661
+ const severities: Array<NewSeverity> =
662
+ await findRecentlyCreatedSeverities();
663
+
664
+ expect(severities).toHaveLength(2);
665
+
666
+ const incident: NewSeverity | undefined = severities.find(
667
+ (severity: NewSeverity): boolean => {
668
+ return severity.kind === "incident";
669
+ },
670
+ );
671
+ const alert: NewSeverity | undefined = severities.find(
672
+ (severity: NewSeverity): boolean => {
673
+ return severity.kind === "alert";
674
+ },
675
+ );
676
+
677
+ expect(incident!.id.toString()).toBe(SEV_NEW.toString());
678
+ expect(incident!.projectId.toString()).toBe(PROJECT_ID.toString());
679
+ expect(alert!.id.toString()).toBe(ALERT_SEV_NEW.toString());
680
+ expect(alert!.projectId.toString()).toBe(PROJECT_ID.toString());
681
+ });
682
+
683
+ test("a row with no id or no project is skipped rather than backfilled against nothing", async () => {
684
+ stubSeverityTables(
685
+ [
686
+ incidentSeverityRow(null, PROJECT_ID),
687
+ incidentSeverityRow(SEV_NEW, null),
688
+ incidentSeverityRow(SEV_1, PROJECT_ID),
689
+ ],
690
+ [alertSeverityRow(null, null)],
691
+ );
692
+
693
+ const severities: Array<NewSeverity> =
694
+ await findRecentlyCreatedSeverities();
695
+
696
+ expect(severities).toHaveLength(1);
697
+ expect(severities[0]!.id.toString()).toBe(SEV_1.toString());
698
+ });
699
+ });
700
+
701
+ /*
702
+ * ========================================================================= *
703
+ * buildResponderIntents - "what would this person want for a new severity?"
704
+ *
705
+ * The answer is: the same channels, at the same delays, that they already chose
706
+ * for this rule type on the severities they did configure.
707
+ * =========================================================================
708
+ */
709
+
710
+ describe("buildResponderIntents", () => {
711
+ function intentsFor(
712
+ rules: Array<UserNotificationRule>,
713
+ severity: NewSeverity,
714
+ ): Map<string, ResponderIntent> {
715
+ return buildResponderIntents(rules, severity);
716
+ }
717
+
718
+ function mirroredOf(intent: ResponderIntent): Array<string> {
719
+ return Array.from(intent.mirroredRules.values()).map(
720
+ (mirrored: MirroredRule): string => {
721
+ return `${mirrored.methodColumn}:${mirrored.methodId.toString()}@${String(
722
+ mirrored.notifyAfterMinutes,
723
+ )}`;
724
+ },
725
+ );
726
+ }
727
+
728
+ test("the same (method, delay) used on three severities is one distinct pair", () => {
729
+ const intents: Map<string, ResponderIntent> = intentsFor(
730
+ [
731
+ makeRule({
732
+ userId: USER_A,
733
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
734
+ incidentSeverityId: SEV_1,
735
+ userEmailId: EMAIL_A,
736
+ notifyAfterMinutes: 0,
737
+ }),
738
+ makeRule({
739
+ userId: USER_A,
740
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
741
+ incidentSeverityId: SEV_2,
742
+ userEmailId: EMAIL_A,
743
+ notifyAfterMinutes: 0,
744
+ }),
745
+ makeRule({
746
+ userId: USER_A,
747
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
748
+ incidentSeverityId: SEV_3,
749
+ userEmailId: EMAIL_A,
750
+ notifyAfterMinutes: 0,
751
+ }),
752
+ ],
753
+ incidentSeverity(SEV_NEW),
754
+ );
755
+
756
+ expect(mirroredOf(intents.get(USER_A.toString())!)).toEqual([
757
+ `userEmailId:${EMAIL_A.toString()}@0`,
758
+ ]);
759
+ });
760
+
761
+ /*
762
+ * The delay is part of the key on purpose. "Email me now" and "email me in
763
+ * fifteen minutes" are two different requests, and a key that ignored the
764
+ * delay would collapse them into one.
765
+ */
766
+ test("the same method at two delays is two distinct pairs", () => {
767
+ const intents: Map<string, ResponderIntent> = intentsFor(
768
+ [
769
+ makeRule({
770
+ userId: USER_A,
771
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
772
+ incidentSeverityId: SEV_1,
773
+ userEmailId: EMAIL_A,
774
+ notifyAfterMinutes: 0,
775
+ }),
776
+ makeRule({
777
+ userId: USER_A,
778
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
779
+ incidentSeverityId: SEV_1,
780
+ userEmailId: EMAIL_A,
781
+ notifyAfterMinutes: 15,
782
+ }),
783
+ ],
784
+ incidentSeverity(SEV_NEW),
785
+ );
786
+
787
+ expect(mirroredOf(intents.get(USER_A.toString())!).sort()).toEqual([
788
+ `userEmailId:${EMAIL_A.toString()}@0`,
789
+ `userEmailId:${EMAIL_A.toString()}@15`,
790
+ ]);
791
+ });
792
+
793
+ test("two different channels at the same delay do not collide", () => {
794
+ const intents: Map<string, ResponderIntent> = intentsFor(
795
+ [
796
+ makeRule({
797
+ userId: USER_A,
798
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
799
+ incidentSeverityId: SEV_1,
800
+ userEmailId: EMAIL_A,
801
+ notifyAfterMinutes: 0,
802
+ }),
803
+ makeRule({
804
+ userId: USER_A,
805
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
806
+ incidentSeverityId: SEV_1,
807
+ userCallId: CALL_A,
808
+ notifyAfterMinutes: 0,
809
+ }),
810
+ ],
811
+ incidentSeverity(SEV_NEW),
812
+ );
813
+
814
+ expect(mirroredOf(intents.get(USER_A.toString())!)).toHaveLength(2);
815
+ });
816
+
817
+ test("responders are kept apart - one person's channels never leak into another's", () => {
818
+ const intents: Map<string, ResponderIntent> = intentsFor(
819
+ [
820
+ makeRule({
821
+ userId: USER_A,
822
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
823
+ incidentSeverityId: SEV_1,
824
+ userCallId: CALL_A,
825
+ notifyAfterMinutes: 0,
826
+ }),
827
+ makeRule({
828
+ userId: USER_B,
829
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
830
+ incidentSeverityId: SEV_1,
831
+ userEmailId: EMAIL_B,
832
+ notifyAfterMinutes: 30,
833
+ }),
834
+ ],
835
+ incidentSeverity(SEV_NEW),
836
+ );
837
+
838
+ expect(intents.size).toBe(2);
839
+ expect(mirroredOf(intents.get(USER_A.toString())!)).toEqual([
840
+ `userCallId:${CALL_A.toString()}@0`,
841
+ ]);
842
+ expect(mirroredOf(intents.get(USER_B.toString())!)).toEqual([
843
+ `userEmailId:${EMAIL_B.toString()}@30`,
844
+ ]);
845
+ });
846
+
847
+ /*
848
+ * A row for the new severity is the idempotency guard, not input: it means an
849
+ * earlier run - or the responder - already covered this cell, and mirroring
850
+ * it back onto itself would be the duplicate the whole design exists to
851
+ * avoid.
852
+ */
853
+ test("a row for the new severity marks the cell covered and is not itself mirrored", () => {
854
+ const intents: Map<string, ResponderIntent> = intentsFor(
855
+ [
856
+ makeRule({
857
+ userId: USER_A,
858
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
859
+ incidentSeverityId: SEV_NEW,
860
+ userEmailId: EMAIL_A,
861
+ notifyAfterMinutes: 0,
862
+ }),
863
+ ],
864
+ incidentSeverity(SEV_NEW),
865
+ );
866
+
867
+ const intent: ResponderIntent = intents.get(USER_A.toString())!;
868
+
869
+ expect(intent.hasRuleForNewSeverity).toBe(true);
870
+ expect(mirroredOf(intent)).toEqual([]);
871
+ });
872
+
873
+ test("an opt-out row is recorded as an opt-out and contributes no channel", () => {
874
+ const intents: Map<string, ResponderIntent> = intentsFor(
875
+ [
876
+ makeRule({
877
+ userId: USER_A,
878
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
879
+ incidentSeverityId: SEV_1,
880
+ isOptOut: true,
881
+ notifyAfterMinutes: 0,
882
+ }),
883
+ ],
884
+ incidentSeverity(SEV_NEW),
885
+ );
886
+
887
+ const intent: ResponderIntent = intents.get(USER_A.toString())!;
888
+
889
+ expect(intent.hasOptOut).toBe(true);
890
+ expect(intent.hasRuleForNewSeverity).toBe(false);
891
+ expect(mirroredOf(intent)).toEqual([]);
892
+ });
893
+
894
+ /*
895
+ * Episode default rules were written without a severity id for a long time
896
+ * (Gap G). Those rows are just as much a statement of "this is how I want to
897
+ * be told" as a severity-scoped one.
898
+ */
899
+ test("a row with no severity at all still counts as intent", () => {
900
+ const intents: Map<string, ResponderIntent> = intentsFor(
901
+ [
902
+ makeRule({
903
+ userId: USER_A,
904
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
905
+ userSmsId: SMS_A,
906
+ notifyAfterMinutes: 5,
907
+ }),
908
+ ],
909
+ incidentSeverity(SEV_NEW),
910
+ );
911
+
912
+ const intent: ResponderIntent = intents.get(USER_A.toString())!;
913
+
914
+ expect(intent.hasRuleForNewSeverity).toBe(false);
915
+ expect(mirroredOf(intent)).toEqual([`userSmsId:${SMS_A.toString()}@5`]);
916
+ });
917
+
918
+ test("a missing delay is read as immediate, not as undefined", () => {
919
+ const intents: Map<string, ResponderIntent> = intentsFor(
920
+ [
921
+ makeRule({
922
+ userId: USER_A,
923
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
924
+ incidentSeverityId: SEV_1,
925
+ userEmailId: EMAIL_A,
926
+ }),
927
+ ],
928
+ incidentSeverity(SEV_NEW),
929
+ );
930
+
931
+ const mirrored: Array<MirroredRule> = Array.from(
932
+ intents.get(USER_A.toString())!.mirroredRules.values(),
933
+ );
934
+
935
+ expect(mirrored[0]!.notifyAfterMinutes).toBe(0);
936
+ });
937
+
938
+ test("a row with no user is ignored entirely", () => {
939
+ const orphan: UserNotificationRule = new UserNotificationRule();
940
+ orphan._id = "orphan-rule";
941
+ orphan.ruleType = NotificationRuleType.ON_CALL_EXECUTED_INCIDENT;
942
+ orphan.userEmailId = EMAIL_A;
943
+
944
+ expect(intentsFor([orphan], incidentSeverity(SEV_NEW)).size).toBe(0);
945
+ });
946
+
947
+ /*
948
+ * The two severity columns are read according to the KIND being backfilled.
949
+ * A rule carrying an incidentSeverityId cannot mark an alert severity
950
+ * covered, however equal the ids happen to look.
951
+ */
952
+ test("an alert backfill reads alertSeverityId, never incidentSeverityId", () => {
953
+ const intents: Map<string, ResponderIntent> = intentsFor(
954
+ [
955
+ makeRule({
956
+ userId: USER_A,
957
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
958
+ incidentSeverityId: ALERT_SEV_NEW,
959
+ userEmailId: EMAIL_A,
960
+ notifyAfterMinutes: 0,
961
+ }),
962
+ ],
963
+ alertSeverity(ALERT_SEV_NEW),
964
+ );
965
+
966
+ const intent: ResponderIntent = intents.get(USER_A.toString())!;
967
+
968
+ expect(intent.hasRuleForNewSeverity).toBe(false);
969
+ expect(mirroredOf(intent)).toEqual([`userEmailId:${EMAIL_A.toString()}@0`]);
970
+ });
971
+ });
972
+
973
+ /*
974
+ * ========================================================================= *
975
+ * backfillSeverity - mirroring existing intent.
976
+ *
977
+ * Note throughout: one severity covers TWO rule types (incident and incident
978
+ * episode, or alert and alert episode), so a responder configured only for the
979
+ * non-episode type is mirrored there and falls back to their verified email for
980
+ * the episode one. The assertions are per rule type for exactly that reason.
981
+ * =========================================================================
982
+ */
983
+
984
+ describe("backfillSeverity mirrors what the responder already asked for", () => {
985
+ /*
986
+ * The headline case. "Sev1 -> call me at once, Sev3 -> email me after fifteen
987
+ * minutes" must become one new rule per pair - not a surprise phone call at a
988
+ * delay they never chose, and not a flattened email-at-zero default.
989
+ */
990
+ test("a responder with two different (method, delay) pairs gets both of them", async () => {
991
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
992
+
993
+ seedRule({
994
+ userId: USER_A,
995
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
996
+ incidentSeverityId: SEV_1,
997
+ userCallId: CALL_A,
998
+ notifyAfterMinutes: 0,
999
+ });
1000
+ seedRule({
1001
+ userId: USER_A,
1002
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1003
+ incidentSeverityId: SEV_3,
1004
+ userEmailId: EMAIL_A,
1005
+ notifyAfterMinutes: 15,
1006
+ });
1007
+
1008
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1009
+
1010
+ const shapes: Array<string> = shapesCreatedFor(
1011
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1012
+ );
1013
+
1014
+ expect(shapes).toHaveLength(2);
1015
+ expect(shapes).toContain(`userCallId:${CALL_A.toString()}@0`);
1016
+ expect(shapes).toContain(`userEmailId:${EMAIL_A.toString()}@15`);
1017
+
1018
+ // The flattened default they must NOT get: their email, immediately.
1019
+ expect(shapes).not.toContain(`userEmailId:${EMAIL_A.toString()}@0`);
1020
+ });
1021
+
1022
+ test("every new rule is bound to the new severity and to no other", async () => {
1023
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1024
+
1025
+ seedRule({
1026
+ userId: USER_A,
1027
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1028
+ incidentSeverityId: SEV_1,
1029
+ userCallId: CALL_A,
1030
+ notifyAfterMinutes: 0,
1031
+ });
1032
+
1033
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1034
+
1035
+ expect(createdRules.length).toBeGreaterThan(0);
1036
+
1037
+ for (const rule of createdRules) {
1038
+ expect(rule.projectId!.toString()).toBe(PROJECT_ID.toString());
1039
+ expect(rule.userId!.toString()).toBe(USER_A.toString());
1040
+ expect(rule.incidentSeverityId!.toString()).toBe(SEV_NEW.toString());
1041
+ expect(rule.alertSeverityId).toBeUndefined();
1042
+ }
1043
+ });
1044
+
1045
+ /*
1046
+ * The failure mode a hardcoded default would produce: a responder who has
1047
+ * only ever used email waking up to a phone call.
1048
+ */
1049
+ test("an email-only responder is never given a phone call", async () => {
1050
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1051
+
1052
+ seedRule({
1053
+ userId: USER_A,
1054
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1055
+ incidentSeverityId: SEV_1,
1056
+ userEmailId: EMAIL_A,
1057
+ notifyAfterMinutes: 10,
1058
+ });
1059
+
1060
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1061
+
1062
+ for (const rule of createdOfType(
1063
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1064
+ )) {
1065
+ expect(rule.userCallId).toBeUndefined();
1066
+ expect(rule.userSmsId).toBeUndefined();
1067
+ expect(rule.notifyAfterMinutes).toBe(10);
1068
+ }
1069
+ });
1070
+
1071
+ test("the same pair repeated across severities produces exactly one new rule", async () => {
1072
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1073
+
1074
+ for (const severityId of [SEV_1, SEV_2, SEV_3]) {
1075
+ seedRule({
1076
+ userId: USER_A,
1077
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1078
+ incidentSeverityId: severityId,
1079
+ userPushId: PUSH_A,
1080
+ notifyAfterMinutes: 0,
1081
+ });
1082
+ }
1083
+
1084
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1085
+
1086
+ expect(
1087
+ shapesCreatedFor(NotificationRuleType.ON_CALL_EXECUTED_INCIDENT),
1088
+ ).toEqual([`userPushId:${PUSH_A.toString()}@0`]);
1089
+ });
1090
+
1091
+ /*
1092
+ * A responder who deliberately muted this rule type gets the mute mirrored.
1093
+ * Leaving the cell empty would hand them to the verified-method fallback in
1094
+ * UserOnCallLogService, which is to say it would page someone who explicitly
1095
+ * asked not to be paged.
1096
+ */
1097
+ test("a responder who opted out is given an opt-out row, not a page", async () => {
1098
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1099
+
1100
+ seedRule({
1101
+ userId: USER_A,
1102
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1103
+ incidentSeverityId: SEV_1,
1104
+ isOptOut: true,
1105
+ notifyAfterMinutes: 0,
1106
+ });
1107
+
1108
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1109
+
1110
+ const created: Array<UserNotificationRule> = createdOfType(
1111
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1112
+ );
1113
+
1114
+ expect(created).toHaveLength(1);
1115
+ expect(created[0]!.isOptOut).toBe(true);
1116
+ expect(methodOf(created[0]!)).toBe("none");
1117
+ expect(created[0]!.notifyAfterMinutes).toBe(0);
1118
+ expect(created[0]!.incidentSeverityId!.toString()).toBe(SEV_NEW.toString());
1119
+ });
1120
+
1121
+ test("an opt-out on one severity does not silence a responder who configured another", async () => {
1122
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1123
+
1124
+ seedRule({
1125
+ userId: USER_A,
1126
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1127
+ incidentSeverityId: SEV_1,
1128
+ isOptOut: true,
1129
+ notifyAfterMinutes: 0,
1130
+ });
1131
+ seedRule({
1132
+ userId: USER_A,
1133
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1134
+ incidentSeverityId: SEV_3,
1135
+ userEmailId: EMAIL_A,
1136
+ notifyAfterMinutes: 0,
1137
+ });
1138
+
1139
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1140
+
1141
+ const created: Array<UserNotificationRule> = createdOfType(
1142
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1143
+ );
1144
+
1145
+ expect(created).toHaveLength(1);
1146
+ expect(created[0]!.isOptOut).toBeUndefined();
1147
+ expect(methodOf(created[0]!)).toBe(`userEmailId:${EMAIL_A.toString()}`);
1148
+ });
1149
+
1150
+ /*
1151
+ * Mirroring is per rule type. An incident-only responder's phone-call rule
1152
+ * must not become an alert rule, and vice versa - the two lists are
1153
+ * configured independently in the UI and mean different things.
1154
+ */
1155
+ test("an alert backfill ignores the responder's incident rules", async () => {
1156
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1157
+
1158
+ seedRule({
1159
+ userId: USER_A,
1160
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1161
+ incidentSeverityId: SEV_1,
1162
+ userCallId: CALL_A,
1163
+ notifyAfterMinutes: 0,
1164
+ });
1165
+
1166
+ await backfillSeverity(alertSeverity(ALERT_SEV_NEW));
1167
+
1168
+ const shapes: Array<string> = shapesCreatedFor(
1169
+ NotificationRuleType.ON_CALL_EXECUTED_ALERT,
1170
+ );
1171
+
1172
+ expect(shapes).toEqual([`userEmailId:${EMAIL_A.toString()}@0`]);
1173
+ expect(shapes).not.toContain(`userCallId:${CALL_A.toString()}@0`);
1174
+ });
1175
+
1176
+ test("both rule types of a severity kind are covered, and only those two", async () => {
1177
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1178
+
1179
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1180
+
1181
+ const typesCovered: Array<NotificationRuleType | undefined> =
1182
+ createdRules.map(
1183
+ (rule: UserNotificationRule): NotificationRuleType | undefined => {
1184
+ return rule.ruleType;
1185
+ },
1186
+ );
1187
+
1188
+ expect(typesCovered.sort()).toEqual([...INCIDENT_RULE_TYPES].sort());
1189
+ });
1190
+
1191
+ test("an episode rule written without a severity is mirrored onto the new severity", async () => {
1192
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1193
+
1194
+ seedRule({
1195
+ userId: USER_A,
1196
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
1197
+ userSmsId: SMS_A,
1198
+ notifyAfterMinutes: 5,
1199
+ });
1200
+
1201
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1202
+
1203
+ // Their stated channel, not the email default they never asked for.
1204
+ expect(
1205
+ shapesCreatedFor(NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE),
1206
+ ).toEqual([`userSmsId:${SMS_A.toString()}@5`]);
1207
+ });
1208
+ });
1209
+
1210
+ /*
1211
+ * ========================================================================= *
1212
+ * backfillSeverity - the verified-email fallback.
1213
+ *
1214
+ * A responder who has said nothing at all about a rule type gets exactly what
1215
+ * addDefaultNotificationRuleForUser would have written for them had the
1216
+ * severity existed when they joined.
1217
+ * =========================================================================
1218
+ */
1219
+
1220
+ describe("backfillSeverity falls back to the responder's verified email", () => {
1221
+ test("a responder with no rules of that type gets their verified email, immediately", async () => {
1222
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1223
+
1224
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1225
+
1226
+ expect(createdRules).toHaveLength(2);
1227
+
1228
+ for (const ruleType of INCIDENT_RULE_TYPES) {
1229
+ expect(shapesCreatedFor(ruleType)).toEqual([
1230
+ `userEmailId:${EMAIL_A.toString()}@0`,
1231
+ ]);
1232
+ }
1233
+
1234
+ for (const rule of createdRules) {
1235
+ expect(rule.isOptOut).toBeUndefined();
1236
+ }
1237
+ });
1238
+
1239
+ /*
1240
+ * The read that decides who is reachable at all. Unverified addresses are
1241
+ * excluded by the query itself, which is what makes "no verified method =>
1242
+ * nothing written" true rather than accidental.
1243
+ */
1244
+ test("only verified addresses in this project are considered", async () => {
1245
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1246
+
1247
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1248
+
1249
+ const query: Record<string, unknown> = queryOf(userEmailFindAllBySpy, 0);
1250
+
1251
+ expect(idOf(query["projectId"])).toBe(PROJECT_ID.toString());
1252
+ expect(query["isVerified"]).toBe(true);
1253
+ expect(propsOf(userEmailFindAllBySpy, 0).isRoot).toBe(true);
1254
+ });
1255
+
1256
+ test("a responder with no verified method gets nothing, and the run still covers everyone else", async () => {
1257
+ // USER_B has no verified email row at all, so the project cannot reach them.
1258
+ verifiedEmails = [
1259
+ { userId: USER_A, emailId: EMAIL_A },
1260
+ { userId: USER_C, emailId: EMAIL_C },
1261
+ ];
1262
+
1263
+ await expect(
1264
+ backfillSeverity(incidentSeverity(SEV_NEW)),
1265
+ ).resolves.toBeUndefined();
1266
+
1267
+ const usersCovered: Array<string> = createdRules.map(
1268
+ (rule: UserNotificationRule): string => {
1269
+ return rule.userId!.toString();
1270
+ },
1271
+ );
1272
+
1273
+ expect(usersCovered).not.toContain(USER_B.toString());
1274
+ expect(createdRules).toHaveLength(4);
1275
+ });
1276
+
1277
+ test("a responder with several verified addresses gets one rule, on the first", async () => {
1278
+ verifiedEmails = [
1279
+ { userId: USER_A, emailId: EMAIL_A },
1280
+ { userId: USER_A, emailId: EMAIL_A_SECOND },
1281
+ ];
1282
+
1283
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1284
+
1285
+ expect(createdRules).toHaveLength(2);
1286
+ expect(
1287
+ shapesCreatedFor(NotificationRuleType.ON_CALL_EXECUTED_INCIDENT),
1288
+ ).toEqual([`userEmailId:${EMAIL_A.toString()}@0`]);
1289
+ });
1290
+
1291
+ /*
1292
+ * The fallback is only for responders who said NOTHING. Someone whose stated
1293
+ * channel is SMS keeps SMS even though the email map is what enumerates the
1294
+ * project.
1295
+ */
1296
+ test("a responder who stated a channel keeps it instead of the email default", async () => {
1297
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1298
+
1299
+ seedRule({
1300
+ userId: USER_A,
1301
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1302
+ incidentSeverityId: SEV_1,
1303
+ userSmsId: SMS_A,
1304
+ notifyAfterMinutes: 0,
1305
+ });
1306
+
1307
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1308
+
1309
+ expect(
1310
+ shapesCreatedFor(NotificationRuleType.ON_CALL_EXECUTED_INCIDENT),
1311
+ ).toEqual([`userSmsId:${SMS_A.toString()}@0`]);
1312
+ });
1313
+ });
1314
+
1315
+ /*
1316
+ * ========================================================================= *
1317
+ * backfillSeverity - idempotence.
1318
+ *
1319
+ * A duplicated rule is a duplicated page. There is no unique index over
1320
+ * (project, user, ruleType, severity, method), so two guards do this work and
1321
+ * they are pinned separately: the per-run snapshot, and the per-row read taken
1322
+ * immediately before each write.
1323
+ * =========================================================================
1324
+ */
1325
+
1326
+ describe("backfillSeverity is idempotent", () => {
1327
+ test("running the same severity a second time writes nothing", async () => {
1328
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1329
+
1330
+ seedRule({
1331
+ userId: USER_A,
1332
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1333
+ incidentSeverityId: SEV_1,
1334
+ userCallId: CALL_A,
1335
+ notifyAfterMinutes: 0,
1336
+ });
1337
+
1338
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1339
+ const afterFirstRun: number = createdRules.length;
1340
+ expect(afterFirstRun).toBeGreaterThan(0);
1341
+
1342
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1343
+
1344
+ expect(createdRules).toHaveLength(afterFirstRun);
1345
+ });
1346
+
1347
+ test("a third and fourth run still write nothing", async () => {
1348
+ verifiedEmails = [
1349
+ { userId: USER_A, emailId: EMAIL_A },
1350
+ { userId: USER_B, emailId: EMAIL_B },
1351
+ ];
1352
+
1353
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1354
+ const afterFirstRun: number = createdRules.length;
1355
+
1356
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1357
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1358
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1359
+
1360
+ expect(createdRules).toHaveLength(afterFirstRun);
1361
+ expect(createdRules).toHaveLength(4);
1362
+ });
1363
+
1364
+ /*
1365
+ * The second guard, in isolation. Both runs took their snapshot before either
1366
+ * wrote, so the snapshot cannot save the loser - only the read taken
1367
+ * immediately before the write can, and it is the one that must fire here.
1368
+ * That overlap is likeliest at exactly the worst moment: a new severity
1369
+ * arrives by two routes at once, the by-name enqueue seconds after creation
1370
+ * and a scheduled sweep already part-way through the same severity.
1371
+ */
1372
+ test("a row written by an overlapping run between the snapshot and the write is not duplicated", async () => {
1373
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1374
+
1375
+ const existing: UserNotificationRule = seedRule({
1376
+ userId: USER_A,
1377
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1378
+ incidentSeverityId: SEV_1,
1379
+ userCallId: CALL_A,
1380
+ notifyAfterMinutes: 0,
1381
+ });
1382
+
1383
+ // The snapshot BOTH overlapping runs took: nothing for the new severity yet.
1384
+ frozenSnapshot = [existing];
1385
+
1386
+ // One run wins the race and writes its rows...
1387
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1388
+ const afterWinner: number = createdRules.length;
1389
+ expect(afterWinner).toBeGreaterThan(0);
1390
+
1391
+ const readsBefore: number = ruleFindOneBySpy.mock.calls.length;
1392
+
1393
+ // ...and the loser is still working from the snapshot it took earlier.
1394
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1395
+
1396
+ expect(createdRules).toHaveLength(afterWinner);
1397
+
1398
+ /*
1399
+ * It really did reach the write path and get stopped there - had the stale
1400
+ * snapshot been what stopped it, no existence check would have been issued.
1401
+ */
1402
+ expect(ruleFindOneBySpy.mock.calls.length).toBeGreaterThan(readsBefore);
1403
+ });
1404
+
1405
+ /*
1406
+ * The existence check keys on the delay as well as the method. A responder
1407
+ * who asked for "email me now" AND "email me in fifteen minutes" must get
1408
+ * both rows on the new severity; a key that ignored the delay would drop the
1409
+ * second one during the same pass that wrote the first.
1410
+ */
1411
+ test("two rules that differ only by delay both survive the existence check", async () => {
1412
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1413
+
1414
+ seedRule({
1415
+ userId: USER_A,
1416
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1417
+ incidentSeverityId: SEV_1,
1418
+ userEmailId: EMAIL_A,
1419
+ notifyAfterMinutes: 0,
1420
+ });
1421
+ seedRule({
1422
+ userId: USER_A,
1423
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1424
+ incidentSeverityId: SEV_1,
1425
+ userEmailId: EMAIL_A,
1426
+ notifyAfterMinutes: 15,
1427
+ });
1428
+
1429
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1430
+
1431
+ const shapes: Array<string> = shapesCreatedFor(
1432
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1433
+ );
1434
+
1435
+ expect(shapes.sort()).toEqual([
1436
+ `userEmailId:${EMAIL_A.toString()}@0`,
1437
+ `userEmailId:${EMAIL_A.toString()}@15`,
1438
+ ]);
1439
+ });
1440
+
1441
+ test("a responder already covered by hand is left completely alone", async () => {
1442
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1443
+
1444
+ seedRule({
1445
+ userId: USER_A,
1446
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1447
+ incidentSeverityId: SEV_1,
1448
+ userCallId: CALL_A,
1449
+ notifyAfterMinutes: 0,
1450
+ });
1451
+ // They already wrote their own rule for the new severity.
1452
+ seedRule({
1453
+ userId: USER_A,
1454
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1455
+ incidentSeverityId: SEV_NEW,
1456
+ userEmailId: EMAIL_A,
1457
+ notifyAfterMinutes: 45,
1458
+ });
1459
+
1460
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1461
+
1462
+ expect(
1463
+ createdOfType(NotificationRuleType.ON_CALL_EXECUTED_INCIDENT),
1464
+ ).toHaveLength(0);
1465
+ });
1466
+
1467
+ /*
1468
+ * The existence check has to name the new severity, or a rule for Sev1 would
1469
+ * satisfy it and the new severity would stay uncovered forever - which is the
1470
+ * original bug wearing a different hat.
1471
+ */
1472
+ test("the pre-write check names the whole row: project, user, type, severity, method and delay", async () => {
1473
+ verifiedEmails = [];
1474
+
1475
+ seedRule({
1476
+ userId: USER_A,
1477
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1478
+ incidentSeverityId: SEV_1,
1479
+ userCallId: CALL_A,
1480
+ notifyAfterMinutes: 20,
1481
+ });
1482
+
1483
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1484
+
1485
+ const query: Record<string, unknown> = queryOf(ruleFindOneBySpy, 0);
1486
+
1487
+ expect(idOf(query["projectId"])).toBe(PROJECT_ID.toString());
1488
+ expect(idOf(query["userId"])).toBe(USER_A.toString());
1489
+ expect(query["ruleType"]).toBe(
1490
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1491
+ );
1492
+ expect(idOf(query["incidentSeverityId"])).toBe(SEV_NEW.toString());
1493
+ expect(idOf(query["userCallId"])).toBe(CALL_A.toString());
1494
+ expect(query["notifyAfterMinutes"]).toBe(20);
1495
+ expect(query["alertSeverityId"]).toBeUndefined();
1496
+ expect(propsOf(ruleFindOneBySpy, 0).isRoot).toBe(true);
1497
+ });
1498
+
1499
+ test("an alert backfill's existence check names alertSeverityId instead", async () => {
1500
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1501
+
1502
+ await backfillSeverity(alertSeverity(ALERT_SEV_NEW));
1503
+
1504
+ const query: Record<string, unknown> = queryOf(ruleFindOneBySpy, 0);
1505
+
1506
+ expect(idOf(query["alertSeverityId"])).toBe(ALERT_SEV_NEW.toString());
1507
+ expect(query["incidentSeverityId"]).toBeUndefined();
1508
+ });
1509
+
1510
+ test("an opt-out row is not duplicated on a second run either", async () => {
1511
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1512
+
1513
+ seedRule({
1514
+ userId: USER_A,
1515
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1516
+ incidentSeverityId: SEV_1,
1517
+ isOptOut: true,
1518
+ notifyAfterMinutes: 0,
1519
+ });
1520
+
1521
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1522
+ const afterFirstRun: number = createdRules.length;
1523
+
1524
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1525
+
1526
+ expect(createdRules).toHaveLength(afterFirstRun);
1527
+ expect(
1528
+ createdOfType(NotificationRuleType.ON_CALL_EXECUTED_INCIDENT),
1529
+ ).toHaveLength(1);
1530
+ });
1531
+ });
1532
+
1533
+ /*
1534
+ * ========================================================================= *
1535
+ * Alert severities take the identical route.
1536
+ * =========================================================================
1537
+ */
1538
+
1539
+ describe("backfillSeverity covers alert severities the same way", () => {
1540
+ test("an alert severity mirrors the responder's alert rules onto alertSeverityId", async () => {
1541
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1542
+
1543
+ seedRule({
1544
+ userId: USER_A,
1545
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
1546
+ alertSeverityId: ALERT_SEV_1,
1547
+ userPushId: PUSH_A,
1548
+ notifyAfterMinutes: 0,
1549
+ });
1550
+
1551
+ await backfillSeverity(alertSeverity(ALERT_SEV_NEW));
1552
+
1553
+ const created: Array<UserNotificationRule> = createdOfType(
1554
+ NotificationRuleType.ON_CALL_EXECUTED_ALERT,
1555
+ );
1556
+
1557
+ expect(created).toHaveLength(1);
1558
+ expect(created[0]!.alertSeverityId!.toString()).toBe(
1559
+ ALERT_SEV_NEW.toString(),
1560
+ );
1561
+ expect(created[0]!.incidentSeverityId).toBeUndefined();
1562
+ expect(methodOf(created[0]!)).toBe(`userPushId:${PUSH_A.toString()}`);
1563
+ });
1564
+
1565
+ test("an alert severity covers both alert rule types and no incident one", async () => {
1566
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1567
+
1568
+ await backfillSeverity(alertSeverity(ALERT_SEV_NEW));
1569
+
1570
+ const typesCovered: Array<NotificationRuleType | undefined> =
1571
+ createdRules.map(
1572
+ (rule: UserNotificationRule): NotificationRuleType | undefined => {
1573
+ return rule.ruleType;
1574
+ },
1575
+ );
1576
+
1577
+ expect(typesCovered.sort()).toEqual([...ALERT_RULE_TYPES].sort());
1578
+ });
1579
+
1580
+ test("an incident backfill never writes an alert rule type", async () => {
1581
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1582
+
1583
+ seedRule({
1584
+ userId: USER_A,
1585
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
1586
+ alertSeverityId: ALERT_SEV_1,
1587
+ userPushId: PUSH_A,
1588
+ notifyAfterMinutes: 0,
1589
+ });
1590
+
1591
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1592
+
1593
+ for (const ruleType of ALERT_RULE_TYPES) {
1594
+ expect(createdOfType(ruleType)).toHaveLength(0);
1595
+ }
1596
+ });
1597
+
1598
+ test("alert severities are idempotent on a second run too", async () => {
1599
+ verifiedEmails = [{ userId: USER_A, emailId: EMAIL_A }];
1600
+
1601
+ await backfillSeverity(alertSeverity(ALERT_SEV_NEW));
1602
+ const afterFirstRun: number = createdRules.length;
1603
+
1604
+ await backfillSeverity(alertSeverity(ALERT_SEV_NEW));
1605
+
1606
+ expect(createdRules).toHaveLength(afterFirstRun);
1607
+ });
1608
+ });
1609
+
1610
+ /*
1611
+ * ========================================================================= *
1612
+ * The fan-out: bounded reads, and failure isolation.
1613
+ * =========================================================================
1614
+ */
1615
+
1616
+ describe("the fan-out is bounded and does not collapse on one responder", () => {
1617
+ function seedProjectWithUsers(count: number): void {
1618
+ verifiedEmails = [];
1619
+
1620
+ for (let index: number = 0; index < count; index++) {
1621
+ verifiedEmails.push({
1622
+ userId: new ObjectID(`bulk-user-${index}`),
1623
+ emailId: new ObjectID(`bulk-user-email-${index}`),
1624
+ });
1625
+ }
1626
+ }
1627
+
1628
+ /*
1629
+ * The N+1 this job was written to avoid: looping the project's responders and
1630
+ * asking the database once per responder per severity. Reads must be a
1631
+ * function of the RULE TYPES, not of the head-count.
1632
+ */
1633
+ test("reads do not grow with the number of responders", async () => {
1634
+ seedProjectWithUsers(25);
1635
+
1636
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1637
+
1638
+ expect(createdRules).toHaveLength(50);
1639
+
1640
+ // One responder read for the severity, one rule read per rule type.
1641
+ expect(userEmailFindAllBySpy).toHaveBeenCalledTimes(1);
1642
+ expect(ruleFindAllBySpy).toHaveBeenCalledTimes(INCIDENT_RULE_TYPES.length);
1643
+ });
1644
+
1645
+ test("the project-wide rule read is scoped to the project and one rule type", async () => {
1646
+ seedProjectWithUsers(3);
1647
+
1648
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1649
+
1650
+ const ruleTypesRead: Array<unknown> = ruleFindAllBySpy.mock.calls.map(
1651
+ (call: Array<unknown>): unknown => {
1652
+ return (call[0] as { query: Record<string, unknown> }).query[
1653
+ "ruleType"
1654
+ ];
1655
+ },
1656
+ );
1657
+
1658
+ expect(ruleTypesRead.sort()).toEqual([...INCIDENT_RULE_TYPES].sort());
1659
+ expect(idOf(queryOf(ruleFindAllBySpy, 0)["projectId"])).toBe(
1660
+ PROJECT_ID.toString(),
1661
+ );
1662
+ expect(propsOf(ruleFindAllBySpy, 0).isRoot).toBe(true);
1663
+ });
1664
+
1665
+ /*
1666
+ * findAllBy is the paging reader: it walks the table in LIMIT_MAX batches
1667
+ * rather than issuing one unbounded query, which is what keeps a project with
1668
+ * more responders than fit in a single page from being read in one gulp. The
1669
+ * real findAllBy runs here; only the underlying page read is stubbed.
1670
+ */
1671
+ test("responders are read in bounded pages, not one unbounded query", async () => {
1672
+ userEmailFindAllBySpy.mockRestore();
1673
+
1674
+ const singleRow: UserEmail = userEmailRow(USER_A, EMAIL_A);
1675
+ let pageIndex: number = 0;
1676
+
1677
+ const emailFindBySpy: jest.SpyInstance = jest
1678
+ .spyOn(UserEmailService, "findBy")
1679
+ .mockImplementation(((): Promise<Array<UserEmail>> => {
1680
+ pageIndex++;
1681
+
1682
+ if (pageIndex === 1) {
1683
+ // A full page, which is what tells findAllBy to ask for another.
1684
+ return Promise.resolve(
1685
+ new Array<UserEmail>(LIMIT_MAX).fill(singleRow),
1686
+ );
1687
+ }
1688
+
1689
+ return Promise.resolve([]);
1690
+ }) as never);
1691
+
1692
+ await backfillSeverity(incidentSeverity(SEV_NEW));
1693
+
1694
+ expect(emailFindBySpy).toHaveBeenCalledTimes(2);
1695
+
1696
+ const firstPage: { skip: number; limit: number } = emailFindBySpy.mock
1697
+ .calls[0]![0] as { skip: number; limit: number };
1698
+ const secondPage: { skip: number; limit: number } = emailFindBySpy.mock
1699
+ .calls[1]![0] as { skip: number; limit: number };
1700
+
1701
+ expect(firstPage.skip).toBe(0);
1702
+ expect(firstPage.limit).toBe(LIMIT_MAX);
1703
+ expect(secondPage.skip).toBe(LIMIT_MAX);
1704
+ expect(secondPage.limit).toBe(LIMIT_MAX);
1705
+
1706
+ // The page was one responder repeated, so one responder is covered.
1707
+ expect(createdRules).toHaveLength(INCIDENT_RULE_TYPES.length);
1708
+ });
1709
+
1710
+ /*
1711
+ * The likeliest real failure: a method row deleted between the existence
1712
+ * check and the write, which cascades the foreign key away. That is worth a
1713
+ * log line for one person, not an aborted backfill for the whole project.
1714
+ */
1715
+ test("a write that fails for one responder does not stop the others", async () => {
1716
+ verifiedEmails = [
1717
+ { userId: USER_A, emailId: EMAIL_A },
1718
+ { userId: USER_B, emailId: EMAIL_B },
1719
+ { userId: USER_C, emailId: EMAIL_C },
1720
+ ];
1721
+
1722
+ ruleCreateSpy.mockImplementation(((data: {
1723
+ data: UserNotificationRule;
1724
+ }): Promise<UserNotificationRule> => {
1725
+ const rule: UserNotificationRule = data.data;
1726
+
1727
+ if (rule.userId && rule.userId.toString() === USER_B.toString()) {
1728
+ return Promise.reject(
1729
+ new Error("insert or update violates foreign key constraint"),
1730
+ );
1731
+ }
1732
+
1733
+ rule._id = `created-rule-${createdRules.length + 1}`;
1734
+ ruleStore.push(rule);
1735
+ createdRules.push(rule);
1736
+
1737
+ return Promise.resolve(rule);
1738
+ }) as never);
1739
+
1740
+ await expect(
1741
+ backfillSeverity(incidentSeverity(SEV_NEW)),
1742
+ ).resolves.toBeUndefined();
1743
+
1744
+ const usersCovered: Array<string> = createdRules.map(
1745
+ (rule: UserNotificationRule): string => {
1746
+ return rule.userId!.toString();
1747
+ },
1748
+ );
1749
+
1750
+ expect(usersCovered).toContain(USER_A.toString());
1751
+ expect(usersCovered).toContain(USER_C.toString());
1752
+ expect(usersCovered).not.toContain(USER_B.toString());
1753
+ expect(createdRules).toHaveLength(4);
1754
+
1755
+ // And the responder who was missed is named, not silently dropped.
1756
+ const logged: Array<string> = loggerErrorSpy.mock.calls.map(
1757
+ (call: Array<unknown>): string => {
1758
+ return String(call[0]);
1759
+ },
1760
+ );
1761
+
1762
+ expect(
1763
+ logged.some((message: string): boolean => {
1764
+ return message.includes(USER_B.toString());
1765
+ }),
1766
+ ).toBe(true);
1767
+ });
1768
+
1769
+ /*
1770
+ * The boundary of that isolation, pinned so a future reader knows where it
1771
+ * is: only the WRITE is wrapped. A failing existence READ propagates and ends
1772
+ * this severity's pass. It is not a lost page - the sweep re-derives its work
1773
+ * set from an hour-wide window every five minutes, so the responders after
1774
+ * the failure are picked up by a later tick - but the rest of THIS pass does
1775
+ * not happen.
1776
+ */
1777
+ test("a failing existence read ends this pass, leaving the rest to the next sweep", async () => {
1778
+ verifiedEmails = [
1779
+ { userId: USER_A, emailId: EMAIL_A },
1780
+ { userId: USER_B, emailId: EMAIL_B },
1781
+ { userId: USER_C, emailId: EMAIL_C },
1782
+ ];
1783
+
1784
+ ruleFindOneBySpy.mockImplementation(((data: {
1785
+ query: Record<string, unknown>;
1786
+ }): Promise<UserNotificationRule | null> => {
1787
+ if (idOf(data.query["userId"]) === USER_B.toString()) {
1788
+ return Promise.reject(new Error("connection terminated unexpectedly"));
1789
+ }
1790
+
1791
+ return Promise.resolve(matchingRules(ruleStore, data.query)[0] || null);
1792
+ }) as never);
1793
+
1794
+ await expect(backfillSeverity(incidentSeverity(SEV_NEW))).rejects.toThrow(
1795
+ "connection terminated unexpectedly",
1796
+ );
1797
+
1798
+ const usersCovered: Array<string> = createdRules.map(
1799
+ (rule: UserNotificationRule): string => {
1800
+ return rule.userId!.toString();
1801
+ },
1802
+ );
1803
+
1804
+ expect(usersCovered).toEqual([USER_A.toString()]);
1805
+ });
1806
+
1807
+ test("a project with nobody in it is a pair of reads and no writes", async () => {
1808
+ verifiedEmails = [];
1809
+
1810
+ await expect(
1811
+ backfillSeverity(incidentSeverity(SEV_NEW)),
1812
+ ).resolves.toBeUndefined();
1813
+
1814
+ expect(createdRules).toHaveLength(0);
1815
+ expect(ruleCreateSpy).not.toHaveBeenCalled();
1816
+ expect(ruleFindOneBySpy).not.toHaveBeenCalled();
1817
+ });
1818
+ });