@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,1744 @@
1
+ import OnCallDutyPolicyExecutionLogTimelineService from "../../../Server/Services/OnCallDutyPolicyExecutionLogTimelineService";
2
+ import OnCallNotificationAlertingService from "../../../Server/Services/OnCallNotificationAlertingService";
3
+ import ProjectService from "../../../Server/Services/ProjectService";
4
+ import UserCallService from "../../../Server/Services/UserCallService";
5
+ import UserEmailService from "../../../Server/Services/UserEmailService";
6
+ import UserNotificationRuleService, {
7
+ ExecuteFallbackNotificationOptions,
8
+ ExecuteNotificationRuleOptions,
9
+ FALLBACK_NOTIFICATION_CLAIM_KEY,
10
+ FallbackNotificationOutcome,
11
+ FallbackNotificationResult,
12
+ } from "../../../Server/Services/UserNotificationRuleService";
13
+ import UserOnCallLogService, {
14
+ NO_NOTIFICATION_RULES_STATUS_MESSAGE,
15
+ } from "../../../Server/Services/UserOnCallLogService";
16
+ import UserPushService from "../../../Server/Services/UserPushService";
17
+ import UserService from "../../../Server/Services/UserService";
18
+ import UserSmsService from "../../../Server/Services/UserSmsService";
19
+ import UserTelegramService from "../../../Server/Services/UserTelegramService";
20
+ import UserWebhookService from "../../../Server/Services/UserWebhookService";
21
+ import UserWhatsAppService from "../../../Server/Services/UserWhatsAppService";
22
+ import logger from "../../../Server/Utils/Logger";
23
+ import Project from "../../../Models/DatabaseModels/Project";
24
+ import UserNotificationRule from "../../../Models/DatabaseModels/UserNotificationRule";
25
+ import UserOnCallLog from "../../../Models/DatabaseModels/UserOnCallLog";
26
+ import Email from "../../../Types/Email";
27
+ import Name from "../../../Types/Name";
28
+ import NotificationRuleType from "../../../Types/NotificationRule/NotificationRuleType";
29
+ import ObjectID from "../../../Types/ObjectID";
30
+ import OnCallDutyExecutionLogTimelineStatus from "../../../Types/OnCallDutyPolicy/OnCalDutyExecutionLogTimelineStatus";
31
+ import Phone from "../../../Types/Phone";
32
+ import UserNotificationEventType from "../../../Types/UserNotification/UserNotificationEventType";
33
+ import UserNotificationExecutionStatus from "../../../Types/UserNotification/UserNotificationExecutionStatus";
34
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
35
+
36
+ /*
37
+ * The verified-method fallback: what happens when a responder is paged and has
38
+ * NO notification rule for the (ruleType x severity) that fired.
39
+ *
40
+ * Before Phase 1 that was a dead-end - an Error row nobody reads, no send, and
41
+ * an escalation timer running down as though the responder had simply declined
42
+ * to acknowledge (Gap C). The fallback pages them anyway, on whatever they have
43
+ * verified. This file pins the four things about it that are easy to get subtly,
44
+ * silently wrong:
45
+ *
46
+ * 1. WHICH CHANNELS. Zero-cost first: a responder with a verified push device
47
+ * and a verified email gets both, because there is no reason to choose and
48
+ * neither costs the project anything. Only a responder with neither is
49
+ * worth spending money on, and then exactly once, down the ladder
50
+ * SMS -> Call -> WhatsApp -> Telegram -> Webhook - and only through
51
+ * channels the PROJECT still has switched on. That last clause is the one
52
+ * with a bill attached: SmsService and CallService check their project flag
53
+ * at send time, but WhatsApp and Telegram only check theirs when the method
54
+ * is created, so a project that switched WhatsApp off would be billed by a
55
+ * fallback that did not look for itself (spec constraint 7). It is driven
56
+ * per channel below.
57
+ *
58
+ * 2. ONE DELIVERY CALL PER CHANNEL. This is the highest-value assertion in the
59
+ * file (spec constraint 1). deliverNotificationForRule keeps ONE mutable
60
+ * UserOnCallLogTimeline instance and mutates it as it walks its channel
61
+ * blocks; after the first create() that instance carries an _id, so a
62
+ * second create() with it UPDATEs the row it already wrote instead of
63
+ * inserting a new one. A fallback that looped channels inside a single
64
+ * delivery call would therefore overwrite the first channel's timeline row
65
+ * with the second's - one page would vanish from the record entirely. So:
66
+ * two channels means two calls, each with its OWN freshly built rule.
67
+ *
68
+ * 3. THE UNSAVED RULE. Each channel is delivered through a UserNotificationRule
69
+ * assembled in memory and never persisted - the user did not ask for this
70
+ * rule, and writing it would rewrite their configuration behind their back.
71
+ * It has to carry the method RELATION (rule.userEmail as a loaded UserEmail
72
+ * with .email and .isVerified), not merely the FK, because every channel
73
+ * block reads the relation and never dereferences the id: a rule with only
74
+ * userEmailId set sends nothing at all, quietly.
75
+ *
76
+ * 4. THE OUTCOME THE CALLER ACTS ON. UserNotificationExecutionStatus.Error is
77
+ * TERMINAL - ExecutePendingExecutions selects Executing and
78
+ * TimeoutStuckExecutions selects Started, so nothing re-selects an Error
79
+ * log. "This responder has nothing we can page them on" is worth burning
80
+ * the log for; "the send raised" is not the same thing at all, and both are
81
+ * notified:false. Hence the three-way FallbackNotificationOutcome, and
82
+ * hence the wiring assertions in section (F).
83
+ *
84
+ * Nothing here touches a database or a sender: deliverNotificationForRule is
85
+ * stubbed at the service boundary (it fans out into template generation, short
86
+ * links, seven providers and the timeline writer), and every lookup is a
87
+ * jest.spyOn. What is under test is the fallback's own decision-making.
88
+ */
89
+
90
+ const PROJECT_ID: ObjectID = new ObjectID(
91
+ "11111111-1111-4111-8111-111111111111",
92
+ );
93
+ const USER_ID: ObjectID = new ObjectID("22222222-2222-4222-8222-222222222222");
94
+ const LOG_ID: ObjectID = new ObjectID("33333333-3333-4333-8333-333333333333");
95
+ const INCIDENT_ID: ObjectID = new ObjectID(
96
+ "44444444-4444-4444-8444-444444444444",
97
+ );
98
+ const POLICY_ID: ObjectID = new ObjectID(
99
+ "55555555-5555-4555-8555-555555555555",
100
+ );
101
+ const TIMELINE_ID: ObjectID = new ObjectID(
102
+ "66666666-6666-4666-8666-666666666666",
103
+ );
104
+ const INCIDENT_SEVERITY_ID: ObjectID = new ObjectID(
105
+ "77777777-7777-4777-8777-777777777777",
106
+ );
107
+
108
+ const EMAIL_METHOD_ID: ObjectID = new ObjectID(
109
+ "a1a1a1a1-a1a1-4a1a-8a1a-a1a1a1a1a1a1",
110
+ );
111
+ const PUSH_METHOD_ID: ObjectID = new ObjectID(
112
+ "a2a2a2a2-a2a2-4a2a-8a2a-a2a2a2a2a2a2",
113
+ );
114
+ const SMS_METHOD_ID: ObjectID = new ObjectID(
115
+ "a3a3a3a3-a3a3-4a3a-8a3a-a3a3a3a3a3a3",
116
+ );
117
+ const CALL_METHOD_ID: ObjectID = new ObjectID(
118
+ "a4a4a4a4-a4a4-4a4a-8a4a-a4a4a4a4a4a4",
119
+ );
120
+ const WHATSAPP_METHOD_ID: ObjectID = new ObjectID(
121
+ "a5a5a5a5-a5a5-4a5a-8a5a-a5a5a5a5a5a5",
122
+ );
123
+ const TELEGRAM_METHOD_ID: ObjectID = new ObjectID(
124
+ "a6a6a6a6-a6a6-4a6a-8a6a-a6a6a6a6a6a6",
125
+ );
126
+ const WEBHOOK_METHOD_ID: ObjectID = new ObjectID(
127
+ "a7a7a7a7-a7a7-4a7a-8a7a-a7a7a7a7a7a7",
128
+ );
129
+
130
+ const RESPONDER_EMAIL: string = "responder@company.com";
131
+ const SEVERITY_NAME: string = "Sev4";
132
+ const RESPONDER_NAME: string = "Jane Doe";
133
+
134
+ /*
135
+ * The four project switches the paid ladder consults, and the flag key each
136
+ * channel is gated on. Kept as one table because the "is this channel allowed"
137
+ * question is asked identically for all four and answered in four separate
138
+ * blocks of production code - exactly the shape that drifts.
139
+ */
140
+ type PaidChannelName = "SMS" | "Call" | "WhatsApp" | "Telegram";
141
+
142
+ const SMS_FLAG: string = "enableSmsNotifications";
143
+ const CALL_FLAG: string = "enableCallNotifications";
144
+ const WHATSAPP_FLAG: string = "enableWhatsAppNotifications";
145
+ const TELEGRAM_FLAG: string = "enableTelegramNotifications";
146
+
147
+ /*
148
+ * A project row as chooseFallbackChannels reads it. Every paid channel starts
149
+ * DISABLED so that a test which forgets to enable one cannot accidentally bill
150
+ * for it - the interesting default is the restrictive one.
151
+ */
152
+ function makeProject(overrides: Record<string, unknown> = {}): Project {
153
+ return {
154
+ _id: PROJECT_ID.toString(),
155
+ enableSmsNotifications: false,
156
+ enableCallNotifications: false,
157
+ enableWhatsAppNotifications: false,
158
+ enableTelegramNotifications: false,
159
+ ...overrides,
160
+ } as unknown as Project;
161
+ }
162
+
163
+ function makeVerifiedPush(): Record<string, unknown> {
164
+ return {
165
+ id: PUSH_METHOD_ID,
166
+ _id: PUSH_METHOD_ID.toString(),
167
+ deviceToken: "device-token",
168
+ deviceType: "iOS",
169
+ isVerified: true,
170
+ };
171
+ }
172
+
173
+ function makeVerifiedEmail(): Record<string, unknown> {
174
+ return {
175
+ id: EMAIL_METHOD_ID,
176
+ _id: EMAIL_METHOD_ID.toString(),
177
+ email: new Email(RESPONDER_EMAIL),
178
+ isVerified: true,
179
+ };
180
+ }
181
+
182
+ function makeVerifiedSms(): Record<string, unknown> {
183
+ return {
184
+ id: SMS_METHOD_ID,
185
+ _id: SMS_METHOD_ID.toString(),
186
+ phone: new Phone("+11234567890"),
187
+ isVerified: true,
188
+ };
189
+ }
190
+
191
+ function makeVerifiedCall(): Record<string, unknown> {
192
+ return {
193
+ id: CALL_METHOD_ID,
194
+ _id: CALL_METHOD_ID.toString(),
195
+ phone: new Phone("+11234567891"),
196
+ isVerified: true,
197
+ };
198
+ }
199
+
200
+ function makeVerifiedWhatsApp(): Record<string, unknown> {
201
+ return {
202
+ id: WHATSAPP_METHOD_ID,
203
+ _id: WHATSAPP_METHOD_ID.toString(),
204
+ phone: new Phone("+11234567892"),
205
+ isVerified: true,
206
+ };
207
+ }
208
+
209
+ function makeVerifiedTelegram(): Record<string, unknown> {
210
+ return {
211
+ id: TELEGRAM_METHOD_ID,
212
+ _id: TELEGRAM_METHOD_ID.toString(),
213
+ telegramChatId: "123456",
214
+ telegramUserHandle: "@responder",
215
+ isVerified: true,
216
+ };
217
+ }
218
+
219
+ /*
220
+ * Deliberately carries no isVerified: UserWebhook has no such column, so
221
+ * presence of a URL is the whole gate. A fixture that invented an isVerified
222
+ * would hide a regression that started requiring one.
223
+ */
224
+ function makeWebhook(): Record<string, unknown> {
225
+ return {
226
+ id: WEBHOOK_METHOD_ID,
227
+ _id: WEBHOOK_METHOD_ID.toString(),
228
+ webhookUrl: "https://hooks.example.com/on-call",
229
+ name: "Pager bridge",
230
+ secret: "s3cr3t",
231
+ };
232
+ }
233
+
234
+ /*
235
+ * deliverNotificationForRule is private - it is the seam the fallback reuses
236
+ * from executeNotificationRuleItem, not part of the service's public surface.
237
+ * Spying on it needs a structural view of the singleton rather than a widened
238
+ * class, so the cast lives here and nowhere else.
239
+ */
240
+ interface UserNotificationRuleServiceInternals {
241
+ deliverNotificationForRule: (
242
+ notificationRuleItem: UserNotificationRule,
243
+ options: ExecuteNotificationRuleOptions,
244
+ ) => Promise<boolean>;
245
+ }
246
+
247
+ function ruleServiceInternals(): UserNotificationRuleServiceInternals {
248
+ return UserNotificationRuleService as unknown as UserNotificationRuleServiceInternals;
249
+ }
250
+
251
+ interface UserOnCallLogServiceInternals {
252
+ handleNoMatchingNotificationRule: (data: {
253
+ createdItem: UserOnCallLog;
254
+ notificationRuleType: NotificationRuleType;
255
+ incidentSeverityId: ObjectID | undefined;
256
+ alertSeverityId: ObjectID | undefined;
257
+ severityName: string;
258
+ }) => Promise<void>;
259
+ }
260
+
261
+ function fallbackOptions(
262
+ overrides: Partial<ExecuteFallbackNotificationOptions> = {},
263
+ ): ExecuteFallbackNotificationOptions {
264
+ return {
265
+ userId: USER_ID,
266
+ projectId: PROJECT_ID,
267
+ userOnCallLogId: LOG_ID,
268
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
269
+ severityName: SEVERITY_NAME,
270
+ userNotificationLogId: LOG_ID,
271
+ userNotificationEventType: UserNotificationEventType.IncidentCreated,
272
+ triggeredByIncidentId: INCIDENT_ID,
273
+ onCallPolicyId: POLICY_ID,
274
+ ...overrides,
275
+ };
276
+ }
277
+
278
+ /* The query bundle every method lookup is handed. */
279
+ interface FindOneByArg {
280
+ query: Record<string, unknown>;
281
+ select: Record<string, unknown>;
282
+ props: { isRoot: boolean };
283
+ }
284
+
285
+ function queryOf(spy: jest.SpyInstance): Record<string, unknown> {
286
+ return (spy.mock.calls[0]![0] as FindOneByArg).query;
287
+ }
288
+
289
+ function flushMicrotasks(): Promise<void> {
290
+ return Promise.resolve()
291
+ .then((): Promise<void> => {
292
+ return Promise.resolve();
293
+ })
294
+ .then((): Promise<void> => {
295
+ return Promise.resolve();
296
+ });
297
+ }
298
+
299
+ /*
300
+ * ------------------------------------------------------------------------- *
301
+ * (A)-(E) executeFallbackNotification itself.
302
+ * -------------------------------------------------------------------------
303
+ */
304
+
305
+ describe("UserNotificationRuleService.executeFallbackNotification", () => {
306
+ let claimSpy: jest.SpyInstance;
307
+ let deliverSpy: jest.SpyInstance;
308
+ let ruleCreateSpy: jest.SpyInstance;
309
+ let pushFindSpy: jest.SpyInstance;
310
+ let emailFindSpy: jest.SpyInstance;
311
+ let smsFindSpy: jest.SpyInstance;
312
+ let callFindSpy: jest.SpyInstance;
313
+ let whatsAppFindSpy: jest.SpyInstance;
314
+ let telegramFindSpy: jest.SpyInstance;
315
+ let webhookFindSpy: jest.SpyInstance;
316
+ let projectFindSpy: jest.SpyInstance;
317
+
318
+ beforeEach(() => {
319
+ jest.spyOn(logger, "error").mockImplementation((): void => {
320
+ return undefined;
321
+ });
322
+ jest.spyOn(logger, "warn").mockImplementation((): void => {
323
+ return undefined;
324
+ });
325
+
326
+ claimSpy = jest
327
+ .spyOn(UserOnCallLogService, "claimNotificationExecution")
328
+ .mockResolvedValue(true as never);
329
+
330
+ /*
331
+ * Stubbed at the service boundary. The real method fans out into template
332
+ * generation, short-link creation, seven providers and the timeline writer;
333
+ * its return value ("was a page actually handed to a sender") is the only
334
+ * part of it the fallback reasons about.
335
+ */
336
+ deliverSpy = jest
337
+ .spyOn(ruleServiceInternals(), "deliverNotificationForRule")
338
+ .mockResolvedValue(true as never);
339
+
340
+ ruleCreateSpy = jest
341
+ .spyOn(UserNotificationRuleService, "create")
342
+ .mockResolvedValue(undefined as never);
343
+
344
+ // A responder with nothing configured: every test opts IN to its methods.
345
+ pushFindSpy = jest
346
+ .spyOn(UserPushService, "findOneBy")
347
+ .mockResolvedValue(null as never);
348
+ emailFindSpy = jest
349
+ .spyOn(UserEmailService, "findOneBy")
350
+ .mockResolvedValue(null as never);
351
+ smsFindSpy = jest
352
+ .spyOn(UserSmsService, "findOneBy")
353
+ .mockResolvedValue(null as never);
354
+ callFindSpy = jest
355
+ .spyOn(UserCallService, "findOneBy")
356
+ .mockResolvedValue(null as never);
357
+ whatsAppFindSpy = jest
358
+ .spyOn(UserWhatsAppService, "findOneBy")
359
+ .mockResolvedValue(null as never);
360
+ telegramFindSpy = jest
361
+ .spyOn(UserTelegramService, "findOneBy")
362
+ .mockResolvedValue(null as never);
363
+ webhookFindSpy = jest
364
+ .spyOn(UserWebhookService, "findOneBy")
365
+ .mockResolvedValue(null as never);
366
+
367
+ projectFindSpy = jest
368
+ .spyOn(ProjectService, "findOneById")
369
+ .mockResolvedValue(makeProject() as never);
370
+ });
371
+
372
+ afterEach(() => {
373
+ jest.restoreAllMocks();
374
+ });
375
+
376
+ function runFallback(
377
+ overrides: Partial<ExecuteFallbackNotificationOptions> = {},
378
+ ): Promise<FallbackNotificationResult> {
379
+ return UserNotificationRuleService.executeFallbackNotification(
380
+ fallbackOptions(overrides),
381
+ );
382
+ }
383
+
384
+ function deliveredRules(): Array<UserNotificationRule> {
385
+ return deliverSpy.mock.calls.map(
386
+ (call: Array<unknown>): UserNotificationRule => {
387
+ return call[0] as UserNotificationRule;
388
+ },
389
+ );
390
+ }
391
+
392
+ function deliveredOptions(): Array<ExecuteNotificationRuleOptions> {
393
+ return deliverSpy.mock.calls.map(
394
+ (call: Array<unknown>): ExecuteNotificationRuleOptions => {
395
+ return call[1] as ExecuteNotificationRuleOptions;
396
+ },
397
+ );
398
+ }
399
+
400
+ function paidChannelFindSpy(channelName: PaidChannelName): jest.SpyInstance {
401
+ const byChannel: Record<PaidChannelName, jest.SpyInstance> = {
402
+ SMS: smsFindSpy,
403
+ Call: callFindSpy,
404
+ WhatsApp: whatsAppFindSpy,
405
+ Telegram: telegramFindSpy,
406
+ };
407
+
408
+ return byChannel[channelName];
409
+ }
410
+
411
+ function everyPaidChannelFindSpy(): Array<jest.SpyInstance> {
412
+ return [smsFindSpy, callFindSpy, whatsAppFindSpy, telegramFindSpy];
413
+ }
414
+
415
+ function giveResponderEveryPaidMethod(): void {
416
+ smsFindSpy.mockResolvedValue(makeVerifiedSms() as never);
417
+ callFindSpy.mockResolvedValue(makeVerifiedCall() as never);
418
+ whatsAppFindSpy.mockResolvedValue(makeVerifiedWhatsApp() as never);
419
+ telegramFindSpy.mockResolvedValue(makeVerifiedTelegram() as never);
420
+ webhookFindSpy.mockResolvedValue(makeWebhook() as never);
421
+ }
422
+
423
+ /*
424
+ * ----------------------------------------------------------------------- *
425
+ * (A) Channel selection - zero-cost channels.
426
+ * -----------------------------------------------------------------------
427
+ */
428
+
429
+ describe("channel selection: the zero-cost pair", () => {
430
+ test("a responder with a verified push device AND a verified email gets BOTH", async () => {
431
+ pushFindSpy.mockResolvedValue(makeVerifiedPush() as never);
432
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
433
+
434
+ const result: FallbackNotificationResult = await runFallback();
435
+
436
+ /*
437
+ * Both, and in this order: push and email cost nothing, so there is no
438
+ * reason to pick between them and every reason to maximise the chance of
439
+ * reaching a human who is not looking at their phone.
440
+ */
441
+ expect(result.channelsUsed).toEqual(["Push", "Email"]);
442
+ expect(result.notified).toBe(true);
443
+ expect(result.outcome).toBe(FallbackNotificationOutcome.Delivered);
444
+ });
445
+
446
+ test("a verified push device alone is enough - no paid channel is even looked up", async () => {
447
+ pushFindSpy.mockResolvedValue(makeVerifiedPush() as never);
448
+
449
+ const result: FallbackNotificationResult = await runFallback();
450
+
451
+ expect(result.channelsUsed).toEqual(["Push"]);
452
+ for (const spy of everyPaidChannelFindSpy()) {
453
+ expect(spy).not.toHaveBeenCalled();
454
+ }
455
+ expect(webhookFindSpy).not.toHaveBeenCalled();
456
+ });
457
+
458
+ test("a verified email alone is enough - no paid channel is even looked up", async () => {
459
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
460
+
461
+ const result: FallbackNotificationResult = await runFallback();
462
+
463
+ expect(result.channelsUsed).toEqual(["Email"]);
464
+ for (const spy of everyPaidChannelFindSpy()) {
465
+ expect(spy).not.toHaveBeenCalled();
466
+ }
467
+ });
468
+
469
+ test("with a zero-cost channel available the project settings are never even read", async () => {
470
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
471
+
472
+ await runFallback();
473
+
474
+ /*
475
+ * The project row is only consulted to decide whether a PAID channel may
476
+ * be used. Reaching it at all on this path would mean the ladder is being
477
+ * evaluated when it should not be.
478
+ */
479
+ expect(projectFindSpy).not.toHaveBeenCalled();
480
+ });
481
+
482
+ test("the push lookup is scoped to this project, this user, and verified devices only", async () => {
483
+ pushFindSpy.mockResolvedValue(makeVerifiedPush() as never);
484
+
485
+ await runFallback();
486
+
487
+ const query: Record<string, unknown> = queryOf(pushFindSpy);
488
+ expect((query["projectId"] as ObjectID).toString()).toBe(
489
+ PROJECT_ID.toString(),
490
+ );
491
+ expect((query["userId"] as ObjectID).toString()).toBe(USER_ID.toString());
492
+ /*
493
+ * The verification predicate lives in the QUERY, which is the only place
494
+ * it can live: nothing downstream re-checks it before the fallback builds
495
+ * a rule around the row. Dropping it would page an unverified device.
496
+ */
497
+ expect(query["isVerified"]).toBe(true);
498
+ });
499
+
500
+ test("the email lookup is scoped to this project, this user, and verified addresses only", async () => {
501
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
502
+
503
+ await runFallback();
504
+
505
+ const query: Record<string, unknown> = queryOf(emailFindSpy);
506
+ expect((query["projectId"] as ObjectID).toString()).toBe(
507
+ PROJECT_ID.toString(),
508
+ );
509
+ expect((query["userId"] as ObjectID).toString()).toBe(USER_ID.toString());
510
+ expect(query["isVerified"]).toBe(true);
511
+ });
512
+ });
513
+
514
+ /*
515
+ * ----------------------------------------------------------------------- *
516
+ * (A continued) Channel selection - the paid ladder and its project gates.
517
+ * -----------------------------------------------------------------------
518
+ */
519
+
520
+ describe("channel selection: the paid ladder", () => {
521
+ test.each<[PaidChannelName, string, Record<string, unknown>]>([
522
+ ["SMS", SMS_FLAG, makeVerifiedSms()],
523
+ ["Call", CALL_FLAG, makeVerifiedCall()],
524
+ ["WhatsApp", WHATSAPP_FLAG, makeVerifiedWhatsApp()],
525
+ ["Telegram", TELEGRAM_FLAG, makeVerifiedTelegram()],
526
+ ])(
527
+ "%s is used when the responder has it verified and the project has it enabled",
528
+ async (
529
+ channelName: PaidChannelName,
530
+ flagKey: string,
531
+ method: Record<string, unknown>,
532
+ ): Promise<void> => {
533
+ projectFindSpy.mockResolvedValue(
534
+ makeProject({ [flagKey]: true }) as never,
535
+ );
536
+ paidChannelFindSpy(channelName).mockResolvedValue(method as never);
537
+
538
+ const result: FallbackNotificationResult = await runFallback();
539
+
540
+ expect(result.channelsUsed).toEqual([channelName]);
541
+ expect(result.outcome).toBe(FallbackNotificationOutcome.Delivered);
542
+ // Exactly one paid channel, never a spree down the ladder.
543
+ expect(deliverSpy).toHaveBeenCalledTimes(1);
544
+ },
545
+ );
546
+
547
+ test.each<[PaidChannelName, string, Record<string, unknown>]>([
548
+ ["SMS", SMS_FLAG, makeVerifiedSms()],
549
+ ["Call", CALL_FLAG, makeVerifiedCall()],
550
+ ["WhatsApp", WHATSAPP_FLAG, makeVerifiedWhatsApp()],
551
+ ["Telegram", TELEGRAM_FLAG, makeVerifiedTelegram()],
552
+ ])(
553
+ "%s is skipped - not even queried - when the project has it disabled",
554
+ async (
555
+ channelName: PaidChannelName,
556
+ _flagKey: string,
557
+ method: Record<string, unknown>,
558
+ ): Promise<void> => {
559
+ /*
560
+ * This is the constraint that stops a fallback billing a project that
561
+ * deliberately switched a channel off. Only SmsService and CallService
562
+ * enforce their flag at send time; WhatsApp and Telegram check theirs
563
+ * when a method is CREATED, so by the time a fallback picks one up the
564
+ * project's answer has never been asked. It is asked here.
565
+ */
566
+ projectFindSpy.mockResolvedValue(makeProject() as never);
567
+ paidChannelFindSpy(channelName).mockResolvedValue(method as never);
568
+
569
+ const result: FallbackNotificationResult = await runFallback();
570
+
571
+ expect(paidChannelFindSpy(channelName)).not.toHaveBeenCalled();
572
+ expect(result.channelsUsed).toEqual([]);
573
+ expect(result.notified).toBe(false);
574
+ expect(result.outcome).toBe(
575
+ FallbackNotificationOutcome.NoUsableNotificationMethod,
576
+ );
577
+ expect(deliverSpy).not.toHaveBeenCalled();
578
+ },
579
+ );
580
+
581
+ test("with everything available and everything enabled, SMS wins and the rest are never queried", async () => {
582
+ giveResponderEveryPaidMethod();
583
+ projectFindSpy.mockResolvedValue(
584
+ makeProject({
585
+ [SMS_FLAG]: true,
586
+ [CALL_FLAG]: true,
587
+ [WHATSAPP_FLAG]: true,
588
+ [TELEGRAM_FLAG]: true,
589
+ }) as never,
590
+ );
591
+
592
+ const result: FallbackNotificationResult = await runFallback();
593
+
594
+ expect(result.channelsUsed).toEqual(["SMS"]);
595
+ expect(callFindSpy).not.toHaveBeenCalled();
596
+ expect(whatsAppFindSpy).not.toHaveBeenCalled();
597
+ expect(telegramFindSpy).not.toHaveBeenCalled();
598
+ expect(webhookFindSpy).not.toHaveBeenCalled();
599
+ });
600
+
601
+ test.each<[string, Record<string, unknown>, string]>([
602
+ [
603
+ "SMS off",
604
+ { [CALL_FLAG]: true, [WHATSAPP_FLAG]: true, [TELEGRAM_FLAG]: true },
605
+ "Call",
606
+ ],
607
+ [
608
+ "SMS and Call off",
609
+ { [WHATSAPP_FLAG]: true, [TELEGRAM_FLAG]: true },
610
+ "WhatsApp",
611
+ ],
612
+ ["SMS, Call and WhatsApp off", { [TELEGRAM_FLAG]: true }, "Telegram"],
613
+ ["every paid channel off", {}, "Webhook"],
614
+ ])(
615
+ "with %s, the ladder falls through to %s",
616
+ async (
617
+ _label: string,
618
+ flags: Record<string, unknown>,
619
+ expectedChannel: string,
620
+ ): Promise<void> => {
621
+ giveResponderEveryPaidMethod();
622
+ projectFindSpy.mockResolvedValue(makeProject(flags) as never);
623
+
624
+ const result: FallbackNotificationResult = await runFallback();
625
+
626
+ expect(result.channelsUsed).toEqual([expectedChannel]);
627
+ },
628
+ );
629
+
630
+ test.each<[PaidChannelName, string, Record<string, unknown>]>([
631
+ ["SMS", SMS_FLAG, makeVerifiedSms()],
632
+ ["Call", CALL_FLAG, makeVerifiedCall()],
633
+ ["WhatsApp", WHATSAPP_FLAG, makeVerifiedWhatsApp()],
634
+ ["Telegram", TELEGRAM_FLAG, makeVerifiedTelegram()],
635
+ ])(
636
+ "the %s lookup is verification-gated and scoped to this responder",
637
+ async (
638
+ channelName: PaidChannelName,
639
+ flagKey: string,
640
+ method: Record<string, unknown>,
641
+ ): Promise<void> => {
642
+ projectFindSpy.mockResolvedValue(
643
+ makeProject({ [flagKey]: true }) as never,
644
+ );
645
+ paidChannelFindSpy(channelName).mockResolvedValue(method as never);
646
+
647
+ await runFallback();
648
+
649
+ const query: Record<string, unknown> = queryOf(
650
+ paidChannelFindSpy(channelName),
651
+ );
652
+ expect(query["isVerified"]).toBe(true);
653
+ expect((query["projectId"] as ObjectID).toString()).toBe(
654
+ PROJECT_ID.toString(),
655
+ );
656
+ expect((query["userId"] as ObjectID).toString()).toBe(
657
+ USER_ID.toString(),
658
+ );
659
+ },
660
+ );
661
+
662
+ test("the paid ladder is only reached when BOTH zero-cost channels are missing", async () => {
663
+ pushFindSpy.mockResolvedValue(makeVerifiedPush() as never);
664
+ giveResponderEveryPaidMethod();
665
+ projectFindSpy.mockResolvedValue(
666
+ makeProject({ [SMS_FLAG]: true }) as never,
667
+ );
668
+
669
+ const result: FallbackNotificationResult = await runFallback();
670
+
671
+ expect(result.channelsUsed).toEqual(["Push"]);
672
+ expect(smsFindSpy).not.toHaveBeenCalled();
673
+ });
674
+ });
675
+
676
+ /*
677
+ * ----------------------------------------------------------------------- *
678
+ * (A continued) Channel selection - the webhook backstop.
679
+ * -----------------------------------------------------------------------
680
+ */
681
+
682
+ describe("channel selection: the webhook backstop", () => {
683
+ test("a webhook qualifies on presence alone - it has no verification concept", async () => {
684
+ webhookFindSpy.mockResolvedValue(makeWebhook() as never);
685
+
686
+ const result: FallbackNotificationResult = await runFallback();
687
+
688
+ expect(result.channelsUsed).toEqual(["Webhook"]);
689
+ expect(result.outcome).toBe(FallbackNotificationOutcome.Delivered);
690
+ });
691
+
692
+ test("the webhook lookup carries NO isVerified predicate", async () => {
693
+ webhookFindSpy.mockResolvedValue(makeWebhook() as never);
694
+
695
+ await runFallback();
696
+
697
+ const query: Record<string, unknown> = queryOf(webhookFindSpy);
698
+ /*
699
+ * UserWebhook has no isVerified column. Adding the predicate to this
700
+ * query for symmetry with the others would match nothing at all, and the
701
+ * backstop would silently stop existing.
702
+ */
703
+ expect(Object.keys(query).sort()).toEqual(["projectId", "userId"]);
704
+ expect(query["isVerified"]).toBeUndefined();
705
+ });
706
+
707
+ test("a webhook is still used when every paid channel is switched off - there is no flag for it", async () => {
708
+ giveResponderEveryPaidMethod();
709
+ projectFindSpy.mockResolvedValue(makeProject() as never);
710
+
711
+ const result: FallbackNotificationResult = await runFallback();
712
+
713
+ expect(result.channelsUsed).toEqual(["Webhook"]);
714
+ for (const spy of everyPaidChannelFindSpy()) {
715
+ expect(spy).not.toHaveBeenCalled();
716
+ }
717
+ });
718
+
719
+ test("a project row that cannot be read bills nothing and still reaches the webhook", async () => {
720
+ projectFindSpy.mockResolvedValue(null as never);
721
+ giveResponderEveryPaidMethod();
722
+
723
+ const result: FallbackNotificationResult = await runFallback();
724
+
725
+ /*
726
+ * `project?.enableSmsNotifications` on a null project is falsy, so a
727
+ * deleted or unreadable project fails CLOSED for everything with a bill
728
+ * attached and open for the one channel that has none.
729
+ */
730
+ for (const spy of everyPaidChannelFindSpy()) {
731
+ expect(spy).not.toHaveBeenCalled();
732
+ }
733
+ expect(result.channelsUsed).toEqual(["Webhook"]);
734
+ });
735
+ });
736
+
737
+ /*
738
+ * ----------------------------------------------------------------------- *
739
+ * (B) One delivery call per channel - spec constraint 1.
740
+ * -----------------------------------------------------------------------
741
+ */
742
+
743
+ describe("one delivery call per channel", () => {
744
+ beforeEach(() => {
745
+ pushFindSpy.mockResolvedValue(makeVerifiedPush() as never);
746
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
747
+ });
748
+
749
+ test("two chosen channels means deliverNotificationForRule is called TWICE", async () => {
750
+ await runFallback();
751
+
752
+ /*
753
+ * Not once with two methods on one rule. deliverNotificationForRule keeps
754
+ * a single mutable UserOnCallLogTimeline instance across its channel
755
+ * blocks, and that instance carries an _id after the first create(); a
756
+ * second create() with it UPDATEs the first row instead of inserting a
757
+ * second. Two channels inside one call would therefore leave ONE timeline
758
+ * row describing the second page and no trace of the first.
759
+ */
760
+ expect(deliverSpy).toHaveBeenCalledTimes(2);
761
+ });
762
+
763
+ test("the two calls are handed DIFFERENT rule instances", async () => {
764
+ await runFallback();
765
+
766
+ const rules: Array<UserNotificationRule> = deliveredRules();
767
+ expect(rules).toHaveLength(2);
768
+ expect(rules[0]).not.toBe(rules[1]);
769
+ });
770
+
771
+ test("each rule carries exactly ONE method relation", async () => {
772
+ await runFallback();
773
+
774
+ const rules: Array<UserNotificationRule> = deliveredRules();
775
+ const pushRule: UserNotificationRule = rules[0]!;
776
+ const emailRule: UserNotificationRule = rules[1]!;
777
+
778
+ expect(pushRule.userPush).toBeDefined();
779
+ expect(pushRule.userEmail).toBeUndefined();
780
+ expect(pushRule.userEmailId).toBeUndefined();
781
+
782
+ expect(emailRule.userEmail).toBeDefined();
783
+ expect(emailRule.userPush).toBeUndefined();
784
+ expect(emailRule.userPushId).toBeUndefined();
785
+ });
786
+
787
+ test("no rule ever carries a second channel's method (the multiplexed-rule regression)", async () => {
788
+ await runFallback();
789
+
790
+ for (const rule of deliveredRules()) {
791
+ const methodsOnRule: Array<unknown> = [
792
+ rule.userEmail,
793
+ rule.userPush,
794
+ rule.userSms,
795
+ rule.userCall,
796
+ rule.userWhatsApp,
797
+ rule.userTelegram,
798
+ rule.userWebhook,
799
+ ].filter((method: unknown): boolean => {
800
+ return Boolean(method);
801
+ });
802
+
803
+ expect(methodsOnRule).toHaveLength(1);
804
+ }
805
+ });
806
+
807
+ test("both calls receive the very same options bundle the caller handed in", async () => {
808
+ const options: ExecuteFallbackNotificationOptions = fallbackOptions();
809
+
810
+ await UserNotificationRuleService.executeFallbackNotification(options);
811
+
812
+ const forwarded: Array<ExecuteNotificationRuleOptions> =
813
+ deliveredOptions();
814
+ /*
815
+ * Identity, not shape: everything that attributes the page back to its
816
+ * escalation (the incident id, the policy, the escalation rule, the
817
+ * on-call timeline row) rides in this object, and both channels have to
818
+ * describe the same page.
819
+ */
820
+ expect(forwarded[0]).toBe(options);
821
+ expect(forwarded[1]).toBe(options);
822
+ });
823
+
824
+ test("a single chosen channel produces exactly one call", async () => {
825
+ pushFindSpy.mockResolvedValue(null as never);
826
+
827
+ await runFallback();
828
+
829
+ expect(deliverSpy).toHaveBeenCalledTimes(1);
830
+ });
831
+ });
832
+
833
+ /*
834
+ * ----------------------------------------------------------------------- *
835
+ * (C) The unsaved rule.
836
+ * -----------------------------------------------------------------------
837
+ */
838
+
839
+ describe("the rule handed to delivery is built in memory and never saved", () => {
840
+ test("the method RELATION is populated, not merely the foreign key", async () => {
841
+ const email: Record<string, unknown> = makeVerifiedEmail();
842
+ emailFindSpy.mockResolvedValue(email as never);
843
+
844
+ await runFallback();
845
+
846
+ const rule: UserNotificationRule = deliveredRules()[0]!;
847
+
848
+ /*
849
+ * Every channel block reads the relation (`userEmail?.email &&
850
+ * userEmail?.isVerified`) and never dereferences the id. A rule carrying
851
+ * only userEmailId would sail through delivery sending absolutely
852
+ * nothing, and the fallback would then report a channel it never used.
853
+ */
854
+ expect(rule.userEmail).toBe(email);
855
+ expect(rule.userEmail!.email!.toString()).toBe(RESPONDER_EMAIL);
856
+ expect(rule.userEmail!.isVerified).toBe(true);
857
+ // ...and the FK too, so the row is self-consistent if it is ever read.
858
+ expect(rule.userEmailId!.toString()).toBe(EMAIL_METHOD_ID.toString());
859
+ });
860
+
861
+ test("the push relation is populated with the device the sender needs", async () => {
862
+ const push: Record<string, unknown> = makeVerifiedPush();
863
+ pushFindSpy.mockResolvedValue(push as never);
864
+
865
+ await runFallback();
866
+
867
+ const rule: UserNotificationRule = deliveredRules()[0]!;
868
+ expect(rule.userPush).toBe(push);
869
+ expect(rule.userPush!.deviceToken).toBe("device-token");
870
+ expect(rule.userPush!.isVerified).toBe(true);
871
+ expect(rule.userPushId!.toString()).toBe(PUSH_METHOD_ID.toString());
872
+ });
873
+
874
+ test("the rule carries the responder, the project, the rule type and notifyAfterMinutes 0", async () => {
875
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
876
+
877
+ await runFallback();
878
+
879
+ const rule: UserNotificationRule = deliveredRules()[0]!;
880
+ expect(rule.userId!.toString()).toBe(USER_ID.toString());
881
+ expect(rule.projectId!.toString()).toBe(PROJECT_ID.toString());
882
+ expect(rule.ruleType).toBe(
883
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
884
+ );
885
+ // Immediate: the page is already late by the time the fallback runs.
886
+ expect(rule.notifyAfterMinutes).toBe(0);
887
+ });
888
+
889
+ test("the rule type follows whatever the caller was paging for", async () => {
890
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
891
+
892
+ await runFallback({
893
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
894
+ });
895
+
896
+ expect(deliveredRules()[0]!.ruleType).toBe(
897
+ NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
898
+ );
899
+ });
900
+
901
+ test("the rule has no id, so no timeline row can point at a rule that does not exist", async () => {
902
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
903
+
904
+ await runFallback();
905
+
906
+ /*
907
+ * buildLogTimelineItem sets userNotificationRuleId only `if
908
+ * (notificationRuleItem.id)`. An id here would be a dangling FK on every
909
+ * timeline row the fallback writes.
910
+ */
911
+ expect(deliveredRules()[0]!.id).toBeNull();
912
+ });
913
+
914
+ test("no fallback rule is EVER persisted", async () => {
915
+ pushFindSpy.mockResolvedValue(makeVerifiedPush() as never);
916
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
917
+
918
+ await runFallback();
919
+
920
+ /*
921
+ * The responder did not ask for these rules. Saving them would silently
922
+ * rewrite their notification configuration, and the next page would find
923
+ * "matching rules" that a human never created.
924
+ */
925
+ expect(ruleCreateSpy).not.toHaveBeenCalled();
926
+ });
927
+ });
928
+
929
+ /*
930
+ * ----------------------------------------------------------------------- *
931
+ * (D) The claim.
932
+ * -----------------------------------------------------------------------
933
+ */
934
+
935
+ describe("the fallback claims the on-call log under a reserved key", () => {
936
+ test("the claim key is the literal __fallback__", async () => {
937
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
938
+
939
+ await runFallback();
940
+
941
+ expect(claimSpy).toHaveBeenCalledTimes(1);
942
+ const claimArg: { userOnCallLogId: ObjectID; claimKey: string } = claimSpy
943
+ .mock.calls[0]![0] as {
944
+ userOnCallLogId: ObjectID;
945
+ claimKey: string;
946
+ };
947
+
948
+ /*
949
+ * There is no rule to claim under - that is the entire reason the
950
+ * fallback is running - so it claims a reserved literal instead.
951
+ * executedNotificationRules is a jsonb map keyed by arbitrary text, so
952
+ * the literal sits beside real rule uuids and can never collide with one.
953
+ */
954
+ expect(claimArg.claimKey).toBe("__fallback__");
955
+ expect(claimArg.userOnCallLogId.toString()).toBe(LOG_ID.toString());
956
+ });
957
+
958
+ test("the exported claim key constant is that same literal", () => {
959
+ // Pinned because the string is the interop contract with existing rows.
960
+ expect(FALLBACK_NOTIFICATION_CLAIM_KEY).toBe("__fallback__");
961
+ });
962
+
963
+ test("the claim strictly precedes any method lookup", async () => {
964
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
965
+
966
+ await runFallback();
967
+
968
+ expect(claimSpy.mock.invocationCallOrder[0]).toBeLessThan(
969
+ emailFindSpy.mock.invocationCallOrder[0] as number,
970
+ );
971
+ });
972
+
973
+ test("a lost claim delivers nothing and does not even look for a method", async () => {
974
+ claimSpy.mockResolvedValue(false as never);
975
+ pushFindSpy.mockResolvedValue(makeVerifiedPush() as never);
976
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
977
+
978
+ const result: FallbackNotificationResult = await runFallback();
979
+
980
+ expect(deliverSpy).not.toHaveBeenCalled();
981
+ expect(pushFindSpy).not.toHaveBeenCalled();
982
+ expect(emailFindSpy).not.toHaveBeenCalled();
983
+ expect(result.notified).toBe(false);
984
+ expect(result.channelsUsed).toEqual([]);
985
+ });
986
+
987
+ test("a lost claim is DeliveryFailed, never NoUsableNotificationMethod", async () => {
988
+ claimSpy.mockResolvedValue(false as never);
989
+
990
+ const result: FallbackNotificationResult = await runFallback();
991
+
992
+ /*
993
+ * The distinction is load-bearing. NoUsableNotificationMethod makes the
994
+ * caller write a TERMINAL Error onto the log - "this responder is
995
+ * unreachable" - which would be stamped over a page another run has in
996
+ * flight right now.
997
+ */
998
+ expect(result.outcome).toBe(FallbackNotificationOutcome.DeliveryFailed);
999
+ expect(result.outcome).not.toBe(
1000
+ FallbackNotificationOutcome.NoUsableNotificationMethod,
1001
+ );
1002
+ });
1003
+
1004
+ test("of two concurrent fallbacks for one log, only the winner delivers", async () => {
1005
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
1006
+ claimSpy
1007
+ .mockResolvedValueOnce(true as never)
1008
+ .mockResolvedValueOnce(false as never);
1009
+
1010
+ const results: Array<FallbackNotificationResult> = await Promise.all([
1011
+ runFallback(),
1012
+ runFallback(),
1013
+ ]);
1014
+
1015
+ // One page, not two - the responder's phone buzzes once.
1016
+ expect(deliverSpy).toHaveBeenCalledTimes(1);
1017
+ expect(results[0]!.notified).toBe(true);
1018
+ expect(results[0]!.channelsUsed).toEqual(["Email"]);
1019
+ expect(results[1]!.notified).toBe(false);
1020
+ expect(results[1]!.channelsUsed).toEqual([]);
1021
+ expect(results[1]!.outcome).toBe(
1022
+ FallbackNotificationOutcome.DeliveryFailed,
1023
+ );
1024
+ });
1025
+ });
1026
+
1027
+ /*
1028
+ * ----------------------------------------------------------------------- *
1029
+ * (E) The three outcomes.
1030
+ * -----------------------------------------------------------------------
1031
+ */
1032
+
1033
+ describe("the outcome the caller branches on", () => {
1034
+ test("Delivered when at least one channel dispatched a page", async () => {
1035
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
1036
+
1037
+ const result: FallbackNotificationResult = await runFallback();
1038
+
1039
+ expect(result.outcome).toBe(FallbackNotificationOutcome.Delivered);
1040
+ expect(result.notified).toBe(true);
1041
+ expect(result.channelsUsed).toEqual(["Email"]);
1042
+ });
1043
+
1044
+ test("NoUsableNotificationMethod when the responder has nothing at all", async () => {
1045
+ const result: FallbackNotificationResult = await runFallback();
1046
+
1047
+ /*
1048
+ * The one permanent ending: a retry finds the same nothing, and only a
1049
+ * human adding a notification method changes it. It is the only outcome
1050
+ * the caller is allowed to turn into a terminal Error.
1051
+ */
1052
+ expect(result.outcome).toBe(
1053
+ FallbackNotificationOutcome.NoUsableNotificationMethod,
1054
+ );
1055
+ expect(result.notified).toBe(false);
1056
+ expect(result.channelsUsed).toEqual([]);
1057
+ expect(deliverSpy).not.toHaveBeenCalled();
1058
+ });
1059
+
1060
+ test("DeliveryFailed when the only chosen channel raised", async () => {
1061
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
1062
+ deliverSpy.mockRejectedValue(new Error("smtp exploded") as never);
1063
+
1064
+ const result: FallbackNotificationResult = await runFallback();
1065
+
1066
+ /*
1067
+ * There WAS something to try, so the responder is reachable and today
1068
+ * simply was not reached. Reporting this as NoUsableNotificationMethod
1069
+ * would tell the operator to go add a notification method that already
1070
+ * exists.
1071
+ */
1072
+ expect(result.outcome).toBe(FallbackNotificationOutcome.DeliveryFailed);
1073
+ expect(result.notified).toBe(false);
1074
+ expect(result.channelsUsed).toEqual([]);
1075
+ });
1076
+
1077
+ test("a raising channel does not take the fallback down with it", async () => {
1078
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
1079
+ deliverSpy.mockRejectedValue(new Error("smtp exploded") as never);
1080
+
1081
+ await expect(runFallback()).resolves.toBeDefined();
1082
+ });
1083
+
1084
+ test("DeliveryFailed when the chosen channel fell through its event-type guard", async () => {
1085
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
1086
+ deliverSpy.mockResolvedValue(false as never);
1087
+
1088
+ const result: FallbackNotificationResult = await runFallback();
1089
+
1090
+ /*
1091
+ * deliverNotificationForRule resolves perfectly happily when no channel
1092
+ * block claimed the event type - it writes an Error timeline row and
1093
+ * returns false. "Did not throw" is emphatically not "paged".
1094
+ */
1095
+ expect(result.outcome).toBe(FallbackNotificationOutcome.DeliveryFailed);
1096
+ expect(result.notified).toBe(false);
1097
+ });
1098
+
1099
+ test("a channel that dispatched NOTHING is not listed in channelsUsed", async () => {
1100
+ pushFindSpy.mockResolvedValue(makeVerifiedPush() as never);
1101
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
1102
+ // Push goes out; email falls through to the guard and sends nothing.
1103
+ deliverSpy
1104
+ .mockResolvedValueOnce(true as never)
1105
+ .mockResolvedValueOnce(false as never);
1106
+
1107
+ const result: FallbackNotificationResult = await runFallback();
1108
+
1109
+ /*
1110
+ * channelsUsed is read back to the operator verbatim as "notified via
1111
+ * fallback (Push, Email)". A name in there that nobody was actually
1112
+ * contacted on is a lie in the single place somebody looks to find out
1113
+ * whether the responder was reached.
1114
+ */
1115
+ expect(result.channelsUsed).toEqual(["Push"]);
1116
+ expect(result.channelsUsed).not.toContain("Email");
1117
+ expect(result.outcome).toBe(FallbackNotificationOutcome.Delivered);
1118
+ expect(result.notified).toBe(true);
1119
+ });
1120
+
1121
+ test("one channel raising does not stop the next channel from being tried", async () => {
1122
+ pushFindSpy.mockResolvedValue(makeVerifiedPush() as never);
1123
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
1124
+ deliverSpy
1125
+ .mockRejectedValueOnce(new Error("APNs down") as never)
1126
+ .mockResolvedValueOnce(true as never);
1127
+
1128
+ const result: FallbackNotificationResult = await runFallback();
1129
+
1130
+ expect(deliverSpy).toHaveBeenCalledTimes(2);
1131
+ expect(result.channelsUsed).toEqual(["Email"]);
1132
+ expect(result.outcome).toBe(FallbackNotificationOutcome.Delivered);
1133
+ expect(result.notified).toBe(true);
1134
+ });
1135
+
1136
+ test("notified always mirrors outcome === Delivered", async () => {
1137
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
1138
+
1139
+ const delivered: FallbackNotificationResult = await runFallback();
1140
+
1141
+ deliverSpy.mockResolvedValue(false as never);
1142
+ const failed: FallbackNotificationResult = await runFallback();
1143
+
1144
+ emailFindSpy.mockResolvedValue(null as never);
1145
+ const nothing: FallbackNotificationResult = await runFallback();
1146
+
1147
+ for (const result of [delivered, failed, nothing]) {
1148
+ expect(result.notified).toBe(
1149
+ result.outcome === FallbackNotificationOutcome.Delivered,
1150
+ );
1151
+ }
1152
+ });
1153
+
1154
+ test("an undelivered outcome always carries an empty channel list", async () => {
1155
+ emailFindSpy.mockResolvedValue(makeVerifiedEmail() as never);
1156
+ deliverSpy.mockResolvedValue(false as never);
1157
+
1158
+ const result: FallbackNotificationResult = await runFallback();
1159
+
1160
+ expect(result.notified).toBe(false);
1161
+ expect(result.channelsUsed).toEqual([]);
1162
+ });
1163
+ });
1164
+ });
1165
+
1166
+ /*
1167
+ * ------------------------------------------------------------------------- *
1168
+ * (F) The wiring: what UserOnCallLogService does with all of that.
1169
+ * -------------------------------------------------------------------------
1170
+ */
1171
+
1172
+ describe("UserOnCallLogService.handleNoMatchingNotificationRule", () => {
1173
+ let logUpdateSpy: jest.SpyInstance;
1174
+ let timelineUpdateSpy: jest.SpyInstance;
1175
+ let optOutFindSpy: jest.SpyInstance;
1176
+ let projectFindSpy: jest.SpyInstance;
1177
+ let fallbackSpy: jest.SpyInstance;
1178
+ let userFindSpy: jest.SpyInstance;
1179
+ let alertOwnersSpy: jest.SpyInstance;
1180
+ let loggerErrorSpy: jest.SpyInstance;
1181
+
1182
+ interface StatusUpdateCall {
1183
+ id: ObjectID;
1184
+ data: {
1185
+ status?: UserNotificationExecutionStatus | undefined;
1186
+ statusMessage?: string | undefined;
1187
+ };
1188
+ props: { isRoot: boolean };
1189
+ }
1190
+
1191
+ interface TimelineUpdateCall {
1192
+ id: ObjectID;
1193
+ data: {
1194
+ status?: OnCallDutyExecutionLogTimelineStatus | undefined;
1195
+ statusMessage?: string | undefined;
1196
+ };
1197
+ props: { isRoot: boolean };
1198
+ }
1199
+
1200
+ interface UndeliverableAlertCall {
1201
+ projectId: ObjectID;
1202
+ userId: ObjectID;
1203
+ ruleType: NotificationRuleType;
1204
+ severityName: string;
1205
+ onCallDutyPolicyId: ObjectID | undefined;
1206
+ fallbackChannelsUsed: Array<string>;
1207
+ }
1208
+
1209
+ function makeCreatedLog(): UserOnCallLog {
1210
+ return {
1211
+ id: LOG_ID,
1212
+ _id: LOG_ID.toString(),
1213
+ projectId: PROJECT_ID,
1214
+ userId: USER_ID,
1215
+ userNotificationEventType: UserNotificationEventType.IncidentCreated,
1216
+ triggeredByIncidentId: INCIDENT_ID,
1217
+ onCallDutyPolicyId: POLICY_ID,
1218
+ onCallDutyPolicyExecutionLogTimelineId: TIMELINE_ID,
1219
+ } as unknown as UserOnCallLog;
1220
+ }
1221
+
1222
+ function callHandleNoMatchingNotificationRule(): Promise<void> {
1223
+ return (
1224
+ UserOnCallLogService as unknown as UserOnCallLogServiceInternals
1225
+ ).handleNoMatchingNotificationRule({
1226
+ createdItem: makeCreatedLog(),
1227
+ notificationRuleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1228
+ incidentSeverityId: INCIDENT_SEVERITY_ID,
1229
+ alertSeverityId: undefined,
1230
+ severityName: SEVERITY_NAME,
1231
+ });
1232
+ }
1233
+
1234
+ function statusUpdates(): Array<StatusUpdateCall> {
1235
+ return logUpdateSpy.mock.calls.map(
1236
+ (call: Array<unknown>): StatusUpdateCall => {
1237
+ return call[0] as StatusUpdateCall;
1238
+ },
1239
+ );
1240
+ }
1241
+
1242
+ function timelineUpdates(): Array<TimelineUpdateCall> {
1243
+ return timelineUpdateSpy.mock.calls.map(
1244
+ (call: Array<unknown>): TimelineUpdateCall => {
1245
+ return call[0] as TimelineUpdateCall;
1246
+ },
1247
+ );
1248
+ }
1249
+
1250
+ function lastStatusUpdate(): StatusUpdateCall {
1251
+ const updates: Array<StatusUpdateCall> = statusUpdates();
1252
+
1253
+ return updates[updates.length - 1]!;
1254
+ }
1255
+
1256
+ beforeEach(() => {
1257
+ loggerErrorSpy = jest
1258
+ .spyOn(logger, "error")
1259
+ .mockImplementation((): void => {
1260
+ return undefined;
1261
+ });
1262
+
1263
+ logUpdateSpy = jest
1264
+ .spyOn(UserOnCallLogService, "updateOneById")
1265
+ .mockResolvedValue(undefined as never);
1266
+
1267
+ timelineUpdateSpy = jest
1268
+ .spyOn(OnCallDutyPolicyExecutionLogTimelineService, "updateOneById")
1269
+ .mockResolvedValue(undefined as never);
1270
+
1271
+ // No opt-out row: the interesting default is the one where a page is due.
1272
+ optOutFindSpy = jest
1273
+ .spyOn(UserNotificationRuleService, "findOneBy")
1274
+ .mockResolvedValue(null as never);
1275
+
1276
+ // Fallback enabled, which is the product default.
1277
+ projectFindSpy = jest
1278
+ .spyOn(ProjectService, "findOneById")
1279
+ .mockResolvedValue(makeProject() as never);
1280
+
1281
+ fallbackSpy = jest
1282
+ .spyOn(UserNotificationRuleService, "executeFallbackNotification")
1283
+ .mockResolvedValue({
1284
+ outcome: FallbackNotificationOutcome.Delivered,
1285
+ notified: true,
1286
+ channelsUsed: ["Push", "Email"],
1287
+ } as never);
1288
+
1289
+ userFindSpy = jest.spyOn(UserService, "findOneById").mockResolvedValue({
1290
+ name: new Name(RESPONDER_NAME),
1291
+ } as never);
1292
+
1293
+ alertOwnersSpy = jest
1294
+ .spyOn(OnCallNotificationAlertingService, "notifyOfUndeliverablePage")
1295
+ .mockResolvedValue(undefined as never);
1296
+ });
1297
+
1298
+ afterEach(() => {
1299
+ jest.restoreAllMocks();
1300
+ });
1301
+
1302
+ describe("an opt-out row means deliberate silence", () => {
1303
+ test("the log is Completed and the timeline says Skipped", async () => {
1304
+ optOutFindSpy.mockResolvedValue({ _id: "opt-out-row" } as never);
1305
+
1306
+ await callHandleNoMatchingNotificationRule();
1307
+
1308
+ /*
1309
+ * Completed, not Error: the system did exactly what it was told to do.
1310
+ * Skipped rather than Notification Sent because nothing was in fact sent,
1311
+ * and an operator scanning the escalation has to be able to tell "muted
1312
+ * on purpose" from "delivered".
1313
+ */
1314
+ expect(lastStatusUpdate().data.status).toBe(
1315
+ UserNotificationExecutionStatus.Completed,
1316
+ );
1317
+ expect(lastStatusUpdate().data.statusMessage).toContain("opted out");
1318
+ expect(timelineUpdates()[0]!.data.status).toBe(
1319
+ OnCallDutyExecutionLogTimelineStatus.Skipped,
1320
+ );
1321
+ expect(timelineUpdates()[0]!.id.toString()).toBe(TIMELINE_ID.toString());
1322
+ });
1323
+
1324
+ test("the fallback is NEVER called, and the project is not even read", async () => {
1325
+ optOutFindSpy.mockResolvedValue({ _id: "opt-out-row" } as never);
1326
+
1327
+ await callHandleNoMatchingNotificationRule();
1328
+
1329
+ /*
1330
+ * The opt-out is checked FIRST, before the project setting and before any
1331
+ * delivery. Paging somebody who asked not to be paged is the one failure
1332
+ * mode the fallback could introduce, and this is what forecloses it.
1333
+ */
1334
+ expect(fallbackSpy).not.toHaveBeenCalled();
1335
+ expect(projectFindSpy).not.toHaveBeenCalled();
1336
+ });
1337
+
1338
+ test("nobody is alerted - an opt-out is not a misconfiguration", async () => {
1339
+ optOutFindSpy.mockResolvedValue({ _id: "opt-out-row" } as never);
1340
+
1341
+ await callHandleNoMatchingNotificationRule();
1342
+
1343
+ expect(alertOwnersSpy).not.toHaveBeenCalled();
1344
+ });
1345
+
1346
+ test("the opt-out lookup asks for exactly the cell that came up empty", async () => {
1347
+ await callHandleNoMatchingNotificationRule();
1348
+
1349
+ const query: Record<string, unknown> = (
1350
+ optOutFindSpy.mock.calls[0]![0] as { query: Record<string, unknown> }
1351
+ ).query;
1352
+
1353
+ expect((query["userId"] as ObjectID).toString()).toBe(USER_ID.toString());
1354
+ expect((query["projectId"] as ObjectID).toString()).toBe(
1355
+ PROJECT_ID.toString(),
1356
+ );
1357
+ expect(query["ruleType"]).toBe(
1358
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1359
+ );
1360
+ expect((query["incidentSeverityId"] as ObjectID).toString()).toBe(
1361
+ INCIDENT_SEVERITY_ID.toString(),
1362
+ );
1363
+ expect(query["isOptOut"]).toBe(true);
1364
+ });
1365
+ });
1366
+
1367
+ describe("disableOnCallNotificationFallback restores the old dead-end", () => {
1368
+ beforeEach(() => {
1369
+ projectFindSpy.mockResolvedValue(
1370
+ makeProject({ disableOnCallNotificationFallback: true }) as never,
1371
+ );
1372
+ });
1373
+
1374
+ test("the log and the timeline both carry the ORIGINAL sentence, verbatim", async () => {
1375
+ await callHandleNoMatchingNotificationRule();
1376
+
1377
+ /*
1378
+ * A project that sets this flag is asking for precisely the pre-fallback
1379
+ * behaviour, so it must receive precisely the pre-fallback words -
1380
+ * operators grep their execution logs for this sentence.
1381
+ */
1382
+ expect(lastStatusUpdate().data.status).toBe(
1383
+ UserNotificationExecutionStatus.Error,
1384
+ );
1385
+ expect(lastStatusUpdate().data.statusMessage).toBe(
1386
+ NO_NOTIFICATION_RULES_STATUS_MESSAGE,
1387
+ );
1388
+ expect(timelineUpdates()[0]!.data.status).toBe(
1389
+ OnCallDutyExecutionLogTimelineStatus.Error,
1390
+ );
1391
+ expect(timelineUpdates()[0]!.data.statusMessage).toBe(
1392
+ NO_NOTIFICATION_RULES_STATUS_MESSAGE,
1393
+ );
1394
+ });
1395
+
1396
+ test("the fallback is never called - the project is never billed for one", async () => {
1397
+ await callHandleNoMatchingNotificationRule();
1398
+
1399
+ expect(fallbackSpy).not.toHaveBeenCalled();
1400
+ });
1401
+
1402
+ test("the owner alert IS still fired, with an empty channel list", async () => {
1403
+ await callHandleNoMatchingNotificationRule();
1404
+
1405
+ /*
1406
+ * This one is a real bug that was written and then fixed: the early
1407
+ * return sat ABOVE the alert, so the owner notification reached every
1408
+ * project except the one that needs it most. A project that has switched
1409
+ * the fallback off is the population still losing pages exactly the way
1410
+ * they were lost before any of this existed.
1411
+ */
1412
+ expect(alertOwnersSpy).toHaveBeenCalledTimes(1);
1413
+
1414
+ const alerted: UndeliverableAlertCall = alertOwnersSpy.mock
1415
+ .calls[0]![0] as UndeliverableAlertCall;
1416
+ expect(alerted.projectId.toString()).toBe(PROJECT_ID.toString());
1417
+ expect(alerted.userId.toString()).toBe(USER_ID.toString());
1418
+ expect(alerted.ruleType).toBe(
1419
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1420
+ );
1421
+ expect(alerted.severityName).toBe(SEVERITY_NAME);
1422
+ expect(alerted.onCallDutyPolicyId!.toString()).toBe(POLICY_ID.toString());
1423
+ // Empty means "this responder was not reached at all".
1424
+ expect(alerted.fallbackChannelsUsed).toEqual([]);
1425
+ });
1426
+ });
1427
+
1428
+ describe("the fallback delivered", () => {
1429
+ test("the log is left Executing, not Completed and not Error", async () => {
1430
+ await callHandleNoMatchingNotificationRule();
1431
+
1432
+ /*
1433
+ * Executing matches the normal path: there are no further rules to fire,
1434
+ * so ExecutePendingExecutions finds an empty rule list on its next tick
1435
+ * and closes the log out as Completed.
1436
+ */
1437
+ expect(lastStatusUpdate().data.status).toBe(
1438
+ UserNotificationExecutionStatus.Executing,
1439
+ );
1440
+ });
1441
+
1442
+ test("the message names the severity and the channels that carried the page", async () => {
1443
+ await callHandleNoMatchingNotificationRule();
1444
+
1445
+ const message: string | undefined = lastStatusUpdate().data.statusMessage;
1446
+ expect(message).toContain(
1447
+ `No notification rule configured for ${SEVERITY_NAME}`,
1448
+ );
1449
+ expect(message).toContain("notified via fallback (Push, Email)");
1450
+ // The old dead-end sentence is not written on this path at all.
1451
+ expect(message).not.toBe(NO_NOTIFICATION_RULES_STATUS_MESSAGE);
1452
+ });
1453
+
1454
+ test("the timeline says Notification Sent with the SAME message as the log", async () => {
1455
+ await callHandleNoMatchingNotificationRule();
1456
+
1457
+ expect(timelineUpdates()).toHaveLength(1);
1458
+ expect(timelineUpdates()[0]!.data.status).toBe(
1459
+ OnCallDutyExecutionLogTimelineStatus.NotificationSent,
1460
+ );
1461
+ // Two records of one event must not tell an operator two stories.
1462
+ expect(timelineUpdates()[0]!.data.statusMessage).toBe(
1463
+ lastStatusUpdate().data.statusMessage,
1464
+ );
1465
+ });
1466
+
1467
+ test("the fallback is handed this log, this responder and this severity", async () => {
1468
+ await callHandleNoMatchingNotificationRule();
1469
+
1470
+ expect(fallbackSpy).toHaveBeenCalledTimes(1);
1471
+ const options: Record<string, unknown> = fallbackSpy.mock
1472
+ .calls[0]![0] as Record<string, unknown>;
1473
+
1474
+ expect((options["userId"] as ObjectID).toString()).toBe(
1475
+ USER_ID.toString(),
1476
+ );
1477
+ expect((options["projectId"] as ObjectID).toString()).toBe(
1478
+ PROJECT_ID.toString(),
1479
+ );
1480
+ expect((options["userOnCallLogId"] as ObjectID).toString()).toBe(
1481
+ LOG_ID.toString(),
1482
+ );
1483
+ expect((options["userNotificationLogId"] as ObjectID).toString()).toBe(
1484
+ LOG_ID.toString(),
1485
+ );
1486
+ expect(options["ruleType"]).toBe(
1487
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1488
+ );
1489
+ expect(options["severityName"]).toBe(SEVERITY_NAME);
1490
+ expect(options["userNotificationEventType"]).toBe(
1491
+ UserNotificationEventType.IncidentCreated,
1492
+ );
1493
+ expect((options["triggeredByIncidentId"] as ObjectID).toString()).toBe(
1494
+ INCIDENT_ID.toString(),
1495
+ );
1496
+ });
1497
+
1498
+ test("the humans are still told - a delivered fallback is still a misconfiguration", async () => {
1499
+ await callHandleNoMatchingNotificationRule();
1500
+
1501
+ expect(alertOwnersSpy).toHaveBeenCalledTimes(1);
1502
+ const alerted: UndeliverableAlertCall = alertOwnersSpy.mock
1503
+ .calls[0]![0] as UndeliverableAlertCall;
1504
+ /*
1505
+ * A non-empty list is the "your configuration is wrong but nobody was
1506
+ * dropped" signal, and it is what tells the two branches apart at the
1507
+ * single call site.
1508
+ */
1509
+ expect(alerted.fallbackChannelsUsed).toEqual(["Push", "Email"]);
1510
+ });
1511
+ });
1512
+
1513
+ describe("the fallback found nothing to page the responder on", () => {
1514
+ beforeEach(() => {
1515
+ fallbackSpy.mockResolvedValue({
1516
+ outcome: FallbackNotificationOutcome.NoUsableNotificationMethod,
1517
+ notified: false,
1518
+ channelsUsed: [],
1519
+ } as never);
1520
+ });
1521
+
1522
+ test("the log ends terminally, naming the responder and what to do about it", async () => {
1523
+ await callHandleNoMatchingNotificationRule();
1524
+
1525
+ expect(lastStatusUpdate().data.status).toBe(
1526
+ UserNotificationExecutionStatus.Error,
1527
+ );
1528
+ expect(lastStatusUpdate().data.statusMessage).toContain(RESPONDER_NAME);
1529
+ expect(lastStatusUpdate().data.statusMessage).toContain(
1530
+ "has no verified notification method",
1531
+ );
1532
+ expect(lastStatusUpdate().data.statusMessage).toContain(
1533
+ "User Settings > Notification Methods",
1534
+ );
1535
+ expect(timelineUpdates()[0]!.data.status).toBe(
1536
+ OnCallDutyExecutionLogTimelineStatus.Error,
1537
+ );
1538
+ });
1539
+
1540
+ test("a responder whose name cannot be read degrades to a neutral phrase", async () => {
1541
+ userFindSpy.mockResolvedValue(null as never);
1542
+
1543
+ await callHandleNoMatchingNotificationRule();
1544
+
1545
+ /*
1546
+ * The lookup only ever feeds a string, so a deleted user must not cost a
1547
+ * status message - let alone throw out of the paging path.
1548
+ */
1549
+ expect(lastStatusUpdate().data.statusMessage).toContain("This responder");
1550
+ expect(lastStatusUpdate().data.status).toBe(
1551
+ UserNotificationExecutionStatus.Error,
1552
+ );
1553
+ });
1554
+
1555
+ test("the owners hear about it with an empty channel list", async () => {
1556
+ await callHandleNoMatchingNotificationRule();
1557
+
1558
+ const alerted: UndeliverableAlertCall = alertOwnersSpy.mock
1559
+ .calls[0]![0] as UndeliverableAlertCall;
1560
+ expect(alerted.fallbackChannelsUsed).toEqual([]);
1561
+ });
1562
+ });
1563
+
1564
+ describe("the fallback tried and failed", () => {
1565
+ test("a DeliveryFailed outcome does NOT claim the responder is unreachable", async () => {
1566
+ fallbackSpy.mockResolvedValue({
1567
+ outcome: FallbackNotificationOutcome.DeliveryFailed,
1568
+ notified: false,
1569
+ channelsUsed: [],
1570
+ } as never);
1571
+
1572
+ await callHandleNoMatchingNotificationRule();
1573
+
1574
+ /*
1575
+ * Sending the operator after a notification method that already exists
1576
+ * wastes the one action the message is supposed to buy.
1577
+ */
1578
+ expect(lastStatusUpdate().data.statusMessage).toContain(
1579
+ "the fallback delivery attempt failed",
1580
+ );
1581
+ expect(lastStatusUpdate().data.statusMessage).not.toContain(
1582
+ "has no verified notification method",
1583
+ );
1584
+ expect(lastStatusUpdate().data.status).toBe(
1585
+ UserNotificationExecutionStatus.Error,
1586
+ );
1587
+ });
1588
+
1589
+ test("a fallback that THREW still ends the log at Error, never at Executing", async () => {
1590
+ fallbackSpy.mockRejectedValue(new Error("smtp exploded") as never);
1591
+
1592
+ await callHandleNoMatchingNotificationRule();
1593
+
1594
+ /*
1595
+ * REGRESSION GUARD, and the reason this test exists at all: leaving the
1596
+ * log Executing here looks like the kind thing to do - "transient
1597
+ * failure, let the worker retry" - and it is a worse bug than the one it
1598
+ * avoids. Nothing re-enters this function; it runs once, from
1599
+ * onCreateSuccess, at the moment the row is written. So the next
1600
+ * ExecutePendingExecutions tick picks the log up, queries for due rules,
1601
+ * finds none (there were never any - that is why we are here), takes its
1602
+ * all-executed path and marks the log COMPLETED. onUpdateSuccess then
1603
+ * translates Completed into Notification Sent on the escalation timeline,
1604
+ * and within a minute the record reads "Alert Sent" for a page that was
1605
+ * never delivered.
1606
+ */
1607
+ expect(lastStatusUpdate().data.status).toBe(
1608
+ UserNotificationExecutionStatus.Error,
1609
+ );
1610
+ expect(lastStatusUpdate().data.status).not.toBe(
1611
+ UserNotificationExecutionStatus.Executing,
1612
+ );
1613
+ expect(timelineUpdates()[0]!.data.status).toBe(
1614
+ OnCallDutyExecutionLogTimelineStatus.Error,
1615
+ );
1616
+ expect(timelineUpdates()[0]!.data.statusMessage).toBe(
1617
+ lastStatusUpdate().data.statusMessage,
1618
+ );
1619
+ });
1620
+
1621
+ test("a fallback that threw is contained: the escalation is not taken down with it", async () => {
1622
+ fallbackSpy.mockRejectedValue(new Error("smtp exploded") as never);
1623
+
1624
+ /*
1625
+ * The caller is part-way through paging an entire escalation level. A
1626
+ * throw out of here would abort every responder queued behind this one.
1627
+ */
1628
+ await expect(
1629
+ callHandleNoMatchingNotificationRule(),
1630
+ ).resolves.toBeUndefined();
1631
+ expect(loggerErrorSpy).toHaveBeenCalled();
1632
+ });
1633
+
1634
+ test("a fallback that threw still tells the humans", async () => {
1635
+ fallbackSpy.mockRejectedValue(new Error("smtp exploded") as never);
1636
+
1637
+ await callHandleNoMatchingNotificationRule();
1638
+
1639
+ expect(alertOwnersSpy).toHaveBeenCalledTimes(1);
1640
+ const alerted: UndeliverableAlertCall = alertOwnersSpy.mock
1641
+ .calls[0]![0] as UndeliverableAlertCall;
1642
+ expect(alerted.fallbackChannelsUsed).toEqual([]);
1643
+ });
1644
+ });
1645
+
1646
+ describe("the owner alert is fire-and-forget", () => {
1647
+ test("a REJECTING notifyOfUndeliverablePage does not reject the paging path", async () => {
1648
+ alertOwnersSpy.mockRejectedValue(
1649
+ new Error("owner lookup exploded") as never,
1650
+ );
1651
+
1652
+ /*
1653
+ * The alert is an explanation of a delivery problem. A bug in the
1654
+ * explanation must never propagate into - or delay - the paging path that
1655
+ * produced it.
1656
+ */
1657
+ await expect(
1658
+ callHandleNoMatchingNotificationRule(),
1659
+ ).resolves.toBeUndefined();
1660
+
1661
+ await flushMicrotasks();
1662
+
1663
+ // Swallowed into the log rather than lost silently.
1664
+ expect(loggerErrorSpy).toHaveBeenCalled();
1665
+ });
1666
+
1667
+ test("a rejecting alert does not disturb the statuses that were already written", async () => {
1668
+ alertOwnersSpy.mockRejectedValue(
1669
+ new Error("owner lookup exploded") as never,
1670
+ );
1671
+
1672
+ await callHandleNoMatchingNotificationRule();
1673
+ await flushMicrotasks();
1674
+
1675
+ expect(lastStatusUpdate().data.status).toBe(
1676
+ UserNotificationExecutionStatus.Executing,
1677
+ );
1678
+ expect(timelineUpdates()[0]!.data.status).toBe(
1679
+ OnCallDutyExecutionLogTimelineStatus.NotificationSent,
1680
+ );
1681
+ });
1682
+
1683
+ test("the alert is NOT awaited - the paging path returns while it is still pending", async () => {
1684
+ let release: () => void = (): void => {
1685
+ return undefined;
1686
+ };
1687
+ const pending: Promise<void> = new Promise<void>(
1688
+ (resolve: () => void): void => {
1689
+ release = resolve;
1690
+ },
1691
+ );
1692
+ alertOwnersSpy.mockReturnValue(pending);
1693
+
1694
+ /*
1695
+ * If the alert were awaited, this await would never settle and the test
1696
+ * would time out. Reaching the next line IS the assertion.
1697
+ */
1698
+ await callHandleNoMatchingNotificationRule();
1699
+
1700
+ expect(alertOwnersSpy).toHaveBeenCalledTimes(1);
1701
+
1702
+ release();
1703
+ await pending;
1704
+ });
1705
+ });
1706
+
1707
+ describe("the writes the no-rule path makes", () => {
1708
+ test("the log is written before the on-call timeline", async () => {
1709
+ await callHandleNoMatchingNotificationRule();
1710
+
1711
+ /*
1712
+ * Order matters: updateOneById's own onUpdateSuccess hook maps the
1713
+ * execution status onto the timeline, and the explicit second write is
1714
+ * what lets this path say something the mapping cannot - Skipped for an
1715
+ * opt-out, Notification Sent under an Executing log.
1716
+ */
1717
+ expect(logUpdateSpy.mock.invocationCallOrder[0]).toBeLessThan(
1718
+ timelineUpdateSpy.mock.invocationCallOrder[0] as number,
1719
+ );
1720
+ });
1721
+
1722
+ test("exactly one log write and one timeline write, both as root", async () => {
1723
+ await callHandleNoMatchingNotificationRule();
1724
+
1725
+ expect(statusUpdates()).toHaveLength(1);
1726
+ expect(timelineUpdates()).toHaveLength(1);
1727
+ expect(statusUpdates()[0]!.id.toString()).toBe(LOG_ID.toString());
1728
+ expect(statusUpdates()[0]!.props.isRoot).toBe(true);
1729
+ expect(timelineUpdates()[0]!.id.toString()).toBe(TIMELINE_ID.toString());
1730
+ expect(timelineUpdates()[0]!.props.isRoot).toBe(true);
1731
+ });
1732
+
1733
+ test("the timeline never says the generic 'Alert Sent' on this path", async () => {
1734
+ await callHandleNoMatchingNotificationRule();
1735
+
1736
+ /*
1737
+ * The normal path writes "Alert Sent". A fallback delivery deliberately
1738
+ * writes something an operator can act on instead, so it can never be
1739
+ * mistaken for a configured one.
1740
+ */
1741
+ expect(timelineUpdates()[0]!.data.statusMessage).not.toBe("Alert Sent");
1742
+ });
1743
+ });
1744
+ });