@oneuptime/common 11.3.25 → 11.3.28
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/DatabaseModels/AIConversation.ts +322 -0
- package/Models/DatabaseModels/AIConversationMessage.ts +495 -0
- package/Models/DatabaseModels/AIRun.ts +584 -0
- package/Models/DatabaseModels/AIRunEvent.ts +443 -0
- package/Models/DatabaseModels/IncidentEpisodeRoleMember.ts +1 -1
- package/Models/DatabaseModels/IncidentMember.ts +1 -1
- package/Models/DatabaseModels/IncomingCallPolicy.ts +13 -0
- package/Models/DatabaseModels/Index.ts +11 -0
- package/Models/DatabaseModels/LlmLog.ts +26 -0
- package/Models/DatabaseModels/LlmProvider.ts +2 -2
- package/Models/DatabaseModels/MigrationFailure.ts +170 -0
- package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +51 -2
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +44 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +6 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.ts +6 -0
- package/Models/DatabaseModels/OnCallDutyPolicyTimeLog.ts +1 -1
- package/Models/DatabaseModels/OnCallDutyPolicyUserOverride.ts +2 -2
- package/Server/API/AIChatAPI.ts +693 -0
- package/Server/API/AlertAPI.ts +1 -1
- package/Server/API/IncidentAPI.ts +2 -2
- package/Server/API/IncidentEpisodeAPI.ts +1 -1
- package/Server/API/LlmProviderAPI.ts +170 -0
- package/Server/API/MicrosoftTeamsAPI.ts +5 -0
- package/Server/API/ScheduledMaintenanceAPI.ts +1 -1
- package/Server/API/SlackAPI.ts +663 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.ts +245 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.ts +36 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.ts +46 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.ts +119 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.ts +81 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.ts +35 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +16 -0
- package/Server/Infrastructure/PostgresDatabase.ts +11 -0
- package/Server/Services/AIConversationMessageService.ts +39 -0
- package/Server/Services/AIConversationService.ts +63 -0
- package/Server/Services/AIRunEventService.ts +39 -0
- package/Server/Services/AIRunService.ts +39 -0
- package/Server/Services/AIService.ts +127 -33
- package/Server/Services/AnalyticsDatabaseService.ts +10 -1
- package/Server/Services/DatabaseService.ts +29 -1
- package/Server/Services/IncidentService.ts +1 -1
- package/Server/Services/IncomingCallPolicyEscalationRuleService.ts +75 -2
- package/Server/Services/IncomingCallPolicyService.ts +42 -1
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/LlmProviderService.ts +110 -0
- package/Server/Services/MigrationFailureService.ts +9 -0
- package/Server/Services/MonitorProbeService.ts +33 -7
- package/Server/Services/MonitorService.ts +19 -7
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleService.ts +185 -41
- package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +23 -15
- package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +39 -0
- package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +148 -0
- package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +11 -2
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +502 -26
- package/Server/Services/OnCallDutyPolicyTimeLogService.ts +49 -7
- package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +218 -4
- package/Server/Services/ProjectCallSMSConfigService.ts +82 -1
- package/Server/Services/ProjectService.ts +24 -0
- package/Server/Services/TeamMemberService.ts +6 -0
- package/Server/Services/UserNotificationRuleService.ts +13 -42
- package/Server/Services/UserOnCallLogService.ts +90 -0
- package/Server/Services/UserOnCallLogTimelineService.ts +43 -1
- package/Server/Types/AnalyticsDatabase/ModelPermission.ts +74 -9
- package/Server/Types/Database/QueryUtil.ts +57 -0
- package/Server/Utils/AI/AIChatPrivacyFilter.ts +28 -0
- package/Server/Utils/AI/AlertAIContextBuilder.ts +6 -1
- package/Server/Utils/AI/Chat/ChatAgentRunner.ts +1066 -0
- package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +239 -0
- package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +51 -0
- package/Server/Utils/AI/IncidentAIContextBuilder.ts +18 -3
- package/Server/Utils/AI/IncidentEpisodeAIContextBuilder.ts +6 -1
- package/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.ts +12 -2
- package/Server/Utils/AI/Toolbox/AlertTools.ts +201 -0
- package/Server/Utils/AI/Toolbox/AlertWriteTools.ts +174 -0
- package/Server/Utils/AI/Toolbox/ContextTools.ts +189 -0
- package/Server/Utils/AI/Toolbox/ExceptionTools.ts +149 -0
- package/Server/Utils/AI/Toolbox/IncidentTools.ts +386 -0
- package/Server/Utils/AI/Toolbox/IncidentWriteTools.ts +350 -0
- package/Server/Utils/AI/Toolbox/Index.ts +231 -0
- package/Server/Utils/AI/Toolbox/LogTools.ts +339 -0
- package/Server/Utils/AI/Toolbox/MetricTools.ts +179 -0
- package/Server/Utils/AI/Toolbox/MonitorTools.ts +193 -0
- package/Server/Utils/AI/Toolbox/RecentChangesTools.ts +208 -0
- package/Server/Utils/AI/Toolbox/Serializer.ts +299 -0
- package/Server/Utils/AI/Toolbox/ToolTypes.ts +249 -0
- package/Server/Utils/AI/Toolbox/TraceTools.ts +402 -0
- package/Server/Utils/AI/Toolbox/WidgetBuilder.ts +189 -0
- package/Server/Utils/Database/MigrationFailureLog.ts +240 -0
- package/Server/Utils/IncomingCallPhoneNumber.ts +41 -0
- package/Server/Utils/LLM/LLMService.ts +605 -96
- package/Server/Utils/Telemetry/IoTSnapshotScan.ts +12 -9
- package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +12 -9
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +512 -19
- package/Server/Utils/Workspace/Slack/Slack.ts +80 -0
- package/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
- package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +0 -8
- package/Tests/Server/Services/OnCallDutyPolicyScheduleLayerUserReorder.test.ts +233 -0
- package/Tests/Server/Services/OnCallDutyPolicyTimeLogServiceScoping.test.ts +242 -0
- package/Tests/Server/Services/OnCallDutyPolicyUserOverrideEdit.test.ts +198 -0
- package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +495 -0
- package/Tests/Server/Types/Database/QueryUtil.test.ts +74 -0
- package/Tests/Server/Utils/AI/AIChatModelACL.test.ts +42 -0
- package/Tests/Server/Utils/AI/ChatAgentHelpers.test.ts +89 -0
- package/Tests/Server/Utils/AI/LLMServiceBaseUrl.test.ts +139 -0
- package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +386 -0
- package/Tests/Server/Utils/AI/ToolArgsGetTimeRange.test.ts +62 -0
- package/Tests/Server/Utils/AI/ToolArgsScopeServiceIds.test.ts +79 -0
- package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +223 -0
- package/Tests/Types/Billing/SubscriptionStatus.test.ts +127 -0
- package/Tests/Types/DateExhaustiveTimezone.test.ts +700 -0
- package/Tests/Types/DateTimezoneWallClock.test.ts +99 -0
- package/Tests/Types/Events/Recurring.test.ts +22 -9
- package/Tests/Types/Metrics/RecordingRuleDefinition.test.ts +213 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixes.test.ts +291 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixesRound2.test.ts +398 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeekendGap.test.ts +114 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeeklyDayShift.test.ts +76 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyMutation.test.ts +128 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyProbe.test.ts +203 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDiffFuzz.test.ts +322 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilEdgeCases.test.ts +280 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveCurrentUser.test.ts +863 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveDaily.test.ts +900 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveMultiLayer.test.ts +1104 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +957 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveWeekly.test.ts +1095 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilInvariants.test.ts +232 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +227 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMonthYearAudit.test.ts +255 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerAudit.test.ts +388 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerEdge.test.ts +300 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilOvernightDSTAudit.test.ts +158 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapFix.test.ts +253 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapRepro.test.ts +157 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilRotationFixes.test.ts +414 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilTimezone.test.ts +243 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilWeekendGapRepro.test.ts +112 -0
- package/Tests/Types/OnCallDutyPolicy/OverrideLensAudit.test.ts +117 -0
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimes.test.ts +8 -6
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimesDefaultDayAudit.test.ts +112 -0
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimesExhaustive.test.ts +821 -0
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimesMutationInvariant.test.ts +886 -0
- package/Tests/Types/OnCallDutyPolicy/UserOverrideUtil.test.ts +255 -0
- package/Tests/Types/OnCallDutyPolicy/UserOverrideUtilExhaustive.test.ts +1126 -0
- package/Types/AI/AIChatMessageRole.ts +6 -0
- package/Types/AI/AIChatMessageStatus.ts +32 -0
- package/Types/AI/AIChatPermissionMode.ts +69 -0
- package/Types/AI/AIChatTypes.ts +231 -0
- package/Types/AI/AIRunEventType.ts +17 -0
- package/Types/AI/AIRunStatus.ts +28 -0
- package/Types/AI/AIRunType.ts +6 -0
- package/Types/Date.ts +200 -15
- package/Types/LLM/LlmType.ts +6 -0
- package/Types/OnCallDutyPolicy/Layer.ts +790 -260
- package/Types/OnCallDutyPolicy/RestrictionTimes.ts +39 -13
- package/Types/OnCallDutyPolicy/UserOverrideUtil.ts +21 -5
- package/UI/Components/Events/RecurringFieldElement.tsx +16 -0
- package/UI/Components/Header/HeaderIconDropdownButton.tsx +22 -12
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +35 -3
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +8 -0
- package/UI/Components/Page/Page.tsx +14 -0
- package/UI/Components/RadioButtons/BasicRadioButtons.tsx +1 -1
- package/UI/Utils/LlmTypeDropdownOptions.ts +40 -0
- package/UI/Utils/TestLLMProvider.ts +59 -0
- package/build/dist/Models/DatabaseModels/AIConversation.js +345 -0
- package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AIConversationMessage.js +521 -0
- package/build/dist/Models/DatabaseModels/AIConversationMessage.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AIRun.js +619 -0
- package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AIRunEvent.js +469 -0
- package/build/dist/Models/DatabaseModels/AIRunEvent.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentMember.js +1 -1
- package/build/dist/Models/DatabaseModels/IncidentMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js +10 -0
- package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +10 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LlmLog.js +28 -0
- package/build/dist/Models/DatabaseModels/LlmLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LlmProvider.js +2 -2
- package/build/dist/Models/DatabaseModels/LlmProvider.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MigrationFailure.js +196 -0
- package/build/dist/Models/DatabaseModels/MigrationFailure.js.map +1 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +52 -2
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +45 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +6 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js +6 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js +2 -2
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +498 -0
- package/build/dist/Server/API/AIChatAPI.js.map +1 -0
- package/build/dist/Server/API/AlertAPI.js +1 -1
- package/build/dist/Server/API/IncidentAPI.js +2 -2
- package/build/dist/Server/API/IncidentEpisodeAPI.js +1 -1
- package/build/dist/Server/API/LlmProviderAPI.js +108 -1
- package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +4 -0
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/ScheduledMaintenanceAPI.js +1 -1
- package/build/dist/Server/API/SlackAPI.js +442 -0
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js +96 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js +25 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js +46 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js +38 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js +16 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +16 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/PostgresDatabase.js +9 -0
- package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
- package/build/dist/Server/Services/AIConversationMessageService.js +34 -0
- package/build/dist/Server/Services/AIConversationMessageService.js.map +1 -0
- package/build/dist/Server/Services/AIConversationService.js +42 -0
- package/build/dist/Server/Services/AIConversationService.js.map +1 -0
- package/build/dist/Server/Services/AIRunEventService.js +34 -0
- package/build/dist/Server/Services/AIRunEventService.js.map +1 -0
- package/build/dist/Server/Services/AIRunService.js +34 -0
- package/build/dist/Server/Services/AIRunService.js.map +1 -0
- package/build/dist/Server/Services/AIService.js +84 -25
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +10 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +20 -1
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +1 -1
- package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js +55 -2
- package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js.map +1 -1
- package/build/dist/Server/Services/IncomingCallPolicyService.js +40 -0
- package/build/dist/Server/Services/IncomingCallPolicyService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/LlmProviderService.js +108 -0
- package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
- package/build/dist/Server/Services/MigrationFailureService.js +9 -0
- package/build/dist/Server/Services/MigrationFailureService.js.map +1 -0
- package/build/dist/Server/Services/MonitorProbeService.js +33 -7
- package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +12 -5
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +81 -22
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js +134 -31
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +21 -13
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +36 -0
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +121 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +11 -2
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +409 -36
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js +52 -8
- package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +170 -3
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
- package/build/dist/Server/Services/ProjectCallSMSConfigService.js +72 -0
- package/build/dist/Server/Services/ProjectCallSMSConfigService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +25 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +6 -0
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +12 -30
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallLogService.js +75 -0
- package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallLogTimelineService.js +31 -1
- package/build/dist/Server/Services/UserOnCallLogTimelineService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +50 -7
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryUtil.js +45 -0
- package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
- package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js +18 -0
- package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js.map +1 -0
- package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js +6 -1
- package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +756 -0
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +165 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +44 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -0
- package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js +18 -3
- package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js.map +1 -1
- package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js +6 -1
- package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js.map +1 -1
- package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js +12 -2
- package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +167 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js +136 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js +141 -0
- package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js +117 -0
- package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +318 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js +280 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +153 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/LogTools.js +246 -0
- package/build/dist/Server/Utils/AI/Toolbox/LogTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +120 -0
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js +158 -0
- package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js +165 -0
- package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +228 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js +142 -0
- package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js +309 -0
- package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js +120 -0
- package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js.map +1 -0
- package/build/dist/Server/Utils/Database/MigrationFailureLog.js +174 -0
- package/build/dist/Server/Utils/Database/MigrationFailureLog.js.map +1 -0
- package/build/dist/Server/Utils/IncomingCallPhoneNumber.js +30 -0
- package/build/dist/Server/Utils/IncomingCallPhoneNumber.js.map +1 -0
- package/build/dist/Server/Utils/LLM/LLMService.js +452 -92
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +11 -8
- package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +11 -8
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +378 -15
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js +59 -0
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
- package/build/dist/Types/AI/AIChatMessageRole.js +7 -0
- package/build/dist/Types/AI/AIChatMessageRole.js.map +1 -0
- package/build/dist/Types/AI/AIChatMessageStatus.js +27 -0
- package/build/dist/Types/AI/AIChatMessageStatus.js.map +1 -0
- package/build/dist/Types/AI/AIChatPermissionMode.js +53 -0
- package/build/dist/Types/AI/AIChatPermissionMode.js.map +1 -0
- package/build/dist/Types/AI/AIChatTypes.js +74 -0
- package/build/dist/Types/AI/AIChatTypes.js.map +1 -0
- package/build/dist/Types/AI/AIRunEventType.js +18 -0
- package/build/dist/Types/AI/AIRunEventType.js.map +1 -0
- package/build/dist/Types/AI/AIRunStatus.js +26 -0
- package/build/dist/Types/AI/AIRunStatus.js.map +1 -0
- package/build/dist/Types/AI/AIRunType.js +7 -0
- package/build/dist/Types/AI/AIRunType.js.map +1 -0
- package/build/dist/Types/Date.js +153 -15
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/LLM/LlmType.js +6 -0
- package/build/dist/Types/LLM/LlmType.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +602 -171
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js +27 -13
- package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js +20 -5
- package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js.map +1 -1
- package/build/dist/UI/Components/Events/RecurringFieldElement.js +13 -0
- package/build/dist/UI/Components/Events/RecurringFieldElement.js.map +1 -1
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +10 -5
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +18 -4
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/UI/Components/Page/Page.js +3 -1
- package/build/dist/UI/Components/Page/Page.js.map +1 -1
- package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js +1 -1
- package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js.map +1 -1
- package/build/dist/UI/Utils/LlmTypeDropdownOptions.js +38 -0
- package/build/dist/UI/Utils/LlmTypeDropdownOptions.js.map +1 -0
- package/build/dist/UI/Utils/TestLLMProvider.js +37 -0
- package/build/dist/UI/Utils/TestLLMProvider.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import OnCallDutyPolicyUserOverrideService from "../../../Server/Services/OnCallDutyPolicyUserOverrideService";
|
|
2
|
+
import OnCallDutyPolicyScheduleService from "../../../Server/Services/OnCallDutyPolicyScheduleService";
|
|
3
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
4
|
+
import logger from "../../../Server/Utils/Logger";
|
|
5
|
+
import { describe, expect, test, afterEach } from "@jest/globals";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Audit finding M2: an override edit that changes overrideUserId from A to A2
|
|
9
|
+
* used to refresh only A2's schedules. Schedules that contained A (but NOT A2)
|
|
10
|
+
* therefore kept showing the substitute forever — until the next natural
|
|
11
|
+
* handoff — because refreshRostersForUserInProject selects schedules by the
|
|
12
|
+
* NEW user only.
|
|
13
|
+
*
|
|
14
|
+
* The fix captures each affected override's PRE-update {projectId,
|
|
15
|
+
* overrideUserId} in onBeforeUpdate (carryForward), and onUpdateSuccess then
|
|
16
|
+
* refreshes rosters for BOTH the OLD users (from carryForward) and the NEW
|
|
17
|
+
* users (looked up post-update by id), deduped by "projectId:overrideUserId".
|
|
18
|
+
*
|
|
19
|
+
* The private refreshRostersForOverrideUser lazy-requires
|
|
20
|
+
* OnCallDutyPolicyScheduleService and calls its
|
|
21
|
+
* refreshRostersForUserInProject({projectId, userId}). Because the test imports
|
|
22
|
+
* the very same singleton the lazy require resolves to, spying that public
|
|
23
|
+
* method observes exactly which {projectId, userId} pairs are refreshed.
|
|
24
|
+
*
|
|
25
|
+
* The hooks are protected, so they are invoked via (service as any).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// Fixed ids so assertions read clearly. ObjectID accepts arbitrary strings.
|
|
29
|
+
const P1: ObjectID = new ObjectID("p1");
|
|
30
|
+
const USER_A: ObjectID = new ObjectID("A");
|
|
31
|
+
const USER_A2: ObjectID = new ObjectID("A2");
|
|
32
|
+
const OV1: ObjectID = new ObjectID("ov1");
|
|
33
|
+
|
|
34
|
+
// Build a minimal root UpdateBy the hooks accept.
|
|
35
|
+
function fakeUpdateBy(): any {
|
|
36
|
+
return {
|
|
37
|
+
query: { _id: OV1 },
|
|
38
|
+
data: {},
|
|
39
|
+
props: { isRoot: true },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The set of userId strings refreshRostersForUserInProject was called with.
|
|
44
|
+
function refreshedUserIds(spy: jest.SpyInstance): Array<string> {
|
|
45
|
+
return spy.mock.calls.map((call: Array<unknown>): string => {
|
|
46
|
+
const arg: { projectId: ObjectID; userId: ObjectID } = call[0] as {
|
|
47
|
+
projectId: ObjectID;
|
|
48
|
+
userId: ObjectID;
|
|
49
|
+
};
|
|
50
|
+
return arg.userId.toString();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("OnCallDutyPolicyUserOverrideService update-roster refresh (audit M2)", () => {
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
jest.restoreAllMocks();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Keep best-effort logger.error noise out of the test output.
|
|
60
|
+
function silenceLoggerError(): void {
|
|
61
|
+
jest.spyOn(logger, "error").mockImplementation((): void => {
|
|
62
|
+
return undefined;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/*
|
|
67
|
+
* ----------------------------------------------------------------------- *
|
|
68
|
+
* (1) onBeforeUpdate captures the OLD override user into carryForward.
|
|
69
|
+
* ----------------------------------------------------------------------- *
|
|
70
|
+
*/
|
|
71
|
+
test("onBeforeUpdate captures the pre-update {projectId, overrideUserId} into carryForward", async () => {
|
|
72
|
+
silenceLoggerError();
|
|
73
|
+
|
|
74
|
+
// findBy returns the row(s) that match the update query, PRE-update.
|
|
75
|
+
const findBy: jest.SpyInstance = jest
|
|
76
|
+
.spyOn(OnCallDutyPolicyUserOverrideService, "findBy")
|
|
77
|
+
.mockResolvedValue([{ projectId: P1, overrideUserId: USER_A }] as any);
|
|
78
|
+
|
|
79
|
+
const onUpdate: any = await (
|
|
80
|
+
OnCallDutyPolicyUserOverrideService as any
|
|
81
|
+
).onBeforeUpdate(fakeUpdateBy());
|
|
82
|
+
|
|
83
|
+
expect(findBy).toHaveBeenCalledTimes(1);
|
|
84
|
+
|
|
85
|
+
const carryForward: Array<{
|
|
86
|
+
projectId: ObjectID;
|
|
87
|
+
overrideUserId: ObjectID;
|
|
88
|
+
}> = onUpdate.carryForward;
|
|
89
|
+
|
|
90
|
+
expect(Array.isArray(carryForward)).toBe(true);
|
|
91
|
+
expect(carryForward).toHaveLength(1);
|
|
92
|
+
expect(carryForward[0]!.projectId.toString()).toBe(P1.toString());
|
|
93
|
+
expect(carryForward[0]!.overrideUserId.toString()).toBe(USER_A.toString());
|
|
94
|
+
|
|
95
|
+
// The original updateBy is threaded through unchanged.
|
|
96
|
+
expect(onUpdate.updateBy.query["_id"].toString()).toBe(OV1.toString());
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/*
|
|
100
|
+
* ----------------------------------------------------------------------- *
|
|
101
|
+
* (2) onUpdateSuccess refreshes BOTH the old (A) and the new (A2) users.
|
|
102
|
+
* ----------------------------------------------------------------------- *
|
|
103
|
+
*/
|
|
104
|
+
test("onUpdateSuccess refreshes rosters for BOTH the old (A) and new (A2) override users", async () => {
|
|
105
|
+
silenceLoggerError();
|
|
106
|
+
|
|
107
|
+
// The post-update row for OV1 now points at the NEW user A2.
|
|
108
|
+
jest
|
|
109
|
+
.spyOn(OnCallDutyPolicyUserOverrideService, "findOneById")
|
|
110
|
+
.mockResolvedValue({ projectId: P1, overrideUserId: USER_A2 } as any);
|
|
111
|
+
|
|
112
|
+
const refresh: jest.SpyInstance = jest
|
|
113
|
+
.spyOn(OnCallDutyPolicyScheduleService, "refreshRostersForUserInProject")
|
|
114
|
+
.mockResolvedValue(undefined);
|
|
115
|
+
|
|
116
|
+
const onUpdate: any = {
|
|
117
|
+
updateBy: fakeUpdateBy(),
|
|
118
|
+
// OLD user captured by onBeforeUpdate.
|
|
119
|
+
carryForward: [{ projectId: P1, overrideUserId: USER_A }],
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
await (OnCallDutyPolicyUserOverrideService as any).onUpdateSuccess(
|
|
123
|
+
onUpdate,
|
|
124
|
+
[OV1],
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const userIds: Array<string> = refreshedUserIds(refresh);
|
|
128
|
+
|
|
129
|
+
// Both the vacated user A and the substitute A2 have their rosters recomputed.
|
|
130
|
+
expect(userIds).toContain(USER_A.toString());
|
|
131
|
+
expect(userIds).toContain(USER_A2.toString());
|
|
132
|
+
expect(refresh).toHaveBeenCalledTimes(2);
|
|
133
|
+
|
|
134
|
+
// Every refresh is scoped to the correct project.
|
|
135
|
+
for (const call of refresh.mock.calls) {
|
|
136
|
+
const arg: { projectId: ObjectID; userId: ObjectID } = call[0] as {
|
|
137
|
+
projectId: ObjectID;
|
|
138
|
+
userId: ObjectID;
|
|
139
|
+
};
|
|
140
|
+
expect(arg.projectId.toString()).toBe(P1.toString());
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
/*
|
|
145
|
+
* ----------------------------------------------------------------------- *
|
|
146
|
+
* (3) DEDUP: an unchanged override user (A -> A) is refreshed exactly once.
|
|
147
|
+
* ----------------------------------------------------------------------- *
|
|
148
|
+
*/
|
|
149
|
+
test("onUpdateSuccess dedupes: when old == new (A -> A) the roster refresh runs exactly ONCE", async () => {
|
|
150
|
+
silenceLoggerError();
|
|
151
|
+
|
|
152
|
+
// Post-update row still points at A (only e.g. the times changed).
|
|
153
|
+
jest
|
|
154
|
+
.spyOn(OnCallDutyPolicyUserOverrideService, "findOneById")
|
|
155
|
+
.mockResolvedValue({ projectId: P1, overrideUserId: USER_A } as any);
|
|
156
|
+
|
|
157
|
+
const refresh: jest.SpyInstance = jest
|
|
158
|
+
.spyOn(OnCallDutyPolicyScheduleService, "refreshRostersForUserInProject")
|
|
159
|
+
.mockResolvedValue(undefined);
|
|
160
|
+
|
|
161
|
+
const onUpdate: any = {
|
|
162
|
+
updateBy: fakeUpdateBy(),
|
|
163
|
+
carryForward: [{ projectId: P1, overrideUserId: USER_A }],
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
await (OnCallDutyPolicyUserOverrideService as any).onUpdateSuccess(
|
|
167
|
+
onUpdate,
|
|
168
|
+
[OV1],
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Same {p1, A} from both the old and new side: refreshed a single time.
|
|
172
|
+
expect(refresh).toHaveBeenCalledTimes(1);
|
|
173
|
+
expect(refreshedUserIds(refresh)).toEqual([USER_A.toString()]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/*
|
|
177
|
+
* ----------------------------------------------------------------------- *
|
|
178
|
+
* (4) onBeforeUpdate is best-effort: a findBy failure yields empty
|
|
179
|
+
* carryForward and does NOT reject the update path.
|
|
180
|
+
* ----------------------------------------------------------------------- *
|
|
181
|
+
*/
|
|
182
|
+
test("onBeforeUpdate is resilient: if findBy throws, it returns an empty carryForward and does not reject", async () => {
|
|
183
|
+
silenceLoggerError();
|
|
184
|
+
|
|
185
|
+
jest
|
|
186
|
+
.spyOn(OnCallDutyPolicyUserOverrideService, "findBy")
|
|
187
|
+
.mockRejectedValue(new Error("db unavailable"));
|
|
188
|
+
|
|
189
|
+
const onUpdate: any = await (
|
|
190
|
+
OnCallDutyPolicyUserOverrideService as any
|
|
191
|
+
).onBeforeUpdate(fakeUpdateBy());
|
|
192
|
+
|
|
193
|
+
expect(Array.isArray(onUpdate.carryForward)).toBe(true);
|
|
194
|
+
expect(onUpdate.carryForward).toHaveLength(0);
|
|
195
|
+
// The updateBy is still threaded through so the CRUD update proceeds.
|
|
196
|
+
expect(onUpdate.updateBy).toBeTruthy();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import UserOnCallLogService from "../../../Server/Services/UserOnCallLogService";
|
|
2
|
+
import Model from "../../../Models/DatabaseModels/UserOnCallLog";
|
|
3
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
4
|
+
import logger from "../../../Server/Utils/Logger";
|
|
5
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
6
|
+
import { describe, expect, test, afterEach, beforeEach } from "@jest/globals";
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* UserOnCallLogService.claimNotificationRuleExecution is the atomic guard added
|
|
10
|
+
* for audit finding F7. Overlapping ExecutePendingExecutions runs (a slow tick,
|
|
11
|
+
* a stalled-job re-delivery, or a burst release across worker replicas) could
|
|
12
|
+
* both observe executedNotificationRules WITHOUT a given delayed rule's key,
|
|
13
|
+
* both write it, and both page the responder for a single escalation.
|
|
14
|
+
*
|
|
15
|
+
* The fix collapses the read-modify-write into ONE conditional UPDATE that only
|
|
16
|
+
* sets executedNotificationRules[ruleId] when the key is absent and RETURNs the
|
|
17
|
+
* affected row, so exactly one concurrent run wins (RETURNING a row => claimed)
|
|
18
|
+
* and every loser skips (no row => already executed).
|
|
19
|
+
*
|
|
20
|
+
* These tests mock the TypeORM manager (no Postgres) and the two inherited
|
|
21
|
+
* persistence helpers (findOneById / updateOneById) to lock in:
|
|
22
|
+
* (a) atomic success (a row returned) => TRUE, and NO read-modify-write;
|
|
23
|
+
* (b) atomic no-op (no row returned) => FALSE, and NO read-modify-write;
|
|
24
|
+
* (c) the emitted SQL is a single UPDATE carrying the `NOT (... jsonb ? key)`
|
|
25
|
+
* existence guard and a RETURNING clause (so the claim is atomic);
|
|
26
|
+
* (d) FAIL-SAFE: if the atomic statement throws, it degrades to the previous
|
|
27
|
+
* non-atomic findOneById + updateOneById marking so a page is never
|
|
28
|
+
* dropped (a rare duplicate page beats a missed page for on-call).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
type QueryCall = [string, Array<unknown>];
|
|
32
|
+
|
|
33
|
+
// Stand-in for a persisted UserOnCallLog row returned by the fallback read.
|
|
34
|
+
function fakeLog(
|
|
35
|
+
id: ObjectID,
|
|
36
|
+
executedNotificationRules: JSONObject | undefined,
|
|
37
|
+
): Model {
|
|
38
|
+
return {
|
|
39
|
+
id,
|
|
40
|
+
executedNotificationRules,
|
|
41
|
+
} as unknown as Model;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Mock this.getRepository().manager.query with the supplied jest mock.
|
|
45
|
+
function mockManagerQuery(query: jest.Mock): void {
|
|
46
|
+
jest.spyOn(UserOnCallLogService, "getRepository").mockReturnValue({
|
|
47
|
+
manager: {
|
|
48
|
+
query,
|
|
49
|
+
},
|
|
50
|
+
} as any);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("UserOnCallLogService.claimNotificationRuleExecution", () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
// Keep the fallback's logger.error noise out of the test output.
|
|
56
|
+
jest.spyOn(logger, "error").mockImplementation((): void => {
|
|
57
|
+
return undefined;
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
jest.restoreAllMocks();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/*
|
|
66
|
+
* ----------------------------------------------------------------------- *
|
|
67
|
+
* (a) Atomic success: a row was returned => THIS call claimed the rule.
|
|
68
|
+
* -----------------------------------------------------------------------
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
test("returns TRUE when the atomic UPDATE returns a row (claim won)", async () => {
|
|
72
|
+
const query: jest.Mock = jest.fn().mockResolvedValue([{ _id: "x" }]);
|
|
73
|
+
mockManagerQuery(query);
|
|
74
|
+
|
|
75
|
+
const findOneById: jest.SpyInstance = jest.spyOn(
|
|
76
|
+
UserOnCallLogService,
|
|
77
|
+
"findOneById",
|
|
78
|
+
);
|
|
79
|
+
const updateOneById: jest.SpyInstance = jest.spyOn(
|
|
80
|
+
UserOnCallLogService,
|
|
81
|
+
"updateOneById",
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const claimed: boolean =
|
|
85
|
+
await UserOnCallLogService.claimNotificationRuleExecution({
|
|
86
|
+
userOnCallLogId: ObjectID.generate(),
|
|
87
|
+
userNotificationRuleId: ObjectID.generate(),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(claimed).toBe(true);
|
|
91
|
+
// Exactly one statement, and it truly was atomic: no read-modify-write.
|
|
92
|
+
expect(query).toHaveBeenCalledTimes(1);
|
|
93
|
+
expect(findOneById).not.toHaveBeenCalled();
|
|
94
|
+
expect(updateOneById).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("returns TRUE even when the driver returns multiple RETURNING rows", async () => {
|
|
98
|
+
const query: jest.Mock = jest
|
|
99
|
+
.fn()
|
|
100
|
+
.mockResolvedValue([{ _id: "a" }, { _id: "b" }]);
|
|
101
|
+
mockManagerQuery(query);
|
|
102
|
+
|
|
103
|
+
const claimed: boolean =
|
|
104
|
+
await UserOnCallLogService.claimNotificationRuleExecution({
|
|
105
|
+
userOnCallLogId: ObjectID.generate(),
|
|
106
|
+
userNotificationRuleId: ObjectID.generate(),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(claimed).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
* ----------------------------------------------------------------------- *
|
|
114
|
+
* (b) Atomic no-op: guard suppressed the write => already executed.
|
|
115
|
+
* -----------------------------------------------------------------------
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
test("returns FALSE when the atomic UPDATE returns no rows (already executed)", async () => {
|
|
119
|
+
const query: jest.Mock = jest.fn().mockResolvedValue([]);
|
|
120
|
+
mockManagerQuery(query);
|
|
121
|
+
|
|
122
|
+
const findOneById: jest.SpyInstance = jest.spyOn(
|
|
123
|
+
UserOnCallLogService,
|
|
124
|
+
"findOneById",
|
|
125
|
+
);
|
|
126
|
+
const updateOneById: jest.SpyInstance = jest.spyOn(
|
|
127
|
+
UserOnCallLogService,
|
|
128
|
+
"updateOneById",
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const claimed: boolean =
|
|
132
|
+
await UserOnCallLogService.claimNotificationRuleExecution({
|
|
133
|
+
userOnCallLogId: ObjectID.generate(),
|
|
134
|
+
userNotificationRuleId: ObjectID.generate(),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(claimed).toBe(false);
|
|
138
|
+
expect(query).toHaveBeenCalledTimes(1);
|
|
139
|
+
// A suppressed write is a decisive "someone else won" — never fall back.
|
|
140
|
+
expect(findOneById).not.toHaveBeenCalled();
|
|
141
|
+
expect(updateOneById).not.toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test.each<[string, unknown]>([
|
|
145
|
+
["undefined", undefined],
|
|
146
|
+
["null", null],
|
|
147
|
+
["a non-array object", {}],
|
|
148
|
+
])(
|
|
149
|
+
"returns FALSE (no claim) when the driver resolves %s instead of a rows array",
|
|
150
|
+
async (_label: string, resolved: unknown) => {
|
|
151
|
+
const query: jest.Mock = jest.fn().mockResolvedValue(resolved);
|
|
152
|
+
mockManagerQuery(query);
|
|
153
|
+
|
|
154
|
+
const updateOneById: jest.SpyInstance = jest.spyOn(
|
|
155
|
+
UserOnCallLogService,
|
|
156
|
+
"updateOneById",
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
/*
|
|
160
|
+
* NOTE: current behavior — the Array.isArray guard treats any
|
|
161
|
+
* non-array (or empty) result as "not claimed"; it resolves (does not
|
|
162
|
+
* reject), so the fail-safe fallback is NOT engaged here.
|
|
163
|
+
*/
|
|
164
|
+
const claimed: boolean =
|
|
165
|
+
await UserOnCallLogService.claimNotificationRuleExecution({
|
|
166
|
+
userOnCallLogId: ObjectID.generate(),
|
|
167
|
+
userNotificationRuleId: ObjectID.generate(),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(claimed).toBe(false);
|
|
171
|
+
expect(updateOneById).not.toHaveBeenCalled();
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
/*
|
|
176
|
+
* ----------------------------------------------------------------------- *
|
|
177
|
+
* (c) The emitted SQL is a single, atomic, guarded UPDATE.
|
|
178
|
+
* -----------------------------------------------------------------------
|
|
179
|
+
*/
|
|
180
|
+
|
|
181
|
+
test("emits a single conditional UPDATE with jsonb existence guard and RETURNING", async () => {
|
|
182
|
+
const query: jest.Mock = jest.fn().mockResolvedValue([{ _id: "x" }]);
|
|
183
|
+
mockManagerQuery(query);
|
|
184
|
+
|
|
185
|
+
const logId: ObjectID = ObjectID.generate();
|
|
186
|
+
const ruleId: ObjectID = ObjectID.generate();
|
|
187
|
+
|
|
188
|
+
await UserOnCallLogService.claimNotificationRuleExecution({
|
|
189
|
+
userOnCallLogId: logId,
|
|
190
|
+
userNotificationRuleId: ruleId,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(query).toHaveBeenCalledTimes(1);
|
|
194
|
+
const [sql, params] = query.mock.calls[0] as QueryCall;
|
|
195
|
+
|
|
196
|
+
// Exactly one statement, and it is an UPDATE against the right table.
|
|
197
|
+
expect(sql.trim().startsWith(`UPDATE "UserOnCallLog"`)).toBe(true);
|
|
198
|
+
expect(sql.match(/UPDATE/g)).toHaveLength(1);
|
|
199
|
+
// No separate read — the claim must not be a SELECT-then-UPDATE.
|
|
200
|
+
expect(sql).not.toContain("SELECT");
|
|
201
|
+
expect(sql).not.toContain("INSERT");
|
|
202
|
+
expect(sql).not.toContain("DELETE");
|
|
203
|
+
|
|
204
|
+
// The atomicity-critical guard: only write when the key is ABSENT.
|
|
205
|
+
expect(sql).toContain("NOT (");
|
|
206
|
+
// jsonb `?` existence operator against the rule key parameter.
|
|
207
|
+
expect(sql).toMatch(/jsonb\s*\?\s*\$2::text/);
|
|
208
|
+
// jsonb_set writes the key idempotently (create-if-missing).
|
|
209
|
+
expect(sql).toContain("jsonb_set");
|
|
210
|
+
// RETURNING lets the caller detect whether THIS statement claimed the row.
|
|
211
|
+
expect(sql).toContain("RETURNING");
|
|
212
|
+
expect(sql).toContain(`"_id"`);
|
|
213
|
+
|
|
214
|
+
// Bound parameters: [logId, ruleKey, nowIso]; no raw interpolation.
|
|
215
|
+
expect(params).toHaveLength(3);
|
|
216
|
+
expect(params[0]).toBe(logId.toString());
|
|
217
|
+
expect(params[1]).toBe(ruleId.toString());
|
|
218
|
+
expect(typeof params[2]).toBe("string");
|
|
219
|
+
// The third parameter is an ISO timestamp string.
|
|
220
|
+
expect(Number.isNaN(Date.parse(params[2] as string))).toBe(false);
|
|
221
|
+
expect(params[2]).toBe(new Date(params[2] as string).toISOString());
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("the guard parameter ($2) is reused for both jsonb_set path and existence check", async () => {
|
|
225
|
+
const query: jest.Mock = jest.fn().mockResolvedValue([{ _id: "x" }]);
|
|
226
|
+
mockManagerQuery(query);
|
|
227
|
+
|
|
228
|
+
await UserOnCallLogService.claimNotificationRuleExecution({
|
|
229
|
+
userOnCallLogId: ObjectID.generate(),
|
|
230
|
+
userNotificationRuleId: ObjectID.generate(),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const [sql] = query.mock.calls[0] as QueryCall;
|
|
234
|
+
/*
|
|
235
|
+
* The same $2 key drives the write path (ARRAY[$2::text]) and the guard,
|
|
236
|
+
* so what we test for absence is exactly what we write.
|
|
237
|
+
*/
|
|
238
|
+
expect(sql).toContain("ARRAY[$2::text]");
|
|
239
|
+
expect(sql).toContain("$2::text)");
|
|
240
|
+
// The log id is matched by uuid-cast primary key.
|
|
241
|
+
expect(sql).toContain(`"_id" = $1::uuid`);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
/*
|
|
245
|
+
* ----------------------------------------------------------------------- *
|
|
246
|
+
* (d) FAIL-SAFE fallback when the atomic statement throws.
|
|
247
|
+
* -----------------------------------------------------------------------
|
|
248
|
+
*/
|
|
249
|
+
|
|
250
|
+
test("falls back to findOneById + updateOneById and returns TRUE when rule not yet marked (empty object)", async () => {
|
|
251
|
+
const query: jest.Mock = jest
|
|
252
|
+
.fn()
|
|
253
|
+
.mockRejectedValue(new Error("jsonb operator unavailable"));
|
|
254
|
+
mockManagerQuery(query);
|
|
255
|
+
|
|
256
|
+
const logId: ObjectID = ObjectID.generate();
|
|
257
|
+
const ruleId: ObjectID = ObjectID.generate();
|
|
258
|
+
|
|
259
|
+
const findOneById: jest.SpyInstance = jest
|
|
260
|
+
.spyOn(UserOnCallLogService, "findOneById")
|
|
261
|
+
.mockResolvedValue(fakeLog(logId, {}));
|
|
262
|
+
const updateOneById: jest.SpyInstance = jest
|
|
263
|
+
.spyOn(UserOnCallLogService, "updateOneById")
|
|
264
|
+
.mockImplementation((): Promise<void> => {
|
|
265
|
+
return Promise.resolve();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const claimed: boolean =
|
|
269
|
+
await UserOnCallLogService.claimNotificationRuleExecution({
|
|
270
|
+
userOnCallLogId: logId,
|
|
271
|
+
userNotificationRuleId: ruleId,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(claimed).toBe(true);
|
|
275
|
+
expect(query).toHaveBeenCalledTimes(1);
|
|
276
|
+
expect(findOneById).toHaveBeenCalledTimes(1);
|
|
277
|
+
|
|
278
|
+
// Fallback reads the right row and selects the field it needs to inspect.
|
|
279
|
+
const findArg: {
|
|
280
|
+
id: ObjectID;
|
|
281
|
+
select: JSONObject;
|
|
282
|
+
props: { isRoot: boolean };
|
|
283
|
+
} = findOneById.mock.calls[0][0] as {
|
|
284
|
+
id: ObjectID;
|
|
285
|
+
select: JSONObject;
|
|
286
|
+
props: { isRoot: boolean };
|
|
287
|
+
};
|
|
288
|
+
expect(findArg.id.toString()).toBe(logId.toString());
|
|
289
|
+
expect(findArg.select["executedNotificationRules"]).toBe(true);
|
|
290
|
+
expect(findArg.props.isRoot).toBe(true);
|
|
291
|
+
|
|
292
|
+
// And it marks exactly this rule as executed.
|
|
293
|
+
expect(updateOneById).toHaveBeenCalledTimes(1);
|
|
294
|
+
const updateArg: {
|
|
295
|
+
id: ObjectID;
|
|
296
|
+
data: { executedNotificationRules: JSONObject };
|
|
297
|
+
props: { isRoot: boolean };
|
|
298
|
+
} = updateOneById.mock.calls[0][0] as {
|
|
299
|
+
id: ObjectID;
|
|
300
|
+
data: { executedNotificationRules: JSONObject };
|
|
301
|
+
props: { isRoot: boolean };
|
|
302
|
+
};
|
|
303
|
+
expect(updateArg.id.toString()).toBe(logId.toString());
|
|
304
|
+
expect(Object.keys(updateArg.data.executedNotificationRules)).toContain(
|
|
305
|
+
ruleId.toString(),
|
|
306
|
+
);
|
|
307
|
+
expect(updateArg.props.isRoot).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("fallback also claims (TRUE) when executedNotificationRules is undefined", async () => {
|
|
311
|
+
const query: jest.Mock = jest.fn().mockRejectedValue(new Error("boom"));
|
|
312
|
+
mockManagerQuery(query);
|
|
313
|
+
|
|
314
|
+
const logId: ObjectID = ObjectID.generate();
|
|
315
|
+
const ruleId: ObjectID = ObjectID.generate();
|
|
316
|
+
|
|
317
|
+
jest
|
|
318
|
+
.spyOn(UserOnCallLogService, "findOneById")
|
|
319
|
+
.mockResolvedValue(fakeLog(logId, undefined));
|
|
320
|
+
const updateOneById: jest.SpyInstance = jest
|
|
321
|
+
.spyOn(UserOnCallLogService, "updateOneById")
|
|
322
|
+
.mockImplementation((): Promise<void> => {
|
|
323
|
+
return Promise.resolve();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const claimed: boolean =
|
|
327
|
+
await UserOnCallLogService.claimNotificationRuleExecution({
|
|
328
|
+
userOnCallLogId: logId,
|
|
329
|
+
userNotificationRuleId: ruleId,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
expect(claimed).toBe(true);
|
|
333
|
+
expect(updateOneById).toHaveBeenCalledTimes(1);
|
|
334
|
+
const updateArg: { data: { executedNotificationRules: JSONObject } } =
|
|
335
|
+
updateOneById.mock.calls[0][0] as {
|
|
336
|
+
data: { executedNotificationRules: JSONObject };
|
|
337
|
+
};
|
|
338
|
+
expect(Object.keys(updateArg.data.executedNotificationRules)).toEqual([
|
|
339
|
+
ruleId.toString(),
|
|
340
|
+
]);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("fallback preserves other already-executed rule keys while adding the new one", async () => {
|
|
344
|
+
const query: jest.Mock = jest.fn().mockRejectedValue(new Error("boom"));
|
|
345
|
+
mockManagerQuery(query);
|
|
346
|
+
|
|
347
|
+
const logId: ObjectID = ObjectID.generate();
|
|
348
|
+
const ruleId: ObjectID = ObjectID.generate();
|
|
349
|
+
const otherRuleKey: string = ObjectID.generate().toString();
|
|
350
|
+
|
|
351
|
+
jest.spyOn(UserOnCallLogService, "findOneById").mockResolvedValue(
|
|
352
|
+
fakeLog(logId, {
|
|
353
|
+
[otherRuleKey]: new Date().toISOString(),
|
|
354
|
+
}),
|
|
355
|
+
);
|
|
356
|
+
const updateOneById: jest.SpyInstance = jest
|
|
357
|
+
.spyOn(UserOnCallLogService, "updateOneById")
|
|
358
|
+
.mockImplementation((): Promise<void> => {
|
|
359
|
+
return Promise.resolve();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const claimed: boolean =
|
|
363
|
+
await UserOnCallLogService.claimNotificationRuleExecution({
|
|
364
|
+
userOnCallLogId: logId,
|
|
365
|
+
userNotificationRuleId: ruleId,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
expect(claimed).toBe(true);
|
|
369
|
+
const updateArg: { data: { executedNotificationRules: JSONObject } } =
|
|
370
|
+
updateOneById.mock.calls[0][0] as {
|
|
371
|
+
data: { executedNotificationRules: JSONObject };
|
|
372
|
+
};
|
|
373
|
+
const keys: Array<string> = Object.keys(
|
|
374
|
+
updateArg.data.executedNotificationRules,
|
|
375
|
+
);
|
|
376
|
+
expect(keys).toContain(otherRuleKey);
|
|
377
|
+
expect(keys).toContain(ruleId.toString());
|
|
378
|
+
expect(keys).toHaveLength(2);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("fallback returns FALSE without updating when the rule is ALREADY in executedNotificationRules", async () => {
|
|
382
|
+
const query: jest.Mock = jest.fn().mockRejectedValue(new Error("boom"));
|
|
383
|
+
mockManagerQuery(query);
|
|
384
|
+
|
|
385
|
+
const logId: ObjectID = ObjectID.generate();
|
|
386
|
+
const ruleId: ObjectID = ObjectID.generate();
|
|
387
|
+
|
|
388
|
+
const findOneById: jest.SpyInstance = jest
|
|
389
|
+
.spyOn(UserOnCallLogService, "findOneById")
|
|
390
|
+
.mockResolvedValue(
|
|
391
|
+
fakeLog(logId, {
|
|
392
|
+
[ruleId.toString()]: new Date().toISOString(),
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
395
|
+
const updateOneById: jest.SpyInstance = jest
|
|
396
|
+
.spyOn(UserOnCallLogService, "updateOneById")
|
|
397
|
+
.mockImplementation((): Promise<void> => {
|
|
398
|
+
return Promise.resolve();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const claimed: boolean =
|
|
402
|
+
await UserOnCallLogService.claimNotificationRuleExecution({
|
|
403
|
+
userOnCallLogId: logId,
|
|
404
|
+
userNotificationRuleId: ruleId,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
expect(claimed).toBe(false);
|
|
408
|
+
expect(findOneById).toHaveBeenCalledTimes(1);
|
|
409
|
+
// Already marked => no duplicate write, no duplicate page.
|
|
410
|
+
expect(updateOneById).not.toHaveBeenCalled();
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("fallback returns FALSE without updating when the log row is missing (findOneById => null)", async () => {
|
|
414
|
+
const query: jest.Mock = jest.fn().mockRejectedValue(new Error("boom"));
|
|
415
|
+
mockManagerQuery(query);
|
|
416
|
+
|
|
417
|
+
const findOneById: jest.SpyInstance = jest
|
|
418
|
+
.spyOn(UserOnCallLogService, "findOneById")
|
|
419
|
+
.mockResolvedValue(null);
|
|
420
|
+
const updateOneById: jest.SpyInstance = jest
|
|
421
|
+
.spyOn(UserOnCallLogService, "updateOneById")
|
|
422
|
+
.mockImplementation((): Promise<void> => {
|
|
423
|
+
return Promise.resolve();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const claimed: boolean =
|
|
427
|
+
await UserOnCallLogService.claimNotificationRuleExecution({
|
|
428
|
+
userOnCallLogId: ObjectID.generate(),
|
|
429
|
+
userNotificationRuleId: ObjectID.generate(),
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
expect(claimed).toBe(false);
|
|
433
|
+
expect(findOneById).toHaveBeenCalledTimes(1);
|
|
434
|
+
expect(updateOneById).not.toHaveBeenCalled();
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("fallback engages exactly once (single atomic attempt) and logs the failure", async () => {
|
|
438
|
+
const query: jest.Mock = jest.fn().mockRejectedValue(new Error("boom"));
|
|
439
|
+
mockManagerQuery(query);
|
|
440
|
+
|
|
441
|
+
const loggerError: jest.SpyInstance = jest.spyOn(logger, "error");
|
|
442
|
+
|
|
443
|
+
jest
|
|
444
|
+
.spyOn(UserOnCallLogService, "findOneById")
|
|
445
|
+
.mockResolvedValue(fakeLog(ObjectID.generate(), {}));
|
|
446
|
+
jest
|
|
447
|
+
.spyOn(UserOnCallLogService, "updateOneById")
|
|
448
|
+
.mockImplementation((): Promise<void> => {
|
|
449
|
+
return Promise.resolve();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
await UserOnCallLogService.claimNotificationRuleExecution({
|
|
453
|
+
userOnCallLogId: ObjectID.generate(),
|
|
454
|
+
userNotificationRuleId: ObjectID.generate(),
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// The atomic path is attempted once, then the fallback takes over.
|
|
458
|
+
expect(query).toHaveBeenCalledTimes(1);
|
|
459
|
+
expect(loggerError).toHaveBeenCalled();
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("distinct rule ids on the same log are claimed independently in the atomic path", async () => {
|
|
463
|
+
/*
|
|
464
|
+
* Model two concurrent-but-different escalation rules: both should claim
|
|
465
|
+
* because the guard is per-key, not per-log.
|
|
466
|
+
*/
|
|
467
|
+
const logId: ObjectID = ObjectID.generate();
|
|
468
|
+
const ruleA: ObjectID = ObjectID.generate();
|
|
469
|
+
const ruleB: ObjectID = ObjectID.generate();
|
|
470
|
+
|
|
471
|
+
const query: jest.Mock = jest.fn().mockResolvedValue([{ _id: "x" }]);
|
|
472
|
+
mockManagerQuery(query);
|
|
473
|
+
|
|
474
|
+
const claimedA: boolean =
|
|
475
|
+
await UserOnCallLogService.claimNotificationRuleExecution({
|
|
476
|
+
userOnCallLogId: logId,
|
|
477
|
+
userNotificationRuleId: ruleA,
|
|
478
|
+
});
|
|
479
|
+
const claimedB: boolean =
|
|
480
|
+
await UserOnCallLogService.claimNotificationRuleExecution({
|
|
481
|
+
userOnCallLogId: logId,
|
|
482
|
+
userNotificationRuleId: ruleB,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
expect(claimedA).toBe(true);
|
|
486
|
+
expect(claimedB).toBe(true);
|
|
487
|
+
expect(query).toHaveBeenCalledTimes(2);
|
|
488
|
+
// Each call bound its own rule key.
|
|
489
|
+
expect((query.mock.calls[0] as QueryCall)[1][1]).toBe(ruleA.toString());
|
|
490
|
+
expect((query.mock.calls[1] as QueryCall)[1][1]).toBe(ruleB.toString());
|
|
491
|
+
// Same log id in both.
|
|
492
|
+
expect((query.mock.calls[0] as QueryCall)[1][0]).toBe(logId.toString());
|
|
493
|
+
expect((query.mock.calls[1] as QueryCall)[1][0]).toBe(logId.toString());
|
|
494
|
+
});
|
|
495
|
+
});
|