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