@oneuptime/common 12.0.8 → 12.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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} +139 -9
- package/Models/DatabaseModels/InventoryItemCustomField.ts +434 -0
- package/Models/DatabaseModels/{TelemetryEntityRelationship.ts → InventoryItemRelationship.ts} +8 -8
- 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/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/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/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 +20 -0
- package/Server/Infrastructure/Queue.ts +78 -13
- 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} +4 -4
- package/Server/Services/{TelemetryEntityService.ts → InventoryItemService.ts} +89 -20
- 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/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/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 +25 -25
- package/Server/Utils/Telemetry/TelemetryEntity.ts +160 -52
- 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/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/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/{TelemetryEntityManualCreate.test.ts → InventoryItemManualCreate.test.ts} +27 -27
- 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/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/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 +15 -15
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +356 -39
- 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/Telemetry/EntityTypeGroups.test.ts +6 -6
- 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/TemplateSyntax.test.ts +112 -0
- package/Tests/Types/Workflow/Templates.test.ts +916 -101
- package/Tests/UI/Components/ActiveFilterChipsOpenRoute.test.tsx +82 -0
- package/Tests/UI/Components/ComponentsModal.test.tsx +564 -7
- package/Tests/UI/Components/KeyboardShortcut.test.tsx +95 -0
- package/Tests/UI/Components/LogDetailsPanelCrossSignal.test.tsx +448 -0
- package/Tests/UI/Components/LogsTableCrossLinks.test.tsx +263 -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/Workflow/GraphLint.test.ts +72 -0
- package/Tests/UI/Components/Workflow/GraphLintSummary.test.ts +755 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditor.test.ts +242 -7
- package/Tests/UI/Components/Workflow/ModelColumnEditorServerContract.test.ts +15 -7
- package/Tests/UI/Components/Workflow/ModelSchema.test.ts +242 -28
- package/Tests/UI/Components/Workflow/RunStatusWatcher.test.ts +59 -1
- 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/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/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/NetworkTopologyUtil.test.ts +487 -0
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +698 -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 +49 -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/Telemetry/EntityRelationshipType.ts +1 -1
- package/Types/Telemetry/EntitySource.ts +1 -1
- package/Types/Telemetry/EntityType.ts +1 -1
- package/Types/Telemetry/EntityTypeGroups.ts +1 -1
- package/Types/Workflow/Components/BaseModel.ts +75 -29
- 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/TemplateSyntax.ts +44 -0
- package/Types/Workflow/Templates.ts +2097 -45
- 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/Dictionary/Dictionary.tsx +48 -9
- package/UI/Components/FormModal/BasicFormModal.tsx +2 -1
- package/UI/Components/Header/HeaderIconDropdownButton.tsx +53 -3
- package/UI/Components/Input/Input.tsx +1 -0
- 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/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/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/Workflow/ArgumentsForm.tsx +155 -9
- package/UI/Components/Workflow/ComponentReturnValueViewer.tsx +26 -0
- package/UI/Components/Workflow/ComponentSettingsModal.tsx +10 -1
- 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 +42 -5
- package/UI/Components/Workflow/GraphLintSummary.ts +390 -0
- package/UI/Components/Workflow/ModelColumnEditor.tsx +154 -22
- package/UI/Components/Workflow/ModelSchema.ts +115 -33
- package/UI/Components/Workflow/RunStatusWatcher.ts +1 -1
- package/UI/Components/Workflow/StepTraceViewer.tsx +1 -1
- package/UI/Components/Workflow/UseRunWatch.ts +212 -0
- package/UI/Components/Workflow/VariableModal.tsx +6 -2
- package/UI/Components/Workflow/Workflow.tsx +49 -3
- 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/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/NetworkTopologyUtil.ts +888 -164
- package/Utils/Telemetry/CrossSignalScope.ts +502 -0
- package/Utils/Telemetry/EntityKey.ts +5 -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} +167 -31
- 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} +27 -27
- 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/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/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/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 +20 -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/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} +6 -6
- package/build/dist/Server/Services/InventoryItemRelationshipService.js.map +1 -0
- package/build/dist/Server/Services/{TelemetryEntityService.js → InventoryItemService.js} +68 -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/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/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 +11 -11
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
- 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/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/Telemetry/EntityRelationshipType.js +1 -1
- package/build/dist/Types/Telemetry/EntitySource.js +1 -1
- package/build/dist/Types/Telemetry/EntityType.js +1 -1
- package/build/dist/Types/Telemetry/EntityTypeGroups.js +1 -1
- package/build/dist/Types/Telemetry/EntityTypeGroups.js.map +1 -1
- package/build/dist/Types/Workflow/Components/BaseModel.js +66 -28
- 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/TemplateSyntax.js +12 -0
- package/build/dist/Types/Workflow/TemplateSyntax.js.map +1 -1
- package/build/dist/Types/Workflow/Templates.js +1931 -38
- package/build/dist/Types/Workflow/Templates.js.map +1 -1
- 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/Dictionary/Dictionary.js +25 -11
- 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/Header/HeaderIconDropdownButton.js +27 -4
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +1 -0
- 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/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/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/Workflow/ArgumentsForm.js +130 -12
- package/build/dist/UI/Components/Workflow/ArgumentsForm.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 +4 -4
- 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 +33 -4
- package/build/dist/UI/Components/Workflow/GraphLint.js.map +1 -1
- 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 +116 -21
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ModelSchema.js +72 -34
- package/build/dist/UI/Components/Workflow/ModelSchema.js.map +1 -1
- package/build/dist/UI/Components/Workflow/RunStatusWatcher.js +1 -1
- package/build/dist/UI/Components/Workflow/RunStatusWatcher.js.map +1 -1
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js +1 -1
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -1
- 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/VariableModal.js +3 -2
- package/build/dist/UI/Components/Workflow/VariableModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Workflow.js +41 -1
- 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/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/NetworkTopologyUtil.js +642 -136
- 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 +5 -5
- 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,2470 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { afterEach, describe, expect, jest, test } from "@jest/globals";
|
|
3
|
+
import {
|
|
4
|
+
cleanup,
|
|
5
|
+
fireEvent,
|
|
6
|
+
render,
|
|
7
|
+
screen,
|
|
8
|
+
waitFor,
|
|
9
|
+
} from "@testing-library/react";
|
|
10
|
+
import React from "react";
|
|
11
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* THE READINESS WARNING, WHERE IT LIVES NOW.
|
|
15
|
+
*
|
|
16
|
+
* It used to be a red block in the footer of the add/edit escalation rule modal,
|
|
17
|
+
* describing whoever the admin had just picked. It is now a LABEL on the
|
|
18
|
+
* escalation level itself, with the detail behind a click, and that move changes
|
|
19
|
+
* what is worth testing:
|
|
20
|
+
*
|
|
21
|
+
* - the warning has to survive the modal being closed, because whether a
|
|
22
|
+
* responder can be paged is a property of their account and not of a form
|
|
23
|
+
* that is open for eight seconds;
|
|
24
|
+
* - it has to attribute a person to the LEVEL that reaches them, including
|
|
25
|
+
* through a team or a schedule, which is how responders are usually added;
|
|
26
|
+
* - it must stay QUIET for the amber state on a normal project, because those
|
|
27
|
+
* pages still arrive - and must go amber the moment the project's fallback
|
|
28
|
+
* is off, because then they do not;
|
|
29
|
+
* - it must be honest about what it has not checked rather than rendering the
|
|
30
|
+
* same nothing a healthy level renders;
|
|
31
|
+
* - and the reminder it offers must report what the SERVER said happened,
|
|
32
|
+
* never that a request was made.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const getMock: MockFunction = getJestMockFunction();
|
|
36
|
+
const postMock: MockFunction = getJestMockFunction();
|
|
37
|
+
const getListMock: MockFunction = getJestMockFunction();
|
|
38
|
+
const getItemMock: MockFunction = getJestMockFunction();
|
|
39
|
+
const getCommonHeadersMock: MockFunction = getJestMockFunction();
|
|
40
|
+
const deleteItemMock: MockFunction = getJestMockFunction();
|
|
41
|
+
|
|
42
|
+
/*
|
|
43
|
+
* The arrow wrappers are load bearing: jest.mock is hoisted above the compiled
|
|
44
|
+
* requires, so the consts above are still in their temporal dead zone when the
|
|
45
|
+
* factory body runs. Dereferencing them lazily, at call time, is what works.
|
|
46
|
+
*/
|
|
47
|
+
jest.mock("../../../UI/Utils/API/API", () => {
|
|
48
|
+
return {
|
|
49
|
+
__esModule: true,
|
|
50
|
+
default: {
|
|
51
|
+
get: (...args: Array<any>) => {
|
|
52
|
+
return getMock(...args);
|
|
53
|
+
},
|
|
54
|
+
post: (...args: Array<any>) => {
|
|
55
|
+
return postMock(...args);
|
|
56
|
+
},
|
|
57
|
+
getFriendlyMessage: (error: unknown) => {
|
|
58
|
+
return error instanceof Error ? error.message : "Something went wrong";
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
65
|
+
return {
|
|
66
|
+
__esModule: true,
|
|
67
|
+
default: {
|
|
68
|
+
getCommonHeaders: (...args: Array<any>) => {
|
|
69
|
+
return getCommonHeadersMock(...args);
|
|
70
|
+
},
|
|
71
|
+
getList: (...args: Array<any>) => {
|
|
72
|
+
return getListMock(...args);
|
|
73
|
+
},
|
|
74
|
+
getItem: (...args: Array<any>) => {
|
|
75
|
+
return getItemMock(...args);
|
|
76
|
+
},
|
|
77
|
+
deleteItem: (...args: Array<any>) => {
|
|
78
|
+
return deleteItemMock(...args);
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
jest.mock("react-i18next", () => {
|
|
85
|
+
return {
|
|
86
|
+
useTranslation: () => {
|
|
87
|
+
return {
|
|
88
|
+
t: (key: string, options?: { defaultValue?: string }): string => {
|
|
89
|
+
return options?.defaultValue ?? key;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
import EscalationRules from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/EscalationRule/EscalationRules";
|
|
97
|
+
import {
|
|
98
|
+
ResponderGroupRef,
|
|
99
|
+
ResponderGroupResolution,
|
|
100
|
+
ResponderGroupResolutions,
|
|
101
|
+
ResponderIssue,
|
|
102
|
+
ResponderRef,
|
|
103
|
+
ResponderVia,
|
|
104
|
+
RuleReadinessDetails,
|
|
105
|
+
RuleReadinessLabel,
|
|
106
|
+
RuleReadinessReport,
|
|
107
|
+
RuleWarningLevel,
|
|
108
|
+
SetupReminderStatus,
|
|
109
|
+
SetupReminderStatuses,
|
|
110
|
+
buildRuleReadinessReport,
|
|
111
|
+
describeResponderVia,
|
|
112
|
+
fetchScheduleUsers,
|
|
113
|
+
fetchTeamMembers,
|
|
114
|
+
getResponderGroupKey,
|
|
115
|
+
getResponderGroupName,
|
|
116
|
+
getRuleWarningLabel,
|
|
117
|
+
getRuleWarningLevel,
|
|
118
|
+
readSetupReminderStatuses,
|
|
119
|
+
requestSetupReminders,
|
|
120
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/EscalationRule/EscalationRuleReadiness";
|
|
121
|
+
import {
|
|
122
|
+
ReadinessIndex,
|
|
123
|
+
ReadinessSummaryWire,
|
|
124
|
+
buildReadinessIndex,
|
|
125
|
+
parseReadinessSummary,
|
|
126
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/Readiness/ReadinessTypes";
|
|
127
|
+
import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
|
|
128
|
+
import HTTPResponse from "../../../Types/API/HTTPResponse";
|
|
129
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
130
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
131
|
+
|
|
132
|
+
const USER_ALEX: string = "aaaaaaaa-1111-4111-8111-111111111111";
|
|
133
|
+
const USER_SAM: string = "bbbbbbbb-2222-4222-8222-222222222222";
|
|
134
|
+
const USER_JO: string = "cccccccc-3333-4333-8333-333333333333";
|
|
135
|
+
|
|
136
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
137
|
+
"dddddddd-4444-4444-8444-444444444444",
|
|
138
|
+
);
|
|
139
|
+
const POLICY_ID: ObjectID = new ObjectID(
|
|
140
|
+
"77777777-9999-4999-8999-999999999999",
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const RULE_ONE_ID: ObjectID = new ObjectID(
|
|
144
|
+
"88888888-1010-4010-8010-101010101010",
|
|
145
|
+
);
|
|
146
|
+
const RULE_TWO_ID: ObjectID = new ObjectID(
|
|
147
|
+
"99999999-2020-4020-8020-202020202020",
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const TEAM_ID: string = "12121212-3030-4030-8030-303030303030";
|
|
151
|
+
const SCHEDULE_ID: string = "23232323-4040-4040-8040-404040404040";
|
|
152
|
+
|
|
153
|
+
type OkResponseFunction = (data: JSONObject) => HTTPResponse<JSONObject>;
|
|
154
|
+
|
|
155
|
+
const okResponse: OkResponseFunction = (
|
|
156
|
+
data: JSONObject,
|
|
157
|
+
): HTTPResponse<JSONObject> => {
|
|
158
|
+
return new HTTPResponse<JSONObject>(200, data, {});
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
afterEach(() => {
|
|
162
|
+
cleanup();
|
|
163
|
+
getMock.mockReset();
|
|
164
|
+
postMock.mockReset();
|
|
165
|
+
getListMock.mockReset();
|
|
166
|
+
getItemMock.mockReset();
|
|
167
|
+
getCommonHeadersMock.mockReset();
|
|
168
|
+
deleteItemMock.mockReset();
|
|
169
|
+
localStorage.clear();
|
|
170
|
+
sessionStorage.clear();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
/*
|
|
174
|
+
* ---------------------------------------------------------------------------
|
|
175
|
+
* FIXTURES
|
|
176
|
+
* ---------------------------------------------------------------------------
|
|
177
|
+
*
|
|
178
|
+
* The readiness rows are built through the real parser rather than as literals,
|
|
179
|
+
* so a change to the payload shape breaks these fixtures the same way it would
|
|
180
|
+
* break the browser.
|
|
181
|
+
*/
|
|
182
|
+
interface UserRow {
|
|
183
|
+
userId: string;
|
|
184
|
+
userName: string;
|
|
185
|
+
status: string;
|
|
186
|
+
methods?: Array<JSONObject> | undefined;
|
|
187
|
+
coverage?: Array<JSONObject> | undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
type SummaryFunction = (params: {
|
|
191
|
+
users: Array<UserRow>;
|
|
192
|
+
isFallbackEnabled?: boolean | undefined;
|
|
193
|
+
}) => ReadinessSummaryWire;
|
|
194
|
+
|
|
195
|
+
const summaryOf: SummaryFunction = (params: {
|
|
196
|
+
users: Array<UserRow>;
|
|
197
|
+
isFallbackEnabled?: boolean | undefined;
|
|
198
|
+
}): ReadinessSummaryWire => {
|
|
199
|
+
return parseReadinessSummary({
|
|
200
|
+
projectId: PROJECT_ID.toString(),
|
|
201
|
+
onCallDutyPolicyId: POLICY_ID.toString(),
|
|
202
|
+
isFallbackEnabled: params.isFallbackEnabled !== false,
|
|
203
|
+
isTruncated: false,
|
|
204
|
+
users: params.users.map((user: UserRow): JSONObject => {
|
|
205
|
+
return {
|
|
206
|
+
userId: user.userId,
|
|
207
|
+
userName: user.userName,
|
|
208
|
+
userEmail: `${user.userName.split(" ")[0]!.toLowerCase()}@example.com`,
|
|
209
|
+
status: user.status,
|
|
210
|
+
methods: user.methods || [],
|
|
211
|
+
coverage: user.coverage || [],
|
|
212
|
+
reasons: [],
|
|
213
|
+
reachedVia: [],
|
|
214
|
+
};
|
|
215
|
+
}),
|
|
216
|
+
} as JSONObject);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
type IndexFunction = (users: Array<UserRow>) => ReadinessIndex;
|
|
220
|
+
|
|
221
|
+
const indexOf: IndexFunction = (users: Array<UserRow>): ReadinessIndex => {
|
|
222
|
+
return buildReadinessIndex(summaryOf({ users: users }));
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const ALEX_UNREACHABLE: UserRow = {
|
|
226
|
+
userId: USER_ALEX,
|
|
227
|
+
userName: "Alex Chen",
|
|
228
|
+
status: "NotReachable",
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const SAM_READY: UserRow = {
|
|
232
|
+
userId: USER_SAM,
|
|
233
|
+
userName: "Sam Doe",
|
|
234
|
+
status: "Ready",
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const JO_PARTIAL: UserRow = {
|
|
238
|
+
userId: USER_JO,
|
|
239
|
+
userName: "Jo Park",
|
|
240
|
+
status: "PartiallyReady",
|
|
241
|
+
methods: [
|
|
242
|
+
{
|
|
243
|
+
methodType: "Email",
|
|
244
|
+
maskedIdentifier: "j•••@example.com",
|
|
245
|
+
isVerified: true,
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
coverage: [
|
|
249
|
+
{
|
|
250
|
+
ruleType: "When incident on-call policy is executed",
|
|
251
|
+
severityId: "eeeeeeee-5555-4555-8555-555555555555",
|
|
252
|
+
severityName: "Sev1",
|
|
253
|
+
hasRule: false,
|
|
254
|
+
isOptOut: false,
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
type ResolvedGroupFunction = (params: {
|
|
260
|
+
kind: "team" | "schedule";
|
|
261
|
+
id: string;
|
|
262
|
+
label: string;
|
|
263
|
+
members: Array<ResponderRef>;
|
|
264
|
+
}) => ResponderGroupResolutions;
|
|
265
|
+
|
|
266
|
+
const resolvedGroup: ResolvedGroupFunction = (params: {
|
|
267
|
+
kind: "team" | "schedule";
|
|
268
|
+
id: string;
|
|
269
|
+
label: string;
|
|
270
|
+
members: Array<ResponderRef>;
|
|
271
|
+
}): ResponderGroupResolutions => {
|
|
272
|
+
const key: string = getResponderGroupKey(params.kind, params.id);
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
[key]: {
|
|
276
|
+
key: key,
|
|
277
|
+
kind: params.kind,
|
|
278
|
+
id: params.id,
|
|
279
|
+
label: params.label,
|
|
280
|
+
state: "resolved",
|
|
281
|
+
members: params.members,
|
|
282
|
+
} as ResponderGroupResolution,
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const TEAM_REF: ResponderGroupRef = {
|
|
287
|
+
kind: "team",
|
|
288
|
+
id: TEAM_ID,
|
|
289
|
+
label: "Payments",
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const SCHEDULE_REF: ResponderGroupRef = {
|
|
293
|
+
kind: "schedule",
|
|
294
|
+
id: SCHEDULE_ID,
|
|
295
|
+
label: "Weekends",
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const FALLBACK_ON: { isFallbackEnabled: boolean } = { isFallbackEnabled: true };
|
|
299
|
+
const FALLBACK_OFF: { isFallbackEnabled: boolean } = {
|
|
300
|
+
isFallbackEnabled: false,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const NO_REMINDERS: SetupReminderStatuses = {};
|
|
304
|
+
|
|
305
|
+
/*
|
|
306
|
+
* ---------------------------------------------------------------------------
|
|
307
|
+
* WHO A LEVEL REACHES
|
|
308
|
+
* ---------------------------------------------------------------------------
|
|
309
|
+
*/
|
|
310
|
+
describe("Escalation level readiness: who the level reaches", () => {
|
|
311
|
+
test("a directly named unreachable responder is reported", () => {
|
|
312
|
+
const report: RuleReadinessReport = buildRuleReadinessReport({
|
|
313
|
+
directUsers: [{ userId: USER_ALEX, label: "Alex Chen" }],
|
|
314
|
+
groups: [],
|
|
315
|
+
resolutions: {},
|
|
316
|
+
index: indexOf([ALEX_UNREACHABLE]),
|
|
317
|
+
isTruncated: false,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
expect(report.unreachable).toHaveLength(1);
|
|
321
|
+
expect(report.unreachable[0]!.name).toBe("Alex Chen");
|
|
322
|
+
expect(report.responderCount).toBe(1);
|
|
323
|
+
expect(report.checkedCount).toBe(1);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("a ready responder produces nothing at all", () => {
|
|
327
|
+
const report: RuleReadinessReport = buildRuleReadinessReport({
|
|
328
|
+
directUsers: [{ userId: USER_SAM, label: "Sam Doe" }],
|
|
329
|
+
groups: [],
|
|
330
|
+
resolutions: {},
|
|
331
|
+
index: indexOf([SAM_READY]),
|
|
332
|
+
isTruncated: false,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
expect(report.unreachable).toHaveLength(0);
|
|
336
|
+
expect(report.gaps).toHaveLength(0);
|
|
337
|
+
expect(report.unchecked).toHaveLength(0);
|
|
338
|
+
expect(report.responderCount).toBe(1);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
/*
|
|
342
|
+
* The path responders are usually added by. A guard that only looked at
|
|
343
|
+
* directly-named users was silent exactly when it mattered most.
|
|
344
|
+
*/
|
|
345
|
+
test("somebody reached through a team is attributed to the team", () => {
|
|
346
|
+
const report: RuleReadinessReport = buildRuleReadinessReport({
|
|
347
|
+
directUsers: [],
|
|
348
|
+
groups: [TEAM_REF],
|
|
349
|
+
resolutions: resolvedGroup({
|
|
350
|
+
kind: "team",
|
|
351
|
+
id: TEAM_ID,
|
|
352
|
+
label: "Payments",
|
|
353
|
+
members: [{ userId: USER_ALEX, label: "Alex Chen" }],
|
|
354
|
+
}),
|
|
355
|
+
index: indexOf([ALEX_UNREACHABLE]),
|
|
356
|
+
isTruncated: false,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
expect(report.unreachable).toHaveLength(1);
|
|
360
|
+
expect(report.unreachable[0]!.via).toEqual([
|
|
361
|
+
{ kind: "team", label: "Payments" },
|
|
362
|
+
]);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("somebody on a schedule's roster is attributed to the schedule", () => {
|
|
366
|
+
const report: RuleReadinessReport = buildRuleReadinessReport({
|
|
367
|
+
directUsers: [],
|
|
368
|
+
groups: [SCHEDULE_REF],
|
|
369
|
+
resolutions: resolvedGroup({
|
|
370
|
+
kind: "schedule",
|
|
371
|
+
id: SCHEDULE_ID,
|
|
372
|
+
label: "Weekends",
|
|
373
|
+
members: [{ userId: USER_ALEX, label: "Alex Chen" }],
|
|
374
|
+
}),
|
|
375
|
+
index: indexOf([ALEX_UNREACHABLE]),
|
|
376
|
+
isTruncated: false,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
expect(report.unreachable[0]!.via).toEqual([
|
|
380
|
+
{ kind: "schedule", label: "Weekends" },
|
|
381
|
+
]);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
/*
|
|
385
|
+
* One person, every door. An admin who removes somebody from a team and finds
|
|
386
|
+
* them still being paged through a schedule has been failed by a surface that
|
|
387
|
+
* named only the first route it found.
|
|
388
|
+
*/
|
|
389
|
+
test("a responder reached twice is reported once, carrying both routes", () => {
|
|
390
|
+
const report: RuleReadinessReport = buildRuleReadinessReport({
|
|
391
|
+
directUsers: [{ userId: USER_ALEX, label: "Alex Chen" }],
|
|
392
|
+
groups: [TEAM_REF],
|
|
393
|
+
resolutions: resolvedGroup({
|
|
394
|
+
kind: "team",
|
|
395
|
+
id: TEAM_ID,
|
|
396
|
+
label: "Payments",
|
|
397
|
+
members: [{ userId: USER_ALEX, label: "Alex Chen" }],
|
|
398
|
+
}),
|
|
399
|
+
index: indexOf([ALEX_UNREACHABLE]),
|
|
400
|
+
isTruncated: false,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
expect(report.unreachable).toHaveLength(1);
|
|
404
|
+
expect(report.responderCount).toBe(1);
|
|
405
|
+
expect(report.unreachable[0]!.via).toEqual([
|
|
406
|
+
{ kind: "direct", label: "" },
|
|
407
|
+
{ kind: "team", label: "Payments" },
|
|
408
|
+
]);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("the same person on three schedule layers is one responder", () => {
|
|
412
|
+
const report: RuleReadinessReport = buildRuleReadinessReport({
|
|
413
|
+
directUsers: [],
|
|
414
|
+
groups: [SCHEDULE_REF],
|
|
415
|
+
resolutions: resolvedGroup({
|
|
416
|
+
kind: "schedule",
|
|
417
|
+
id: SCHEDULE_ID,
|
|
418
|
+
label: "Weekends",
|
|
419
|
+
members: [
|
|
420
|
+
{ userId: USER_ALEX, label: "Alex Chen" },
|
|
421
|
+
{ userId: USER_ALEX, label: "Alex Chen" },
|
|
422
|
+
{ userId: USER_ALEX, label: "Alex Chen" },
|
|
423
|
+
],
|
|
424
|
+
}),
|
|
425
|
+
index: indexOf([ALEX_UNREACHABLE]),
|
|
426
|
+
isTruncated: false,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
expect(report.unreachable).toHaveLength(1);
|
|
430
|
+
expect(report.responderCount).toBe(1);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("several unreachable responders are listed by name, alphabetically", () => {
|
|
434
|
+
const report: RuleReadinessReport = buildRuleReadinessReport({
|
|
435
|
+
directUsers: [
|
|
436
|
+
{ userId: USER_SAM, label: "Sam Doe" },
|
|
437
|
+
{ userId: USER_ALEX, label: "Alex Chen" },
|
|
438
|
+
],
|
|
439
|
+
groups: [],
|
|
440
|
+
resolutions: {},
|
|
441
|
+
index: indexOf([
|
|
442
|
+
ALEX_UNREACHABLE,
|
|
443
|
+
{ ...SAM_READY, status: "NotReachable" },
|
|
444
|
+
]),
|
|
445
|
+
isTruncated: false,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
expect(
|
|
449
|
+
report.unreachable.map((issue: ResponderIssue): string => {
|
|
450
|
+
return issue.name;
|
|
451
|
+
}),
|
|
452
|
+
).toEqual(["Alex Chen", "Sam Doe"]);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
/*
|
|
456
|
+
* The readiness row's own name wins: a person renamed since the join row was
|
|
457
|
+
* written should be talked about by their current name.
|
|
458
|
+
*/
|
|
459
|
+
test("the readiness row's name is preferred over the join row's", () => {
|
|
460
|
+
const report: RuleReadinessReport = buildRuleReadinessReport({
|
|
461
|
+
directUsers: [{ userId: USER_ALEX, label: "Stale Name" }],
|
|
462
|
+
groups: [],
|
|
463
|
+
resolutions: {},
|
|
464
|
+
index: indexOf([ALEX_UNREACHABLE]),
|
|
465
|
+
isTruncated: false,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
expect(report.unreachable[0]!.name).toBe("Alex Chen");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("a responder with no id is not a responder", () => {
|
|
472
|
+
const report: RuleReadinessReport = buildRuleReadinessReport({
|
|
473
|
+
directUsers: [{ userId: "", label: "Nobody" }],
|
|
474
|
+
groups: [],
|
|
475
|
+
resolutions: {},
|
|
476
|
+
index: indexOf([ALEX_UNREACHABLE]),
|
|
477
|
+
isTruncated: false,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
expect(report.responderCount).toBe(0);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
describe("Escalation level readiness: what it admits to not knowing", () => {
|
|
485
|
+
/*
|
|
486
|
+
* A group whose membership could not be read is the case that used to render
|
|
487
|
+
* identically to a group full of reachable people.
|
|
488
|
+
*/
|
|
489
|
+
test("a team that could not be read is named rather than skipped", () => {
|
|
490
|
+
const key: string = getResponderGroupKey("team", TEAM_ID);
|
|
491
|
+
|
|
492
|
+
const report: RuleReadinessReport = buildRuleReadinessReport({
|
|
493
|
+
directUsers: [],
|
|
494
|
+
groups: [TEAM_REF],
|
|
495
|
+
resolutions: {
|
|
496
|
+
[key]: {
|
|
497
|
+
key: key,
|
|
498
|
+
kind: "team",
|
|
499
|
+
id: TEAM_ID,
|
|
500
|
+
label: "Payments",
|
|
501
|
+
state: "unavailable",
|
|
502
|
+
members: [],
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
index: indexOf([ALEX_UNREACHABLE]),
|
|
506
|
+
isTruncated: false,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
expect(report.unreadableGroups).toEqual([TEAM_REF]);
|
|
510
|
+
expect(report.isResolving).toBe(false);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test("a group still being expanded is reported as in flight, not as a problem", () => {
|
|
514
|
+
const key: string = getResponderGroupKey("team", TEAM_ID);
|
|
515
|
+
|
|
516
|
+
const report: RuleReadinessReport = buildRuleReadinessReport({
|
|
517
|
+
directUsers: [],
|
|
518
|
+
groups: [TEAM_REF],
|
|
519
|
+
resolutions: {
|
|
520
|
+
[key]: {
|
|
521
|
+
key: key,
|
|
522
|
+
kind: "team",
|
|
523
|
+
id: TEAM_ID,
|
|
524
|
+
label: "Payments",
|
|
525
|
+
state: "resolving",
|
|
526
|
+
members: [],
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
index: indexOf([ALEX_UNREACHABLE]),
|
|
530
|
+
isTruncated: false,
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
expect(report.isResolving).toBe(true);
|
|
534
|
+
expect(report.unreadableGroups).toHaveLength(0);
|
|
535
|
+
expect(getRuleWarningLevel(report, FALLBACK_ON)).toBe("none");
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test("a group nothing is known about yet is in flight too", () => {
|
|
539
|
+
const report: RuleReadinessReport = buildRuleReadinessReport({
|
|
540
|
+
directUsers: [],
|
|
541
|
+
groups: [TEAM_REF],
|
|
542
|
+
resolutions: {},
|
|
543
|
+
index: indexOf([ALEX_UNREACHABLE]),
|
|
544
|
+
isTruncated: false,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
expect(report.isResolving).toBe(true);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
/*
|
|
551
|
+
* A responder with no readiness row on a COMPLETE answer is somebody the
|
|
552
|
+
* server did not resolve onto this policy. Inventing a warning out of that
|
|
553
|
+
* would put a permanent grey badge on levels that are fine.
|
|
554
|
+
*/
|
|
555
|
+
test("a responder missing from a complete answer is not accused", () => {
|
|
556
|
+
const report: RuleReadinessReport = buildRuleReadinessReport({
|
|
557
|
+
directUsers: [{ userId: USER_SAM, label: "Sam Doe" }],
|
|
558
|
+
groups: [],
|
|
559
|
+
resolutions: {},
|
|
560
|
+
index: indexOf([ALEX_UNREACHABLE]),
|
|
561
|
+
isTruncated: false,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
expect(report.unchecked).toHaveLength(0);
|
|
565
|
+
expect(getRuleWarningLevel(report, FALLBACK_ON)).toBe("none");
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
/*
|
|
569
|
+
* The same absence on a TRUNCATED answer means the opposite: nobody looked.
|
|
570
|
+
*/
|
|
571
|
+
test("a responder missing from a truncated answer is reported as unchecked", () => {
|
|
572
|
+
const report: RuleReadinessReport = buildRuleReadinessReport({
|
|
573
|
+
directUsers: [{ userId: USER_SAM, label: "Sam Doe" }],
|
|
574
|
+
groups: [],
|
|
575
|
+
resolutions: {},
|
|
576
|
+
index: indexOf([ALEX_UNREACHABLE]),
|
|
577
|
+
isTruncated: true,
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
expect(report.unchecked).toHaveLength(1);
|
|
581
|
+
expect(report.unchecked[0]!.name).toBe("Sam Doe");
|
|
582
|
+
expect(getRuleWarningLevel(report, FALLBACK_ON)).toBe("unknown");
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
/*
|
|
587
|
+
* ---------------------------------------------------------------------------
|
|
588
|
+
* HOW LOUD THE LABEL IS ALLOWED TO BE
|
|
589
|
+
* ---------------------------------------------------------------------------
|
|
590
|
+
*/
|
|
591
|
+
describe("Escalation level readiness: the label's volume", () => {
|
|
592
|
+
type ReportFunction = (
|
|
593
|
+
overrides: Partial<RuleReadinessReport>,
|
|
594
|
+
) => RuleReadinessReport;
|
|
595
|
+
|
|
596
|
+
const reportWith: ReportFunction = (
|
|
597
|
+
overrides: Partial<RuleReadinessReport>,
|
|
598
|
+
): RuleReadinessReport => {
|
|
599
|
+
return {
|
|
600
|
+
unreachable: [],
|
|
601
|
+
gaps: [],
|
|
602
|
+
unchecked: [],
|
|
603
|
+
unreadableGroups: [],
|
|
604
|
+
isResolving: false,
|
|
605
|
+
responderCount: 3,
|
|
606
|
+
checkedCount: 3,
|
|
607
|
+
...overrides,
|
|
608
|
+
};
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
const issue: (name: string) => ResponderIssue = (
|
|
612
|
+
name: string,
|
|
613
|
+
): ResponderIssue => {
|
|
614
|
+
return {
|
|
615
|
+
userId: USER_ALEX,
|
|
616
|
+
name: name,
|
|
617
|
+
kind: "unreachable",
|
|
618
|
+
readiness: null,
|
|
619
|
+
via: [],
|
|
620
|
+
};
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
test("a healthy level says nothing", () => {
|
|
624
|
+
expect(getRuleWarningLevel(reportWith({}), FALLBACK_ON)).toBe("none");
|
|
625
|
+
expect(getRuleWarningLabel(reportWith({}), FALLBACK_ON)).toBe("");
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test("somebody who cannot be paged is critical", () => {
|
|
629
|
+
const report: RuleReadinessReport = reportWith({
|
|
630
|
+
unreachable: [issue("Alex Chen")],
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
expect(getRuleWarningLevel(report, FALLBACK_ON)).toBe("critical");
|
|
634
|
+
expect(getRuleWarningLabel(report, FALLBACK_ON)).toBe(
|
|
635
|
+
"1 person can't be paged",
|
|
636
|
+
);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test("the count is people, and it is pluralised", () => {
|
|
640
|
+
const report: RuleReadinessReport = reportWith({
|
|
641
|
+
unreachable: [issue("Alex Chen"), issue("Sam Doe")],
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
expect(getRuleWarningLabel(report, FALLBACK_ON)).toBe(
|
|
645
|
+
"2 people can't be paged",
|
|
646
|
+
);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
/*
|
|
650
|
+
* The alarm-fatigue property, asserted rather than assumed. On a project with
|
|
651
|
+
* the fallback on - the default - a PartiallyReady responder's pages still
|
|
652
|
+
* arrive, and most responders on a project with many severities are amber. A
|
|
653
|
+
* label that fires for them fires on almost every level of almost every
|
|
654
|
+
* policy.
|
|
655
|
+
*/
|
|
656
|
+
test("rule gaps are silent while the project's fallback is on", () => {
|
|
657
|
+
const report: RuleReadinessReport = reportWith({
|
|
658
|
+
gaps: [issue("Jo Park")],
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
expect(getRuleWarningLevel(report, FALLBACK_ON)).toBe("none");
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
/*
|
|
665
|
+
* And the reverse mistake, which is the expensive one: with the fallback off
|
|
666
|
+
* those same gaps are dropped pages.
|
|
667
|
+
*/
|
|
668
|
+
test("rule gaps are a warning the moment the fallback is off", () => {
|
|
669
|
+
const report: RuleReadinessReport = reportWith({
|
|
670
|
+
gaps: [issue("Jo Park")],
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
expect(getRuleWarningLevel(report, FALLBACK_OFF)).toBe("warning");
|
|
674
|
+
expect(getRuleWarningLabel(report, FALLBACK_OFF)).toBe(
|
|
675
|
+
"1 person loses pages",
|
|
676
|
+
);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
test("cannot-be-paged outranks both gaps and unknowns", () => {
|
|
680
|
+
const report: RuleReadinessReport = reportWith({
|
|
681
|
+
unreachable: [issue("Alex Chen")],
|
|
682
|
+
gaps: [issue("Jo Park")],
|
|
683
|
+
unchecked: [issue("Sam Doe")],
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
expect(getRuleWarningLevel(report, FALLBACK_OFF)).toBe("critical");
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test("an unreadable team is an unknown, counted with unchecked people", () => {
|
|
690
|
+
const report: RuleReadinessReport = reportWith({
|
|
691
|
+
unchecked: [issue("Sam Doe")],
|
|
692
|
+
unreadableGroups: [TEAM_REF],
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
expect(getRuleWarningLevel(report, FALLBACK_ON)).toBe("unknown");
|
|
696
|
+
expect(getRuleWarningLabel(report, FALLBACK_ON)).toBe(
|
|
697
|
+
"2 responders not checked",
|
|
698
|
+
);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
test("one unknown is singular", () => {
|
|
702
|
+
const report: RuleReadinessReport = reportWith({
|
|
703
|
+
unreadableGroups: [TEAM_REF],
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
expect(getRuleWarningLabel(report, FALLBACK_ON)).toBe(
|
|
707
|
+
"1 responder not checked",
|
|
708
|
+
);
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
describe("Escalation level readiness: naming where a responder came from", () => {
|
|
713
|
+
test("a direct pick is described as direct", () => {
|
|
714
|
+
expect(describeResponderVia([{ kind: "direct", label: "" }])).toBe(
|
|
715
|
+
"Reached directly.",
|
|
716
|
+
);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
test("a team and a schedule are named", () => {
|
|
720
|
+
expect(describeResponderVia([{ kind: "team", label: "Payments" }])).toBe(
|
|
721
|
+
"Reached through the Payments team.",
|
|
722
|
+
);
|
|
723
|
+
expect(
|
|
724
|
+
describeResponderVia([{ kind: "schedule", label: "Weekends" }]),
|
|
725
|
+
).toBe("Reached through the Weekends schedule.");
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
test("every route is listed, in the order they were found", () => {
|
|
729
|
+
const via: Array<ResponderVia> = [
|
|
730
|
+
{ kind: "direct", label: "" },
|
|
731
|
+
{ kind: "team", label: "Payments" },
|
|
732
|
+
{ kind: "schedule", label: "Weekends" },
|
|
733
|
+
];
|
|
734
|
+
|
|
735
|
+
expect(describeResponderVia(via)).toBe(
|
|
736
|
+
"Reached directly, through the Payments team and through the Weekends schedule.",
|
|
737
|
+
);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
/*
|
|
741
|
+
* Never a bare id: an admin who reads "cba1f3e2-... could not be checked"
|
|
742
|
+
* learns nothing they can act on.
|
|
743
|
+
*/
|
|
744
|
+
test("a nameless group is described by its kind rather than its id", () => {
|
|
745
|
+
expect(describeResponderVia([{ kind: "team", label: "" }])).toBe(
|
|
746
|
+
"Reached through the a team team.",
|
|
747
|
+
);
|
|
748
|
+
expect(getResponderGroupName({ kind: "team", label: "" })).toBe("a team");
|
|
749
|
+
expect(getResponderGroupName({ kind: "schedule", label: "" })).toBe(
|
|
750
|
+
"a schedule",
|
|
751
|
+
);
|
|
752
|
+
expect(getResponderGroupName({ kind: "team", label: "Payments" })).toBe(
|
|
753
|
+
"Payments",
|
|
754
|
+
);
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
test("no route at all produces no sentence", () => {
|
|
758
|
+
expect(describeResponderVia([])).toBe("");
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
/*
|
|
763
|
+
* ---------------------------------------------------------------------------
|
|
764
|
+
* THE LABEL ITSELF
|
|
765
|
+
* ---------------------------------------------------------------------------
|
|
766
|
+
*/
|
|
767
|
+
describe("The readiness label", () => {
|
|
768
|
+
type LabelReportFunction = (
|
|
769
|
+
overrides: Partial<RuleReadinessReport>,
|
|
770
|
+
) => RuleReadinessReport;
|
|
771
|
+
|
|
772
|
+
const reportWith: LabelReportFunction = (
|
|
773
|
+
overrides: Partial<RuleReadinessReport>,
|
|
774
|
+
): RuleReadinessReport => {
|
|
775
|
+
return {
|
|
776
|
+
unreachable: [],
|
|
777
|
+
gaps: [],
|
|
778
|
+
unchecked: [],
|
|
779
|
+
unreadableGroups: [],
|
|
780
|
+
isResolving: false,
|
|
781
|
+
responderCount: 2,
|
|
782
|
+
checkedCount: 2,
|
|
783
|
+
...overrides,
|
|
784
|
+
};
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
const unreachableIssue: ResponderIssue = {
|
|
788
|
+
userId: USER_ALEX,
|
|
789
|
+
name: "Alex Chen",
|
|
790
|
+
kind: "unreachable",
|
|
791
|
+
readiness: null,
|
|
792
|
+
via: [],
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
test("a healthy level renders no label at all", () => {
|
|
796
|
+
const { container } = render(
|
|
797
|
+
<RuleReadinessLabel
|
|
798
|
+
report={reportWith({})}
|
|
799
|
+
delivery={FALLBACK_ON}
|
|
800
|
+
ruleName="First Responders"
|
|
801
|
+
onClick={() => {}}
|
|
802
|
+
/>,
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
expect(container).toBeEmptyDOMElement();
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
test("it states the problem and offers the way in", () => {
|
|
809
|
+
render(
|
|
810
|
+
<RuleReadinessLabel
|
|
811
|
+
report={reportWith({ unreachable: [unreachableIssue] })}
|
|
812
|
+
delivery={FALLBACK_ON}
|
|
813
|
+
ruleName="First Responders"
|
|
814
|
+
onClick={() => {}}
|
|
815
|
+
/>,
|
|
816
|
+
);
|
|
817
|
+
|
|
818
|
+
const label: HTMLElement = screen.getByTestId("rule-readiness-label");
|
|
819
|
+
|
|
820
|
+
expect(label).toHaveTextContent("1 person can't be paged");
|
|
821
|
+
expect(label).toHaveAttribute("data-warning-level", "critical");
|
|
822
|
+
expect(label).toHaveAttribute(
|
|
823
|
+
"aria-label",
|
|
824
|
+
"1 person can't be paged on First Responders. See who, and send a setup reminder.",
|
|
825
|
+
);
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
/*
|
|
829
|
+
* A button, not a badge with a tooltip: it has to be reachable from a
|
|
830
|
+
* keyboard, and the thing behind it is a list of people rather than a
|
|
831
|
+
* sentence.
|
|
832
|
+
*/
|
|
833
|
+
test("it is a real button, and clicking it opens the detail", () => {
|
|
834
|
+
const onClick: MockFunction = getJestMockFunction();
|
|
835
|
+
|
|
836
|
+
render(
|
|
837
|
+
<RuleReadinessLabel
|
|
838
|
+
report={reportWith({ unreachable: [unreachableIssue] })}
|
|
839
|
+
delivery={FALLBACK_ON}
|
|
840
|
+
ruleName="First Responders"
|
|
841
|
+
onClick={onClick as unknown as () => void}
|
|
842
|
+
/>,
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
const label: HTMLElement = screen.getByTestId("rule-readiness-label");
|
|
846
|
+
|
|
847
|
+
expect(label.tagName).toBe("BUTTON");
|
|
848
|
+
|
|
849
|
+
fireEvent.click(label);
|
|
850
|
+
|
|
851
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
test("an unknown renders grey rather than as an accusation", () => {
|
|
855
|
+
render(
|
|
856
|
+
<RuleReadinessLabel
|
|
857
|
+
report={reportWith({ unreadableGroups: [TEAM_REF] })}
|
|
858
|
+
delivery={FALLBACK_ON}
|
|
859
|
+
ruleName="First Responders"
|
|
860
|
+
onClick={() => {}}
|
|
861
|
+
/>,
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
expect(screen.getByTestId("rule-readiness-label")).toHaveAttribute(
|
|
865
|
+
"data-warning-level",
|
|
866
|
+
"unknown",
|
|
867
|
+
);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
test("the fallback-off gap renders amber", () => {
|
|
871
|
+
render(
|
|
872
|
+
<RuleReadinessLabel
|
|
873
|
+
report={reportWith({
|
|
874
|
+
gaps: [{ ...unreachableIssue, kind: "gap", name: "Jo Park" }],
|
|
875
|
+
})}
|
|
876
|
+
delivery={FALLBACK_OFF}
|
|
877
|
+
ruleName="First Responders"
|
|
878
|
+
onClick={() => {}}
|
|
879
|
+
/>,
|
|
880
|
+
);
|
|
881
|
+
|
|
882
|
+
expect(screen.getByTestId("rule-readiness-label")).toHaveAttribute(
|
|
883
|
+
"data-warning-level",
|
|
884
|
+
"warning",
|
|
885
|
+
);
|
|
886
|
+
});
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
/*
|
|
890
|
+
* ---------------------------------------------------------------------------
|
|
891
|
+
* THE DETAIL BEHIND THE LABEL
|
|
892
|
+
* ---------------------------------------------------------------------------
|
|
893
|
+
*/
|
|
894
|
+
describe("The readiness detail", () => {
|
|
895
|
+
type DetailReportFunction = (
|
|
896
|
+
overrides: Partial<RuleReadinessReport>,
|
|
897
|
+
) => RuleReadinessReport;
|
|
898
|
+
|
|
899
|
+
const reportWith: DetailReportFunction = (
|
|
900
|
+
overrides: Partial<RuleReadinessReport>,
|
|
901
|
+
): RuleReadinessReport => {
|
|
902
|
+
return {
|
|
903
|
+
unreachable: [],
|
|
904
|
+
gaps: [],
|
|
905
|
+
unchecked: [],
|
|
906
|
+
unreadableGroups: [],
|
|
907
|
+
isResolving: false,
|
|
908
|
+
responderCount: 2,
|
|
909
|
+
checkedCount: 2,
|
|
910
|
+
...overrides,
|
|
911
|
+
};
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
type IssueFunction = (params: {
|
|
915
|
+
userId: string;
|
|
916
|
+
name: string;
|
|
917
|
+
row: UserRow;
|
|
918
|
+
via?: Array<ResponderVia> | undefined;
|
|
919
|
+
kind?: "unreachable" | "gap" | undefined;
|
|
920
|
+
}) => ResponderIssue;
|
|
921
|
+
|
|
922
|
+
const issueFor: IssueFunction = (params: {
|
|
923
|
+
userId: string;
|
|
924
|
+
name: string;
|
|
925
|
+
row: UserRow;
|
|
926
|
+
via?: Array<ResponderVia> | undefined;
|
|
927
|
+
kind?: "unreachable" | "gap" | undefined;
|
|
928
|
+
}): ResponderIssue => {
|
|
929
|
+
const summary: ReadinessSummaryWire = summaryOf({ users: [params.row] });
|
|
930
|
+
|
|
931
|
+
return {
|
|
932
|
+
userId: params.userId,
|
|
933
|
+
name: params.name,
|
|
934
|
+
kind: params.kind || "unreachable",
|
|
935
|
+
readiness: summary.users[0]!,
|
|
936
|
+
via: params.via || [],
|
|
937
|
+
};
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
const ALEX_ISSUE: ResponderIssue = issueFor({
|
|
941
|
+
userId: USER_ALEX,
|
|
942
|
+
name: "Alex Chen",
|
|
943
|
+
row: ALEX_UNREACHABLE,
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
type RenderDetailFunction = (params: {
|
|
947
|
+
report: RuleReadinessReport;
|
|
948
|
+
delivery?: { isFallbackEnabled: boolean } | undefined;
|
|
949
|
+
reminders?: SetupReminderStatuses | undefined;
|
|
950
|
+
onSendReminder?: ((userIds: Array<string>) => void) | undefined;
|
|
951
|
+
isSendingReminders?: boolean | undefined;
|
|
952
|
+
}) => void;
|
|
953
|
+
|
|
954
|
+
const renderDetail: RenderDetailFunction = (params: {
|
|
955
|
+
report: RuleReadinessReport;
|
|
956
|
+
delivery?: { isFallbackEnabled: boolean } | undefined;
|
|
957
|
+
reminders?: SetupReminderStatuses | undefined;
|
|
958
|
+
onSendReminder?: ((userIds: Array<string>) => void) | undefined;
|
|
959
|
+
isSendingReminders?: boolean | undefined;
|
|
960
|
+
}): void => {
|
|
961
|
+
render(
|
|
962
|
+
<RuleReadinessDetails
|
|
963
|
+
ruleName="First Responders"
|
|
964
|
+
report={params.report}
|
|
965
|
+
delivery={params.delivery || FALLBACK_ON}
|
|
966
|
+
reminders={params.reminders || NO_REMINDERS}
|
|
967
|
+
isSendingReminders={params.isSendingReminders === true}
|
|
968
|
+
onSendReminder={params.onSendReminder || (() => {})}
|
|
969
|
+
onClose={() => {}}
|
|
970
|
+
/>,
|
|
971
|
+
);
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
test("it names the level and the number of people on it", () => {
|
|
975
|
+
renderDetail({ report: reportWith({ unreachable: [ALEX_ISSUE] }) });
|
|
976
|
+
|
|
977
|
+
expect(screen.getByText("First Responders")).toBeInTheDocument();
|
|
978
|
+
expect(
|
|
979
|
+
screen.getByText(
|
|
980
|
+
"Whether the 2 people this level notifies can actually be paged.",
|
|
981
|
+
),
|
|
982
|
+
).toBeInTheDocument();
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
test("an unreachable responder is named, and the consequence is stated", () => {
|
|
986
|
+
renderDetail({ report: reportWith({ unreachable: [ALEX_ISSUE] }) });
|
|
987
|
+
|
|
988
|
+
expect(
|
|
989
|
+
screen.getByTestId("rule-responder-unreachable"),
|
|
990
|
+
).toBeInTheDocument();
|
|
991
|
+
expect(screen.getByText("Alex Chen")).toBeInTheDocument();
|
|
992
|
+
expect(
|
|
993
|
+
screen.getByText(
|
|
994
|
+
/Alex Chen has no verified notification method, so every page routed to them is dropped\./,
|
|
995
|
+
),
|
|
996
|
+
).toBeInTheDocument();
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
/*
|
|
1000
|
+
* The two NotReachable branches are different problems with different fixes -
|
|
1001
|
+
* "they never verified anything" versus "this project switched off the only
|
|
1002
|
+
* channel they have" - and an admin sent to chase the wrong one wastes the
|
|
1003
|
+
* trip.
|
|
1004
|
+
*/
|
|
1005
|
+
test("methods on switched-off channels get their own sentence", () => {
|
|
1006
|
+
renderDetail({
|
|
1007
|
+
report: reportWith({
|
|
1008
|
+
unreachable: [
|
|
1009
|
+
issueFor({
|
|
1010
|
+
userId: USER_ALEX,
|
|
1011
|
+
name: "Alex Chen",
|
|
1012
|
+
row: {
|
|
1013
|
+
...ALEX_UNREACHABLE,
|
|
1014
|
+
methods: [
|
|
1015
|
+
{
|
|
1016
|
+
methodType: "SMS",
|
|
1017
|
+
maskedIdentifier: "+1 ••• ••• 4821",
|
|
1018
|
+
isVerified: true,
|
|
1019
|
+
},
|
|
1020
|
+
],
|
|
1021
|
+
},
|
|
1022
|
+
}),
|
|
1023
|
+
],
|
|
1024
|
+
}),
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
expect(
|
|
1028
|
+
screen.getByText(/this project has those channels switched off/),
|
|
1029
|
+
).toBeInTheDocument();
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
test("it says where a responder nobody picked came from", () => {
|
|
1033
|
+
renderDetail({
|
|
1034
|
+
report: reportWith({
|
|
1035
|
+
unreachable: [
|
|
1036
|
+
issueFor({
|
|
1037
|
+
userId: USER_ALEX,
|
|
1038
|
+
name: "Alex Chen",
|
|
1039
|
+
row: ALEX_UNREACHABLE,
|
|
1040
|
+
via: [{ kind: "team", label: "Payments" }],
|
|
1041
|
+
}),
|
|
1042
|
+
],
|
|
1043
|
+
}),
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
expect(
|
|
1047
|
+
screen.getByText("Reached through the Payments team."),
|
|
1048
|
+
).toBeInTheDocument();
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
test("it says which verified methods exist, or that none do", () => {
|
|
1052
|
+
renderDetail({ report: reportWith({ unreachable: [ALEX_ISSUE] }) });
|
|
1053
|
+
|
|
1054
|
+
expect(
|
|
1055
|
+
screen.getByText("No verified notification method on their account."),
|
|
1056
|
+
).toBeInTheDocument();
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
test("it says the fix is theirs to apply, not the admin's", () => {
|
|
1060
|
+
renderDetail({ report: reportWith({ unreachable: [ALEX_ISSUE] }) });
|
|
1061
|
+
|
|
1062
|
+
expect(
|
|
1063
|
+
screen.getByText(
|
|
1064
|
+
/Only these people can add and verify a notification method on their own account/,
|
|
1065
|
+
),
|
|
1066
|
+
).toBeInTheDocument();
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
test("a gap responder is listed separately, with the cells they are missing", () => {
|
|
1070
|
+
renderDetail({
|
|
1071
|
+
report: reportWith({
|
|
1072
|
+
gaps: [
|
|
1073
|
+
issueFor({
|
|
1074
|
+
userId: USER_JO,
|
|
1075
|
+
name: "Jo Park",
|
|
1076
|
+
row: JO_PARTIAL,
|
|
1077
|
+
kind: "gap",
|
|
1078
|
+
}),
|
|
1079
|
+
],
|
|
1080
|
+
}),
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
expect(screen.getByTestId("rule-responder-gap")).toBeInTheDocument();
|
|
1084
|
+
expect(
|
|
1085
|
+
screen.getByText("Paged, but not the way they asked"),
|
|
1086
|
+
).toBeInTheDocument();
|
|
1087
|
+
expect(screen.getByText("Incident · Sev1")).toBeInTheDocument();
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
/*
|
|
1091
|
+
* The same gap, in a project that drops it. The heading has to change with the
|
|
1092
|
+
* fact, because "not the way they asked" over a dropped page is a lie.
|
|
1093
|
+
*/
|
|
1094
|
+
test("with the fallback off, gaps are described as dropped pages", () => {
|
|
1095
|
+
renderDetail({
|
|
1096
|
+
delivery: FALLBACK_OFF,
|
|
1097
|
+
report: reportWith({
|
|
1098
|
+
gaps: [
|
|
1099
|
+
issueFor({
|
|
1100
|
+
userId: USER_JO,
|
|
1101
|
+
name: "Jo Park",
|
|
1102
|
+
row: JO_PARTIAL,
|
|
1103
|
+
kind: "gap",
|
|
1104
|
+
}),
|
|
1105
|
+
],
|
|
1106
|
+
}),
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
expect(
|
|
1110
|
+
screen.getByText("Pages dropped - no fallback in this project"),
|
|
1111
|
+
).toBeInTheDocument();
|
|
1112
|
+
expect(
|
|
1113
|
+
screen.getByText(
|
|
1114
|
+
/this project has on-call notification fallback switched off/,
|
|
1115
|
+
),
|
|
1116
|
+
).toBeInTheDocument();
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
test("a gap responder is offered no reminder, because there is nothing to remind them of", () => {
|
|
1120
|
+
renderDetail({
|
|
1121
|
+
report: reportWith({
|
|
1122
|
+
gaps: [
|
|
1123
|
+
issueFor({
|
|
1124
|
+
userId: USER_JO,
|
|
1125
|
+
name: "Jo Park",
|
|
1126
|
+
row: JO_PARTIAL,
|
|
1127
|
+
kind: "gap",
|
|
1128
|
+
}),
|
|
1129
|
+
],
|
|
1130
|
+
}),
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
expect(screen.queryByText("Send setup reminder")).not.toBeInTheDocument();
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
test("an unchecked responder is named as unknown rather than as broken", () => {
|
|
1137
|
+
renderDetail({
|
|
1138
|
+
report: reportWith({
|
|
1139
|
+
unchecked: [
|
|
1140
|
+
{
|
|
1141
|
+
userId: USER_SAM,
|
|
1142
|
+
name: "Sam Doe",
|
|
1143
|
+
kind: "unchecked",
|
|
1144
|
+
readiness: null,
|
|
1145
|
+
via: [],
|
|
1146
|
+
},
|
|
1147
|
+
],
|
|
1148
|
+
}),
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
expect(screen.getByTestId("rule-responder-unchecked")).toHaveTextContent(
|
|
1152
|
+
"does not cover Sam Doe, so they are unknown rather than ready",
|
|
1153
|
+
);
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
test("a team that could not be read says so, and says what follows from it", () => {
|
|
1157
|
+
renderDetail({
|
|
1158
|
+
report: reportWith({ unreadableGroups: [TEAM_REF] }),
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
expect(screen.getByTestId("rule-responder-unchecked")).toHaveTextContent(
|
|
1162
|
+
"We could not read who is in Payments, so nobody on that team has been checked.",
|
|
1163
|
+
);
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
test("an expansion still in flight says it is still checking", () => {
|
|
1167
|
+
renderDetail({ report: reportWith({ isResolving: true }) });
|
|
1168
|
+
|
|
1169
|
+
expect(screen.getByTestId("rule-responder-unchecked")).toHaveTextContent(
|
|
1170
|
+
"Still checking who this level reaches",
|
|
1171
|
+
);
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
test("a level with nothing wrong says so rather than showing an empty modal", () => {
|
|
1175
|
+
renderDetail({ report: reportWith({}) });
|
|
1176
|
+
|
|
1177
|
+
expect(screen.getByTestId("rule-readiness-all-clear")).toBeInTheDocument();
|
|
1178
|
+
});
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
describe("The readiness detail: the reminder it offers", () => {
|
|
1182
|
+
const issueFor: (row: UserRow, name: string) => ResponderIssue = (
|
|
1183
|
+
row: UserRow,
|
|
1184
|
+
name: string,
|
|
1185
|
+
): ResponderIssue => {
|
|
1186
|
+
return {
|
|
1187
|
+
userId: row.userId,
|
|
1188
|
+
name: name,
|
|
1189
|
+
kind: "unreachable",
|
|
1190
|
+
readiness: summaryOf({ users: [row] }).users[0]!,
|
|
1191
|
+
via: [],
|
|
1192
|
+
};
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
const ALEX_ISSUE: ResponderIssue = issueFor(ALEX_UNREACHABLE, "Alex Chen");
|
|
1196
|
+
const SAM_ISSUE: ResponderIssue = issueFor(
|
|
1197
|
+
{ ...SAM_READY, status: "NotReachable" },
|
|
1198
|
+
"Sam Doe",
|
|
1199
|
+
);
|
|
1200
|
+
|
|
1201
|
+
type RenderFunction = (params: {
|
|
1202
|
+
unreachable: Array<ResponderIssue>;
|
|
1203
|
+
reminders?: SetupReminderStatuses | undefined;
|
|
1204
|
+
onSendReminder?: ((userIds: Array<string>) => void) | undefined;
|
|
1205
|
+
}) => void;
|
|
1206
|
+
|
|
1207
|
+
const renderDetail: RenderFunction = (params: {
|
|
1208
|
+
unreachable: Array<ResponderIssue>;
|
|
1209
|
+
reminders?: SetupReminderStatuses | undefined;
|
|
1210
|
+
onSendReminder?: ((userIds: Array<string>) => void) | undefined;
|
|
1211
|
+
}): void => {
|
|
1212
|
+
render(
|
|
1213
|
+
<RuleReadinessDetails
|
|
1214
|
+
ruleName="First Responders"
|
|
1215
|
+
report={{
|
|
1216
|
+
unreachable: params.unreachable,
|
|
1217
|
+
gaps: [],
|
|
1218
|
+
unchecked: [],
|
|
1219
|
+
unreadableGroups: [],
|
|
1220
|
+
isResolving: false,
|
|
1221
|
+
responderCount: params.unreachable.length,
|
|
1222
|
+
checkedCount: params.unreachable.length,
|
|
1223
|
+
}}
|
|
1224
|
+
delivery={FALLBACK_ON}
|
|
1225
|
+
reminders={params.reminders || NO_REMINDERS}
|
|
1226
|
+
isSendingReminders={false}
|
|
1227
|
+
onSendReminder={params.onSendReminder || (() => {})}
|
|
1228
|
+
onClose={() => {}}
|
|
1229
|
+
/>,
|
|
1230
|
+
);
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
test("the reminder is offered, and asks for exactly that user", () => {
|
|
1234
|
+
const onSendReminder: MockFunction = getJestMockFunction();
|
|
1235
|
+
|
|
1236
|
+
renderDetail({
|
|
1237
|
+
unreachable: [ALEX_ISSUE],
|
|
1238
|
+
onSendReminder: onSendReminder as unknown as (
|
|
1239
|
+
userIds: Array<string>,
|
|
1240
|
+
) => void,
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
fireEvent.click(screen.getByText("Send setup reminder"));
|
|
1244
|
+
|
|
1245
|
+
expect(onSendReminder).toHaveBeenCalledWith([USER_ALEX]);
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
test("a sent reminder says who it reached, and offers no second send", () => {
|
|
1249
|
+
renderDetail({
|
|
1250
|
+
unreachable: [ALEX_ISSUE],
|
|
1251
|
+
reminders: { [USER_ALEX]: { state: "sent", message: "" } },
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
expect(screen.getByTestId("setup-reminder-sent")).toHaveTextContent(
|
|
1255
|
+
"Setup reminder sent to Alex Chen.",
|
|
1256
|
+
);
|
|
1257
|
+
expect(screen.queryByText("Send setup reminder")).not.toBeInTheDocument();
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
/*
|
|
1261
|
+
* The failure this control was disabled for two phases to avoid: a reminder
|
|
1262
|
+
* that did not go anywhere must not leave a tick behind it. A throttle is not
|
|
1263
|
+
* a send.
|
|
1264
|
+
*/
|
|
1265
|
+
test("a skipped reminder is not reported as sent", () => {
|
|
1266
|
+
renderDetail({
|
|
1267
|
+
unreachable: [ALEX_ISSUE],
|
|
1268
|
+
reminders: {
|
|
1269
|
+
[USER_ALEX]: {
|
|
1270
|
+
state: "skipped",
|
|
1271
|
+
message: "Alex Chen was already reminded today.",
|
|
1272
|
+
},
|
|
1273
|
+
},
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
expect(screen.queryByTestId("setup-reminder-sent")).not.toBeInTheDocument();
|
|
1277
|
+
expect(screen.getByTestId("setup-reminder-not-sent")).toHaveTextContent(
|
|
1278
|
+
"No reminder was sent. Alex Chen was already reminded today.",
|
|
1279
|
+
);
|
|
1280
|
+
// And the fix that is always available is still offered.
|
|
1281
|
+
expect(screen.getByTestId("setup-reminder-not-sent")).toHaveTextContent(
|
|
1282
|
+
"Ask Alex Chen to finish their notification setup in User Settings.",
|
|
1283
|
+
);
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
test("a failed reminder shows the server's own words and can be retried", () => {
|
|
1287
|
+
renderDetail({
|
|
1288
|
+
unreachable: [ALEX_ISSUE],
|
|
1289
|
+
reminders: {
|
|
1290
|
+
[USER_ALEX]: { state: "failed", message: "No email address on file." },
|
|
1291
|
+
},
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
expect(screen.getByTestId("setup-reminder-not-sent")).toHaveTextContent(
|
|
1295
|
+
"No email address on file.",
|
|
1296
|
+
);
|
|
1297
|
+
expect(screen.getByText("Send setup reminder")).toBeInTheDocument();
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
/*
|
|
1301
|
+
* The state that exists so nobody presses the button twice. A request that
|
|
1302
|
+
* died on the way back is not the same as mail that never left, and telling a
|
|
1303
|
+
* reader it was not sent is how somebody gets reminded twice.
|
|
1304
|
+
*/
|
|
1305
|
+
test("an unconfirmed reminder claims neither success nor failure", () => {
|
|
1306
|
+
renderDetail({
|
|
1307
|
+
unreachable: [ALEX_ISSUE],
|
|
1308
|
+
reminders: {
|
|
1309
|
+
[USER_ALEX]: { state: "unknown", message: "Gateway timeout." },
|
|
1310
|
+
},
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
expect(screen.queryByTestId("setup-reminder-sent")).not.toBeInTheDocument();
|
|
1314
|
+
expect(
|
|
1315
|
+
screen.queryByTestId("setup-reminder-not-sent"),
|
|
1316
|
+
).not.toBeInTheDocument();
|
|
1317
|
+
expect(screen.getByTestId("setup-reminder-unknown")).toHaveTextContent(
|
|
1318
|
+
"We could not confirm whether a reminder reached Alex Chen. Gateway timeout. Check with Alex before sending another.",
|
|
1319
|
+
);
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
test("the button is held while a send is in flight", () => {
|
|
1323
|
+
renderDetail({
|
|
1324
|
+
unreachable: [ALEX_ISSUE],
|
|
1325
|
+
reminders: { [USER_ALEX]: { state: "sending", message: "" } },
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
expect(screen.getByText("Sending...")).toBeInTheDocument();
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
/*
|
|
1332
|
+
* A level that reaches a whole team of unverified accounts is one click, not
|
|
1333
|
+
* eight.
|
|
1334
|
+
*/
|
|
1335
|
+
test("two or more unreachable responders can be reminded at once", () => {
|
|
1336
|
+
const onSendReminder: MockFunction = getJestMockFunction();
|
|
1337
|
+
|
|
1338
|
+
renderDetail({
|
|
1339
|
+
unreachable: [ALEX_ISSUE, SAM_ISSUE],
|
|
1340
|
+
onSendReminder: onSendReminder as unknown as (
|
|
1341
|
+
userIds: Array<string>,
|
|
1342
|
+
) => void,
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
fireEvent.click(screen.getByText("Remind all 2"));
|
|
1346
|
+
|
|
1347
|
+
expect(onSendReminder).toHaveBeenCalledWith([USER_ALEX, USER_SAM]);
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
test("the batch never re-sends to somebody who has already been told", () => {
|
|
1351
|
+
const onSendReminder: MockFunction = getJestMockFunction();
|
|
1352
|
+
|
|
1353
|
+
renderDetail({
|
|
1354
|
+
unreachable: [ALEX_ISSUE, SAM_ISSUE],
|
|
1355
|
+
reminders: { [USER_ALEX]: { state: "sent", message: "" } },
|
|
1356
|
+
onSendReminder: onSendReminder as unknown as (
|
|
1357
|
+
userIds: Array<string>,
|
|
1358
|
+
) => void,
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
/*
|
|
1362
|
+
* One person left to remind, so the batch control is gone entirely: the row
|
|
1363
|
+
* for Sam still carries their own button, and a "Remind all 1" would be a
|
|
1364
|
+
* second control for the same single send.
|
|
1365
|
+
*/
|
|
1366
|
+
expect(screen.queryByText("Remind all 1")).not.toBeInTheDocument();
|
|
1367
|
+
expect(screen.queryByText("Remind all 2")).not.toBeInTheDocument();
|
|
1368
|
+
|
|
1369
|
+
fireEvent.click(screen.getByText("Send setup reminder"));
|
|
1370
|
+
|
|
1371
|
+
expect(onSendReminder).toHaveBeenCalledWith([USER_SAM]);
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
test("one unreachable responder gets a row button and no batch button", () => {
|
|
1375
|
+
renderDetail({ unreachable: [ALEX_ISSUE] });
|
|
1376
|
+
|
|
1377
|
+
expect(screen.getByText("Send setup reminder")).toBeInTheDocument();
|
|
1378
|
+
expect(screen.queryByText("Remind all 1")).not.toBeInTheDocument();
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
/*
|
|
1382
|
+
* A batch that vanishes the moment it is pressed looks like a control that
|
|
1383
|
+
* broke rather than one that is working. It stays, and the count it names does
|
|
1384
|
+
* not change under the reader while the send it started is in flight.
|
|
1385
|
+
*/
|
|
1386
|
+
test("the batch control stays put while its own send is running", () => {
|
|
1387
|
+
render(
|
|
1388
|
+
<RuleReadinessDetails
|
|
1389
|
+
ruleName="First Responders"
|
|
1390
|
+
report={{
|
|
1391
|
+
unreachable: [ALEX_ISSUE, SAM_ISSUE],
|
|
1392
|
+
gaps: [],
|
|
1393
|
+
unchecked: [],
|
|
1394
|
+
unreadableGroups: [],
|
|
1395
|
+
isResolving: false,
|
|
1396
|
+
responderCount: 2,
|
|
1397
|
+
checkedCount: 2,
|
|
1398
|
+
}}
|
|
1399
|
+
delivery={FALLBACK_ON}
|
|
1400
|
+
reminders={{
|
|
1401
|
+
[USER_ALEX]: { state: "sending", message: "" },
|
|
1402
|
+
[USER_SAM]: { state: "sending", message: "" },
|
|
1403
|
+
}}
|
|
1404
|
+
isSendingReminders={true}
|
|
1405
|
+
onSendReminder={() => {}}
|
|
1406
|
+
onClose={() => {}}
|
|
1407
|
+
/>,
|
|
1408
|
+
);
|
|
1409
|
+
|
|
1410
|
+
expect(screen.getByText("Remind all 2")).toBeInTheDocument();
|
|
1411
|
+
expect(screen.getAllByText("Sending...")).toHaveLength(2);
|
|
1412
|
+
});
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
/*
|
|
1416
|
+
* ---------------------------------------------------------------------------
|
|
1417
|
+
* READING THE REMINDER ENDPOINT'S ANSWER
|
|
1418
|
+
* ---------------------------------------------------------------------------
|
|
1419
|
+
*/
|
|
1420
|
+
describe("Reading the reminder endpoint's answer", () => {
|
|
1421
|
+
test("a Sent outcome is a send", () => {
|
|
1422
|
+
expect(
|
|
1423
|
+
readSetupReminderStatuses(
|
|
1424
|
+
{
|
|
1425
|
+
sentCount: 1,
|
|
1426
|
+
results: [
|
|
1427
|
+
{ userId: USER_ALEX, outcome: "Sent", message: "Emailed." },
|
|
1428
|
+
],
|
|
1429
|
+
},
|
|
1430
|
+
[USER_ALEX],
|
|
1431
|
+
),
|
|
1432
|
+
).toEqual({ [USER_ALEX]: { state: "sent", message: "Emailed." } });
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
test("a throttled outcome is a skip carrying the server's explanation", () => {
|
|
1436
|
+
expect(
|
|
1437
|
+
readSetupReminderStatuses(
|
|
1438
|
+
{
|
|
1439
|
+
results: [
|
|
1440
|
+
{
|
|
1441
|
+
userId: USER_ALEX,
|
|
1442
|
+
outcome: "SkippedThrottled",
|
|
1443
|
+
message: "Already reminded in the last 24 hours.",
|
|
1444
|
+
},
|
|
1445
|
+
],
|
|
1446
|
+
},
|
|
1447
|
+
[USER_ALEX],
|
|
1448
|
+
)[USER_ALEX],
|
|
1449
|
+
).toEqual({
|
|
1450
|
+
state: "skipped",
|
|
1451
|
+
message: "Already reminded in the last 24 hours.",
|
|
1452
|
+
});
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
test("a failed outcome is a failure", () => {
|
|
1456
|
+
expect(
|
|
1457
|
+
readSetupReminderStatuses(
|
|
1458
|
+
{
|
|
1459
|
+
results: [
|
|
1460
|
+
{
|
|
1461
|
+
userId: USER_ALEX,
|
|
1462
|
+
outcome: "Failed",
|
|
1463
|
+
message: "Mail provider rejected the message.",
|
|
1464
|
+
},
|
|
1465
|
+
],
|
|
1466
|
+
},
|
|
1467
|
+
[USER_ALEX],
|
|
1468
|
+
)[USER_ALEX]!.state,
|
|
1469
|
+
).toBe("failed");
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
/*
|
|
1473
|
+
* A 200 whose results say nothing about the user we asked about is the one
|
|
1474
|
+
* shape that could quietly become a tick over a reminder that never left.
|
|
1475
|
+
*/
|
|
1476
|
+
test("a 200 that says nothing about this user is not a send", () => {
|
|
1477
|
+
expect(
|
|
1478
|
+
readSetupReminderStatuses(
|
|
1479
|
+
{ results: [{ userId: USER_SAM, outcome: "Sent", message: "" }] },
|
|
1480
|
+
[USER_ALEX],
|
|
1481
|
+
),
|
|
1482
|
+
).toEqual({
|
|
1483
|
+
[USER_ALEX]: {
|
|
1484
|
+
state: "unknown",
|
|
1485
|
+
message: "The server did not report an outcome for this reminder.",
|
|
1486
|
+
},
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
expect(readSetupReminderStatuses({}, [USER_ALEX])[USER_ALEX]!.state).toBe(
|
|
1490
|
+
"unknown",
|
|
1491
|
+
);
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
test("a batch answer is split per user", () => {
|
|
1495
|
+
const statuses: SetupReminderStatuses = readSetupReminderStatuses(
|
|
1496
|
+
{
|
|
1497
|
+
results: [
|
|
1498
|
+
{ userId: USER_ALEX, outcome: "Sent", message: "Emailed." },
|
|
1499
|
+
{
|
|
1500
|
+
userId: USER_SAM,
|
|
1501
|
+
outcome: "SkippedNotAMember",
|
|
1502
|
+
message: "Not a member of this project.",
|
|
1503
|
+
},
|
|
1504
|
+
],
|
|
1505
|
+
},
|
|
1506
|
+
[USER_ALEX, USER_SAM],
|
|
1507
|
+
);
|
|
1508
|
+
|
|
1509
|
+
expect(statuses[USER_ALEX]!.state).toBe("sent");
|
|
1510
|
+
expect(statuses[USER_SAM]!.state).toBe("skipped");
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
test("an outcome this build has never heard of still reads as not-sent", () => {
|
|
1514
|
+
expect(
|
|
1515
|
+
readSetupReminderStatuses(
|
|
1516
|
+
{
|
|
1517
|
+
results: [
|
|
1518
|
+
{
|
|
1519
|
+
userId: USER_ALEX,
|
|
1520
|
+
outcome: "SkippedSomethingNewEntirely",
|
|
1521
|
+
message: "A future reason.",
|
|
1522
|
+
},
|
|
1523
|
+
],
|
|
1524
|
+
},
|
|
1525
|
+
[USER_ALEX],
|
|
1526
|
+
)[USER_ALEX],
|
|
1527
|
+
).toEqual({ state: "skipped", message: "A future reason." });
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
test("an outcome with no message still produces a true sentence", () => {
|
|
1531
|
+
expect(
|
|
1532
|
+
readSetupReminderStatuses(
|
|
1533
|
+
{ results: [{ userId: USER_ALEX, outcome: "Failed" }] },
|
|
1534
|
+
[USER_ALEX],
|
|
1535
|
+
)[USER_ALEX]!.message,
|
|
1536
|
+
).toBe('The server reported "Failed" without an explanation.');
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
test("a results payload that is not a list is not an answer", () => {
|
|
1540
|
+
expect(
|
|
1541
|
+
readSetupReminderStatuses({ results: "everything went fine" }, [
|
|
1542
|
+
USER_ALEX,
|
|
1543
|
+
])[USER_ALEX]!.state,
|
|
1544
|
+
).toBe("unknown");
|
|
1545
|
+
});
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
describe("Sending the reminder", () => {
|
|
1549
|
+
test("it posts the user ids to the shared reminder endpoint", async () => {
|
|
1550
|
+
getCommonHeadersMock.mockReturnValue({ tenantid: "a-project" });
|
|
1551
|
+
postMock.mockResolvedValue(
|
|
1552
|
+
okResponse({
|
|
1553
|
+
sentCount: 1,
|
|
1554
|
+
results: [{ userId: USER_ALEX, outcome: "Sent", message: "Emailed." }],
|
|
1555
|
+
}) as never,
|
|
1556
|
+
);
|
|
1557
|
+
|
|
1558
|
+
const statuses: SetupReminderStatuses = await requestSetupReminders([
|
|
1559
|
+
USER_ALEX,
|
|
1560
|
+
]);
|
|
1561
|
+
|
|
1562
|
+
const request: any = (postMock as any).mock.calls[0][0];
|
|
1563
|
+
|
|
1564
|
+
expect(request.url.toString()).toContain(
|
|
1565
|
+
"/on-call-readiness/send-setup-reminder",
|
|
1566
|
+
);
|
|
1567
|
+
expect(request.data).toEqual({ userIds: [USER_ALEX] });
|
|
1568
|
+
expect(request.headers).toEqual({ tenantid: "a-project" });
|
|
1569
|
+
expect(statuses[USER_ALEX]!.state).toBe("sent");
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
/*
|
|
1573
|
+
* The refusals that happen BEFORE any mail is sent - body validation, the
|
|
1574
|
+
* permission gate, the service's pre-flight checks - are the only failures
|
|
1575
|
+
* that earn the definite "no reminder was sent".
|
|
1576
|
+
*/
|
|
1577
|
+
test("a refusal from the application itself is a definite failure", async () => {
|
|
1578
|
+
getCommonHeadersMock.mockReturnValue({});
|
|
1579
|
+
postMock.mockResolvedValue(
|
|
1580
|
+
new HTTPErrorResponse(403, { message: "Not allowed." }, {}) as never,
|
|
1581
|
+
);
|
|
1582
|
+
|
|
1583
|
+
expect((await requestSetupReminders([USER_ALEX]))[USER_ALEX]!.state).toBe(
|
|
1584
|
+
"failed",
|
|
1585
|
+
);
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
/*
|
|
1589
|
+
* And everything else is not. A 502 or a dropped connection can happen after
|
|
1590
|
+
* the mail has left, and an admin told "nothing was sent" presses the button
|
|
1591
|
+
* again.
|
|
1592
|
+
*/
|
|
1593
|
+
test("a gateway failure is unknown rather than a definite failure", async () => {
|
|
1594
|
+
getCommonHeadersMock.mockReturnValue({});
|
|
1595
|
+
postMock.mockResolvedValue(
|
|
1596
|
+
new HTTPErrorResponse(502, { message: "Bad gateway." }, {}) as never,
|
|
1597
|
+
);
|
|
1598
|
+
|
|
1599
|
+
expect((await requestSetupReminders([USER_ALEX]))[USER_ALEX]!.state).toBe(
|
|
1600
|
+
"unknown",
|
|
1601
|
+
);
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
test("a transport failure carries the message through as unknown", async () => {
|
|
1605
|
+
getCommonHeadersMock.mockReturnValue({});
|
|
1606
|
+
postMock.mockRejectedValue(new Error("Network down") as never);
|
|
1607
|
+
|
|
1608
|
+
expect((await requestSetupReminders([USER_ALEX]))[USER_ALEX]).toEqual({
|
|
1609
|
+
state: "unknown",
|
|
1610
|
+
message: "Network down",
|
|
1611
|
+
});
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
test("a failed batch reports the same outcome for every user asked about", async () => {
|
|
1615
|
+
getCommonHeadersMock.mockReturnValue({});
|
|
1616
|
+
postMock.mockRejectedValue(new Error("Network down") as never);
|
|
1617
|
+
|
|
1618
|
+
const statuses: SetupReminderStatuses = await requestSetupReminders([
|
|
1619
|
+
USER_ALEX,
|
|
1620
|
+
USER_SAM,
|
|
1621
|
+
]);
|
|
1622
|
+
|
|
1623
|
+
expect(Object.keys(statuses).sort()).toEqual([USER_ALEX, USER_SAM].sort());
|
|
1624
|
+
});
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
/*
|
|
1628
|
+
* ---------------------------------------------------------------------------
|
|
1629
|
+
* EXPANDING TEAMS AND SCHEDULES
|
|
1630
|
+
* ---------------------------------------------------------------------------
|
|
1631
|
+
*/
|
|
1632
|
+
describe("Expanding the groups a level notifies", () => {
|
|
1633
|
+
test("a team expands to its members", async () => {
|
|
1634
|
+
getListMock.mockResolvedValue({
|
|
1635
|
+
data: [
|
|
1636
|
+
{ user: { id: new ObjectID(USER_ALEX), name: "Alex Chen" } },
|
|
1637
|
+
{ user: { id: new ObjectID(USER_SAM), name: "Sam Doe" } },
|
|
1638
|
+
],
|
|
1639
|
+
count: 2,
|
|
1640
|
+
skip: 0,
|
|
1641
|
+
limit: 50,
|
|
1642
|
+
} as never);
|
|
1643
|
+
|
|
1644
|
+
const resolution: ResponderGroupResolution = await fetchTeamMembers({
|
|
1645
|
+
group: TEAM_REF,
|
|
1646
|
+
projectId: PROJECT_ID,
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
expect(resolution.state).toBe("resolved");
|
|
1650
|
+
expect(resolution.members).toEqual([
|
|
1651
|
+
{ userId: USER_ALEX, label: "Alex Chen" },
|
|
1652
|
+
{ userId: USER_SAM, label: "Sam Doe" },
|
|
1653
|
+
]);
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
/*
|
|
1657
|
+
* No hasAcceptedInvitation filter, mirroring TeamMemberService.getUsersInTeam:
|
|
1658
|
+
* somebody who never accepted their invite still gets paged, so they still
|
|
1659
|
+
* have to be checked.
|
|
1660
|
+
*/
|
|
1661
|
+
test("the team read does not filter on invitations being accepted", async () => {
|
|
1662
|
+
getListMock.mockResolvedValue({
|
|
1663
|
+
data: [],
|
|
1664
|
+
count: 0,
|
|
1665
|
+
skip: 0,
|
|
1666
|
+
limit: 50,
|
|
1667
|
+
} as never);
|
|
1668
|
+
|
|
1669
|
+
await fetchTeamMembers({ group: TEAM_REF, projectId: PROJECT_ID });
|
|
1670
|
+
|
|
1671
|
+
const query: any = (getListMock as any).mock.calls[0][0].query;
|
|
1672
|
+
|
|
1673
|
+
expect(query.hasAcceptedInvitation).toBeUndefined();
|
|
1674
|
+
expect(query.teamId.toString()).toBe(TEAM_ID);
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
test("a schedule expands to its whole layer roster, not just today's", async () => {
|
|
1678
|
+
getListMock.mockResolvedValue({
|
|
1679
|
+
data: [
|
|
1680
|
+
{ user: { id: new ObjectID(USER_ALEX), name: "Alex Chen" } },
|
|
1681
|
+
{ user: { id: new ObjectID(USER_ALEX), name: "Alex Chen" } },
|
|
1682
|
+
],
|
|
1683
|
+
count: 2,
|
|
1684
|
+
skip: 0,
|
|
1685
|
+
limit: 50,
|
|
1686
|
+
} as never);
|
|
1687
|
+
|
|
1688
|
+
const resolution: ResponderGroupResolution = await fetchScheduleUsers({
|
|
1689
|
+
group: SCHEDULE_REF,
|
|
1690
|
+
projectId: PROJECT_ID,
|
|
1691
|
+
});
|
|
1692
|
+
|
|
1693
|
+
const query: any = (getListMock as any).mock.calls[0][0].query;
|
|
1694
|
+
|
|
1695
|
+
expect(query.onCallDutyPolicyScheduleId.toString()).toBe(SCHEDULE_ID);
|
|
1696
|
+
// The same person on two layers is one responder.
|
|
1697
|
+
expect(resolution.members).toEqual([
|
|
1698
|
+
{ userId: USER_ALEX, label: "Alex Chen" },
|
|
1699
|
+
]);
|
|
1700
|
+
});
|
|
1701
|
+
|
|
1702
|
+
/*
|
|
1703
|
+
* The whole reason "unavailable" exists. A rejected read that produced an
|
|
1704
|
+
* empty membership would render as "everybody in that team is fine".
|
|
1705
|
+
*/
|
|
1706
|
+
test("a team that cannot be read is unavailable, never empty", async () => {
|
|
1707
|
+
getListMock.mockRejectedValue(new Error("teams are down") as never);
|
|
1708
|
+
|
|
1709
|
+
const resolution: ResponderGroupResolution = await fetchTeamMembers({
|
|
1710
|
+
group: TEAM_REF,
|
|
1711
|
+
projectId: PROJECT_ID,
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
expect(resolution.state).toBe("unavailable");
|
|
1715
|
+
expect(resolution.members).toEqual([]);
|
|
1716
|
+
expect(resolution.label).toBe("Payments");
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
test("a schedule that cannot be read is unavailable too", async () => {
|
|
1720
|
+
getListMock.mockRejectedValue(new Error("schedules are down") as never);
|
|
1721
|
+
|
|
1722
|
+
expect(
|
|
1723
|
+
(
|
|
1724
|
+
await fetchScheduleUsers({
|
|
1725
|
+
group: SCHEDULE_REF,
|
|
1726
|
+
projectId: PROJECT_ID,
|
|
1727
|
+
})
|
|
1728
|
+
).state,
|
|
1729
|
+
).toBe("unavailable");
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
test("a member row with no user is dropped rather than counted as a nameless responder", async () => {
|
|
1733
|
+
getListMock.mockResolvedValue({
|
|
1734
|
+
data: [{}, { user: { id: new ObjectID(USER_ALEX), name: "Alex Chen" } }],
|
|
1735
|
+
count: 2,
|
|
1736
|
+
skip: 0,
|
|
1737
|
+
limit: 50,
|
|
1738
|
+
} as never);
|
|
1739
|
+
|
|
1740
|
+
const resolution: ResponderGroupResolution = await fetchTeamMembers({
|
|
1741
|
+
group: TEAM_REF,
|
|
1742
|
+
projectId: PROJECT_ID,
|
|
1743
|
+
});
|
|
1744
|
+
|
|
1745
|
+
expect(resolution.members).toEqual([
|
|
1746
|
+
{ userId: USER_ALEX, label: "Alex Chen" },
|
|
1747
|
+
]);
|
|
1748
|
+
});
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
/*
|
|
1752
|
+
* ---------------------------------------------------------------------------
|
|
1753
|
+
* THE ESCALATION PAGE, WIRED
|
|
1754
|
+
* ---------------------------------------------------------------------------
|
|
1755
|
+
*
|
|
1756
|
+
* The pure helpers above prove the arithmetic; this proves it is CONNECTED. A
|
|
1757
|
+
* label whose copy is perfect and which nothing renders has warned nobody, and
|
|
1758
|
+
* that failure is invisible to every test that stops at the function boundary.
|
|
1759
|
+
*/
|
|
1760
|
+
interface PolicyFixture {
|
|
1761
|
+
users?: Array<JSONObject> | undefined;
|
|
1762
|
+
isTruncated?: boolean | undefined;
|
|
1763
|
+
isFallbackEnabled?: boolean | undefined;
|
|
1764
|
+
teamOnRuleOne?: boolean | undefined;
|
|
1765
|
+
teamMembers?: Array<JSONObject> | undefined;
|
|
1766
|
+
teamReadFails?: boolean | undefined;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
type MockPolicyFunction = (fixture: PolicyFixture) => void;
|
|
1770
|
+
|
|
1771
|
+
const mockPolicy: MockPolicyFunction = (fixture: PolicyFixture): void => {
|
|
1772
|
+
getCommonHeadersMock.mockReturnValue({});
|
|
1773
|
+
|
|
1774
|
+
getItemMock.mockResolvedValue({
|
|
1775
|
+
repeatPolicyIfNoOneAcknowledges: false,
|
|
1776
|
+
repeatPolicyIfNoOneAcknowledgesNoOfTimes: 0,
|
|
1777
|
+
} as never);
|
|
1778
|
+
|
|
1779
|
+
/*
|
|
1780
|
+
* Keyed on the SELECT rather than the model class, because the reads issue the
|
|
1781
|
+
* same query shape against the same policy and only their selects tell them
|
|
1782
|
+
* apart - which is also exactly how a reader of the component tells them
|
|
1783
|
+
* apart.
|
|
1784
|
+
*/
|
|
1785
|
+
getListMock.mockImplementation((params: any): Promise<any> => {
|
|
1786
|
+
const select: Record<string, unknown> = params.select || {};
|
|
1787
|
+
const query: Record<string, unknown> = params.query || {};
|
|
1788
|
+
|
|
1789
|
+
if (select["escalateAfterInMinutes"]) {
|
|
1790
|
+
return Promise.resolve({
|
|
1791
|
+
data: [
|
|
1792
|
+
{
|
|
1793
|
+
id: RULE_ONE_ID,
|
|
1794
|
+
name: "First Responders",
|
|
1795
|
+
escalateAfterInMinutes: 5,
|
|
1796
|
+
order: 1,
|
|
1797
|
+
},
|
|
1798
|
+
{
|
|
1799
|
+
id: RULE_TWO_ID,
|
|
1800
|
+
name: "Backup",
|
|
1801
|
+
escalateAfterInMinutes: 10,
|
|
1802
|
+
order: 2,
|
|
1803
|
+
},
|
|
1804
|
+
],
|
|
1805
|
+
count: 2,
|
|
1806
|
+
skip: 0,
|
|
1807
|
+
limit: 50,
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// The team-membership expansion.
|
|
1812
|
+
if (query["teamId"]) {
|
|
1813
|
+
if (fixture.teamReadFails) {
|
|
1814
|
+
return Promise.reject(new Error("teams are down"));
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
return Promise.resolve({
|
|
1818
|
+
data: fixture.teamMembers || [],
|
|
1819
|
+
count: (fixture.teamMembers || []).length,
|
|
1820
|
+
skip: 0,
|
|
1821
|
+
limit: 50,
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
if (query["onCallDutyPolicyScheduleId"]) {
|
|
1826
|
+
return Promise.resolve({ data: [], count: 0, skip: 0, limit: 50 });
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// The rule -> user join rows.
|
|
1830
|
+
if (select["user"]) {
|
|
1831
|
+
return Promise.resolve({
|
|
1832
|
+
data: [
|
|
1833
|
+
{
|
|
1834
|
+
id: new ObjectID("aaaaaaaa-0001-4001-8001-000000000001"),
|
|
1835
|
+
onCallDutyPolicyEscalationRuleId: RULE_ONE_ID,
|
|
1836
|
+
user: { id: new ObjectID(USER_ALEX), name: "Alex Chen" },
|
|
1837
|
+
},
|
|
1838
|
+
{
|
|
1839
|
+
id: new ObjectID("aaaaaaaa-0003-4003-8003-000000000003"),
|
|
1840
|
+
onCallDutyPolicyEscalationRuleId: RULE_TWO_ID,
|
|
1841
|
+
user: { id: new ObjectID(USER_SAM), name: "Sam Doe" },
|
|
1842
|
+
},
|
|
1843
|
+
],
|
|
1844
|
+
count: 2,
|
|
1845
|
+
skip: 0,
|
|
1846
|
+
limit: 50,
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// The rule -> team join rows.
|
|
1851
|
+
if (select["team"]) {
|
|
1852
|
+
return Promise.resolve({
|
|
1853
|
+
data: fixture.teamOnRuleOne
|
|
1854
|
+
? [
|
|
1855
|
+
{
|
|
1856
|
+
id: new ObjectID("aaaaaaaa-0004-4004-8004-000000000004"),
|
|
1857
|
+
onCallDutyPolicyEscalationRuleId: RULE_ONE_ID,
|
|
1858
|
+
team: { id: new ObjectID(TEAM_ID), name: "Payments" },
|
|
1859
|
+
},
|
|
1860
|
+
]
|
|
1861
|
+
: [],
|
|
1862
|
+
count: fixture.teamOnRuleOne ? 1 : 0,
|
|
1863
|
+
skip: 0,
|
|
1864
|
+
limit: 50,
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
return Promise.resolve({ data: [], count: 0, skip: 0, limit: 50 });
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
getMock.mockResolvedValue(
|
|
1872
|
+
okResponse({
|
|
1873
|
+
projectId: PROJECT_ID.toString(),
|
|
1874
|
+
onCallDutyPolicyId: POLICY_ID.toString(),
|
|
1875
|
+
isFallbackEnabled: fixture.isFallbackEnabled !== false,
|
|
1876
|
+
isTruncated: fixture.isTruncated === true,
|
|
1877
|
+
totalCount: (fixture.users || []).length,
|
|
1878
|
+
hasMore: false,
|
|
1879
|
+
users: fixture.users || [],
|
|
1880
|
+
}) as never,
|
|
1881
|
+
);
|
|
1882
|
+
};
|
|
1883
|
+
|
|
1884
|
+
type ReadinessUserFunction = (params: {
|
|
1885
|
+
userId: string;
|
|
1886
|
+
userName: string;
|
|
1887
|
+
status: string;
|
|
1888
|
+
}) => JSONObject;
|
|
1889
|
+
|
|
1890
|
+
const readinessUser: ReadinessUserFunction = (params: {
|
|
1891
|
+
userId: string;
|
|
1892
|
+
userName: string;
|
|
1893
|
+
status: string;
|
|
1894
|
+
}): JSONObject => {
|
|
1895
|
+
return {
|
|
1896
|
+
userId: params.userId,
|
|
1897
|
+
userName: params.userName,
|
|
1898
|
+
userEmail: `${params.userName.split(" ")[0]!.toLowerCase()}@example.com`,
|
|
1899
|
+
status: params.status,
|
|
1900
|
+
methods: [],
|
|
1901
|
+
coverage: [],
|
|
1902
|
+
reasons: [],
|
|
1903
|
+
reachedVia: [],
|
|
1904
|
+
};
|
|
1905
|
+
};
|
|
1906
|
+
|
|
1907
|
+
describe("The escalation page, wired", () => {
|
|
1908
|
+
test("the level that reaches an unreachable responder wears the label - and the other does not", async () => {
|
|
1909
|
+
mockPolicy({
|
|
1910
|
+
users: [
|
|
1911
|
+
readinessUser({
|
|
1912
|
+
userId: USER_ALEX,
|
|
1913
|
+
userName: "Alex Chen",
|
|
1914
|
+
status: "NotReachable",
|
|
1915
|
+
}),
|
|
1916
|
+
readinessUser({
|
|
1917
|
+
userId: USER_SAM,
|
|
1918
|
+
userName: "Sam Doe",
|
|
1919
|
+
status: "Ready",
|
|
1920
|
+
}),
|
|
1921
|
+
],
|
|
1922
|
+
});
|
|
1923
|
+
|
|
1924
|
+
render(
|
|
1925
|
+
<EscalationRules onCallDutyPolicyId={POLICY_ID} projectId={PROJECT_ID} />,
|
|
1926
|
+
);
|
|
1927
|
+
|
|
1928
|
+
await waitFor(() => {
|
|
1929
|
+
expect(screen.getByTestId("rule-readiness-label")).toBeInTheDocument();
|
|
1930
|
+
});
|
|
1931
|
+
|
|
1932
|
+
// Exactly one level is accused, and it is the one Alex is on.
|
|
1933
|
+
expect(screen.getAllByTestId("rule-readiness-label")).toHaveLength(1);
|
|
1934
|
+
expect(screen.getByTestId("rule-readiness-label")).toHaveAttribute(
|
|
1935
|
+
"aria-label",
|
|
1936
|
+
expect.stringContaining("First Responders"),
|
|
1937
|
+
);
|
|
1938
|
+
expect(screen.getByTestId("rule-readiness-label")).toHaveTextContent(
|
|
1939
|
+
"1 person can't be paged",
|
|
1940
|
+
);
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
test("a policy where everyone can be paged wears no label at all", async () => {
|
|
1944
|
+
mockPolicy({
|
|
1945
|
+
users: [
|
|
1946
|
+
readinessUser({
|
|
1947
|
+
userId: USER_ALEX,
|
|
1948
|
+
userName: "Alex Chen",
|
|
1949
|
+
status: "Ready",
|
|
1950
|
+
}),
|
|
1951
|
+
readinessUser({
|
|
1952
|
+
userId: USER_SAM,
|
|
1953
|
+
userName: "Sam Doe",
|
|
1954
|
+
status: "Ready",
|
|
1955
|
+
}),
|
|
1956
|
+
],
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
render(
|
|
1960
|
+
<EscalationRules onCallDutyPolicyId={POLICY_ID} projectId={PROJECT_ID} />,
|
|
1961
|
+
);
|
|
1962
|
+
|
|
1963
|
+
await waitFor(() => {
|
|
1964
|
+
expect(screen.getAllByLabelText("Edit rule").length).toBeGreaterThan(0);
|
|
1965
|
+
});
|
|
1966
|
+
|
|
1967
|
+
expect(
|
|
1968
|
+
screen.queryByTestId("rule-readiness-label"),
|
|
1969
|
+
).not.toBeInTheDocument();
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
test("clicking the label opens the detail, and the detail names the person and the fix", async () => {
|
|
1973
|
+
mockPolicy({
|
|
1974
|
+
users: [
|
|
1975
|
+
readinessUser({
|
|
1976
|
+
userId: USER_ALEX,
|
|
1977
|
+
userName: "Alex Chen",
|
|
1978
|
+
status: "NotReachable",
|
|
1979
|
+
}),
|
|
1980
|
+
],
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1983
|
+
render(
|
|
1984
|
+
<EscalationRules onCallDutyPolicyId={POLICY_ID} projectId={PROJECT_ID} />,
|
|
1985
|
+
);
|
|
1986
|
+
|
|
1987
|
+
await waitFor(() => {
|
|
1988
|
+
expect(screen.getByTestId("rule-readiness-label")).toBeInTheDocument();
|
|
1989
|
+
});
|
|
1990
|
+
|
|
1991
|
+
fireEvent.click(screen.getByTestId("rule-readiness-label"));
|
|
1992
|
+
|
|
1993
|
+
await waitFor(() => {
|
|
1994
|
+
expect(screen.getByTestId("rule-readiness-details")).toBeInTheDocument();
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1997
|
+
expect(
|
|
1998
|
+
screen.getByText(
|
|
1999
|
+
/Alex Chen has no verified notification method, so every page routed to them is dropped\./,
|
|
2000
|
+
),
|
|
2001
|
+
).toBeInTheDocument();
|
|
2002
|
+
expect(screen.getByText("Send setup reminder")).toBeInTheDocument();
|
|
2003
|
+
});
|
|
2004
|
+
|
|
2005
|
+
/*
|
|
2006
|
+
* The reminder, end to end, through the real component: the click, the
|
|
2007
|
+
* request, and a confirmation that is only allowed to appear because the
|
|
2008
|
+
* SERVER said the mail went.
|
|
2009
|
+
*/
|
|
2010
|
+
test("sending a reminder from the detail reports what the server said happened", async () => {
|
|
2011
|
+
mockPolicy({
|
|
2012
|
+
users: [
|
|
2013
|
+
readinessUser({
|
|
2014
|
+
userId: USER_ALEX,
|
|
2015
|
+
userName: "Alex Chen",
|
|
2016
|
+
status: "NotReachable",
|
|
2017
|
+
}),
|
|
2018
|
+
],
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
postMock.mockResolvedValue(
|
|
2022
|
+
okResponse({
|
|
2023
|
+
sentCount: 1,
|
|
2024
|
+
results: [{ userId: USER_ALEX, outcome: "Sent", message: "Emailed." }],
|
|
2025
|
+
}) as never,
|
|
2026
|
+
);
|
|
2027
|
+
|
|
2028
|
+
render(
|
|
2029
|
+
<EscalationRules onCallDutyPolicyId={POLICY_ID} projectId={PROJECT_ID} />,
|
|
2030
|
+
);
|
|
2031
|
+
|
|
2032
|
+
await waitFor(() => {
|
|
2033
|
+
expect(screen.getByTestId("rule-readiness-label")).toBeInTheDocument();
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
fireEvent.click(screen.getByTestId("rule-readiness-label"));
|
|
2037
|
+
|
|
2038
|
+
await waitFor(() => {
|
|
2039
|
+
expect(screen.getByText("Send setup reminder")).toBeInTheDocument();
|
|
2040
|
+
});
|
|
2041
|
+
|
|
2042
|
+
fireEvent.click(screen.getByText("Send setup reminder"));
|
|
2043
|
+
|
|
2044
|
+
await waitFor(() => {
|
|
2045
|
+
expect(screen.getByTestId("setup-reminder-sent")).toHaveTextContent(
|
|
2046
|
+
"Setup reminder sent to Alex Chen.",
|
|
2047
|
+
);
|
|
2048
|
+
});
|
|
2049
|
+
|
|
2050
|
+
expect((postMock as any).mock.calls[0][0].data).toEqual({
|
|
2051
|
+
userIds: [USER_ALEX],
|
|
2052
|
+
});
|
|
2053
|
+
});
|
|
2054
|
+
|
|
2055
|
+
test("a throttled reminder leaves no tick behind it", async () => {
|
|
2056
|
+
mockPolicy({
|
|
2057
|
+
users: [
|
|
2058
|
+
readinessUser({
|
|
2059
|
+
userId: USER_ALEX,
|
|
2060
|
+
userName: "Alex Chen",
|
|
2061
|
+
status: "NotReachable",
|
|
2062
|
+
}),
|
|
2063
|
+
],
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2066
|
+
postMock.mockResolvedValue(
|
|
2067
|
+
okResponse({
|
|
2068
|
+
results: [
|
|
2069
|
+
{
|
|
2070
|
+
userId: USER_ALEX,
|
|
2071
|
+
outcome: "SkippedThrottled",
|
|
2072
|
+
message: "Already reminded in the last 24 hours.",
|
|
2073
|
+
},
|
|
2074
|
+
],
|
|
2075
|
+
}) as never,
|
|
2076
|
+
);
|
|
2077
|
+
|
|
2078
|
+
render(
|
|
2079
|
+
<EscalationRules onCallDutyPolicyId={POLICY_ID} projectId={PROJECT_ID} />,
|
|
2080
|
+
);
|
|
2081
|
+
|
|
2082
|
+
await waitFor(() => {
|
|
2083
|
+
expect(screen.getByTestId("rule-readiness-label")).toBeInTheDocument();
|
|
2084
|
+
});
|
|
2085
|
+
|
|
2086
|
+
fireEvent.click(screen.getByTestId("rule-readiness-label"));
|
|
2087
|
+
|
|
2088
|
+
await waitFor(() => {
|
|
2089
|
+
expect(screen.getByText("Send setup reminder")).toBeInTheDocument();
|
|
2090
|
+
});
|
|
2091
|
+
|
|
2092
|
+
fireEvent.click(screen.getByText("Send setup reminder"));
|
|
2093
|
+
|
|
2094
|
+
await waitFor(() => {
|
|
2095
|
+
expect(screen.getByTestId("setup-reminder-not-sent")).toHaveTextContent(
|
|
2096
|
+
"Already reminded in the last 24 hours.",
|
|
2097
|
+
);
|
|
2098
|
+
});
|
|
2099
|
+
|
|
2100
|
+
expect(screen.queryByTestId("setup-reminder-sent")).not.toBeInTheDocument();
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2103
|
+
/*
|
|
2104
|
+
* The common path: nobody picked this person, the team they are in was picked.
|
|
2105
|
+
*/
|
|
2106
|
+
test("somebody reached only through a team is found, and the team is named", async () => {
|
|
2107
|
+
mockPolicy({
|
|
2108
|
+
teamOnRuleOne: true,
|
|
2109
|
+
teamMembers: [{ user: { id: new ObjectID(USER_JO), name: "Jo Park" } }],
|
|
2110
|
+
users: [
|
|
2111
|
+
readinessUser({
|
|
2112
|
+
userId: USER_ALEX,
|
|
2113
|
+
userName: "Alex Chen",
|
|
2114
|
+
status: "Ready",
|
|
2115
|
+
}),
|
|
2116
|
+
readinessUser({
|
|
2117
|
+
userId: USER_SAM,
|
|
2118
|
+
userName: "Sam Doe",
|
|
2119
|
+
status: "Ready",
|
|
2120
|
+
}),
|
|
2121
|
+
readinessUser({
|
|
2122
|
+
userId: USER_JO,
|
|
2123
|
+
userName: "Jo Park",
|
|
2124
|
+
status: "NotReachable",
|
|
2125
|
+
}),
|
|
2126
|
+
],
|
|
2127
|
+
});
|
|
2128
|
+
|
|
2129
|
+
render(
|
|
2130
|
+
<EscalationRules onCallDutyPolicyId={POLICY_ID} projectId={PROJECT_ID} />,
|
|
2131
|
+
);
|
|
2132
|
+
|
|
2133
|
+
await waitFor(() => {
|
|
2134
|
+
expect(screen.getByTestId("rule-readiness-label")).toBeInTheDocument();
|
|
2135
|
+
});
|
|
2136
|
+
|
|
2137
|
+
fireEvent.click(screen.getByTestId("rule-readiness-label"));
|
|
2138
|
+
|
|
2139
|
+
await waitFor(() => {
|
|
2140
|
+
expect(screen.getByTestId("rule-readiness-details")).toBeInTheDocument();
|
|
2141
|
+
});
|
|
2142
|
+
|
|
2143
|
+
expect(
|
|
2144
|
+
screen.getByText("Reached through the Payments team."),
|
|
2145
|
+
).toBeInTheDocument();
|
|
2146
|
+
});
|
|
2147
|
+
|
|
2148
|
+
test("a team whose membership cannot be read is admitted to rather than skipped", async () => {
|
|
2149
|
+
mockPolicy({
|
|
2150
|
+
teamOnRuleOne: true,
|
|
2151
|
+
teamReadFails: true,
|
|
2152
|
+
users: [
|
|
2153
|
+
readinessUser({
|
|
2154
|
+
userId: USER_ALEX,
|
|
2155
|
+
userName: "Alex Chen",
|
|
2156
|
+
status: "Ready",
|
|
2157
|
+
}),
|
|
2158
|
+
readinessUser({
|
|
2159
|
+
userId: USER_SAM,
|
|
2160
|
+
userName: "Sam Doe",
|
|
2161
|
+
status: "Ready",
|
|
2162
|
+
}),
|
|
2163
|
+
],
|
|
2164
|
+
});
|
|
2165
|
+
|
|
2166
|
+
render(
|
|
2167
|
+
<EscalationRules onCallDutyPolicyId={POLICY_ID} projectId={PROJECT_ID} />,
|
|
2168
|
+
);
|
|
2169
|
+
|
|
2170
|
+
await waitFor(() => {
|
|
2171
|
+
expect(screen.getByTestId("rule-readiness-label")).toBeInTheDocument();
|
|
2172
|
+
});
|
|
2173
|
+
|
|
2174
|
+
expect(screen.getByTestId("rule-readiness-label")).toHaveAttribute(
|
|
2175
|
+
"data-warning-level",
|
|
2176
|
+
"unknown",
|
|
2177
|
+
);
|
|
2178
|
+
|
|
2179
|
+
fireEvent.click(screen.getByTestId("rule-readiness-label"));
|
|
2180
|
+
|
|
2181
|
+
await waitFor(() => {
|
|
2182
|
+
expect(screen.getByTestId("rule-responder-unchecked")).toHaveTextContent(
|
|
2183
|
+
"We could not read who is in Payments",
|
|
2184
|
+
);
|
|
2185
|
+
});
|
|
2186
|
+
});
|
|
2187
|
+
|
|
2188
|
+
/*
|
|
2189
|
+
* The move itself. The warning is no longer allowed to live in the modal: an
|
|
2190
|
+
* admin opening the add/edit form must not be shown a readiness block there,
|
|
2191
|
+
* and the readiness lookups the old block issued per keystroke must be gone
|
|
2192
|
+
* with it.
|
|
2193
|
+
*/
|
|
2194
|
+
test("the add-rule modal carries no readiness warning", async () => {
|
|
2195
|
+
mockPolicy({
|
|
2196
|
+
users: [
|
|
2197
|
+
readinessUser({
|
|
2198
|
+
userId: USER_ALEX,
|
|
2199
|
+
userName: "Alex Chen",
|
|
2200
|
+
status: "NotReachable",
|
|
2201
|
+
}),
|
|
2202
|
+
],
|
|
2203
|
+
});
|
|
2204
|
+
|
|
2205
|
+
render(
|
|
2206
|
+
<EscalationRules onCallDutyPolicyId={POLICY_ID} projectId={PROJECT_ID} />,
|
|
2207
|
+
);
|
|
2208
|
+
|
|
2209
|
+
await waitFor(() => {
|
|
2210
|
+
expect(screen.getByTestId("rule-readiness-label")).toBeInTheDocument();
|
|
2211
|
+
});
|
|
2212
|
+
|
|
2213
|
+
fireEvent.click(screen.getByText("Add Escalation Rule"));
|
|
2214
|
+
|
|
2215
|
+
await waitFor(() => {
|
|
2216
|
+
expect(screen.getByText("Create Rule")).toBeInTheDocument();
|
|
2217
|
+
});
|
|
2218
|
+
|
|
2219
|
+
expect(
|
|
2220
|
+
screen.queryByTestId("add-responder-warning"),
|
|
2221
|
+
).not.toBeInTheDocument();
|
|
2222
|
+
expect(
|
|
2223
|
+
screen.queryByText(
|
|
2224
|
+
"You can still add them - this is a warning, not a block.",
|
|
2225
|
+
),
|
|
2226
|
+
).not.toBeInTheDocument();
|
|
2227
|
+
});
|
|
2228
|
+
|
|
2229
|
+
/*
|
|
2230
|
+
* One readiness answer for the whole page. Two components each calling the
|
|
2231
|
+
* hook is two sets of paged requests for one payload, and they can disagree
|
|
2232
|
+
* with each other while they settle.
|
|
2233
|
+
*/
|
|
2234
|
+
test("the policy's readiness is read once for the page, not once per surface", async () => {
|
|
2235
|
+
mockPolicy({
|
|
2236
|
+
users: [
|
|
2237
|
+
readinessUser({
|
|
2238
|
+
userId: USER_ALEX,
|
|
2239
|
+
userName: "Alex Chen",
|
|
2240
|
+
status: "NotReachable",
|
|
2241
|
+
}),
|
|
2242
|
+
],
|
|
2243
|
+
});
|
|
2244
|
+
|
|
2245
|
+
render(
|
|
2246
|
+
<EscalationRules onCallDutyPolicyId={POLICY_ID} projectId={PROJECT_ID} />,
|
|
2247
|
+
);
|
|
2248
|
+
|
|
2249
|
+
await waitFor(() => {
|
|
2250
|
+
expect(screen.getByTestId("rule-readiness-label")).toBeInTheDocument();
|
|
2251
|
+
});
|
|
2252
|
+
|
|
2253
|
+
const readinessCalls: Array<any> = (getMock as any).mock.calls.filter(
|
|
2254
|
+
(call: Array<any>): boolean => {
|
|
2255
|
+
return String(call[0].url).includes("/on-call-readiness/policy/");
|
|
2256
|
+
},
|
|
2257
|
+
);
|
|
2258
|
+
|
|
2259
|
+
expect(readinessCalls).toHaveLength(1);
|
|
2260
|
+
});
|
|
2261
|
+
|
|
2262
|
+
/*
|
|
2263
|
+
* The page opening is not a recompute. Several surfaces ask the same question
|
|
2264
|
+
* when a screen loads, and the server's 60s cache is exactly right for that.
|
|
2265
|
+
*/
|
|
2266
|
+
test("the first read takes the server's cached answer", async () => {
|
|
2267
|
+
mockPolicy({
|
|
2268
|
+
users: [
|
|
2269
|
+
readinessUser({
|
|
2270
|
+
userId: USER_ALEX,
|
|
2271
|
+
userName: "Alex Chen",
|
|
2272
|
+
status: "NotReachable",
|
|
2273
|
+
}),
|
|
2274
|
+
],
|
|
2275
|
+
});
|
|
2276
|
+
|
|
2277
|
+
render(
|
|
2278
|
+
<EscalationRules onCallDutyPolicyId={POLICY_ID} projectId={PROJECT_ID} />,
|
|
2279
|
+
);
|
|
2280
|
+
|
|
2281
|
+
await waitFor(() => {
|
|
2282
|
+
expect(screen.getByTestId("rule-readiness-label")).toBeInTheDocument();
|
|
2283
|
+
});
|
|
2284
|
+
|
|
2285
|
+
expect(String((getMock as any).mock.calls[0][0].url)).not.toContain(
|
|
2286
|
+
"refresh=true",
|
|
2287
|
+
);
|
|
2288
|
+
});
|
|
2289
|
+
|
|
2290
|
+
/*
|
|
2291
|
+
* And pressing Refresh is. An admin who has just changed something and is
|
|
2292
|
+
* shown the answer from before the change concludes the fix did not work.
|
|
2293
|
+
*/
|
|
2294
|
+
test("refreshing asks the server to recompute rather than redraw", async () => {
|
|
2295
|
+
mockPolicy({
|
|
2296
|
+
users: [
|
|
2297
|
+
readinessUser({
|
|
2298
|
+
userId: USER_ALEX,
|
|
2299
|
+
userName: "Alex Chen",
|
|
2300
|
+
status: "NotReachable",
|
|
2301
|
+
}),
|
|
2302
|
+
],
|
|
2303
|
+
});
|
|
2304
|
+
|
|
2305
|
+
render(
|
|
2306
|
+
<EscalationRules onCallDutyPolicyId={POLICY_ID} projectId={PROJECT_ID} />,
|
|
2307
|
+
);
|
|
2308
|
+
|
|
2309
|
+
await waitFor(() => {
|
|
2310
|
+
expect(screen.getByTestId("rule-readiness-label")).toBeInTheDocument();
|
|
2311
|
+
});
|
|
2312
|
+
|
|
2313
|
+
fireEvent.click(screen.getByText("Refresh"));
|
|
2314
|
+
|
|
2315
|
+
await waitFor(() => {
|
|
2316
|
+
const refreshed: Array<any> = (getMock as any).mock.calls.filter(
|
|
2317
|
+
(call: Array<any>): boolean => {
|
|
2318
|
+
return String(call[0].url).includes("refresh=true");
|
|
2319
|
+
},
|
|
2320
|
+
);
|
|
2321
|
+
|
|
2322
|
+
expect(refreshed).toHaveLength(1);
|
|
2323
|
+
});
|
|
2324
|
+
});
|
|
2325
|
+
|
|
2326
|
+
test("a truncated readiness answer marks the people it did not cover", async () => {
|
|
2327
|
+
mockPolicy({
|
|
2328
|
+
isTruncated: true,
|
|
2329
|
+
users: [
|
|
2330
|
+
readinessUser({
|
|
2331
|
+
userId: USER_ALEX,
|
|
2332
|
+
userName: "Alex Chen",
|
|
2333
|
+
status: "Ready",
|
|
2334
|
+
}),
|
|
2335
|
+
],
|
|
2336
|
+
});
|
|
2337
|
+
|
|
2338
|
+
render(
|
|
2339
|
+
<EscalationRules onCallDutyPolicyId={POLICY_ID} projectId={PROJECT_ID} />,
|
|
2340
|
+
);
|
|
2341
|
+
|
|
2342
|
+
await waitFor(() => {
|
|
2343
|
+
expect(screen.getByTestId("rule-readiness-label")).toBeInTheDocument();
|
|
2344
|
+
});
|
|
2345
|
+
|
|
2346
|
+
const label: HTMLElement = screen.getByTestId("rule-readiness-label");
|
|
2347
|
+
|
|
2348
|
+
expect(label).toHaveAttribute("data-warning-level", "unknown");
|
|
2349
|
+
expect(label).toHaveTextContent("1 responder not checked");
|
|
2350
|
+
});
|
|
2351
|
+
|
|
2352
|
+
test("the label survives the detail being closed", async () => {
|
|
2353
|
+
mockPolicy({
|
|
2354
|
+
users: [
|
|
2355
|
+
readinessUser({
|
|
2356
|
+
userId: USER_ALEX,
|
|
2357
|
+
userName: "Alex Chen",
|
|
2358
|
+
status: "NotReachable",
|
|
2359
|
+
}),
|
|
2360
|
+
],
|
|
2361
|
+
});
|
|
2362
|
+
|
|
2363
|
+
render(
|
|
2364
|
+
<EscalationRules onCallDutyPolicyId={POLICY_ID} projectId={PROJECT_ID} />,
|
|
2365
|
+
);
|
|
2366
|
+
|
|
2367
|
+
await waitFor(() => {
|
|
2368
|
+
expect(screen.getByTestId("rule-readiness-label")).toBeInTheDocument();
|
|
2369
|
+
});
|
|
2370
|
+
|
|
2371
|
+
fireEvent.click(screen.getByTestId("rule-readiness-label"));
|
|
2372
|
+
|
|
2373
|
+
await waitFor(() => {
|
|
2374
|
+
expect(screen.getByTestId("rule-readiness-details")).toBeInTheDocument();
|
|
2375
|
+
});
|
|
2376
|
+
|
|
2377
|
+
fireEvent.click(screen.getByText("Close"));
|
|
2378
|
+
|
|
2379
|
+
await waitFor(() => {
|
|
2380
|
+
expect(
|
|
2381
|
+
screen.queryByTestId("rule-readiness-details"),
|
|
2382
|
+
).not.toBeInTheDocument();
|
|
2383
|
+
});
|
|
2384
|
+
|
|
2385
|
+
expect(screen.getByTestId("rule-readiness-label")).toBeInTheDocument();
|
|
2386
|
+
});
|
|
2387
|
+
});
|
|
2388
|
+
|
|
2389
|
+
/*
|
|
2390
|
+
* The one claim this surface must never make: an identifier that was masked on
|
|
2391
|
+
* the wire must not be reconstructed in the DOM.
|
|
2392
|
+
*/
|
|
2393
|
+
describe("no unmasked identifier reaches the DOM", () => {
|
|
2394
|
+
test("only the masked form of a method is rendered", () => {
|
|
2395
|
+
const issue: ResponderIssue = {
|
|
2396
|
+
userId: USER_ALEX,
|
|
2397
|
+
name: "Alex Chen",
|
|
2398
|
+
kind: "unreachable",
|
|
2399
|
+
readiness: summaryOf({
|
|
2400
|
+
users: [
|
|
2401
|
+
{
|
|
2402
|
+
...ALEX_UNREACHABLE,
|
|
2403
|
+
methods: [
|
|
2404
|
+
{
|
|
2405
|
+
methodType: "SMS",
|
|
2406
|
+
maskedIdentifier: "+1 ••• ••• 4821",
|
|
2407
|
+
isVerified: true,
|
|
2408
|
+
},
|
|
2409
|
+
],
|
|
2410
|
+
},
|
|
2411
|
+
],
|
|
2412
|
+
}).users[0]!,
|
|
2413
|
+
via: [],
|
|
2414
|
+
};
|
|
2415
|
+
|
|
2416
|
+
const { container } = render(
|
|
2417
|
+
<RuleReadinessDetails
|
|
2418
|
+
ruleName="First Responders"
|
|
2419
|
+
report={{
|
|
2420
|
+
unreachable: [issue],
|
|
2421
|
+
gaps: [],
|
|
2422
|
+
unchecked: [],
|
|
2423
|
+
unreadableGroups: [],
|
|
2424
|
+
isResolving: false,
|
|
2425
|
+
responderCount: 1,
|
|
2426
|
+
checkedCount: 1,
|
|
2427
|
+
}}
|
|
2428
|
+
delivery={FALLBACK_ON}
|
|
2429
|
+
reminders={NO_REMINDERS}
|
|
2430
|
+
isSendingReminders={false}
|
|
2431
|
+
onSendReminder={() => {}}
|
|
2432
|
+
onClose={() => {}}
|
|
2433
|
+
/>,
|
|
2434
|
+
);
|
|
2435
|
+
|
|
2436
|
+
expect(container.textContent).toContain("+1 ••• ••• 4821");
|
|
2437
|
+
/*
|
|
2438
|
+
* The masked identifier is the ONLY form of it on screen: the payload never
|
|
2439
|
+
* carries the full number, and nothing here reconstructs one.
|
|
2440
|
+
*/
|
|
2441
|
+
expect(container.textContent).not.toContain("+14821");
|
|
2442
|
+
expect(container.textContent).toContain("SMS +1 ••• ••• 4821");
|
|
2443
|
+
});
|
|
2444
|
+
});
|
|
2445
|
+
|
|
2446
|
+
/*
|
|
2447
|
+
* A report with nothing in it must resolve to "none" - the state that renders no
|
|
2448
|
+
* label at all. This is the default every healthy level on every policy sits in,
|
|
2449
|
+
* so it is the one branch whose regression would be silent.
|
|
2450
|
+
*/
|
|
2451
|
+
describe("the quiet default", () => {
|
|
2452
|
+
test("an empty report is silent", () => {
|
|
2453
|
+
const empty: RuleReadinessReport = {
|
|
2454
|
+
unreachable: [],
|
|
2455
|
+
gaps: [],
|
|
2456
|
+
unchecked: [],
|
|
2457
|
+
unreadableGroups: [],
|
|
2458
|
+
isResolving: false,
|
|
2459
|
+
responderCount: 0,
|
|
2460
|
+
checkedCount: 0,
|
|
2461
|
+
};
|
|
2462
|
+
|
|
2463
|
+
const level: RuleWarningLevel = getRuleWarningLevel(empty, FALLBACK_ON);
|
|
2464
|
+
const idle: SetupReminderStatus = { state: "idle", message: "" };
|
|
2465
|
+
|
|
2466
|
+
expect(level).toBe("none");
|
|
2467
|
+
expect(getRuleWarningLabel(empty, FALLBACK_OFF)).toBe("");
|
|
2468
|
+
expect(idle.state).toBe("idle");
|
|
2469
|
+
});
|
|
2470
|
+
});
|