@oneuptime/common 12.0.7 → 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 (873) 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} +184 -9
  9. package/Models/DatabaseModels/InventoryItemCustomField.ts +434 -0
  10. package/Models/DatabaseModels/{TelemetryEntityRelationship.ts → InventoryItemRelationship.ts} +33 -7
  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/WorkflowLog.ts +38 -0
  27. package/Models/DatabaseModels/WorkflowVariable.ts +12 -0
  28. package/Server/API/AIChatAPI.ts +315 -1
  29. package/Server/API/DashboardAPI.ts +217 -1
  30. package/Server/API/OnCallReadinessAPI.ts +841 -0
  31. package/Server/API/TeamComplianceAPI.ts +69 -17
  32. package/Server/API/TelemetryAPI.ts +220 -12
  33. package/Server/API/UserAPI.ts +16 -1
  34. package/Server/EnvironmentConfig.ts +52 -0
  35. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +22 -0
  36. package/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.ts +4 -4
  37. package/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.ts +5 -5
  38. package/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.ts +41 -0
  39. package/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.ts +17 -0
  40. package/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.ts +35 -0
  41. package/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.ts +82 -0
  42. package/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.ts +91 -0
  43. package/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.ts +47 -0
  44. package/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.ts +255 -0
  45. package/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.ts +107 -0
  46. package/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.ts +90 -0
  47. package/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.ts +39 -0
  48. package/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.ts +33 -0
  49. package/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.ts +59 -0
  50. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +24 -0
  51. package/Server/Infrastructure/Queue.ts +78 -13
  52. package/Server/Middleware/MasterAdminAuthorization.ts +11 -6
  53. package/Server/Middleware/PublicDashboardRateLimit.ts +593 -0
  54. package/Server/Services/AIService.ts +7 -0
  55. package/Server/Services/AlertEpisodeStateTimelineService.ts +29 -0
  56. package/Server/Services/AlertSeverityService.ts +63 -0
  57. package/Server/Services/DashboardService.ts +9 -10
  58. package/Server/Services/DatabaseService.ts +32 -2
  59. package/Server/Services/IncidentEpisodeStateTimelineService.ts +29 -0
  60. package/Server/Services/IncidentSeverityService.ts +76 -0
  61. package/Server/Services/Index.ts +12 -4
  62. package/Server/Services/InventoryItemCustomFieldService.ts +9 -0
  63. package/Server/Services/{TelemetryEntityRelationshipService.ts → InventoryItemRelationshipService.ts} +7 -4
  64. package/Server/Services/{TelemetryEntityService.ts → InventoryItemService.ts} +203 -21
  65. package/Server/Services/LogAggregationService.ts +45 -8
  66. package/Server/Services/MetricAggregationService.ts +121 -0
  67. package/Server/Services/MetricService.ts +7 -7
  68. package/Server/Services/NetworkDeviceLinkRuleService.ts +10 -0
  69. package/Server/Services/NetworkDeviceLinkService.ts +84 -0
  70. package/Server/Services/NetworkDeviceService.ts +140 -0
  71. package/Server/Services/NetworkSiteService.ts +77 -25
  72. package/Server/Services/NetworkTopologySuppressionService.ts +84 -0
  73. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +41 -29
  74. package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +8 -0
  75. package/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.ts +62 -13
  76. package/Server/Services/OnCallDutyPolicyScheduleService.ts +61 -1
  77. package/Server/Services/OnCallNotificationAlertingService.ts +742 -0
  78. package/Server/Services/OnCallReadinessService.ts +2803 -0
  79. package/Server/Services/OnCallSetupReminderService.ts +955 -0
  80. package/Server/Services/ProfileAggregationService.ts +123 -0
  81. package/Server/Services/StatusPageService.ts +9 -10
  82. package/Server/Services/TeamComplianceService.ts +429 -252
  83. package/Server/Services/UserCallService.ts +26 -1
  84. package/Server/Services/UserEmailService.ts +26 -1
  85. package/Server/Services/UserNotificationRuleAdminService.ts +1183 -0
  86. package/Server/Services/UserNotificationRuleService.ts +3812 -333
  87. package/Server/Services/UserOnCallLogService.ts +561 -48
  88. package/Server/Services/UserPushService.ts +29 -0
  89. package/Server/Services/UserService.ts +11 -0
  90. package/Server/Services/UserSmsService.ts +26 -1
  91. package/Server/Services/UserTelegramService.ts +24 -1
  92. package/Server/Services/UserWebhookService.ts +28 -1
  93. package/Server/Services/UserWhatsAppService.ts +24 -1
  94. package/Server/Types/Database/Permissions/BasePermission.ts +19 -0
  95. package/Server/Types/Database/Permissions/CreatePermission.ts +164 -0
  96. package/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.ts +340 -0
  97. package/Server/Types/Database/Permissions/QueryPermission.ts +48 -0
  98. package/Server/Types/Database/Permissions/TenantPermission.ts +8 -1
  99. package/Server/Types/Workflow/Components/API/Delete.ts +1 -1
  100. package/Server/Types/Workflow/Components/API/Get.ts +1 -1
  101. package/Server/Types/Workflow/Components/API/Patch.ts +1 -1
  102. package/Server/Types/Workflow/Components/API/Post.ts +1 -1
  103. package/Server/Types/Workflow/Components/API/Put.ts +1 -1
  104. package/Server/Types/Workflow/Components/API/Utils.ts +44 -1
  105. package/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.ts +29 -5
  106. package/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.ts +18 -10
  107. package/Server/Types/Workflow/Components/BaseModel/ModelArguments.ts +55 -0
  108. package/Server/Types/Workflow/Components/Conditions/IfElse.ts +3 -17
  109. package/Server/Types/Workflow/Components/Email.ts +25 -7
  110. package/Server/Types/Workflow/Components/JavaScript.ts +10 -3
  111. package/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.ts +1 -1
  112. package/Server/Types/Workflow/TriggerCode.ts +12 -0
  113. package/Server/Types/Workflow/Workflow.ts +5 -0
  114. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +643 -48
  115. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +32 -3
  116. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +20 -6
  117. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +7 -0
  118. package/Server/Utils/AI/Toolbox/AIActionTools.ts +2 -2
  119. package/Server/Utils/AI/Toolbox/AIMetaTools.ts +863 -0
  120. package/Server/Utils/AI/Toolbox/AlertTools.ts +177 -15
  121. package/Server/Utils/AI/Toolbox/IncidentTools.ts +191 -10
  122. package/Server/Utils/AI/Toolbox/Index.ts +48 -0
  123. package/Server/Utils/AI/Toolbox/MonitorTools.ts +298 -11
  124. package/Server/Utils/AI/Toolbox/NoteWriteTools.ts +295 -0
  125. package/Server/Utils/AI/Toolbox/OnCallTools.ts +1246 -0
  126. package/Server/Utils/AI/Toolbox/RunbookTools.ts +424 -0
  127. package/Server/Utils/AI/Toolbox/SloTools.ts +456 -0
  128. package/Server/Utils/AI/Toolbox/StatusPageTools.ts +559 -0
  129. package/Server/Utils/AI/Toolbox/TeamTools.ts +327 -0
  130. package/Server/Utils/AI/Toolbox/TimelineTools.ts +615 -0
  131. package/Server/Utils/AI/Toolbox/WorkflowProbeTools.ts +664 -0
  132. package/Server/Utils/ClientIp.ts +221 -0
  133. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +47 -0
  134. package/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.ts +163 -0
  135. package/Server/Utils/Dashboard/PublicDashboardSloWidget.ts +147 -0
  136. package/Server/Utils/Express.ts +12 -17
  137. package/Server/Utils/LLM/LLMService.ts +85 -8
  138. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +204 -10
  139. package/Server/Utils/SSRFProtection.ts +98 -23
  140. package/Server/Utils/StartServer.ts +12 -3
  141. package/Server/Utils/Telemetry/EntityRegistry.ts +205 -18
  142. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +689 -0
  143. package/Server/Utils/Telemetry/TelemetryEntity.ts +160 -52
  144. package/Server/Utils/VM/VMAPI.ts +56 -8
  145. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +7 -3
  146. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +2146 -0
  147. package/Tests/App/Dashboard/CreateWorkflowModal.test.tsx +561 -0
  148. package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +2470 -0
  149. package/Tests/App/Dashboard/MonitorCriteriaAttributeFilter.test.tsx +574 -0
  150. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +1897 -0
  151. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +3606 -0
  152. package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +784 -0
  153. package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +1119 -0
  154. package/Tests/App/Dashboard/SloWidgetFetching.test.tsx +531 -0
  155. package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +1312 -0
  156. package/Tests/App/Dashboard/UserSettingsSetupChecklistPage.test.tsx +1390 -0
  157. package/Tests/Models/InventoryItemModel.test.ts +174 -0
  158. package/Tests/Models/InventoryItemNaming.test.ts +302 -0
  159. package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +437 -0
  160. package/Tests/Server/API/DashboardPublicRateLimit.test.ts +659 -0
  161. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +18 -0
  162. package/Tests/Server/API/DashboardPublicSloAPI.test.ts +880 -0
  163. package/Tests/Server/API/Helpers.ts +24 -15
  164. package/Tests/Server/API/OnCallReadinessAPI.test.ts +2680 -0
  165. package/Tests/Server/API/OnCallSetupReminderAPI.test.ts +915 -0
  166. package/Tests/Server/API/UserProjectsAPI.test.ts +478 -4
  167. package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +533 -0
  168. package/Tests/Server/Infrastructure/Postgres/InventoryItemArchiveMigration.test.ts +213 -0
  169. package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +432 -0
  170. package/Tests/Server/Infrastructure/Queue.test.ts +293 -0
  171. package/Tests/Server/Middleware/PublicDashboardRateLimit.test.ts +1645 -0
  172. package/Tests/Server/Services/AdminRuleEditGuards.test.ts +2848 -0
  173. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +1393 -0
  174. package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +1802 -0
  175. package/Tests/Server/Services/EpisodeStateTimelineNote.test.ts +304 -0
  176. package/Tests/Server/Services/InventoryItemDisplayName.test.ts +339 -0
  177. package/Tests/Server/Services/InventoryItemManualCreate.test.ts +248 -0
  178. package/Tests/Server/Services/IpAllowlistSpoofing.test.ts +450 -0
  179. package/Tests/Server/Services/LogAggregationService.test.ts +235 -1
  180. package/Tests/Server/Services/MetricAggregationService.test.ts +231 -0
  181. package/Tests/Server/Services/MetricEntityMVKeyParity.test.ts +80 -29
  182. package/Tests/Server/Services/MetricServiceAggregate.test.ts +30 -30
  183. package/Tests/Server/Services/NetworkSiteService.test.ts +18 -3
  184. package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +1728 -0
  185. package/Tests/Server/Services/NotificationDeletionImpact.test.ts +2402 -0
  186. package/Tests/Server/Services/OnCallDutyPolicyExecutionLogTimelineGapFeed.test.ts +394 -0
  187. package/Tests/Server/Services/OnCallNotificationFallback.test.ts +1744 -0
  188. package/Tests/Server/Services/OnCallReadinessService.test.ts +4295 -0
  189. package/Tests/Server/Services/OnCallSetupReminder.test.ts +1272 -0
  190. package/Tests/Server/Services/OnCallWeeklyReadinessDigest.test.ts +1021 -0
  191. package/Tests/Server/Services/ProfileAggregationService.test.ts +296 -0
  192. package/Tests/Server/Services/SeverityCreationRuleBackfill.test.ts +1536 -0
  193. package/Tests/Server/Services/SeverityRuleBackfill.test.ts +1818 -0
  194. package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +1845 -0
  195. package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +1394 -0
  196. package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +1166 -0
  197. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +1468 -0
  198. package/Tests/Server/Services/UserOnCallLogNoNotificationRules.test.ts +1457 -0
  199. package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +1546 -0
  200. package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +529 -0
  201. package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +1219 -0
  202. package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +1089 -0
  203. package/Tests/Server/Types/Workflow/Components/ApiComponentErrorPort.test.ts +2 -1
  204. package/Tests/Server/Types/Workflow/Components/ApiComponentHeaders.test.ts +192 -0
  205. package/Tests/Server/Types/Workflow/Components/BaseModelDatabaseComponents.test.ts +190 -0
  206. package/Tests/Server/Types/Workflow/Components/ChatWebhookComponents.test.ts +44 -14
  207. package/Tests/Server/Types/Workflow/Components/Email.test.ts +151 -0
  208. package/Tests/Server/Types/Workflow/Components/IfElse.test.ts +98 -0
  209. package/Tests/Server/Types/Workflow/Components/JavaScript.test.ts +51 -0
  210. package/Tests/Server/Utils/AI/AIMetaTools.test.ts +586 -0
  211. package/Tests/Server/Utils/AI/AlertMonitorFilters.test.ts +582 -0
  212. package/Tests/Server/Utils/AI/ChatAgentRunner.test.ts +726 -0
  213. package/Tests/Server/Utils/AI/IncidentToolsFilters.test.ts +315 -0
  214. package/Tests/Server/Utils/AI/LLMServiceStopReason.test.ts +314 -0
  215. package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +26 -3
  216. package/Tests/Server/Utils/AI/NoteWriteTools.test.ts +268 -0
  217. package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +169 -0
  218. package/Tests/Server/Utils/AI/OnCallTools.test.ts +664 -0
  219. package/Tests/Server/Utils/AI/RunbookTools.test.ts +325 -0
  220. package/Tests/Server/Utils/AI/SloTools.test.ts +306 -0
  221. package/Tests/Server/Utils/AI/StatusPageTools.test.ts +391 -0
  222. package/Tests/Server/Utils/AI/TeamTools.test.ts +257 -0
  223. package/Tests/Server/Utils/AI/TimelineTools.test.ts +472 -0
  224. package/Tests/Server/Utils/AI/WorkflowProbeTools.test.ts +428 -0
  225. package/Tests/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.test.ts +152 -0
  226. package/Tests/Server/Utils/ClientIp.test.ts +438 -0
  227. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +171 -0
  228. package/Tests/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.test.ts +383 -0
  229. package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +23 -25
  230. package/Tests/Server/Utils/MicrosoftTeamsWebhookUrlValidation.test.ts +6 -0
  231. package/Tests/Server/Utils/Monitor/Criteria/DnssecMonitorCriteria.test.ts +307 -0
  232. package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +468 -0
  233. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorTelemetryDeepLinks.test.ts +460 -0
  234. package/Tests/Server/Utils/ResponseRateLimitStatusCodes.test.ts +137 -0
  235. package/Tests/Server/Utils/SSRFProtectionBypasses.test.ts +40 -8
  236. package/Tests/Server/Utils/SSRFProtectionUserInfo.test.ts +351 -0
  237. package/Tests/Server/Utils/Telemetry/EntityRegistryRetirement.test.ts +531 -0
  238. package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +322 -0
  239. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +356 -39
  240. package/Tests/Server/Utils/VM/VMAPISubstitution.test.ts +243 -0
  241. package/Tests/Types/IP/IP.test.ts +263 -0
  242. package/Tests/Types/IP/IPWhitelist.test.ts +197 -0
  243. package/Tests/Types/IP/IPv6.test.ts +12 -1
  244. package/Tests/Types/Monitor/SnmpOid.test.ts +64 -0
  245. package/Tests/Types/NetworkDevice/NetworkDeviceMonitoringMethod.test.ts +110 -0
  246. package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +106 -0
  247. package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeDifferential.test.ts +511 -0
  248. package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageEndToEnd.test.ts +489 -0
  249. package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageGapTolerance.test.ts +479 -0
  250. package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageState.test.ts +822 -0
  251. package/Tests/Types/SerializableObjectDictionaryImportCycle.test.ts +197 -0
  252. package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +140 -0
  253. package/Tests/Types/Workflow/BaseModelComponents.test.ts +429 -0
  254. package/Tests/Types/Workflow/Components/BaseModel.test.ts +225 -0
  255. package/Tests/Types/Workflow/IntegrationCredentialMetadata.test.ts +100 -0
  256. package/Tests/Types/Workflow/StepTrace.test.ts +235 -0
  257. package/Tests/Types/Workflow/TemplateSyntax.test.ts +491 -0
  258. package/Tests/Types/Workflow/Templates.test.ts +1218 -0
  259. package/Tests/UI/Components/ActiveFilterChipsOpenRoute.test.tsx +82 -0
  260. package/Tests/UI/Components/ComponentsModal.test.tsx +564 -7
  261. package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +459 -0
  262. package/Tests/UI/Components/DictionaryAttributeFilterRow.test.tsx +477 -0
  263. package/Tests/UI/Components/Forms/AnchoredFieldPopupKeyboard.test.tsx +382 -0
  264. package/Tests/UI/Components/Forms/ColorPickerPicking.test.tsx +569 -0
  265. package/Tests/UI/Components/Forms/ValidationJSON.test.ts +241 -0
  266. package/Tests/UI/Components/KeyboardShortcut.test.tsx +95 -0
  267. package/Tests/UI/Components/LogDetailsPanelCrossSignal.test.tsx +448 -0
  268. package/Tests/UI/Components/LogTimeRangePicker.test.tsx +42 -0
  269. package/Tests/UI/Components/LogsTableCrossLinks.test.tsx +263 -0
  270. package/Tests/UI/Components/ModalPortalDismissal.test.tsx +90 -0
  271. package/Tests/UI/Components/PendingProjectInvitations.test.tsx +913 -0
  272. package/Tests/UI/Components/SimpleLogViewer.test.tsx +228 -0
  273. package/Tests/UI/Components/TableRowSelectability.test.tsx +312 -0
  274. package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +49 -7
  275. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +325 -0
  276. package/Tests/UI/Components/Workflow/GraphLint.test.ts +893 -0
  277. package/Tests/UI/Components/Workflow/GraphLintSummary.test.ts +755 -0
  278. package/Tests/UI/Components/Workflow/ModelColumnEditor.test.ts +522 -0
  279. package/Tests/UI/Components/Workflow/ModelColumnEditorServerContract.test.ts +193 -0
  280. package/Tests/UI/Components/Workflow/ModelSchema.test.ts +388 -0
  281. package/Tests/UI/Components/Workflow/RunStatusWatcher.test.ts +210 -0
  282. package/Tests/UI/Components/Workflow/StepTraceViewer.test.tsx +256 -0
  283. package/Tests/UI/Components/Workflow/UseRunWatch.test.tsx +665 -0
  284. package/Tests/UI/Components/Workflow/Utils.test.ts +192 -0
  285. package/Tests/UI/Components/Workflow/WorkflowIssuesModal.test.tsx +485 -0
  286. package/Tests/UI/Components/Workflow/WorkflowLogModal.test.tsx +478 -0
  287. package/Tests/UI/Components/Workflow/WorkflowStatusBar.test.tsx +379 -0
  288. package/Tests/UI/EsbuildConfig.test.ts +607 -0
  289. package/Tests/UI/Monitor/MonitorStepCriteriaView.test.tsx +432 -0
  290. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
  291. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +11 -4
  292. package/Tests/UI/Utils/ModelAPICreateMiscData.test.ts +94 -0
  293. package/Tests/UI/Utils/Platform.test.ts +147 -0
  294. package/Tests/UI/Utils/ProjectInvitationDisplay.test.ts +357 -0
  295. package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +198 -0
  296. package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +514 -0
  297. package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +727 -0
  298. package/Tests/Utils/Schema/AnalyticsModelSchema.test.ts +469 -0
  299. package/Tests/Utils/Schema/ModelSchema.test.ts +268 -0
  300. package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +698 -0
  301. package/Tests/Utils/Telemetry/EntityKeyNonTelemetry.test.ts +193 -0
  302. package/Tests/__mocks__/bullmq.js +55 -0
  303. package/Types/AI/AIChatMessageStatus.ts +8 -1
  304. package/Types/AI/AIChatTypes.ts +12 -0
  305. package/Types/Database/AccessControl/OwnerOnlyColumn.ts +88 -0
  306. package/Types/Exception/ExceptionCode.ts +2 -0
  307. package/Types/Exception/ServiceUnavailableException.ts +8 -0
  308. package/Types/Exception/TooManyRequestsException.ts +8 -0
  309. package/Types/IP/IP.ts +93 -47
  310. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +80 -3
  311. package/Types/NetworkDevice/NetworkDeviceMonitoringMethod.ts +55 -0
  312. package/Types/OnCallDutyPolicy/Layer.ts +203 -149
  313. package/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.ts +13 -0
  314. package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +155 -10
  315. package/Types/Permission.ts +193 -0
  316. package/Types/SerializableObjectDictionary.ts +133 -39
  317. package/Types/Telemetry/EntityRelationshipType.ts +1 -1
  318. package/Types/Telemetry/EntitySource.ts +40 -0
  319. package/Types/Telemetry/EntityType.ts +28 -1
  320. package/Types/Telemetry/EntityTypeGroups.ts +65 -0
  321. package/Types/Workflow/Component.ts +29 -0
  322. package/Types/Workflow/Components/API.ts +35 -0
  323. package/Types/Workflow/Components/BaseModel.ts +77 -27
  324. package/Types/Workflow/Components/Discord.ts +1 -0
  325. package/Types/Workflow/Components/Email.ts +12 -3
  326. package/Types/Workflow/Components/JavaScript.ts +7 -0
  327. package/Types/Workflow/Components/MicrosoftTeams.ts +3 -2
  328. package/Types/Workflow/Components/Slack.ts +1 -0
  329. package/Types/Workflow/Components/Telegram.ts +1 -0
  330. package/Types/Workflow/StepTrace.ts +181 -0
  331. package/Types/Workflow/TemplateSyntax.ts +543 -0
  332. package/Types/Workflow/Templates.ts +2368 -0
  333. package/UI/Components/Calendar/Calendar.css +43 -0
  334. package/UI/Components/Calendar/Calendar.tsx +8 -0
  335. package/UI/Components/Card/Card.tsx +2 -2
  336. package/UI/Components/Checkbox/Checkbox.tsx +16 -0
  337. package/UI/Components/Date/CustomTimeRangeModal.tsx +278 -0
  338. package/UI/Components/Date/TimeRangePickerDropdown.tsx +250 -0
  339. package/UI/Components/Dictionary/Dictionary.tsx +71 -15
  340. package/UI/Components/FormModal/BasicFormModal.tsx +2 -1
  341. package/UI/Components/Forms/Fields/ColorPicker.tsx +66 -10
  342. package/UI/Components/Forms/Fields/IconPicker.tsx +48 -10
  343. package/UI/Components/Forms/Types/Field.ts +7 -0
  344. package/UI/Components/Forms/Validation.ts +63 -1
  345. package/UI/Components/Header/HeaderIconDropdownButton.tsx +53 -3
  346. package/UI/Components/Input/Input.tsx +32 -3
  347. package/UI/Components/KeyboardShortcut/KeyboardKey.ts +185 -0
  348. package/UI/Components/KeyboardShortcut/KeyboardShortcut.tsx +87 -0
  349. package/UI/Components/LogsViewer/LogsViewer.tsx +28 -0
  350. package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +31 -0
  351. package/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.tsx +18 -18
  352. package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +363 -14
  353. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +11 -216
  354. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +11 -0
  355. package/UI/Components/LogsViewer/components/LogsTable.tsx +155 -12
  356. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +29 -0
  357. package/UI/Components/LogsViewer/types.ts +23 -0
  358. package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +9 -2
  359. package/UI/Components/Modal/Modal.tsx +37 -4
  360. package/UI/Components/Navbar/NavBarMenuModal.tsx +15 -30
  361. package/UI/Components/ProjectInvitations/PendingProjectInvitations.tsx +442 -0
  362. package/UI/Components/SimpleLogViewer/SimpleLogViewer.tsx +23 -1
  363. package/UI/Components/Table/Table.tsx +49 -16
  364. package/UI/Components/Table/TableBody.tsx +53 -28
  365. package/UI/Components/Table/TableHeader.tsx +13 -0
  366. package/UI/Components/Table/TableRow.tsx +58 -26
  367. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +11 -210
  368. package/UI/Components/Workflow/ArgumentsForm.tsx +341 -8
  369. package/UI/Components/Workflow/Component.tsx +28 -26
  370. package/UI/Components/Workflow/ComponentReturnValueViewer.tsx +26 -0
  371. package/UI/Components/Workflow/ComponentSettingsModal.tsx +79 -9
  372. package/UI/Components/Workflow/ComponentValuePickerModal.tsx +135 -7
  373. package/UI/Components/Workflow/ComponentsModal.tsx +116 -22
  374. package/UI/Components/Workflow/DocumentationViewer.tsx +59 -9
  375. package/UI/Components/Workflow/GraphLint.ts +628 -0
  376. package/UI/Components/Workflow/GraphLintSummary.ts +390 -0
  377. package/UI/Components/Workflow/ModelColumnEditor.tsx +528 -0
  378. package/UI/Components/Workflow/ModelSchema.ts +278 -0
  379. package/UI/Components/Workflow/RunForm.tsx +41 -7
  380. package/UI/Components/Workflow/RunStatusWatcher.ts +122 -0
  381. package/UI/Components/Workflow/StepTraceViewer.tsx +186 -0
  382. package/UI/Components/Workflow/UseRunWatch.ts +212 -0
  383. package/UI/Components/Workflow/Utils.ts +93 -1
  384. package/UI/Components/Workflow/VariableModal.tsx +6 -2
  385. package/UI/Components/Workflow/Workflow.tsx +126 -7
  386. package/UI/Components/Workflow/WorkflowIssuesModal.tsx +255 -0
  387. package/UI/Components/Workflow/WorkflowLogModal.tsx +128 -0
  388. package/UI/Components/Workflow/WorkflowStatusBar.tsx +224 -0
  389. package/UI/Types/LayeredDismissal.ts +31 -0
  390. package/UI/Types/UseAnchoredFieldPopup.ts +99 -0
  391. package/UI/Utils/AIChatExport/ConversationMarkdown.ts +10 -0
  392. package/UI/Utils/ModelAPI/ModelAPI.ts +7 -1
  393. package/UI/Utils/Platform.ts +149 -0
  394. package/UI/Utils/ProjectInvitationDisplay.ts +118 -0
  395. package/UI/esbuild-config.js +22 -1
  396. package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +187 -0
  397. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +533 -0
  398. package/Utils/Monitor/NetworkTopologyUtil.ts +917 -155
  399. package/Utils/Telemetry/CrossSignalScope.ts +502 -0
  400. package/Utils/Telemetry/EntityKey.ts +71 -5
  401. package/Utils/Telemetry/EntityRelationship.ts +1 -1
  402. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +1 -1
  403. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +1 -1
  404. package/build/dist/Models/DatabaseModels/AIConversation.js +32 -0
  405. package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -1
  406. package/build/dist/Models/DatabaseModels/AIConversationMessage.js +42 -0
  407. package/build/dist/Models/DatabaseModels/AIConversationMessage.js.map +1 -1
  408. package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js +28 -0
  409. package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js.map +1 -1
  410. package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js +29 -0
  411. package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js.map +1 -1
  412. package/build/dist/Models/DatabaseModels/Index.js +12 -4
  413. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  414. package/build/dist/Models/DatabaseModels/{TelemetryEntity.js → InventoryItem.js} +212 -29
  415. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -0
  416. package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js +454 -0
  417. package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js.map +1 -0
  418. package/build/dist/Models/DatabaseModels/{TelemetryEntityRelationship.js → InventoryItemRelationship.js} +52 -25
  419. package/build/dist/Models/DatabaseModels/InventoryItemRelationship.js.map +1 -0
  420. package/build/dist/Models/DatabaseModels/NetworkDevice.js +141 -0
  421. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  422. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +719 -0
  423. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -0
  424. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +475 -0
  425. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -0
  426. package/build/dist/Models/DatabaseModels/NetworkTopologySuppression.js +446 -0
  427. package/build/dist/Models/DatabaseModels/NetworkTopologySuppression.js.map +1 -0
  428. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyFeed.js +9 -0
  429. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyFeed.js.map +1 -1
  430. package/build/dist/Models/DatabaseModels/Project.js +80 -0
  431. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  432. package/build/dist/Models/DatabaseModels/UserCall.js +46 -2
  433. package/build/dist/Models/DatabaseModels/UserCall.js.map +1 -1
  434. package/build/dist/Models/DatabaseModels/UserEmail.js +48 -2
  435. package/build/dist/Models/DatabaseModels/UserEmail.js.map +1 -1
  436. package/build/dist/Models/DatabaseModels/UserNotificationRule.js +424 -55
  437. package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
  438. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +24 -2
  439. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
  440. package/build/dist/Models/DatabaseModels/UserPush.js +51 -1
  441. package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
  442. package/build/dist/Models/DatabaseModels/UserSMS.js +47 -2
  443. package/build/dist/Models/DatabaseModels/UserSMS.js.map +1 -1
  444. package/build/dist/Models/DatabaseModels/UserTelegram.js +65 -3
  445. package/build/dist/Models/DatabaseModels/UserTelegram.js.map +1 -1
  446. package/build/dist/Models/DatabaseModels/UserWebhook.js +56 -2
  447. package/build/dist/Models/DatabaseModels/UserWebhook.js.map +1 -1
  448. package/build/dist/Models/DatabaseModels/UserWhatsApp.js +45 -2
  449. package/build/dist/Models/DatabaseModels/UserWhatsApp.js.map +1 -1
  450. package/build/dist/Models/DatabaseModels/WorkflowLog.js +39 -0
  451. package/build/dist/Models/DatabaseModels/WorkflowLog.js.map +1 -1
  452. package/build/dist/Models/DatabaseModels/WorkflowVariable.js +12 -0
  453. package/build/dist/Models/DatabaseModels/WorkflowVariable.js.map +1 -1
  454. package/build/dist/Server/API/AIChatAPI.js +237 -1
  455. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  456. package/build/dist/Server/API/DashboardAPI.js +165 -13
  457. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  458. package/build/dist/Server/API/OnCallReadinessAPI.js +599 -0
  459. package/build/dist/Server/API/OnCallReadinessAPI.js.map +1 -0
  460. package/build/dist/Server/API/TeamComplianceAPI.js +68 -9
  461. package/build/dist/Server/API/TeamComplianceAPI.js.map +1 -1
  462. package/build/dist/Server/API/TelemetryAPI.js +129 -18
  463. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  464. package/build/dist/Server/API/UserAPI.js +16 -1
  465. package/build/dist/Server/API/UserAPI.js.map +1 -1
  466. package/build/dist/Server/EnvironmentConfig.js +45 -0
  467. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  468. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +22 -0
  469. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  470. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js +4 -4
  471. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js +20 -0
  472. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js.map +1 -0
  473. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js +12 -0
  474. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js.map +1 -0
  475. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.js +18 -0
  476. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.js.map +1 -0
  477. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.js +39 -0
  478. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.js.map +1 -0
  479. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.js +38 -0
  480. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.js.map +1 -0
  481. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.js +22 -0
  482. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.js.map +1 -0
  483. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.js +150 -0
  484. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.js.map +1 -0
  485. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.js +57 -0
  486. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.js.map +1 -0
  487. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.js +69 -0
  488. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.js.map +1 -0
  489. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.js +32 -0
  490. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.js.map +1 -0
  491. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.js +26 -0
  492. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.js.map +1 -0
  493. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.js +48 -0
  494. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.js.map +1 -0
  495. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +24 -0
  496. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  497. package/build/dist/Server/Infrastructure/Queue.js +72 -13
  498. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  499. package/build/dist/Server/Middleware/MasterAdminAuthorization.js +11 -6
  500. package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
  501. package/build/dist/Server/Middleware/PublicDashboardRateLimit.js +399 -0
  502. package/build/dist/Server/Middleware/PublicDashboardRateLimit.js.map +1 -0
  503. package/build/dist/Server/Services/AIService.js +1 -0
  504. package/build/dist/Server/Services/AIService.js.map +1 -1
  505. package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js +24 -3
  506. package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js.map +1 -1
  507. package/build/dist/Server/Services/AlertSeverityService.js +54 -0
  508. package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
  509. package/build/dist/Server/Services/DashboardService.js +10 -9
  510. package/build/dist/Server/Services/DashboardService.js.map +1 -1
  511. package/build/dist/Server/Services/DatabaseService.js +24 -2
  512. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  513. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +24 -3
  514. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
  515. package/build/dist/Server/Services/IncidentSeverityService.js +67 -0
  516. package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
  517. package/build/dist/Server/Services/Index.js +12 -4
  518. package/build/dist/Server/Services/Index.js.map +1 -1
  519. package/build/dist/Server/Services/InventoryItemCustomFieldService.js +9 -0
  520. package/build/dist/Server/Services/InventoryItemCustomFieldService.js.map +1 -0
  521. package/build/dist/Server/Services/{TelemetryEntityRelationshipService.js → InventoryItemRelationshipService.js} +9 -6
  522. package/build/dist/Server/Services/InventoryItemRelationshipService.js.map +1 -0
  523. package/build/dist/Server/Services/{TelemetryEntityService.js → InventoryItemService.js} +158 -23
  524. package/build/dist/Server/Services/InventoryItemService.js.map +1 -0
  525. package/build/dist/Server/Services/LogAggregationService.js +27 -8
  526. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  527. package/build/dist/Server/Services/MetricAggregationService.js +80 -0
  528. package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
  529. package/build/dist/Server/Services/MetricService.js +6 -6
  530. package/build/dist/Server/Services/MetricService.js.map +1 -1
  531. package/build/dist/Server/Services/NetworkDeviceLinkRuleService.js +9 -0
  532. package/build/dist/Server/Services/NetworkDeviceLinkRuleService.js.map +1 -0
  533. package/build/dist/Server/Services/NetworkDeviceLinkService.js +71 -0
  534. package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -0
  535. package/build/dist/Server/Services/NetworkDeviceService.js +113 -0
  536. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  537. package/build/dist/Server/Services/NetworkSiteService.js +53 -13
  538. package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
  539. package/build/dist/Server/Services/NetworkTopologySuppressionService.js +85 -0
  540. package/build/dist/Server/Services/NetworkTopologySuppressionService.js.map +1 -0
  541. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +48 -32
  542. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  543. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +8 -0
  544. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
  545. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.js +51 -12
  546. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.js.map +1 -1
  547. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +57 -13
  548. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  549. package/build/dist/Server/Services/OnCallNotificationAlertingService.js +548 -0
  550. package/build/dist/Server/Services/OnCallNotificationAlertingService.js.map +1 -0
  551. package/build/dist/Server/Services/OnCallReadinessService.js +1961 -0
  552. package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -0
  553. package/build/dist/Server/Services/OnCallSetupReminderService.js +738 -0
  554. package/build/dist/Server/Services/OnCallSetupReminderService.js.map +1 -0
  555. package/build/dist/Server/Services/ProfileAggregationService.js +68 -4
  556. package/build/dist/Server/Services/ProfileAggregationService.js.map +1 -1
  557. package/build/dist/Server/Services/StatusPageService.js +11 -10
  558. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  559. package/build/dist/Server/Services/TeamComplianceService.js +312 -160
  560. package/build/dist/Server/Services/TeamComplianceService.js.map +1 -1
  561. package/build/dist/Server/Services/UserCallService.js +24 -1
  562. package/build/dist/Server/Services/UserCallService.js.map +1 -1
  563. package/build/dist/Server/Services/UserEmailService.js +24 -1
  564. package/build/dist/Server/Services/UserEmailService.js.map +1 -1
  565. package/build/dist/Server/Services/UserNotificationRuleAdminService.js +858 -0
  566. package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -0
  567. package/build/dist/Server/Services/UserNotificationRuleService.js +2830 -175
  568. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  569. package/build/dist/Server/Services/UserOnCallLogService.js +488 -43
  570. package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
  571. package/build/dist/Server/Services/UserPushService.js +26 -0
  572. package/build/dist/Server/Services/UserPushService.js.map +1 -1
  573. package/build/dist/Server/Services/UserService.js +10 -0
  574. package/build/dist/Server/Services/UserService.js.map +1 -1
  575. package/build/dist/Server/Services/UserSmsService.js +24 -1
  576. package/build/dist/Server/Services/UserSmsService.js.map +1 -1
  577. package/build/dist/Server/Services/UserTelegramService.js +22 -1
  578. package/build/dist/Server/Services/UserTelegramService.js.map +1 -1
  579. package/build/dist/Server/Services/UserWebhookService.js +25 -1
  580. package/build/dist/Server/Services/UserWebhookService.js.map +1 -1
  581. package/build/dist/Server/Services/UserWhatsAppService.js +22 -1
  582. package/build/dist/Server/Services/UserWhatsAppService.js.map +1 -1
  583. package/build/dist/Server/Types/Database/Permissions/BasePermission.js +12 -1
  584. package/build/dist/Server/Types/Database/Permissions/BasePermission.js.map +1 -1
  585. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +126 -0
  586. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
  587. package/build/dist/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.js +254 -0
  588. package/build/dist/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.js.map +1 -0
  589. package/build/dist/Server/Types/Database/Permissions/QueryPermission.js +47 -2
  590. package/build/dist/Server/Types/Database/Permissions/QueryPermission.js.map +1 -1
  591. package/build/dist/Server/Types/Database/Permissions/TenantPermission.js +7 -0
  592. package/build/dist/Server/Types/Database/Permissions/TenantPermission.js.map +1 -1
  593. package/build/dist/Server/Types/Workflow/Components/API/Delete.js +1 -1
  594. package/build/dist/Server/Types/Workflow/Components/API/Delete.js.map +1 -1
  595. package/build/dist/Server/Types/Workflow/Components/API/Get.js +1 -1
  596. package/build/dist/Server/Types/Workflow/Components/API/Get.js.map +1 -1
  597. package/build/dist/Server/Types/Workflow/Components/API/Patch.js +1 -1
  598. package/build/dist/Server/Types/Workflow/Components/API/Patch.js.map +1 -1
  599. package/build/dist/Server/Types/Workflow/Components/API/Post.js +1 -1
  600. package/build/dist/Server/Types/Workflow/Components/API/Post.js.map +1 -1
  601. package/build/dist/Server/Types/Workflow/Components/API/Put.js +1 -1
  602. package/build/dist/Server/Types/Workflow/Components/API/Put.js.map +1 -1
  603. package/build/dist/Server/Types/Workflow/Components/API/Utils.js +28 -0
  604. package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
  605. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js +21 -5
  606. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js.map +1 -1
  607. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js +14 -10
  608. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js.map +1 -1
  609. package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js +31 -0
  610. package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js.map +1 -1
  611. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js +3 -9
  612. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
  613. package/build/dist/Server/Types/Workflow/Components/Email.js +15 -4
  614. package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
  615. package/build/dist/Server/Types/Workflow/Components/JavaScript.js +7 -2
  616. package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
  617. package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js +1 -1
  618. package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js.map +1 -1
  619. package/build/dist/Server/Types/Workflow/TriggerCode.js.map +1 -1
  620. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +514 -56
  621. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
  622. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +20 -3
  623. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
  624. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +19 -6
  625. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -1
  626. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +7 -0
  627. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  628. package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js +2 -2
  629. package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js.map +1 -1
  630. package/build/dist/Server/Utils/AI/Toolbox/AIMetaTools.js +692 -0
  631. package/build/dist/Server/Utils/AI/Toolbox/AIMetaTools.js.map +1 -0
  632. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +148 -12
  633. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -1
  634. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +157 -10
  635. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -1
  636. package/build/dist/Server/Utils/AI/Toolbox/Index.js +37 -0
  637. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  638. package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js +259 -14
  639. package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js.map +1 -1
  640. package/build/dist/Server/Utils/AI/Toolbox/NoteWriteTools.js +235 -0
  641. package/build/dist/Server/Utils/AI/Toolbox/NoteWriteTools.js.map +1 -0
  642. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +1000 -0
  643. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -0
  644. package/build/dist/Server/Utils/AI/Toolbox/RunbookTools.js +356 -0
  645. package/build/dist/Server/Utils/AI/Toolbox/RunbookTools.js.map +1 -0
  646. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +394 -0
  647. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -0
  648. package/build/dist/Server/Utils/AI/Toolbox/StatusPageTools.js +465 -0
  649. package/build/dist/Server/Utils/AI/Toolbox/StatusPageTools.js.map +1 -0
  650. package/build/dist/Server/Utils/AI/Toolbox/TeamTools.js +280 -0
  651. package/build/dist/Server/Utils/AI/Toolbox/TeamTools.js.map +1 -0
  652. package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js +527 -0
  653. package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js.map +1 -0
  654. package/build/dist/Server/Utils/AI/Toolbox/WorkflowProbeTools.js +548 -0
  655. package/build/dist/Server/Utils/AI/Toolbox/WorkflowProbeTools.js.map +1 -0
  656. package/build/dist/Server/Utils/ClientIp.js +137 -0
  657. package/build/dist/Server/Utils/ClientIp.js.map +1 -0
  658. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +38 -0
  659. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  660. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.js +89 -0
  661. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.js.map +1 -0
  662. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloWidget.js +77 -0
  663. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloWidget.js.map +1 -0
  664. package/build/dist/Server/Utils/Express.js +12 -12
  665. package/build/dist/Server/Utils/Express.js.map +1 -1
  666. package/build/dist/Server/Utils/LLM/LLMService.js +70 -7
  667. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  668. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +121 -11
  669. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  670. package/build/dist/Server/Utils/SSRFProtection.js +82 -21
  671. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  672. package/build/dist/Server/Utils/StartServer.js +12 -4
  673. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  674. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +165 -18
  675. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  676. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +507 -0
  677. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -0
  678. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +122 -47
  679. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  680. package/build/dist/Server/Utils/VM/VMAPI.js +51 -4
  681. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  682. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +7 -3
  683. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  684. package/build/dist/Types/AI/AIChatMessageStatus.js +8 -1
  685. package/build/dist/Types/AI/AIChatMessageStatus.js.map +1 -1
  686. package/build/dist/Types/AI/AIChatTypes.js +12 -0
  687. package/build/dist/Types/AI/AIChatTypes.js.map +1 -1
  688. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +60 -0
  689. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -0
  690. package/build/dist/Types/Exception/ExceptionCode.js +2 -0
  691. package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
  692. package/build/dist/Types/Exception/ServiceUnavailableException.js +8 -0
  693. package/build/dist/Types/Exception/ServiceUnavailableException.js.map +1 -0
  694. package/build/dist/Types/Exception/TooManyRequestsException.js +8 -0
  695. package/build/dist/Types/Exception/TooManyRequestsException.js.map +1 -0
  696. package/build/dist/Types/IP/IP.js +87 -43
  697. package/build/dist/Types/IP/IP.js.map +1 -1
  698. package/build/dist/Types/NetworkDevice/NetworkDeviceMonitoringMethod.js +50 -0
  699. package/build/dist/Types/NetworkDevice/NetworkDeviceMonitoringMethod.js.map +1 -0
  700. package/build/dist/Types/OnCallDutyPolicy/Layer.js +186 -123
  701. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  702. package/build/dist/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.js +13 -0
  703. package/build/dist/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.js.map +1 -1
  704. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +105 -11
  705. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
  706. package/build/dist/Types/Permission.js +174 -0
  707. package/build/dist/Types/Permission.js.map +1 -1
  708. package/build/dist/Types/SerializableObjectDictionary.js +133 -39
  709. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  710. package/build/dist/Types/Telemetry/EntityRelationshipType.js +1 -1
  711. package/build/dist/Types/Telemetry/EntitySource.js +39 -0
  712. package/build/dist/Types/Telemetry/EntitySource.js.map +1 -0
  713. package/build/dist/Types/Telemetry/EntityType.js +28 -1
  714. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  715. package/build/dist/Types/Telemetry/EntityTypeGroups.js +57 -0
  716. package/build/dist/Types/Telemetry/EntityTypeGroups.js.map +1 -0
  717. package/build/dist/Types/Workflow/Component.js +21 -0
  718. package/build/dist/Types/Workflow/Component.js.map +1 -1
  719. package/build/dist/Types/Workflow/Components/API.js +35 -0
  720. package/build/dist/Types/Workflow/Components/API.js.map +1 -1
  721. package/build/dist/Types/Workflow/Components/BaseModel.js +68 -26
  722. package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
  723. package/build/dist/Types/Workflow/Components/Discord.js +1 -0
  724. package/build/dist/Types/Workflow/Components/Discord.js.map +1 -1
  725. package/build/dist/Types/Workflow/Components/Email.js +12 -3
  726. package/build/dist/Types/Workflow/Components/Email.js.map +1 -1
  727. package/build/dist/Types/Workflow/Components/JavaScript.js +7 -0
  728. package/build/dist/Types/Workflow/Components/JavaScript.js.map +1 -1
  729. package/build/dist/Types/Workflow/Components/MicrosoftTeams.js +3 -2
  730. package/build/dist/Types/Workflow/Components/MicrosoftTeams.js.map +1 -1
  731. package/build/dist/Types/Workflow/Components/Slack.js +1 -0
  732. package/build/dist/Types/Workflow/Components/Slack.js.map +1 -1
  733. package/build/dist/Types/Workflow/Components/Telegram.js +1 -0
  734. package/build/dist/Types/Workflow/Components/Telegram.js.map +1 -1
  735. package/build/dist/Types/Workflow/StepTrace.js +104 -0
  736. package/build/dist/Types/Workflow/StepTrace.js.map +1 -0
  737. package/build/dist/Types/Workflow/TemplateSyntax.js +338 -0
  738. package/build/dist/Types/Workflow/TemplateSyntax.js.map +1 -0
  739. package/build/dist/Types/Workflow/Templates.js +2111 -0
  740. package/build/dist/Types/Workflow/Templates.js.map +1 -0
  741. package/build/dist/UI/Components/Calendar/Calendar.js +1 -1
  742. package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
  743. package/build/dist/UI/Components/Checkbox/Checkbox.js +1 -1
  744. package/build/dist/UI/Components/Checkbox/Checkbox.js.map +1 -1
  745. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +126 -0
  746. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -0
  747. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +123 -0
  748. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -0
  749. package/build/dist/UI/Components/Dictionary/Dictionary.js +47 -17
  750. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  751. package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
  752. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +52 -13
  753. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
  754. package/build/dist/UI/Components/Forms/Fields/IconPicker.js +28 -12
  755. package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
  756. package/build/dist/UI/Components/Forms/Validation.js +44 -0
  757. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  758. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +27 -4
  759. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  760. package/build/dist/UI/Components/Input/Input.js +13 -4
  761. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  762. package/build/dist/UI/Components/KeyboardShortcut/KeyboardKey.js +163 -0
  763. package/build/dist/UI/Components/KeyboardShortcut/KeyboardKey.js.map +1 -0
  764. package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcut.js +47 -0
  765. package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcut.js.map +1 -0
  766. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +4 -4
  767. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  768. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +11 -1
  769. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -1
  770. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js +10 -8
  771. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js.map +1 -1
  772. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +227 -14
  773. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
  774. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +5 -108
  775. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
  776. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +5 -0
  777. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  778. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +69 -5
  779. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
  780. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +8 -0
  781. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  782. package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
  783. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +9 -2
  784. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
  785. package/build/dist/UI/Components/Modal/Modal.js +31 -5
  786. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  787. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +8 -21
  788. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  789. package/build/dist/UI/Components/ProjectInvitations/PendingProjectInvitations.js +251 -0
  790. package/build/dist/UI/Components/ProjectInvitations/PendingProjectInvitations.js.map +1 -0
  791. package/build/dist/UI/Components/SimpleLogViewer/SimpleLogViewer.js +9 -2
  792. package/build/dist/UI/Components/SimpleLogViewer/SimpleLogViewer.js.map +1 -1
  793. package/build/dist/UI/Components/Table/Table.js +27 -15
  794. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  795. package/build/dist/UI/Components/Table/TableBody.js +24 -18
  796. package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
  797. package/build/dist/UI/Components/Table/TableHeader.js +9 -1
  798. package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
  799. package/build/dist/UI/Components/Table/TableRow.js +29 -21
  800. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  801. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +5 -105
  802. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
  803. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +261 -14
  804. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  805. package/build/dist/UI/Components/Workflow/Component.js +20 -19
  806. package/build/dist/UI/Components/Workflow/Component.js.map +1 -1
  807. package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js +10 -1
  808. package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js.map +1 -1
  809. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +35 -7
  810. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
  811. package/build/dist/UI/Components/Workflow/ComponentValuePickerModal.js +57 -7
  812. package/build/dist/UI/Components/Workflow/ComponentValuePickerModal.js.map +1 -1
  813. package/build/dist/UI/Components/Workflow/ComponentsModal.js +53 -18
  814. package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
  815. package/build/dist/UI/Components/Workflow/DocumentationViewer.js +19 -6
  816. package/build/dist/UI/Components/Workflow/DocumentationViewer.js.map +1 -1
  817. package/build/dist/UI/Components/Workflow/GraphLint.js +425 -0
  818. package/build/dist/UI/Components/Workflow/GraphLint.js.map +1 -0
  819. package/build/dist/UI/Components/Workflow/GraphLintSummary.js +231 -0
  820. package/build/dist/UI/Components/Workflow/GraphLintSummary.js.map +1 -0
  821. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +320 -0
  822. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -0
  823. package/build/dist/UI/Components/Workflow/ModelSchema.js +156 -0
  824. package/build/dist/UI/Components/Workflow/ModelSchema.js.map +1 -0
  825. package/build/dist/UI/Components/Workflow/RunForm.js +22 -6
  826. package/build/dist/UI/Components/Workflow/RunForm.js.map +1 -1
  827. package/build/dist/UI/Components/Workflow/RunStatusWatcher.js +76 -0
  828. package/build/dist/UI/Components/Workflow/RunStatusWatcher.js.map +1 -0
  829. package/build/dist/UI/Components/Workflow/StepTraceViewer.js +76 -0
  830. package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -0
  831. package/build/dist/UI/Components/Workflow/UseRunWatch.js +123 -0
  832. package/build/dist/UI/Components/Workflow/UseRunWatch.js.map +1 -0
  833. package/build/dist/UI/Components/Workflow/Utils.js +73 -1
  834. package/build/dist/UI/Components/Workflow/Utils.js.map +1 -1
  835. package/build/dist/UI/Components/Workflow/VariableModal.js +3 -2
  836. package/build/dist/UI/Components/Workflow/VariableModal.js.map +1 -1
  837. package/build/dist/UI/Components/Workflow/Workflow.js +92 -7
  838. package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
  839. package/build/dist/UI/Components/Workflow/WorkflowIssuesModal.js +99 -0
  840. package/build/dist/UI/Components/Workflow/WorkflowIssuesModal.js.map +1 -0
  841. package/build/dist/UI/Components/Workflow/WorkflowLogModal.js +56 -0
  842. package/build/dist/UI/Components/Workflow/WorkflowLogModal.js.map +1 -0
  843. package/build/dist/UI/Components/Workflow/WorkflowStatusBar.js +92 -0
  844. package/build/dist/UI/Components/Workflow/WorkflowStatusBar.js.map +1 -0
  845. package/build/dist/UI/Types/LayeredDismissal.js +21 -0
  846. package/build/dist/UI/Types/LayeredDismissal.js.map +1 -0
  847. package/build/dist/UI/Types/UseAnchoredFieldPopup.js +74 -1
  848. package/build/dist/UI/Types/UseAnchoredFieldPopup.js.map +1 -1
  849. package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js +9 -0
  850. package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js.map +1 -1
  851. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +1 -1
  852. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
  853. package/build/dist/UI/Utils/Platform.js +118 -0
  854. package/build/dist/UI/Utils/Platform.js.map +1 -0
  855. package/build/dist/UI/Utils/ProjectInvitationDisplay.js +106 -0
  856. package/build/dist/UI/Utils/ProjectInvitationDisplay.js.map +1 -0
  857. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +108 -0
  858. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -0
  859. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +386 -0
  860. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -0
  861. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +661 -126
  862. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  863. package/build/dist/Utils/Telemetry/CrossSignalScope.js +328 -0
  864. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -0
  865. package/build/dist/Utils/Telemetry/EntityKey.js +57 -5
  866. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  867. package/build/dist/Utils/Telemetry/EntityRelationship.js +1 -1
  868. package/jest.config.json +1 -0
  869. package/package.json +1 -1
  870. package/build/dist/Models/DatabaseModels/TelemetryEntity.js.map +0 -1
  871. package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js.map +0 -1
  872. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +0 -1
  873. package/build/dist/Server/Services/TelemetryEntityService.js.map +0 -1
@@ -0,0 +1,2402 @@
1
+ import AlertSeverityService from "../../../Server/Services/AlertSeverityService";
2
+ import IncidentSeverityService from "../../../Server/Services/IncidentSeverityService";
3
+ import OnCallDutyPolicyEscalationRuleTeamService from "../../../Server/Services/OnCallDutyPolicyEscalationRuleTeamService";
4
+ import OnCallDutyPolicyEscalationRuleUserService from "../../../Server/Services/OnCallDutyPolicyEscalationRuleUserService";
5
+ import OnCallDutyPolicyScheduleLayerUserService from "../../../Server/Services/OnCallDutyPolicyScheduleLayerUserService";
6
+ import OnCallDutyPolicyUserOverrideService from "../../../Server/Services/OnCallDutyPolicyUserOverrideService";
7
+ import OnCallReadinessService, {
8
+ ReadinessMethod,
9
+ ReadinessMethodType,
10
+ ReadinessStatus,
11
+ ResponderSource,
12
+ UserReadiness,
13
+ } from "../../../Server/Services/OnCallReadinessService";
14
+ import ProjectService from "../../../Server/Services/ProjectService";
15
+ import TeamMemberService from "../../../Server/Services/TeamMemberService";
16
+ import UserCallService from "../../../Server/Services/UserCallService";
17
+ import UserEmailService from "../../../Server/Services/UserEmailService";
18
+ import UserNotificationRuleService, {
19
+ CoverageLossCell,
20
+ NotificationDeletionImpact,
21
+ PostDeletionReachability,
22
+ } from "../../../Server/Services/UserNotificationRuleService";
23
+ import UserPushService from "../../../Server/Services/UserPushService";
24
+ import UserSmsService from "../../../Server/Services/UserSmsService";
25
+ import UserTelegramService from "../../../Server/Services/UserTelegramService";
26
+ import UserWebhookService from "../../../Server/Services/UserWebhookService";
27
+ import UserWhatsAppService from "../../../Server/Services/UserWhatsAppService";
28
+ import logger from "../../../Server/Utils/Logger";
29
+ import AlertSeverity from "../../../Models/DatabaseModels/AlertSeverity";
30
+ import IncidentSeverity from "../../../Models/DatabaseModels/IncidentSeverity";
31
+ import Project from "../../../Models/DatabaseModels/Project";
32
+ import UserCall from "../../../Models/DatabaseModels/UserCall";
33
+ import UserEmail from "../../../Models/DatabaseModels/UserEmail";
34
+ import UserNotificationRule from "../../../Models/DatabaseModels/UserNotificationRule";
35
+ import UserPush from "../../../Models/DatabaseModels/UserPush";
36
+ import UserSMS from "../../../Models/DatabaseModels/UserSMS";
37
+ import UserTelegram from "../../../Models/DatabaseModels/UserTelegram";
38
+ import UserWebhook from "../../../Models/DatabaseModels/UserWebhook";
39
+ import UserWhatsApp from "../../../Models/DatabaseModels/UserWhatsApp";
40
+ import SortOrder from "../../../Types/BaseDatabase/SortOrder";
41
+ import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
42
+ import Email from "../../../Types/Email";
43
+ import NotificationRuleType from "../../../Types/NotificationRule/NotificationRuleType";
44
+ import ObjectID from "../../../Types/ObjectID";
45
+ import Phone from "../../../Types/Phone";
46
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
47
+
48
+ /*
49
+ * "What would I lose by deleting this?" - asked BEFORE the delete, answered
50
+ * without blocking it.
51
+ *
52
+ * There are two ways a responder can delete themselves into silence, and
53
+ * neither one looks like that from the screen it happens on:
54
+ *
55
+ * (a) Deleting a RULE that is the last one covering a (ruleType x severity)
56
+ * cell. The rule table is a list of rows, not a coverage grid, so "this
57
+ * is the only thing left for Sev1 incidents" is written nowhere.
58
+ *
59
+ * (b) Deleting a METHOD, which CASCADES. Every method foreign key on
60
+ * UserNotificationRule is onDelete: "CASCADE" - and six of the seven
61
+ * method services delete the rows themselves in onBeforeDelete besides -
62
+ * so removing one phone number destroys every rule that pointed at it.
63
+ * The delete dialog for a phone number mentions notification rules
64
+ * nowhere at all.
65
+ *
66
+ * The properties pinned here are the ones whose failure is silent:
67
+ *
68
+ * ADVISORY, NOT BLOCKING. Nothing in this file may write, and nothing may
69
+ * throw for a delete that is merely inconvenient. A hook that threw would
70
+ * break the legitimate deletes too - a user leaving a project, an admin
71
+ * retiring a decommissioned number - and this is the user's own
72
+ * configuration. Section (G) stands guard over that.
73
+ *
74
+ * ONE ANSWER TO "IS THIS PERSON ON CALL". It comes from
75
+ * OnCallReadinessService and is never re-derived. A delete dialog that said
76
+ * "not on call" while the readiness page said "NotReachable on 3 policies"
77
+ * would leave an admin with no way to tell which to believe - and they would
78
+ * believe the reassuring one. Section (D) asserts both that readiness is
79
+ * asked and that nothing else is.
80
+ *
81
+ * THE OPT-OUT PREDICATE. `isOptOut` is nullable and was added long after
82
+ * these rows started existing, so it is NULL on every pre-existing rule; the
83
+ * naive `=== false` split would classify all of them as neither rules nor
84
+ * opt-outs and report a fully configured user as having nothing to lose.
85
+ * And an opt-out only makes silence deliberate if it SURVIVES: deleting the
86
+ * opt-out along with the last rule leaves neither.
87
+ *
88
+ * THE SEVERITY COLUMN PER RULE TYPE. Incident and incident-episode rules are
89
+ * scoped by incidentSeverityId, alert and alert-episode by alertSeverityId.
90
+ * A rule read through the wrong column matches no page at runtime, so
91
+ * counting it as coverage would certify a gap as covered.
92
+ *
93
+ * CERTAINTY ABOUT REACHABILITY. Push, Email and Webhook have no project
94
+ * switch, which is what makes "a verified one of those survives" a certain
95
+ * answer and "only paid channels survive" an honest "depends". Inventing a
96
+ * green in the second case is the exact false reassurance this whole feature
97
+ * exists to prevent.
98
+ *
99
+ * Nothing here touches a database. Every read is a jest.spyOn at the service
100
+ * boundary, including OnCallReadinessService itself, so what is under test is
101
+ * this service's own decision-making.
102
+ */
103
+
104
+ const PROJECT_ID: ObjectID = new ObjectID(
105
+ "11111111-1111-4111-8111-111111111111",
106
+ );
107
+ const OTHER_PROJECT_ID: ObjectID = new ObjectID(
108
+ "1e1e1e1e-1e1e-4e1e-8e1e-1e1e1e1e1e1e",
109
+ );
110
+ const USER_ID: ObjectID = new ObjectID("22222222-2222-4222-8222-222222222222");
111
+ const OTHER_USER_ID: ObjectID = new ObjectID(
112
+ "2f2f2f2f-2f2f-4f2f-8f2f-2f2f2f2f2f2f",
113
+ );
114
+
115
+ const SEV1_ID: ObjectID = new ObjectID("33333333-3333-4333-8333-333333333333");
116
+ const SEV2_ID: ObjectID = new ObjectID("44444444-4444-4444-8444-444444444444");
117
+ const ALERT_SEV1_ID: ObjectID = new ObjectID(
118
+ "55555555-5555-4555-8555-555555555555",
119
+ );
120
+ const ALERT_SEV2_ID: ObjectID = new ObjectID(
121
+ "66666666-6666-4666-8666-666666666666",
122
+ );
123
+
124
+ const RULE_1_ID: ObjectID = new ObjectID(
125
+ "77777777-7777-4777-8777-777777777777",
126
+ );
127
+ const RULE_2_ID: ObjectID = new ObjectID(
128
+ "88888888-8888-4888-8888-888888888888",
129
+ );
130
+ const RULE_3_ID: ObjectID = new ObjectID(
131
+ "99999999-9999-4999-8999-999999999999",
132
+ );
133
+ const RULE_4_ID: ObjectID = new ObjectID(
134
+ "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
135
+ );
136
+
137
+ const SMS_METHOD_ID: ObjectID = new ObjectID(
138
+ "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
139
+ );
140
+ const EMAIL_METHOD_ID: ObjectID = new ObjectID(
141
+ "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
142
+ );
143
+ const WHATSAPP_METHOD_ID: ObjectID = new ObjectID(
144
+ "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
145
+ );
146
+
147
+ interface FindByCall {
148
+ query: Record<string, unknown>;
149
+ select?: Record<string, unknown> | undefined;
150
+ sort?: Record<string, unknown> | undefined;
151
+ limit?: number | undefined;
152
+ skip?: number | undefined;
153
+ props?: { isRoot?: boolean } | undefined;
154
+ }
155
+
156
+ interface FindOneByCall {
157
+ query: Record<string, unknown>;
158
+ select?: Record<string, unknown> | undefined;
159
+ props?: { isRoot?: boolean } | undefined;
160
+ }
161
+
162
+ /*
163
+ * The mutable world every spy reads from. Reset wholesale in beforeEach: a
164
+ * leaked rule row would change a coverage verdict somewhere far away, and a
165
+ * leaked readiness answer would change every sentence in the warning list.
166
+ */
167
+ let ruleRows: Array<UserNotificationRule> = [];
168
+ let incidentSeverityRows: Array<IncidentSeverity> = [];
169
+ let alertSeverityRows: Array<AlertSeverity> = [];
170
+ let readinessRows: Array<UserReadiness> = [];
171
+ let projectRow: Project | null = null;
172
+
173
+ /*
174
+ * When set, the rule read returns a FULL page every time regardless of skip,
175
+ * which is the only way to drive the page ceiling without materialising half a
176
+ * million rows.
177
+ */
178
+ let ruleReadAlwaysReturnsAFullPage: boolean = false;
179
+
180
+ let ruleFindBy: jest.SpyInstance;
181
+ let ruleCreate: jest.SpyInstance;
182
+ let ruleDeleteBy: jest.SpyInstance;
183
+ let ruleUpdateOneBy: jest.SpyInstance;
184
+ let incidentSeverityFindBy: jest.SpyInstance;
185
+ let alertSeverityFindBy: jest.SpyInstance;
186
+ let readinessForUsers: jest.SpyInstance;
187
+ let projectFindOneById: jest.SpyInstance;
188
+ let emailFindOneBy: jest.SpyInstance;
189
+ let smsFindOneBy: jest.SpyInstance;
190
+ let callFindOneBy: jest.SpyInstance;
191
+ let pushFindOneBy: jest.SpyInstance;
192
+ let whatsAppFindOneBy: jest.SpyInstance;
193
+ let telegramFindOneBy: jest.SpyInstance;
194
+ let webhookFindOneBy: jest.SpyInstance;
195
+ let escalationUserFindBy: jest.SpyInstance;
196
+ let escalationTeamFindBy: jest.SpyInstance;
197
+ let scheduleLayerUserFindBy: jest.SpyInstance;
198
+ let overrideFindBy: jest.SpyInstance;
199
+ let teamMemberFindBy: jest.SpyInstance;
200
+ let loggerError: jest.SpyInstance;
201
+
202
+ function rule(data: {
203
+ id?: ObjectID | undefined;
204
+ ruleType: NotificationRuleType;
205
+ incidentSeverityId?: ObjectID | undefined;
206
+ alertSeverityId?: ObjectID | undefined;
207
+ isOptOut?: boolean | undefined;
208
+ userEmailId?: ObjectID | undefined;
209
+ userSmsId?: ObjectID | undefined;
210
+ userCallId?: ObjectID | undefined;
211
+ userPushId?: ObjectID | undefined;
212
+ userWhatsAppId?: ObjectID | undefined;
213
+ userTelegramId?: ObjectID | undefined;
214
+ userWebhookId?: ObjectID | undefined;
215
+ }): UserNotificationRule {
216
+ const model: UserNotificationRule = new UserNotificationRule();
217
+ model.projectId = PROJECT_ID;
218
+ model.userId = USER_ID;
219
+ model.ruleType = data.ruleType;
220
+
221
+ if (data.id) {
222
+ model.id = data.id;
223
+ }
224
+
225
+ if (data.incidentSeverityId) {
226
+ model.incidentSeverityId = data.incidentSeverityId;
227
+ }
228
+
229
+ if (data.alertSeverityId) {
230
+ model.alertSeverityId = data.alertSeverityId;
231
+ }
232
+
233
+ if (data.isOptOut !== undefined) {
234
+ model.isOptOut = data.isOptOut;
235
+ }
236
+
237
+ if (data.userEmailId) {
238
+ model.userEmailId = data.userEmailId;
239
+ }
240
+
241
+ if (data.userSmsId) {
242
+ model.userSmsId = data.userSmsId;
243
+ }
244
+
245
+ if (data.userCallId) {
246
+ model.userCallId = data.userCallId;
247
+ }
248
+
249
+ if (data.userPushId) {
250
+ model.userPushId = data.userPushId;
251
+ }
252
+
253
+ if (data.userWhatsAppId) {
254
+ model.userWhatsAppId = data.userWhatsAppId;
255
+ }
256
+
257
+ if (data.userTelegramId) {
258
+ model.userTelegramId = data.userTelegramId;
259
+ }
260
+
261
+ if (data.userWebhookId) {
262
+ model.userWebhookId = data.userWebhookId;
263
+ }
264
+
265
+ return model;
266
+ }
267
+
268
+ /** The most common shape: an incident rule on one severity, on one method. */
269
+ function incidentRule(
270
+ id: ObjectID,
271
+ severityId: ObjectID,
272
+ methodId: ObjectID = SMS_METHOD_ID,
273
+ ): UserNotificationRule {
274
+ return rule({
275
+ id: id,
276
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
277
+ incidentSeverityId: severityId,
278
+ userSmsId: methodId,
279
+ });
280
+ }
281
+
282
+ function incidentSeverity(id: ObjectID, name: string): IncidentSeverity {
283
+ const model: IncidentSeverity = new IncidentSeverity();
284
+ model.id = id;
285
+ model.name = name;
286
+
287
+ return model;
288
+ }
289
+
290
+ function alertSeverity(id: ObjectID, name: string): AlertSeverity {
291
+ const model: AlertSeverity = new AlertSeverity();
292
+ model.id = id;
293
+ model.name = name;
294
+
295
+ return model;
296
+ }
297
+
298
+ /*
299
+ * `methodId` is the id of the method ROW this entry describes, and every entry
300
+ * gets its own by default because readiness emits one entry per row - a
301
+ * responder with two verified SMS numbers is two entries, not one. The
302
+ * deletion-impact code below still identifies the row being deleted by its
303
+ * CHANNEL rather than by this id (see the comment on remainingVerifiedMethods),
304
+ * so the value is never compared against anything here; giving each entry a
305
+ * distinct one keeps the fixture honest rather than implying two rows share an
306
+ * id. Callers that need a specific id can pass one.
307
+ */
308
+ function method(
309
+ methodType: ReadinessMethodType,
310
+ isVerified: boolean = true,
311
+ methodId: ObjectID = ObjectID.generate(),
312
+ ): ReadinessMethod {
313
+ return {
314
+ methodId: methodId,
315
+ methodType: methodType,
316
+ maskedIdentifier: "•••",
317
+ isVerified: isVerified,
318
+ };
319
+ }
320
+
321
+ function readiness(data: {
322
+ status?: ReadinessStatus | undefined;
323
+ methods?: Array<ReadinessMethod> | undefined;
324
+ reachedVia?: Array<ResponderSource> | undefined;
325
+ }): UserReadiness {
326
+ return {
327
+ userId: USER_ID,
328
+ userName: "Ada Lovelace",
329
+ userEmail: "ada@corp.example.com",
330
+ status: data.status || ReadinessStatus.Ready,
331
+ methods: data.methods || [method(ReadinessMethodType.Email)],
332
+ coverage: [],
333
+ reasons: [],
334
+ reachedVia: data.reachedVia || [ResponderSource.Direct],
335
+ teams: [],
336
+ };
337
+ }
338
+
339
+ function project(disableOnCallNotificationFallback: boolean): Project {
340
+ const model: Project = new Project();
341
+ model.id = PROJECT_ID;
342
+ model.disableOnCallNotificationFallback = disableOnCallNotificationFallback;
343
+
344
+ return model;
345
+ }
346
+
347
+ function firstCall(spy: jest.SpyInstance): FindByCall {
348
+ return spy.mock.calls[0]![0] as FindByCall;
349
+ }
350
+
351
+ function ruleDeletionImpact(
352
+ ...ruleIds: Array<ObjectID>
353
+ ): Promise<NotificationDeletionImpact> {
354
+ return UserNotificationRuleService.getRuleDeletionImpact({
355
+ projectId: PROJECT_ID,
356
+ userId: USER_ID,
357
+ notificationRuleIds: ruleIds,
358
+ });
359
+ }
360
+
361
+ function methodDeletionImpact(
362
+ methodType: ReadinessMethodType,
363
+ methodId: ObjectID,
364
+ ): Promise<NotificationDeletionImpact> {
365
+ return UserNotificationRuleService.getNotificationMethodDeletionImpact({
366
+ projectId: PROJECT_ID,
367
+ methodType: methodType,
368
+ methodId: methodId,
369
+ });
370
+ }
371
+
372
+ function cellFor(
373
+ impact: NotificationDeletionImpact,
374
+ ruleType: NotificationRuleType,
375
+ severityId: ObjectID,
376
+ ): CoverageLossCell | undefined {
377
+ return impact.coverageLost.find((cell: CoverageLossCell): boolean => {
378
+ return (
379
+ cell.ruleType === ruleType &&
380
+ cell.severityId?.toString() === severityId.toString()
381
+ );
382
+ });
383
+ }
384
+
385
+ function warningsMatching(
386
+ impact: NotificationDeletionImpact,
387
+ fragment: string,
388
+ ): Array<string> {
389
+ return impact.warnings.filter((warning: string): boolean => {
390
+ return warning.includes(fragment);
391
+ });
392
+ }
393
+
394
+ beforeEach(() => {
395
+ /*
396
+ * Readiness caches its answers for a minute behind static maps, and this file
397
+ * spies on the method rather than the cache - but a previous file in the same
398
+ * jest process may have left entries behind, and a cached hit would bypass a
399
+ * spy that a later test asserts the call count of.
400
+ */
401
+ OnCallReadinessService.clearCache();
402
+
403
+ ruleRows = [];
404
+ incidentSeverityRows = [
405
+ incidentSeverity(SEV1_ID, "Sev1"),
406
+ incidentSeverity(SEV2_ID, "Sev2"),
407
+ ];
408
+ alertSeverityRows = [
409
+ alertSeverity(ALERT_SEV1_ID, "Alert Sev1"),
410
+ alertSeverity(ALERT_SEV2_ID, "Alert Sev2"),
411
+ ];
412
+ readinessRows = [readiness({})];
413
+ projectRow = project(false);
414
+ ruleReadAlwaysReturnsAFullPage = false;
415
+
416
+ ruleFindBy = jest
417
+ .spyOn(UserNotificationRuleService, "findBy")
418
+ .mockImplementation((async (
419
+ data: FindByCall,
420
+ ): Promise<Array<UserNotificationRule>> => {
421
+ if (ruleReadAlwaysReturnsAFullPage) {
422
+ return new Array(LIMIT_PER_PROJECT).fill(ruleRows[0]);
423
+ }
424
+
425
+ const skip: number = data.skip || 0;
426
+ const limit: number = data.limit || ruleRows.length;
427
+
428
+ return ruleRows.slice(skip, skip + limit);
429
+ }) as never);
430
+
431
+ /*
432
+ * The three writes this service could make. Every one of them is asserted to
433
+ * be zero across the whole suite in section (G): a preview that writes is not
434
+ * a preview.
435
+ */
436
+ ruleCreate = jest
437
+ .spyOn(UserNotificationRuleService, "create")
438
+ .mockResolvedValue(new UserNotificationRule() as never);
439
+ ruleDeleteBy = jest
440
+ .spyOn(UserNotificationRuleService, "deleteBy")
441
+ .mockResolvedValue(0 as never);
442
+ ruleUpdateOneBy = jest
443
+ .spyOn(UserNotificationRuleService, "updateOneBy")
444
+ .mockResolvedValue(undefined as never);
445
+
446
+ incidentSeverityFindBy = jest
447
+ .spyOn(IncidentSeverityService, "findBy")
448
+ .mockImplementation((async (): Promise<Array<IncidentSeverity>> => {
449
+ return incidentSeverityRows;
450
+ }) as never);
451
+ alertSeverityFindBy = jest
452
+ .spyOn(AlertSeverityService, "findBy")
453
+ .mockImplementation((async (): Promise<Array<AlertSeverity>> => {
454
+ return alertSeverityRows;
455
+ }) as never);
456
+
457
+ readinessForUsers = jest
458
+ .spyOn(OnCallReadinessService, "getReadinessForUsers")
459
+ .mockImplementation((async (): Promise<Array<UserReadiness>> => {
460
+ return readinessRows;
461
+ }) as never);
462
+
463
+ projectFindOneById = jest
464
+ .spyOn(ProjectService, "findOneById")
465
+ .mockImplementation((async (): Promise<Project | null> => {
466
+ return projectRow;
467
+ }) as never);
468
+
469
+ const smsRow: UserSMS = new UserSMS();
470
+ smsRow.id = SMS_METHOD_ID;
471
+ smsRow.userId = USER_ID;
472
+ smsRow.projectId = PROJECT_ID;
473
+ smsRow.phone = new Phone("+14155554821");
474
+ smsRow.isVerified = true;
475
+
476
+ const emailRow: UserEmail = new UserEmail();
477
+ emailRow.id = EMAIL_METHOD_ID;
478
+ emailRow.userId = USER_ID;
479
+ emailRow.projectId = PROJECT_ID;
480
+ emailRow.email = new Email("ada@corp.example.com");
481
+ emailRow.isVerified = true;
482
+
483
+ const callRow: UserCall = new UserCall();
484
+ callRow.id = SMS_METHOD_ID;
485
+ callRow.userId = USER_ID;
486
+ callRow.isVerified = true;
487
+
488
+ const pushRow: UserPush = new UserPush();
489
+ pushRow.id = SMS_METHOD_ID;
490
+ pushRow.userId = USER_ID;
491
+ pushRow.isVerified = true;
492
+
493
+ const whatsAppRow: UserWhatsApp = new UserWhatsApp();
494
+ whatsAppRow.id = WHATSAPP_METHOD_ID;
495
+ whatsAppRow.userId = USER_ID;
496
+ whatsAppRow.isVerified = true;
497
+
498
+ const telegramRow: UserTelegram = new UserTelegram();
499
+ telegramRow.id = SMS_METHOD_ID;
500
+ telegramRow.userId = USER_ID;
501
+ telegramRow.isVerified = true;
502
+
503
+ /*
504
+ * Deliberately carries NO isVerified. UserWebhook has no such column at all,
505
+ * and the service has to treat presence as the whole test - a fixture that
506
+ * set the field would let a regression to `Boolean(row.isVerified)` pass.
507
+ */
508
+ const webhookRow: UserWebhook = new UserWebhook();
509
+ webhookRow.id = SMS_METHOD_ID;
510
+ webhookRow.userId = USER_ID;
511
+
512
+ emailFindOneBy = jest
513
+ .spyOn(UserEmailService, "findOneBy")
514
+ .mockResolvedValue(emailRow as never);
515
+ smsFindOneBy = jest
516
+ .spyOn(UserSmsService, "findOneBy")
517
+ .mockResolvedValue(smsRow as never);
518
+ callFindOneBy = jest
519
+ .spyOn(UserCallService, "findOneBy")
520
+ .mockResolvedValue(callRow as never);
521
+ pushFindOneBy = jest
522
+ .spyOn(UserPushService, "findOneBy")
523
+ .mockResolvedValue(pushRow as never);
524
+ whatsAppFindOneBy = jest
525
+ .spyOn(UserWhatsAppService, "findOneBy")
526
+ .mockResolvedValue(whatsAppRow as never);
527
+ telegramFindOneBy = jest
528
+ .spyOn(UserTelegramService, "findOneBy")
529
+ .mockResolvedValue(telegramRow as never);
530
+ webhookFindOneBy = jest
531
+ .spyOn(UserWebhookService, "findOneBy")
532
+ .mockResolvedValue(webhookRow as never);
533
+
534
+ /*
535
+ * The responder-set reads. These exist purely so section (D) can assert they
536
+ * are NEVER touched: the moment this service answers "is this person on call"
537
+ * for itself, there are two answers to that question in the product.
538
+ */
539
+ escalationUserFindBy = jest
540
+ .spyOn(OnCallDutyPolicyEscalationRuleUserService, "findBy")
541
+ .mockResolvedValue([] as never);
542
+ escalationTeamFindBy = jest
543
+ .spyOn(OnCallDutyPolicyEscalationRuleTeamService, "findBy")
544
+ .mockResolvedValue([] as never);
545
+ scheduleLayerUserFindBy = jest
546
+ .spyOn(OnCallDutyPolicyScheduleLayerUserService, "findBy")
547
+ .mockResolvedValue([] as never);
548
+ overrideFindBy = jest
549
+ .spyOn(OnCallDutyPolicyUserOverrideService, "findBy")
550
+ .mockResolvedValue([] as never);
551
+ teamMemberFindBy = jest
552
+ .spyOn(TeamMemberService, "findBy")
553
+ .mockResolvedValue([] as never);
554
+
555
+ loggerError = jest.spyOn(logger, "error").mockImplementation((): void => {
556
+ return undefined;
557
+ });
558
+ });
559
+
560
+ afterEach(() => {
561
+ jest.restoreAllMocks();
562
+ OnCallReadinessService.clearCache();
563
+ });
564
+
565
+ /*
566
+ * ---------------------------------------------------------------------------
567
+ * (A) Deleting a RULE: what coverage actually disappears.
568
+ *
569
+ * Coverage is a property of what is LEFT, not of what goes, which is why the
570
+ * service reads every rule the user has rather than only the ones being
571
+ * deleted. Each test below is a shape where counting the deleted rules alone
572
+ * gives the wrong answer.
573
+ * ---------------------------------------------------------------------------
574
+ */
575
+
576
+ describe("deleting a rule: coverage loss", () => {
577
+ test("deleting the only rule covering a cell reports that cell as lost", async () => {
578
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
579
+
580
+ const impact: NotificationDeletionImpact =
581
+ await ruleDeletionImpact(RULE_1_ID);
582
+
583
+ expect(impact.rulesDeletedCount).toBe(1);
584
+ expect(impact.coverageLost).toHaveLength(1);
585
+ expect(impact.coverageLost[0]!.ruleType).toBe(
586
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
587
+ );
588
+ expect(impact.coverageLost[0]!.severityId?.toString()).toBe(
589
+ SEV1_ID.toString(),
590
+ );
591
+ expect(impact.coverageLost[0]!.severityName).toBe("Sev1");
592
+ expect(impact.coverageLost[0]!.rulesRemoved).toBe(1);
593
+ });
594
+
595
+ test("deleting ONE of two rules on the same cell loses nothing - the other still covers it", async () => {
596
+ ruleRows = [
597
+ incidentRule(RULE_1_ID, SEV1_ID, SMS_METHOD_ID),
598
+ incidentRule(RULE_2_ID, SEV1_ID, EMAIL_METHOD_ID),
599
+ ];
600
+
601
+ const impact: NotificationDeletionImpact =
602
+ await ruleDeletionImpact(RULE_1_ID);
603
+
604
+ expect(impact.rulesDeletedCount).toBe(1);
605
+ expect(impact.coverageLost).toHaveLength(0);
606
+ });
607
+
608
+ test("deleting BOTH rules on a cell does lose it", async () => {
609
+ ruleRows = [
610
+ incidentRule(RULE_1_ID, SEV1_ID, SMS_METHOD_ID),
611
+ incidentRule(RULE_2_ID, SEV1_ID, EMAIL_METHOD_ID),
612
+ ];
613
+
614
+ const impact: NotificationDeletionImpact = await ruleDeletionImpact(
615
+ RULE_1_ID,
616
+ RULE_2_ID,
617
+ );
618
+
619
+ expect(impact.rulesDeletedCount).toBe(2);
620
+ expect(impact.coverageLost).toHaveLength(1);
621
+ expect(impact.coverageLost[0]!.rulesRemoved).toBe(2);
622
+ });
623
+
624
+ test("other cells are untouched by a deletion that empties one of them", async () => {
625
+ ruleRows = [
626
+ incidentRule(RULE_1_ID, SEV1_ID),
627
+ incidentRule(RULE_2_ID, SEV2_ID),
628
+ ];
629
+
630
+ const impact: NotificationDeletionImpact =
631
+ await ruleDeletionImpact(RULE_1_ID);
632
+
633
+ expect(impact.coverageLost).toHaveLength(1);
634
+ expect(
635
+ cellFor(impact, NotificationRuleType.ON_CALL_EXECUTED_INCIDENT, SEV2_ID),
636
+ ).toBeUndefined();
637
+ });
638
+
639
+ test("a cell whose only rules are for a DIFFERENT rule type is a different cell", async () => {
640
+ ruleRows = [
641
+ incidentRule(RULE_1_ID, SEV1_ID),
642
+ rule({
643
+ id: RULE_2_ID,
644
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
645
+ incidentSeverityId: SEV1_ID,
646
+ userSmsId: SMS_METHOD_ID,
647
+ }),
648
+ ];
649
+
650
+ const impact: NotificationDeletionImpact =
651
+ await ruleDeletionImpact(RULE_1_ID);
652
+
653
+ /*
654
+ * The episode rule is on the same severity and does NOT cover the incident
655
+ * cell. Readiness keys coverage on (userId, ruleType, severityId) for the
656
+ * same reason: a rule of the wrong type matches no page of this type.
657
+ */
658
+ expect(impact.coverageLost).toHaveLength(1);
659
+ expect(impact.coverageLost[0]!.ruleType).toBe(
660
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
661
+ );
662
+ });
663
+
664
+ test("an empty id list deletes nothing and still answers the other questions", async () => {
665
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
666
+
667
+ const impact: NotificationDeletionImpact = await ruleDeletionImpact();
668
+
669
+ expect(impact.rulesDeletedCount).toBe(0);
670
+ expect(impact.coverageLost).toHaveLength(0);
671
+ expect(impact.isOnCallResponder).toBe(true);
672
+ expect(impact.reachability).toBe(PostDeletionReachability.Reachable);
673
+ expect(impact.isFallbackEnabled).toBe(true);
674
+ });
675
+
676
+ test("rulesRemoved counts only the rules taken out of that cell", async () => {
677
+ ruleRows = [
678
+ incidentRule(RULE_1_ID, SEV1_ID, SMS_METHOD_ID),
679
+ incidentRule(RULE_2_ID, SEV1_ID, EMAIL_METHOD_ID),
680
+ incidentRule(RULE_3_ID, SEV2_ID, SMS_METHOD_ID),
681
+ ];
682
+
683
+ const impact: NotificationDeletionImpact = await ruleDeletionImpact(
684
+ RULE_1_ID,
685
+ RULE_2_ID,
686
+ RULE_3_ID,
687
+ );
688
+
689
+ expect(impact.rulesDeletedCount).toBe(3);
690
+ expect(
691
+ cellFor(impact, NotificationRuleType.ON_CALL_EXECUTED_INCIDENT, SEV1_ID)!
692
+ .rulesRemoved,
693
+ ).toBe(2);
694
+ expect(
695
+ cellFor(impact, NotificationRuleType.ON_CALL_EXECUTED_INCIDENT, SEV2_ID)!
696
+ .rulesRemoved,
697
+ ).toBe(1);
698
+ });
699
+ });
700
+
701
+ /*
702
+ * ---------------------------------------------------------------------------
703
+ * (B) The opt-out predicate.
704
+ *
705
+ * Two separate traps live here. The first is `isOptOut === true` versus
706
+ * `=== false`: the column is nullable and NULL on every rule written before it
707
+ * existed. The second is subtler - an opt-out only means "this silence is
708
+ * deliberate" if it SURVIVES the deletion.
709
+ * ---------------------------------------------------------------------------
710
+ */
711
+
712
+ describe("opt-out rows", () => {
713
+ test("a NULL isOptOut is a real rule, not an opt-out (the pre-existing-rows trap)", async () => {
714
+ /*
715
+ * The builder leaves isOptOut unset, which is what the column actually
716
+ * looks like on every rule written before it existed.
717
+ */
718
+ const legacyRule: UserNotificationRule = incidentRule(RULE_1_ID, SEV1_ID);
719
+
720
+ expect(legacyRule.isOptOut).toBeUndefined();
721
+
722
+ ruleRows = [legacyRule];
723
+
724
+ const impact: NotificationDeletionImpact =
725
+ await ruleDeletionImpact(RULE_1_ID);
726
+
727
+ /*
728
+ * If NULL were read as "neither a rule nor an opt-out", this cell would
729
+ * have had no rules to lose and the deletion would look free.
730
+ */
731
+ expect(impact.coverageLost).toHaveLength(1);
732
+ });
733
+
734
+ test("a surviving opt-out means losing the last rule is not a gap", async () => {
735
+ ruleRows = [
736
+ incidentRule(RULE_1_ID, SEV1_ID),
737
+ rule({
738
+ id: RULE_2_ID,
739
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
740
+ incidentSeverityId: SEV1_ID,
741
+ isOptOut: true,
742
+ }),
743
+ ];
744
+
745
+ const impact: NotificationDeletionImpact =
746
+ await ruleDeletionImpact(RULE_1_ID);
747
+
748
+ expect(impact.rulesDeletedCount).toBe(1);
749
+ expect(impact.coverageLost).toHaveLength(0);
750
+ });
751
+
752
+ test("deleting the opt-out ALONGSIDE the last rule does report the gap", async () => {
753
+ ruleRows = [
754
+ incidentRule(RULE_1_ID, SEV1_ID),
755
+ rule({
756
+ id: RULE_2_ID,
757
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
758
+ incidentSeverityId: SEV1_ID,
759
+ isOptOut: true,
760
+ }),
761
+ ];
762
+
763
+ const impact: NotificationDeletionImpact = await ruleDeletionImpact(
764
+ RULE_1_ID,
765
+ RULE_2_ID,
766
+ );
767
+
768
+ /*
769
+ * After this write there is neither a rule nor a stated intention to be
770
+ * silent, which is exactly the accidental silence this whole feature is
771
+ * about. An opt-out that is itself being deleted must not suppress it.
772
+ */
773
+ expect(impact.rulesDeletedCount).toBe(2);
774
+ expect(impact.coverageLost).toHaveLength(1);
775
+ });
776
+
777
+ test("deleting ONLY the opt-out is not a coverage loss - it is the opposite", async () => {
778
+ ruleRows = [
779
+ rule({
780
+ id: RULE_2_ID,
781
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
782
+ incidentSeverityId: SEV1_ID,
783
+ isOptOut: true,
784
+ }),
785
+ ];
786
+
787
+ const impact: NotificationDeletionImpact =
788
+ await ruleDeletionImpact(RULE_2_ID);
789
+
790
+ expect(impact.rulesDeletedCount).toBe(1);
791
+ expect(impact.coverageLost).toHaveLength(0);
792
+ });
793
+
794
+ test("an opt-out row is never counted as coverage of its own cell", async () => {
795
+ ruleRows = [
796
+ rule({
797
+ id: RULE_1_ID,
798
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
799
+ incidentSeverityId: SEV1_ID,
800
+ isOptOut: true,
801
+ }),
802
+ incidentRule(RULE_2_ID, SEV1_ID),
803
+ ];
804
+
805
+ const impact: NotificationDeletionImpact =
806
+ await ruleDeletionImpact(RULE_2_ID);
807
+
808
+ /*
809
+ * rulesBefore counts real rules only. If the opt-out were counted, this
810
+ * cell would read "1 of 2 removed" and the loss would be missed - which is
811
+ * the same bug in the other direction.
812
+ */
813
+ expect(impact.coverageLost).toHaveLength(0);
814
+ });
815
+ });
816
+
817
+ /*
818
+ * ---------------------------------------------------------------------------
819
+ * (C) The severity column follows the RULE TYPE, never whichever column
820
+ * happens to be populated. An alert rule matched against an incident severity
821
+ * matches no page at runtime - counting it would certify a gap as covered,
822
+ * which is the exact shape of Gap G.
823
+ * ---------------------------------------------------------------------------
824
+ */
825
+
826
+ describe("the severity model per rule type", () => {
827
+ test("an alert rule is scoped by alertSeverityId, and covers an alert cell", async () => {
828
+ ruleRows = [
829
+ rule({
830
+ id: RULE_1_ID,
831
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
832
+ alertSeverityId: ALERT_SEV1_ID,
833
+ userSmsId: SMS_METHOD_ID,
834
+ }),
835
+ ];
836
+
837
+ const impact: NotificationDeletionImpact =
838
+ await ruleDeletionImpact(RULE_1_ID);
839
+
840
+ expect(impact.coverageLost).toHaveLength(1);
841
+ expect(impact.coverageLost[0]!.severityId?.toString()).toBe(
842
+ ALERT_SEV1_ID.toString(),
843
+ );
844
+ expect(impact.coverageLost[0]!.severityName).toBe("Alert Sev1");
845
+ });
846
+
847
+ test("an alert rule carrying ONLY an incidentSeverityId covers nothing", async () => {
848
+ ruleRows = [
849
+ rule({
850
+ id: RULE_1_ID,
851
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
852
+ incidentSeverityId: SEV1_ID,
853
+ userSmsId: SMS_METHOD_ID,
854
+ }),
855
+ ];
856
+
857
+ const impact: NotificationDeletionImpact =
858
+ await ruleDeletionImpact(RULE_1_ID);
859
+
860
+ /*
861
+ * It is still counted as a deleted row - it does exist - but it covered no
862
+ * cell, because nothing at runtime would ever match it.
863
+ */
864
+ expect(impact.rulesDeletedCount).toBe(1);
865
+ expect(impact.coverageLost).toHaveLength(0);
866
+ });
867
+
868
+ test("a severity-scoped rule with NO severity at all covers nothing (the Gap G corpse)", async () => {
869
+ ruleRows = [
870
+ rule({
871
+ id: RULE_1_ID,
872
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
873
+ userSmsId: SMS_METHOD_ID,
874
+ }),
875
+ ];
876
+
877
+ const impact: NotificationDeletionImpact =
878
+ await ruleDeletionImpact(RULE_1_ID);
879
+
880
+ expect(impact.rulesDeletedCount).toBe(1);
881
+ expect(impact.coverageLost).toHaveLength(0);
882
+ });
883
+
884
+ test("all four paging rule types produce their own cells", async () => {
885
+ ruleRows = [
886
+ rule({
887
+ id: RULE_1_ID,
888
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
889
+ incidentSeverityId: SEV1_ID,
890
+ userSmsId: SMS_METHOD_ID,
891
+ }),
892
+ rule({
893
+ id: RULE_2_ID,
894
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
895
+ incidentSeverityId: SEV1_ID,
896
+ userSmsId: SMS_METHOD_ID,
897
+ }),
898
+ rule({
899
+ id: RULE_3_ID,
900
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
901
+ alertSeverityId: ALERT_SEV1_ID,
902
+ userSmsId: SMS_METHOD_ID,
903
+ }),
904
+ rule({
905
+ id: RULE_4_ID,
906
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
907
+ alertSeverityId: ALERT_SEV1_ID,
908
+ userSmsId: SMS_METHOD_ID,
909
+ }),
910
+ ];
911
+
912
+ const impact: NotificationDeletionImpact = await ruleDeletionImpact(
913
+ RULE_1_ID,
914
+ RULE_2_ID,
915
+ RULE_3_ID,
916
+ RULE_4_ID,
917
+ );
918
+
919
+ expect(impact.coverageLost).toHaveLength(4);
920
+ expect(
921
+ impact.coverageLost.map(
922
+ (cell: CoverageLossCell): NotificationRuleType => {
923
+ return cell.ruleType;
924
+ },
925
+ ),
926
+ ).toEqual([
927
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
928
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
929
+ NotificationRuleType.ON_CALL_EXECUTED_ALERT,
930
+ NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
931
+ ]);
932
+ });
933
+
934
+ test("an incident severity name never names an alert cell, even on a colliding id", async () => {
935
+ /*
936
+ * Incident and alert severities live in different tables with
937
+ * independently generated ids. A name map keyed on the id alone would let
938
+ * one label the other; keying on (kind, id) is what makes that impossible.
939
+ */
940
+ incidentSeverityRows = [incidentSeverity(SEV1_ID, "INCIDENT NAME")];
941
+ alertSeverityRows = [alertSeverity(SEV1_ID, "ALERT NAME")];
942
+
943
+ ruleRows = [
944
+ rule({
945
+ id: RULE_1_ID,
946
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
947
+ alertSeverityId: SEV1_ID,
948
+ userSmsId: SMS_METHOD_ID,
949
+ }),
950
+ ];
951
+
952
+ const impact: NotificationDeletionImpact =
953
+ await ruleDeletionImpact(RULE_1_ID);
954
+
955
+ expect(impact.coverageLost[0]!.severityName).toBe("ALERT NAME");
956
+ });
957
+
958
+ test("a severity the project no longer lists degrades to a nameless cell, not a crash", async () => {
959
+ incidentSeverityRows = [];
960
+
961
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
962
+
963
+ const impact: NotificationDeletionImpact =
964
+ await ruleDeletionImpact(RULE_1_ID);
965
+
966
+ expect(impact.coverageLost).toHaveLength(1);
967
+ expect(impact.coverageLost[0]!.severityName).toBeUndefined();
968
+ expect(warningsMatching(impact, "this severity")).toHaveLength(1);
969
+ });
970
+ });
971
+
972
+ /*
973
+ * ---------------------------------------------------------------------------
974
+ * (D) Handoff rules are reported apart from paging coverage.
975
+ *
976
+ * Nobody is waiting on "you are now on call". Mixing it into the same list as a
977
+ * missed Sev1 page at the same weight is how a warning surface teaches people
978
+ * to skim it.
979
+ * ---------------------------------------------------------------------------
980
+ */
981
+
982
+ describe("handoff (shift change) rules", () => {
983
+ test("losing the last on-call handoff rule is reported separately, not as coverage", async () => {
984
+ ruleRows = [
985
+ rule({
986
+ id: RULE_1_ID,
987
+ ruleType: NotificationRuleType.WHEN_USER_GOES_ON_CALL,
988
+ userSmsId: SMS_METHOD_ID,
989
+ }),
990
+ ];
991
+
992
+ const impact: NotificationDeletionImpact =
993
+ await ruleDeletionImpact(RULE_1_ID);
994
+
995
+ expect(impact.coverageLost).toHaveLength(0);
996
+ expect(impact.handoffNotificationsLost).toEqual([
997
+ NotificationRuleType.WHEN_USER_GOES_ON_CALL,
998
+ ]);
999
+ expect(warningsMatching(impact, "when they go on call")).toHaveLength(1);
1000
+ });
1001
+
1002
+ test("both handoff types can be lost at once, in a stable order", async () => {
1003
+ ruleRows = [
1004
+ rule({
1005
+ id: RULE_1_ID,
1006
+ ruleType: NotificationRuleType.WHEN_USER_GOES_ON_CALL,
1007
+ userSmsId: SMS_METHOD_ID,
1008
+ }),
1009
+ rule({
1010
+ id: RULE_2_ID,
1011
+ ruleType: NotificationRuleType.WHEN_USER_GOES_OFF_CALL,
1012
+ userSmsId: SMS_METHOD_ID,
1013
+ }),
1014
+ ];
1015
+
1016
+ const impact: NotificationDeletionImpact = await ruleDeletionImpact(
1017
+ RULE_1_ID,
1018
+ RULE_2_ID,
1019
+ );
1020
+
1021
+ expect(impact.handoffNotificationsLost).toEqual([
1022
+ NotificationRuleType.WHEN_USER_GOES_ON_CALL,
1023
+ NotificationRuleType.WHEN_USER_GOES_OFF_CALL,
1024
+ ]);
1025
+ });
1026
+
1027
+ test("a surviving handoff rule on another method loses nothing", async () => {
1028
+ ruleRows = [
1029
+ rule({
1030
+ id: RULE_1_ID,
1031
+ ruleType: NotificationRuleType.WHEN_USER_GOES_ON_CALL,
1032
+ userSmsId: SMS_METHOD_ID,
1033
+ }),
1034
+ rule({
1035
+ id: RULE_2_ID,
1036
+ ruleType: NotificationRuleType.WHEN_USER_GOES_ON_CALL,
1037
+ userEmailId: EMAIL_METHOD_ID,
1038
+ }),
1039
+ ];
1040
+
1041
+ const impact: NotificationDeletionImpact =
1042
+ await ruleDeletionImpact(RULE_1_ID);
1043
+
1044
+ expect(impact.handoffNotificationsLost).toHaveLength(0);
1045
+ });
1046
+ });
1047
+
1048
+ /*
1049
+ * ---------------------------------------------------------------------------
1050
+ * (E) Deleting a METHOD: the cascade.
1051
+ *
1052
+ * This is the shape that costs people pages. The rules are not what was
1053
+ * clicked, and their disappearance is the whole point of the preview.
1054
+ * ---------------------------------------------------------------------------
1055
+ */
1056
+
1057
+ describe("deleting a notification method: the cascade", () => {
1058
+ test("every rule pointing at the method is counted and its cells are lost", async () => {
1059
+ ruleRows = [
1060
+ incidentRule(RULE_1_ID, SEV1_ID, SMS_METHOD_ID),
1061
+ incidentRule(RULE_2_ID, SEV2_ID, SMS_METHOD_ID),
1062
+ ];
1063
+
1064
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1065
+ ReadinessMethodType.SMS,
1066
+ SMS_METHOD_ID,
1067
+ );
1068
+
1069
+ expect(impact.rulesDeletedCount).toBe(2);
1070
+ expect(impact.coverageLost).toHaveLength(2);
1071
+ expect(
1072
+ warningsMatching(impact, "also deletes 2 notification rules"),
1073
+ ).toHaveLength(1);
1074
+ });
1075
+
1076
+ test("rules on a DIFFERENT method of the same channel survive", async () => {
1077
+ const otherSmsId: ObjectID = new ObjectID(
1078
+ "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
1079
+ );
1080
+
1081
+ ruleRows = [
1082
+ incidentRule(RULE_1_ID, SEV1_ID, SMS_METHOD_ID),
1083
+ incidentRule(RULE_2_ID, SEV1_ID, otherSmsId),
1084
+ ];
1085
+
1086
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1087
+ ReadinessMethodType.SMS,
1088
+ SMS_METHOD_ID,
1089
+ );
1090
+
1091
+ expect(impact.rulesDeletedCount).toBe(1);
1092
+ expect(impact.coverageLost).toHaveLength(0);
1093
+ });
1094
+
1095
+ test("the foreign key tested follows the CHANNEL, not merely the id value", async () => {
1096
+ /*
1097
+ * The crossed-column regression. A rule that points at a WhatsApp method
1098
+ * whose id happens to equal the SMS id being deleted must not be reported
1099
+ * as going away - and the reverse mistake would leave a rule out of a
1100
+ * cascade that really does take it.
1101
+ */
1102
+ ruleRows = [
1103
+ rule({
1104
+ id: RULE_1_ID,
1105
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1106
+ incidentSeverityId: SEV1_ID,
1107
+ userWhatsAppId: SMS_METHOD_ID,
1108
+ }),
1109
+ ];
1110
+
1111
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1112
+ ReadinessMethodType.SMS,
1113
+ SMS_METHOD_ID,
1114
+ );
1115
+
1116
+ expect(impact.rulesDeletedCount).toBe(0);
1117
+ expect(impact.coverageLost).toHaveLength(0);
1118
+ });
1119
+
1120
+ test("each of the seven channels reads its own foreign key", async () => {
1121
+ const cases: Array<{
1122
+ methodType: ReadinessMethodType;
1123
+ makeRule: () => UserNotificationRule;
1124
+ }> = [
1125
+ {
1126
+ methodType: ReadinessMethodType.Email,
1127
+ makeRule: (): UserNotificationRule => {
1128
+ return rule({
1129
+ id: RULE_1_ID,
1130
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1131
+ incidentSeverityId: SEV1_ID,
1132
+ userEmailId: SMS_METHOD_ID,
1133
+ });
1134
+ },
1135
+ },
1136
+ {
1137
+ methodType: ReadinessMethodType.SMS,
1138
+ makeRule: (): UserNotificationRule => {
1139
+ return rule({
1140
+ id: RULE_1_ID,
1141
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1142
+ incidentSeverityId: SEV1_ID,
1143
+ userSmsId: SMS_METHOD_ID,
1144
+ });
1145
+ },
1146
+ },
1147
+ {
1148
+ methodType: ReadinessMethodType.Call,
1149
+ makeRule: (): UserNotificationRule => {
1150
+ return rule({
1151
+ id: RULE_1_ID,
1152
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1153
+ incidentSeverityId: SEV1_ID,
1154
+ userCallId: SMS_METHOD_ID,
1155
+ });
1156
+ },
1157
+ },
1158
+ {
1159
+ methodType: ReadinessMethodType.Push,
1160
+ makeRule: (): UserNotificationRule => {
1161
+ return rule({
1162
+ id: RULE_1_ID,
1163
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1164
+ incidentSeverityId: SEV1_ID,
1165
+ userPushId: SMS_METHOD_ID,
1166
+ });
1167
+ },
1168
+ },
1169
+ {
1170
+ methodType: ReadinessMethodType.WhatsApp,
1171
+ makeRule: (): UserNotificationRule => {
1172
+ return rule({
1173
+ id: RULE_1_ID,
1174
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1175
+ incidentSeverityId: SEV1_ID,
1176
+ userWhatsAppId: WHATSAPP_METHOD_ID,
1177
+ });
1178
+ },
1179
+ },
1180
+ {
1181
+ methodType: ReadinessMethodType.Telegram,
1182
+ makeRule: (): UserNotificationRule => {
1183
+ return rule({
1184
+ id: RULE_1_ID,
1185
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1186
+ incidentSeverityId: SEV1_ID,
1187
+ userTelegramId: SMS_METHOD_ID,
1188
+ });
1189
+ },
1190
+ },
1191
+ {
1192
+ methodType: ReadinessMethodType.Webhook,
1193
+ makeRule: (): UserNotificationRule => {
1194
+ return rule({
1195
+ id: RULE_1_ID,
1196
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1197
+ incidentSeverityId: SEV1_ID,
1198
+ userWebhookId: SMS_METHOD_ID,
1199
+ });
1200
+ },
1201
+ },
1202
+ ];
1203
+
1204
+ for (const one of cases) {
1205
+ ruleRows = [one.makeRule()];
1206
+
1207
+ const methodId: ObjectID =
1208
+ one.methodType === ReadinessMethodType.WhatsApp
1209
+ ? WHATSAPP_METHOD_ID
1210
+ : SMS_METHOD_ID;
1211
+
1212
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1213
+ one.methodType,
1214
+ methodId,
1215
+ );
1216
+
1217
+ expect(impact.rulesDeletedCount).toBe(1);
1218
+ expect(impact.coverageLost).toHaveLength(1);
1219
+ }
1220
+ });
1221
+
1222
+ test("the method row is looked up scoped by projectId, not by id alone", async () => {
1223
+ ruleRows = [];
1224
+
1225
+ await methodDeletionImpact(ReadinessMethodType.SMS, SMS_METHOD_ID);
1226
+
1227
+ const call: FindOneByCall = smsFindOneBy.mock.calls[0]![0] as FindOneByCall;
1228
+
1229
+ expect(call.query["_id"]?.toString()).toBe(SMS_METHOD_ID.toString());
1230
+ expect(call.query["projectId"]?.toString()).toBe(PROJECT_ID.toString());
1231
+ });
1232
+
1233
+ test("the userId comes from the METHOD ROW, and the rule read uses it", async () => {
1234
+ /*
1235
+ * The caller never supplies a userId on this path. Taking it from the row
1236
+ * is what makes it impossible to ask about one user's method under another
1237
+ * user's name and get an answer that belongs to neither.
1238
+ */
1239
+ const foreignRow: UserSMS = new UserSMS();
1240
+ foreignRow.id = SMS_METHOD_ID;
1241
+ foreignRow.userId = OTHER_USER_ID;
1242
+ foreignRow.isVerified = true;
1243
+ smsFindOneBy.mockResolvedValue(foreignRow as never);
1244
+
1245
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1246
+ ReadinessMethodType.SMS,
1247
+ SMS_METHOD_ID,
1248
+ );
1249
+
1250
+ expect(impact.userId.toString()).toBe(OTHER_USER_ID.toString());
1251
+ expect(firstCall(ruleFindBy).query["userId"]?.toString()).toBe(
1252
+ OTHER_USER_ID.toString(),
1253
+ );
1254
+ expect(readinessForUsers.mock.calls[0]![0]![0].toString()).toBe(
1255
+ OTHER_USER_ID.toString(),
1256
+ );
1257
+ });
1258
+
1259
+ test("a method that is not there throws rather than reporting an empty impact", async () => {
1260
+ smsFindOneBy.mockResolvedValue(null as never);
1261
+
1262
+ await expect(
1263
+ methodDeletionImpact(ReadinessMethodType.SMS, SMS_METHOD_ID),
1264
+ ).rejects.toThrow("Notification method not found");
1265
+ });
1266
+
1267
+ test("a method row with no userId throws rather than guessing", async () => {
1268
+ const orphan: UserSMS = new UserSMS();
1269
+ orphan.id = SMS_METHOD_ID;
1270
+ smsFindOneBy.mockResolvedValue(orphan as never);
1271
+
1272
+ await expect(
1273
+ methodDeletionImpact(ReadinessMethodType.SMS, SMS_METHOD_ID),
1274
+ ).rejects.toThrow("Notification method not found");
1275
+ });
1276
+
1277
+ test("the cascade sentence is singular for one rule", async () => {
1278
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID, SMS_METHOD_ID)];
1279
+
1280
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1281
+ ReadinessMethodType.SMS,
1282
+ SMS_METHOD_ID,
1283
+ );
1284
+
1285
+ expect(
1286
+ warningsMatching(impact, "also deletes 1 notification rule that use it"),
1287
+ ).toHaveLength(1);
1288
+ });
1289
+
1290
+ test("no cascade sentence when the method carries no rules at all", async () => {
1291
+ ruleRows = [];
1292
+
1293
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1294
+ ReadinessMethodType.SMS,
1295
+ SMS_METHOD_ID,
1296
+ );
1297
+
1298
+ expect(warningsMatching(impact, "also deletes")).toHaveLength(0);
1299
+ });
1300
+
1301
+ test("the cascade sentence comes FIRST - it is the part nobody clicked", async () => {
1302
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID, SMS_METHOD_ID)];
1303
+
1304
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1305
+ ReadinessMethodType.SMS,
1306
+ SMS_METHOD_ID,
1307
+ );
1308
+
1309
+ expect(impact.warnings[0]).toContain("also deletes");
1310
+ });
1311
+ });
1312
+
1313
+ /*
1314
+ * ---------------------------------------------------------------------------
1315
+ * (F) Reachability after the deletion.
1316
+ *
1317
+ * Two of the four answers are certain and two are not, and the split is
1318
+ * structural: a method must be VERIFIED to be used at all, and Push, Email and
1319
+ * Webhook have no project switch that can turn them off. Anything that turns
1320
+ * the honest "depends" into a green is a false reassurance in the one dialog
1321
+ * where it costs a page.
1322
+ * ---------------------------------------------------------------------------
1323
+ */
1324
+
1325
+ describe("reachability after the deletion", () => {
1326
+ test("deleting the last verified method reports NotReachable and says so", async () => {
1327
+ readinessRows = [
1328
+ readiness({
1329
+ status: ReadinessStatus.Ready,
1330
+ methods: [method(ReadinessMethodType.SMS)],
1331
+ }),
1332
+ ];
1333
+
1334
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1335
+ ReadinessMethodType.SMS,
1336
+ SMS_METHOD_ID,
1337
+ );
1338
+
1339
+ expect(impact.reachability).toBe(PostDeletionReachability.NotReachable);
1340
+ expect(impact.verifiedMethodCountAfterDeletion).toBe(0);
1341
+ expect(
1342
+ warningsMatching(impact, "last verified notification method"),
1343
+ ).toHaveLength(1);
1344
+ });
1345
+
1346
+ test("a surviving verified Email is a CERTAIN yes - no project switch can turn it off", async () => {
1347
+ readinessRows = [
1348
+ readiness({
1349
+ methods: [
1350
+ method(ReadinessMethodType.SMS),
1351
+ method(ReadinessMethodType.Email),
1352
+ ],
1353
+ }),
1354
+ ];
1355
+
1356
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1357
+ ReadinessMethodType.SMS,
1358
+ SMS_METHOD_ID,
1359
+ );
1360
+
1361
+ expect(impact.reachability).toBe(PostDeletionReachability.Reachable);
1362
+ expect(impact.verifiedMethodCountAfterDeletion).toBe(1);
1363
+ });
1364
+
1365
+ test("a surviving verified Push is equally certain", async () => {
1366
+ readinessRows = [
1367
+ readiness({
1368
+ methods: [
1369
+ method(ReadinessMethodType.SMS),
1370
+ method(ReadinessMethodType.Push),
1371
+ ],
1372
+ }),
1373
+ ];
1374
+
1375
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1376
+ ReadinessMethodType.SMS,
1377
+ SMS_METHOD_ID,
1378
+ );
1379
+
1380
+ expect(impact.reachability).toBe(PostDeletionReachability.Reachable);
1381
+ });
1382
+
1383
+ test("a surviving Webhook counts, because a webhook has no verification concept", async () => {
1384
+ readinessRows = [
1385
+ readiness({
1386
+ methods: [
1387
+ method(ReadinessMethodType.SMS),
1388
+ method(ReadinessMethodType.Webhook),
1389
+ ],
1390
+ }),
1391
+ ];
1392
+
1393
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1394
+ ReadinessMethodType.SMS,
1395
+ SMS_METHOD_ID,
1396
+ );
1397
+
1398
+ expect(impact.reachability).toBe(PostDeletionReachability.Reachable);
1399
+ });
1400
+
1401
+ test("only PAID channels surviving is an honest 'depends', never a green", async () => {
1402
+ readinessRows = [
1403
+ readiness({
1404
+ methods: [
1405
+ method(ReadinessMethodType.Email),
1406
+ method(ReadinessMethodType.SMS),
1407
+ method(ReadinessMethodType.Call),
1408
+ ],
1409
+ }),
1410
+ ];
1411
+
1412
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1413
+ ReadinessMethodType.Email,
1414
+ EMAIL_METHOD_ID,
1415
+ );
1416
+
1417
+ expect(impact.reachability).toBe(
1418
+ PostDeletionReachability.DependsOnProjectSettings,
1419
+ );
1420
+ expect(impact.verifiedMethodCountAfterDeletion).toBe(2);
1421
+
1422
+ const warning: Array<string> = warningsMatching(
1423
+ impact,
1424
+ "channel the project can switch off",
1425
+ );
1426
+
1427
+ expect(warning).toHaveLength(1);
1428
+ expect(warning[0]).toContain("SMS, Call");
1429
+ expect(warning[0]).toContain("On-Call > Readiness");
1430
+ });
1431
+
1432
+ test("deleting an UNVERIFIED method changes nothing about reachability", async () => {
1433
+ const unverified: UserSMS = new UserSMS();
1434
+ unverified.id = SMS_METHOD_ID;
1435
+ unverified.userId = USER_ID;
1436
+ unverified.isVerified = false;
1437
+ smsFindOneBy.mockResolvedValue(unverified as never);
1438
+
1439
+ readinessRows = [
1440
+ readiness({
1441
+ methods: [
1442
+ method(ReadinessMethodType.Email),
1443
+ method(ReadinessMethodType.SMS, false),
1444
+ ],
1445
+ }),
1446
+ ];
1447
+
1448
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1449
+ ReadinessMethodType.SMS,
1450
+ SMS_METHOD_ID,
1451
+ );
1452
+
1453
+ /*
1454
+ * An unverified method is never used by anything, so removing it cannot
1455
+ * cost a page - and a scary sentence in front of somebody cleaning up a
1456
+ * typo'd number is exactly the noise that gets warnings ignored.
1457
+ */
1458
+ expect(impact.reachability).toBe(PostDeletionReachability.Reachable);
1459
+ expect(impact.verifiedMethodCountAfterDeletion).toBe(1);
1460
+ });
1461
+
1462
+ test("exactly ONE entry of the channel is removed - a second number of the same kind survives", async () => {
1463
+ readinessRows = [
1464
+ readiness({
1465
+ methods: [
1466
+ method(ReadinessMethodType.SMS),
1467
+ method(ReadinessMethodType.SMS),
1468
+ ],
1469
+ }),
1470
+ ];
1471
+
1472
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1473
+ ReadinessMethodType.SMS,
1474
+ SMS_METHOD_ID,
1475
+ );
1476
+
1477
+ expect(impact.verifiedMethodCountAfterDeletion).toBe(1);
1478
+ expect(impact.reachability).toBe(
1479
+ PostDeletionReachability.DependsOnProjectSettings,
1480
+ );
1481
+ });
1482
+
1483
+ test("a user readiness already calls NotReachable is AlreadyNotReachable, not a new break", async () => {
1484
+ readinessRows = [
1485
+ readiness({
1486
+ status: ReadinessStatus.NotReachable,
1487
+ methods: [method(ReadinessMethodType.SMS)],
1488
+ }),
1489
+ ];
1490
+
1491
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1492
+ ReadinessMethodType.SMS,
1493
+ SMS_METHOD_ID,
1494
+ );
1495
+
1496
+ expect(impact.reachability).toBe(
1497
+ PostDeletionReachability.AlreadyNotReachable,
1498
+ );
1499
+ expect(
1500
+ warningsMatching(impact, "already has no usable notification method"),
1501
+ ).toHaveLength(1);
1502
+ expect(
1503
+ warningsMatching(impact, "last verified notification method"),
1504
+ ).toHaveLength(0);
1505
+ });
1506
+
1507
+ test("NotReachable status wins over a surviving verified method on a switched-off channel", async () => {
1508
+ /*
1509
+ * Readiness says NotReachable when nothing is USABLE, which includes "every
1510
+ * verified method is on a channel this project switched off". Checking that
1511
+ * first is what stops the branches below promising reachability on the
1512
+ * strength of a method nothing can send on.
1513
+ */
1514
+ readinessRows = [
1515
+ readiness({
1516
+ status: ReadinessStatus.NotReachable,
1517
+ methods: [
1518
+ method(ReadinessMethodType.SMS),
1519
+ method(ReadinessMethodType.Call),
1520
+ ],
1521
+ }),
1522
+ ];
1523
+
1524
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1525
+ ReadinessMethodType.SMS,
1526
+ SMS_METHOD_ID,
1527
+ );
1528
+
1529
+ expect(impact.reachability).toBe(
1530
+ PostDeletionReachability.AlreadyNotReachable,
1531
+ );
1532
+ });
1533
+
1534
+ test("no readiness at all is Unknown, and is never guessed at", async () => {
1535
+ readinessRows = [];
1536
+
1537
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1538
+ ReadinessMethodType.SMS,
1539
+ SMS_METHOD_ID,
1540
+ );
1541
+
1542
+ expect(impact.reachability).toBe(PostDeletionReachability.Unknown);
1543
+ expect(impact.isOnCallResponder).toBe(false);
1544
+ expect(impact.reachedVia).toEqual([]);
1545
+ expect(warningsMatching(impact, "could not be determined")).toHaveLength(1);
1546
+ });
1547
+
1548
+ test("deleting a RULE never changes the verified method count", async () => {
1549
+ readinessRows = [
1550
+ readiness({
1551
+ methods: [
1552
+ method(ReadinessMethodType.Email),
1553
+ method(ReadinessMethodType.SMS),
1554
+ ],
1555
+ }),
1556
+ ];
1557
+
1558
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
1559
+
1560
+ const impact: NotificationDeletionImpact =
1561
+ await ruleDeletionImpact(RULE_1_ID);
1562
+
1563
+ expect(impact.verifiedMethodCountAfterDeletion).toBe(2);
1564
+ expect(impact.reachability).toBe(PostDeletionReachability.Reachable);
1565
+ });
1566
+
1567
+ test("unverified methods never count towards the surviving total", async () => {
1568
+ readinessRows = [
1569
+ readiness({
1570
+ methods: [
1571
+ method(ReadinessMethodType.SMS),
1572
+ method(ReadinessMethodType.Email, false),
1573
+ method(ReadinessMethodType.Call, false),
1574
+ ],
1575
+ }),
1576
+ ];
1577
+
1578
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1579
+ ReadinessMethodType.SMS,
1580
+ SMS_METHOD_ID,
1581
+ );
1582
+
1583
+ expect(impact.verifiedMethodCountAfterDeletion).toBe(0);
1584
+ expect(impact.reachability).toBe(PostDeletionReachability.NotReachable);
1585
+ });
1586
+ });
1587
+
1588
+ /*
1589
+ * ---------------------------------------------------------------------------
1590
+ * (G) "Is this person on call anywhere" comes from OnCallReadinessService and
1591
+ * from nowhere else.
1592
+ *
1593
+ * A second answer to that question that disagreed with the readiness page
1594
+ * would be worse than no answer at all.
1595
+ * ---------------------------------------------------------------------------
1596
+ */
1597
+
1598
+ describe("responder status is reused, never re-derived", () => {
1599
+ test("readiness is asked exactly once, for exactly this user and project", async () => {
1600
+ await ruleDeletionImpact();
1601
+
1602
+ expect(readinessForUsers).toHaveBeenCalledTimes(1);
1603
+
1604
+ const userIds: Array<ObjectID> = readinessForUsers.mock
1605
+ .calls[0]![0] as Array<ObjectID>;
1606
+
1607
+ expect(userIds).toHaveLength(1);
1608
+ expect(userIds[0]!.toString()).toBe(USER_ID.toString());
1609
+ expect((readinessForUsers.mock.calls[0]![1] as ObjectID).toString()).toBe(
1610
+ PROJECT_ID.toString(),
1611
+ );
1612
+ });
1613
+
1614
+ test("the responder set is NEVER resolved here - no escalation, schedule, override or team read", async () => {
1615
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
1616
+
1617
+ await ruleDeletionImpact(RULE_1_ID);
1618
+
1619
+ expect(escalationUserFindBy).not.toHaveBeenCalled();
1620
+ expect(escalationTeamFindBy).not.toHaveBeenCalled();
1621
+ expect(scheduleLayerUserFindBy).not.toHaveBeenCalled();
1622
+ expect(overrideFindBy).not.toHaveBeenCalled();
1623
+ expect(teamMemberFindBy).not.toHaveBeenCalled();
1624
+ });
1625
+
1626
+ test("reachedVia is carried through verbatim, and named in prose", async () => {
1627
+ readinessRows = [
1628
+ readiness({
1629
+ reachedVia: [ResponderSource.Team, ResponderSource.Override],
1630
+ }),
1631
+ ];
1632
+
1633
+ const impact: NotificationDeletionImpact = await ruleDeletionImpact();
1634
+
1635
+ expect(impact.isOnCallResponder).toBe(true);
1636
+ expect(impact.reachedVia).toEqual([
1637
+ ResponderSource.Team,
1638
+ ResponderSource.Override,
1639
+ ]);
1640
+
1641
+ const warning: Array<string> = warningsMatching(impact, "is on call");
1642
+
1643
+ expect(warning).toHaveLength(1);
1644
+ expect(warning[0]).toContain("through a team");
1645
+ expect(warning[0]).toContain("through an override");
1646
+ });
1647
+
1648
+ test("all four sources have prose, none leaks a bare enum value", async () => {
1649
+ readinessRows = [
1650
+ readiness({
1651
+ reachedVia: [
1652
+ ResponderSource.Direct,
1653
+ ResponderSource.Team,
1654
+ ResponderSource.Schedule,
1655
+ ResponderSource.Override,
1656
+ ],
1657
+ }),
1658
+ ];
1659
+
1660
+ const impact: NotificationDeletionImpact = await ruleDeletionImpact();
1661
+
1662
+ const warning: string = warningsMatching(impact, "is on call")[0]!;
1663
+
1664
+ expect(warning).toContain("directly on an escalation rule");
1665
+ expect(warning).toContain("through a team");
1666
+ expect(warning).toContain("through a schedule");
1667
+ expect(warning).toContain("through an override");
1668
+ });
1669
+
1670
+ test("a user on no policy is told so OUT LOUD, not by the absence of a warning", async () => {
1671
+ readinessRows = [readiness({ reachedVia: [] })];
1672
+
1673
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
1674
+
1675
+ const impact: NotificationDeletionImpact =
1676
+ await ruleDeletionImpact(RULE_1_ID);
1677
+
1678
+ expect(impact.isOnCallResponder).toBe(false);
1679
+ expect(
1680
+ warningsMatching(impact, "not on any on-call policy right now"),
1681
+ ).toHaveLength(1);
1682
+ });
1683
+
1684
+ test("a user on no policy is STILL told what coverage disappears", async () => {
1685
+ readinessRows = [readiness({ reachedVia: [] })];
1686
+
1687
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
1688
+
1689
+ const impact: NotificationDeletionImpact =
1690
+ await ruleDeletionImpact(RULE_1_ID);
1691
+
1692
+ /*
1693
+ * They may be added to a policy tomorrow, and the configuration they are
1694
+ * deleting today is what will page them then.
1695
+ */
1696
+ expect(impact.coverageLost).toHaveLength(1);
1697
+ expect(
1698
+ warningsMatching(impact, "no rule covers Sev1 incidents"),
1699
+ ).toHaveLength(1);
1700
+ });
1701
+ });
1702
+
1703
+ /*
1704
+ * ---------------------------------------------------------------------------
1705
+ * (H) The consequence clause: what actually happens to an uncovered page.
1706
+ *
1707
+ * "No rule for Sev4" is a shrug. "No rule for Sev4, and those pages are
1708
+ * dropped" is a decision. The two possible endings are the project's fallback
1709
+ * switch, and this surface must agree with the readiness page about it.
1710
+ * ---------------------------------------------------------------------------
1711
+ */
1712
+
1713
+ describe("the fallback consequence", () => {
1714
+ test("with the fallback ON, an uncovered cell falls back", async () => {
1715
+ projectRow = project(false);
1716
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
1717
+
1718
+ const impact: NotificationDeletionImpact =
1719
+ await ruleDeletionImpact(RULE_1_ID);
1720
+
1721
+ expect(impact.isFallbackEnabled).toBe(true);
1722
+ expect(
1723
+ warningsMatching(impact, "fall back to whatever this user has verified"),
1724
+ ).toHaveLength(1);
1725
+ expect(warningsMatching(impact, "dropped")).toHaveLength(0);
1726
+ });
1727
+
1728
+ test("with the fallback OFF, an uncovered cell is a dropped page and says so", async () => {
1729
+ projectRow = project(true);
1730
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
1731
+
1732
+ const impact: NotificationDeletionImpact =
1733
+ await ruleDeletionImpact(RULE_1_ID);
1734
+
1735
+ expect(impact.isFallbackEnabled).toBe(false);
1736
+
1737
+ const warning: Array<string> = warningsMatching(impact, "are dropped");
1738
+
1739
+ expect(warning).toHaveLength(1);
1740
+ expect(warning[0]).toContain(
1741
+ "on-call fallback is disabled for this project",
1742
+ );
1743
+ });
1744
+
1745
+ test("a project row that cannot be read reads as fallback ON, exactly as readiness reads it", async () => {
1746
+ projectRow = null;
1747
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
1748
+
1749
+ const impact: NotificationDeletionImpact =
1750
+ await ruleDeletionImpact(RULE_1_ID);
1751
+
1752
+ expect(impact.isFallbackEnabled).toBe(true);
1753
+ });
1754
+
1755
+ test("the fallback switch is read once, for the project asked about", async () => {
1756
+ await ruleDeletionImpact();
1757
+
1758
+ expect(projectFindOneById).toHaveBeenCalledTimes(1);
1759
+
1760
+ const call: {
1761
+ id: ObjectID;
1762
+ select: Record<string, unknown>;
1763
+ props: { isRoot?: boolean };
1764
+ } = projectFindOneById.mock.calls[0]![0] as {
1765
+ id: ObjectID;
1766
+ select: Record<string, unknown>;
1767
+ props: { isRoot?: boolean };
1768
+ };
1769
+
1770
+ expect(call.id.toString()).toBe(PROJECT_ID.toString());
1771
+ expect(call.select["disableOnCallNotificationFallback"]).toBe(true);
1772
+ expect(call.props.isRoot).toBe(true);
1773
+ });
1774
+
1775
+ test("one sentence per rule type listing its severities, not one per cell", async () => {
1776
+ ruleRows = [
1777
+ incidentRule(RULE_1_ID, SEV1_ID),
1778
+ incidentRule(RULE_2_ID, SEV2_ID),
1779
+ ];
1780
+
1781
+ const impact: NotificationDeletionImpact = await ruleDeletionImpact(
1782
+ RULE_1_ID,
1783
+ RULE_2_ID,
1784
+ );
1785
+
1786
+ const warning: Array<string> = warningsMatching(impact, "no rule covers");
1787
+
1788
+ expect(warning).toHaveLength(1);
1789
+ expect(warning[0]).toContain("Sev1, Sev2 incidents");
1790
+ });
1791
+
1792
+ test("severities are listed in the project's own order, not alphabetically", async () => {
1793
+ /*
1794
+ * "Sev1, Sev2" sorts the same both ways and would hide this. "Critical,
1795
+ * High, Low" does not: severity order is Critical then High then Low, and
1796
+ * alphabetical order is the same by accident, so the fixture uses names
1797
+ * whose alphabetical order is the REVERSE of their severity order.
1798
+ */
1799
+ incidentSeverityRows = [
1800
+ incidentSeverity(SEV1_ID, "Urgent"),
1801
+ incidentSeverity(SEV2_ID, "Amber"),
1802
+ ];
1803
+
1804
+ ruleRows = [
1805
+ incidentRule(RULE_1_ID, SEV2_ID),
1806
+ incidentRule(RULE_2_ID, SEV1_ID),
1807
+ ];
1808
+
1809
+ const impact: NotificationDeletionImpact = await ruleDeletionImpact(
1810
+ RULE_1_ID,
1811
+ RULE_2_ID,
1812
+ );
1813
+
1814
+ expect(warningsMatching(impact, "no rule covers")[0]).toContain(
1815
+ "Urgent, Amber incidents",
1816
+ );
1817
+ });
1818
+
1819
+ test("the severity read asks for the project's severity order", async () => {
1820
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
1821
+
1822
+ await ruleDeletionImpact(RULE_1_ID);
1823
+
1824
+ expect(firstCall(incidentSeverityFindBy).sort).toEqual({
1825
+ order: SortOrder.Ascending,
1826
+ });
1827
+ });
1828
+
1829
+ test("severities are not read at all when nothing is lost", async () => {
1830
+ ruleRows = [
1831
+ incidentRule(RULE_1_ID, SEV1_ID, SMS_METHOD_ID),
1832
+ incidentRule(RULE_2_ID, SEV1_ID, EMAIL_METHOD_ID),
1833
+ ];
1834
+
1835
+ await ruleDeletionImpact(RULE_1_ID);
1836
+
1837
+ expect(incidentSeverityFindBy).not.toHaveBeenCalled();
1838
+ expect(alertSeverityFindBy).not.toHaveBeenCalled();
1839
+ });
1840
+
1841
+ test("only the severity table a lost cell needs is read", async () => {
1842
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
1843
+
1844
+ await ruleDeletionImpact(RULE_1_ID);
1845
+
1846
+ expect(incidentSeverityFindBy).toHaveBeenCalledTimes(1);
1847
+ expect(alertSeverityFindBy).not.toHaveBeenCalled();
1848
+ });
1849
+ });
1850
+
1851
+ /*
1852
+ * ---------------------------------------------------------------------------
1853
+ * (I) Row scoping.
1854
+ *
1855
+ * Permission.CurrentUser is auto-granted to every authenticated caller, so a
1856
+ * COLUMN access list can never mean "only my own row". Row scoping comes from
1857
+ * the query, and only from the query.
1858
+ * ---------------------------------------------------------------------------
1859
+ */
1860
+
1861
+ describe("row scoping", () => {
1862
+ test("the rule read is scoped by BOTH projectId and userId", async () => {
1863
+ await ruleDeletionImpact(RULE_1_ID);
1864
+
1865
+ const call: FindByCall = firstCall(ruleFindBy);
1866
+
1867
+ expect(call.query["projectId"]?.toString()).toBe(PROJECT_ID.toString());
1868
+ expect(call.query["userId"]?.toString()).toBe(USER_ID.toString());
1869
+ });
1870
+
1871
+ test("a rule id that belongs to nobody in this scope contributes nothing", async () => {
1872
+ /*
1873
+ * The ids are intersected with what the SCOPED read returned rather than
1874
+ * trusted. A caller passing somebody else's rule id gets an impact that
1875
+ * describes nothing, not an impact that describes their rule.
1876
+ */
1877
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
1878
+
1879
+ const impact: NotificationDeletionImpact = await ruleDeletionImpact(
1880
+ new ObjectID("ffffffff-ffff-4fff-8fff-ffffffffffff"),
1881
+ );
1882
+
1883
+ expect(impact.rulesDeletedCount).toBe(0);
1884
+ expect(impact.coverageLost).toHaveLength(0);
1885
+ });
1886
+
1887
+ test("the project the caller asked about is the project everything is read from", async () => {
1888
+ await UserNotificationRuleService.getRuleDeletionImpact({
1889
+ projectId: OTHER_PROJECT_ID,
1890
+ userId: USER_ID,
1891
+ notificationRuleIds: [],
1892
+ });
1893
+
1894
+ expect(firstCall(ruleFindBy).query["projectId"]?.toString()).toBe(
1895
+ OTHER_PROJECT_ID.toString(),
1896
+ );
1897
+ expect((readinessForUsers.mock.calls[0]![1] as ObjectID).toString()).toBe(
1898
+ OTHER_PROJECT_ID.toString(),
1899
+ );
1900
+ });
1901
+
1902
+ test("every read this service makes is a root read with an explicit scope", async () => {
1903
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
1904
+
1905
+ await ruleDeletionImpact(RULE_1_ID);
1906
+
1907
+ expect(firstCall(ruleFindBy).props?.isRoot).toBe(true);
1908
+ expect(firstCall(incidentSeverityFindBy).props?.isRoot).toBe(true);
1909
+ expect(
1910
+ firstCall(incidentSeverityFindBy).query["projectId"]?.toString(),
1911
+ ).toBe(PROJECT_ID.toString());
1912
+ });
1913
+ });
1914
+
1915
+ /*
1916
+ * ---------------------------------------------------------------------------
1917
+ * (J) Advisory, never blocking.
1918
+ *
1919
+ * The delete still happens through the ordinary CRUD path. Nothing here may
1920
+ * write, and nothing may refuse a delete that is merely inconvenient - a hook
1921
+ * that threw would break a user leaving a project just as surely as it would
1922
+ * stop an accident.
1923
+ * ---------------------------------------------------------------------------
1924
+ */
1925
+
1926
+ describe("advisory, not blocking", () => {
1927
+ test("a preview writes NOTHING", async () => {
1928
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
1929
+
1930
+ await ruleDeletionImpact(RULE_1_ID);
1931
+ await methodDeletionImpact(ReadinessMethodType.SMS, SMS_METHOD_ID);
1932
+
1933
+ expect(ruleCreate).not.toHaveBeenCalled();
1934
+ expect(ruleDeleteBy).not.toHaveBeenCalled();
1935
+ expect(ruleUpdateOneBy).not.toHaveBeenCalled();
1936
+ });
1937
+
1938
+ test("the worst possible impact still RESOLVES - it never throws to stop a delete", async () => {
1939
+ projectRow = project(true);
1940
+ readinessRows = [
1941
+ readiness({
1942
+ status: ReadinessStatus.Ready,
1943
+ methods: [method(ReadinessMethodType.SMS)],
1944
+ reachedVia: [ResponderSource.Direct],
1945
+ }),
1946
+ ];
1947
+ ruleRows = [
1948
+ incidentRule(RULE_1_ID, SEV1_ID, SMS_METHOD_ID),
1949
+ incidentRule(RULE_2_ID, SEV2_ID, SMS_METHOD_ID),
1950
+ ];
1951
+
1952
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
1953
+ ReadinessMethodType.SMS,
1954
+ SMS_METHOD_ID,
1955
+ );
1956
+
1957
+ expect(impact.reachability).toBe(PostDeletionReachability.NotReachable);
1958
+ expect(impact.coverageLost).toHaveLength(2);
1959
+ expect(impact.warnings.length).toBeGreaterThan(3);
1960
+ });
1961
+
1962
+ test("a user who has left the project gets an honest answer, not an exception", async () => {
1963
+ /*
1964
+ * getReadinessForUsers OMITS a non-member rather than throwing, and this
1965
+ * has to stay that way: somebody being removed from a project is one of the
1966
+ * legitimate reasons their methods are being deleted, and an admin doing
1967
+ * housekeeping must not be handed an error.
1968
+ */
1969
+ readinessRows = [];
1970
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
1971
+
1972
+ const impact: NotificationDeletionImpact =
1973
+ await ruleDeletionImpact(RULE_1_ID);
1974
+
1975
+ expect(impact.isOnCallResponder).toBe(false);
1976
+ expect(impact.reachability).toBe(PostDeletionReachability.Unknown);
1977
+ expect(impact.coverageLost).toHaveLength(1);
1978
+ });
1979
+ });
1980
+
1981
+ /*
1982
+ * ---------------------------------------------------------------------------
1983
+ * (K) Paging and truncation.
1984
+ *
1985
+ * A truncated read is not symmetric in its consequences. Unread rules that
1986
+ * would have SURVIVED make this over-warn, which costs a moment. Unread rules
1987
+ * that would have been DELETED make it under-warn, which is the failure this
1988
+ * feature exists to prevent. Either way the caller is told.
1989
+ * ---------------------------------------------------------------------------
1990
+ */
1991
+
1992
+ describe("paging the rule read", () => {
1993
+ test("the read is sorted by _id ascending, so OFFSET paging is stable", async () => {
1994
+ await ruleDeletionImpact();
1995
+
1996
+ expect(firstCall(ruleFindBy).sort).toEqual({ _id: SortOrder.Ascending });
1997
+ });
1998
+
1999
+ test("the read asks for a full page and starts at zero", async () => {
2000
+ await ruleDeletionImpact();
2001
+
2002
+ const call: FindByCall = firstCall(ruleFindBy);
2003
+
2004
+ expect(call.limit).toBe(LIMIT_PER_PROJECT);
2005
+ expect(call.skip).toBe(0);
2006
+ });
2007
+
2008
+ test("a short page ends the read after ONE query", async () => {
2009
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
2010
+
2011
+ await ruleDeletionImpact(RULE_1_ID);
2012
+
2013
+ expect(ruleFindBy).toHaveBeenCalledTimes(1);
2014
+ });
2015
+
2016
+ test("a completely empty rule set is not an error", async () => {
2017
+ ruleRows = [];
2018
+
2019
+ const impact: NotificationDeletionImpact =
2020
+ await ruleDeletionImpact(RULE_1_ID);
2021
+
2022
+ expect(impact.rulesDeletedCount).toBe(0);
2023
+ expect(impact.coverageLost).toHaveLength(0);
2024
+ expect(impact.isTruncated).toBe(false);
2025
+ });
2026
+
2027
+ test("hitting the page ceiling sets isTruncated, logs, and warns in prose", async () => {
2028
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
2029
+ ruleReadAlwaysReturnsAFullPage = true;
2030
+
2031
+ const impact: NotificationDeletionImpact =
2032
+ await ruleDeletionImpact(RULE_1_ID);
2033
+
2034
+ expect(impact.isTruncated).toBe(true);
2035
+ expect(loggerError).toHaveBeenCalled();
2036
+ expect(warningsMatching(impact, "This preview is incomplete")).toHaveLength(
2037
+ 1,
2038
+ );
2039
+ });
2040
+
2041
+ test("an untruncated read never claims to be truncated", async () => {
2042
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID)];
2043
+
2044
+ const impact: NotificationDeletionImpact =
2045
+ await ruleDeletionImpact(RULE_1_ID);
2046
+
2047
+ expect(impact.isTruncated).toBe(false);
2048
+ expect(warningsMatching(impact, "This preview is incomplete")).toHaveLength(
2049
+ 0,
2050
+ );
2051
+ });
2052
+ });
2053
+
2054
+ /*
2055
+ * ---------------------------------------------------------------------------
2056
+ * (L) The per-method-service entry points.
2057
+ *
2058
+ * Each of the seven notification-method services can answer for its own row,
2059
+ * which is where the delete is actually initiated from. Each must name its own
2060
+ * channel: a service that passed the wrong one would preview the cascade of a
2061
+ * method the user is not deleting.
2062
+ * ---------------------------------------------------------------------------
2063
+ */
2064
+
2065
+ describe("the per-method-service entry points", () => {
2066
+ test("each service previews its OWN channel", async () => {
2067
+ const cases: Array<{
2068
+ name: string;
2069
+ call: () => Promise<NotificationDeletionImpact>;
2070
+ ruleWithThisMethod: UserNotificationRule;
2071
+ }> = [
2072
+ {
2073
+ name: "UserEmailService",
2074
+ call: (): Promise<NotificationDeletionImpact> => {
2075
+ return UserEmailService.getDeletionImpact({
2076
+ itemId: SMS_METHOD_ID,
2077
+ projectId: PROJECT_ID,
2078
+ });
2079
+ },
2080
+ ruleWithThisMethod: rule({
2081
+ id: RULE_1_ID,
2082
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
2083
+ incidentSeverityId: SEV1_ID,
2084
+ userEmailId: SMS_METHOD_ID,
2085
+ }),
2086
+ },
2087
+ {
2088
+ name: "UserSmsService",
2089
+ call: (): Promise<NotificationDeletionImpact> => {
2090
+ return UserSmsService.getDeletionImpact({
2091
+ itemId: SMS_METHOD_ID,
2092
+ projectId: PROJECT_ID,
2093
+ });
2094
+ },
2095
+ ruleWithThisMethod: rule({
2096
+ id: RULE_1_ID,
2097
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
2098
+ incidentSeverityId: SEV1_ID,
2099
+ userSmsId: SMS_METHOD_ID,
2100
+ }),
2101
+ },
2102
+ {
2103
+ name: "UserCallService",
2104
+ call: (): Promise<NotificationDeletionImpact> => {
2105
+ return UserCallService.getDeletionImpact({
2106
+ itemId: SMS_METHOD_ID,
2107
+ projectId: PROJECT_ID,
2108
+ });
2109
+ },
2110
+ ruleWithThisMethod: rule({
2111
+ id: RULE_1_ID,
2112
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
2113
+ incidentSeverityId: SEV1_ID,
2114
+ userCallId: SMS_METHOD_ID,
2115
+ }),
2116
+ },
2117
+ {
2118
+ name: "UserPushService",
2119
+ call: (): Promise<NotificationDeletionImpact> => {
2120
+ return UserPushService.getDeletionImpact({
2121
+ itemId: SMS_METHOD_ID,
2122
+ projectId: PROJECT_ID,
2123
+ });
2124
+ },
2125
+ ruleWithThisMethod: rule({
2126
+ id: RULE_1_ID,
2127
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
2128
+ incidentSeverityId: SEV1_ID,
2129
+ userPushId: SMS_METHOD_ID,
2130
+ }),
2131
+ },
2132
+ {
2133
+ name: "UserWhatsAppService",
2134
+ call: (): Promise<NotificationDeletionImpact> => {
2135
+ return UserWhatsAppService.getDeletionImpact({
2136
+ itemId: WHATSAPP_METHOD_ID,
2137
+ projectId: PROJECT_ID,
2138
+ });
2139
+ },
2140
+ ruleWithThisMethod: rule({
2141
+ id: RULE_1_ID,
2142
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
2143
+ incidentSeverityId: SEV1_ID,
2144
+ userWhatsAppId: WHATSAPP_METHOD_ID,
2145
+ }),
2146
+ },
2147
+ {
2148
+ name: "UserTelegramService",
2149
+ call: (): Promise<NotificationDeletionImpact> => {
2150
+ return UserTelegramService.getDeletionImpact({
2151
+ itemId: SMS_METHOD_ID,
2152
+ projectId: PROJECT_ID,
2153
+ });
2154
+ },
2155
+ ruleWithThisMethod: rule({
2156
+ id: RULE_1_ID,
2157
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
2158
+ incidentSeverityId: SEV1_ID,
2159
+ userTelegramId: SMS_METHOD_ID,
2160
+ }),
2161
+ },
2162
+ {
2163
+ name: "UserWebhookService",
2164
+ call: (): Promise<NotificationDeletionImpact> => {
2165
+ return UserWebhookService.getDeletionImpact({
2166
+ itemId: SMS_METHOD_ID,
2167
+ projectId: PROJECT_ID,
2168
+ });
2169
+ },
2170
+ ruleWithThisMethod: rule({
2171
+ id: RULE_1_ID,
2172
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
2173
+ incidentSeverityId: SEV1_ID,
2174
+ userWebhookId: SMS_METHOD_ID,
2175
+ }),
2176
+ },
2177
+ ];
2178
+
2179
+ for (const one of cases) {
2180
+ ruleRows = [one.ruleWithThisMethod];
2181
+
2182
+ const impact: NotificationDeletionImpact = await one.call();
2183
+
2184
+ /*
2185
+ * The rule pointing at THIS channel's foreign key is the one that goes.
2186
+ * A service naming the wrong channel would find no rule at all here.
2187
+ */
2188
+ expect(impact.rulesDeletedCount).toBe(1);
2189
+ expect(impact.coverageLost).toHaveLength(1);
2190
+ }
2191
+ });
2192
+
2193
+ test("each service reads its own table, scoped to the project", async () => {
2194
+ ruleRows = [];
2195
+
2196
+ await UserEmailService.getDeletionImpact({
2197
+ itemId: EMAIL_METHOD_ID,
2198
+ projectId: PROJECT_ID,
2199
+ });
2200
+
2201
+ expect(emailFindOneBy).toHaveBeenCalledTimes(1);
2202
+ expect(smsFindOneBy).not.toHaveBeenCalled();
2203
+
2204
+ const call: FindOneByCall = emailFindOneBy.mock
2205
+ .calls[0]![0] as FindOneByCall;
2206
+
2207
+ expect(call.query["_id"]?.toString()).toBe(EMAIL_METHOD_ID.toString());
2208
+ expect(call.query["projectId"]?.toString()).toBe(PROJECT_ID.toString());
2209
+ });
2210
+
2211
+ test("a webhook counts as verified without an isVerified column to read", async () => {
2212
+ /*
2213
+ * The fixture carries no isVerified at all, which is the truth of the
2214
+ * model. If presence were not the whole test, deleting the one channel
2215
+ * guaranteed to work would look free.
2216
+ */
2217
+ readinessRows = [
2218
+ readiness({ methods: [method(ReadinessMethodType.Webhook)] }),
2219
+ ];
2220
+ ruleRows = [];
2221
+
2222
+ const impact: NotificationDeletionImpact =
2223
+ await UserWebhookService.getDeletionImpact({
2224
+ itemId: SMS_METHOD_ID,
2225
+ projectId: PROJECT_ID,
2226
+ });
2227
+
2228
+ expect(impact.verifiedMethodCountAfterDeletion).toBe(0);
2229
+ expect(impact.reachability).toBe(PostDeletionReachability.NotReachable);
2230
+ });
2231
+
2232
+ test("the seven services do not read each other's tables", async () => {
2233
+ ruleRows = [];
2234
+
2235
+ await UserTelegramService.getDeletionImpact({
2236
+ itemId: SMS_METHOD_ID,
2237
+ projectId: PROJECT_ID,
2238
+ });
2239
+
2240
+ expect(telegramFindOneBy).toHaveBeenCalledTimes(1);
2241
+ expect(emailFindOneBy).not.toHaveBeenCalled();
2242
+ expect(smsFindOneBy).not.toHaveBeenCalled();
2243
+ expect(callFindOneBy).not.toHaveBeenCalled();
2244
+ expect(pushFindOneBy).not.toHaveBeenCalled();
2245
+ expect(whatsAppFindOneBy).not.toHaveBeenCalled();
2246
+ expect(webhookFindOneBy).not.toHaveBeenCalled();
2247
+ });
2248
+ });
2249
+
2250
+ /*
2251
+ * ---------------------------------------------------------------------------
2252
+ * (M) The import cycle.
2253
+ *
2254
+ * This module and OnCallReadinessService import each other, which is fine as
2255
+ * long as neither one READS the other at module-evaluation time. Whichever is
2256
+ * evaluated second sees the first's exports still empty, so a top-level
2257
+ * `ReadinessMethodType.Push` throws on exactly one of the two load orders and
2258
+ * not the other - a crash whose presence depends on which file some unrelated
2259
+ * caller happened to import first, and which therefore shows up as an
2260
+ * apparently unrelated test suite failing to load at all.
2261
+ * ---------------------------------------------------------------------------
2262
+ */
2263
+
2264
+ describe("the OnCallReadinessService import cycle", () => {
2265
+ test("loading readiness FIRST still leaves this module usable", () => {
2266
+ jest.isolateModules((): void => {
2267
+ expect((): void => {
2268
+ /*
2269
+ * The order that used to throw: loading readiness pulls in the policy
2270
+ * services, which pull in this module, which re-enters readiness while
2271
+ * it is still half-evaluated. isolateModules gives both a fresh
2272
+ * registry, so the order actually gets re-run rather than served from
2273
+ * whatever this file's own imports already cached.
2274
+ */
2275
+ jest.requireActual("../../../Server/Services/OnCallReadinessService");
2276
+ jest.requireActual(
2277
+ "../../../Server/Services/UserNotificationRuleService",
2278
+ );
2279
+ }).not.toThrow();
2280
+ });
2281
+ });
2282
+
2283
+ test("loading this module FIRST is equally fine", () => {
2284
+ jest.isolateModules((): void => {
2285
+ expect((): void => {
2286
+ jest.requireActual(
2287
+ "../../../Server/Services/UserNotificationRuleService",
2288
+ );
2289
+ jest.requireActual("../../../Server/Services/OnCallReadinessService");
2290
+ }).not.toThrow();
2291
+ });
2292
+ });
2293
+ });
2294
+
2295
+ /*
2296
+ * ---------------------------------------------------------------------------
2297
+ * (N) The whole answer, on a realistic shape.
2298
+ *
2299
+ * A responder deleting the phone number that carries their entire default
2300
+ * configuration - which is what the default rule seeding produces, one rule per
2301
+ * (rule type x severity) on the method that was verified.
2302
+ * ---------------------------------------------------------------------------
2303
+ */
2304
+
2305
+ describe("a responder deleting the number their whole configuration hangs off", () => {
2306
+ test("the impact names the cascade, the coverage, the handoffs and the silence", async () => {
2307
+ readinessRows = [
2308
+ readiness({
2309
+ status: ReadinessStatus.Ready,
2310
+ methods: [method(ReadinessMethodType.SMS)],
2311
+ reachedVia: [ResponderSource.Schedule],
2312
+ }),
2313
+ ];
2314
+
2315
+ ruleRows = [
2316
+ rule({
2317
+ id: new ObjectID("10000000-0000-4000-8000-000000000001"),
2318
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
2319
+ incidentSeverityId: SEV1_ID,
2320
+ userSmsId: SMS_METHOD_ID,
2321
+ }),
2322
+ rule({
2323
+ id: new ObjectID("10000000-0000-4000-8000-000000000002"),
2324
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
2325
+ incidentSeverityId: SEV2_ID,
2326
+ userSmsId: SMS_METHOD_ID,
2327
+ }),
2328
+ rule({
2329
+ id: new ObjectID("10000000-0000-4000-8000-000000000003"),
2330
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
2331
+ alertSeverityId: ALERT_SEV1_ID,
2332
+ userSmsId: SMS_METHOD_ID,
2333
+ }),
2334
+ rule({
2335
+ id: new ObjectID("10000000-0000-4000-8000-000000000004"),
2336
+ ruleType: NotificationRuleType.WHEN_USER_GOES_ON_CALL,
2337
+ userSmsId: SMS_METHOD_ID,
2338
+ }),
2339
+ rule({
2340
+ id: new ObjectID("10000000-0000-4000-8000-000000000005"),
2341
+ ruleType: NotificationRuleType.WHEN_USER_GOES_OFF_CALL,
2342
+ userSmsId: SMS_METHOD_ID,
2343
+ }),
2344
+ ];
2345
+
2346
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
2347
+ ReadinessMethodType.SMS,
2348
+ SMS_METHOD_ID,
2349
+ );
2350
+
2351
+ expect(impact.rulesDeletedCount).toBe(5);
2352
+ expect(impact.coverageLost).toHaveLength(3);
2353
+ expect(impact.handoffNotificationsLost).toHaveLength(2);
2354
+ expect(impact.isOnCallResponder).toBe(true);
2355
+ expect(impact.reachability).toBe(PostDeletionReachability.NotReachable);
2356
+ expect(impact.verifiedMethodCountAfterDeletion).toBe(0);
2357
+
2358
+ expect(impact.warnings[0]).toContain("also deletes 5 notification rules");
2359
+ expect(impact.warnings[1]).toContain("last verified notification method");
2360
+ expect(impact.warnings[2]).toContain("is on call in this project");
2361
+ expect(warningsMatching(impact, "Sev1, Sev2 incidents")).toHaveLength(1);
2362
+ expect(warningsMatching(impact, "Alert Sev1 alerts")).toHaveLength(1);
2363
+ expect(warningsMatching(impact, "when they go on call")).toHaveLength(1);
2364
+ expect(warningsMatching(impact, "when they go off call")).toHaveLength(1);
2365
+ });
2366
+
2367
+ test("the same responder deleting a SPARE number is told it costs nothing", async () => {
2368
+ const spareSmsId: ObjectID = new ObjectID(
2369
+ "20000000-0000-4000-8000-000000000001",
2370
+ );
2371
+
2372
+ const spareRow: UserSMS = new UserSMS();
2373
+ spareRow.id = spareSmsId;
2374
+ spareRow.userId = USER_ID;
2375
+ spareRow.isVerified = true;
2376
+ smsFindOneBy.mockResolvedValue(spareRow as never);
2377
+
2378
+ readinessRows = [
2379
+ readiness({
2380
+ methods: [
2381
+ method(ReadinessMethodType.SMS),
2382
+ method(ReadinessMethodType.Email),
2383
+ ],
2384
+ reachedVia: [ResponderSource.Schedule],
2385
+ }),
2386
+ ];
2387
+
2388
+ ruleRows = [incidentRule(RULE_1_ID, SEV1_ID, SMS_METHOD_ID)];
2389
+
2390
+ const impact: NotificationDeletionImpact = await methodDeletionImpact(
2391
+ ReadinessMethodType.SMS,
2392
+ spareSmsId,
2393
+ );
2394
+
2395
+ expect(impact.rulesDeletedCount).toBe(0);
2396
+ expect(impact.coverageLost).toHaveLength(0);
2397
+ expect(impact.handoffNotificationsLost).toHaveLength(0);
2398
+ expect(impact.reachability).toBe(PostDeletionReachability.Reachable);
2399
+ expect(warningsMatching(impact, "also deletes")).toHaveLength(0);
2400
+ expect(warningsMatching(impact, "no rule covers")).toHaveLength(0);
2401
+ });
2402
+ });