@oneuptime/common 11.3.26 → 11.4.0
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/AIRun.ts +50 -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 +3 -0
- 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/Models/DatabaseModels/Project.ts +60 -0
- package/Server/API/AIInvestigationAPI.ts +148 -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 +1 -0
- package/Server/API/ScheduledMaintenanceAPI.ts +1 -1
- 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/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -0
- package/Server/Infrastructure/PostgresDatabase.ts +11 -0
- package/Server/Services/AIService.ts +74 -2
- package/Server/Services/AlertService.ts +25 -0
- package/Server/Services/AnalyticsDatabaseService.ts +10 -1
- package/Server/Services/DatabaseService.ts +10 -1
- package/Server/Services/IncidentService.ts +26 -1
- package/Server/Services/IncidentStateTimelineService.ts +19 -0
- package/Server/Services/IncomingCallPolicyEscalationRuleService.ts +75 -2
- package/Server/Services/IncomingCallPolicyService.ts +42 -1
- package/Server/Services/Index.ts +2 -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/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/Database/QueryUtil.ts +57 -0
- package/Server/Utils/AI/AlertAIContextBuilder.ts +6 -1
- package/Server/Utils/AI/Chat/ChatAgentRunner.ts +14 -2
- package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +119 -11
- 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/Sentinel/AlertInvestigationRunner.ts +141 -0
- package/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.ts +178 -0
- package/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.ts +105 -0
- package/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.ts +405 -0
- package/Server/Utils/AI/Sentinel/SentinelMemory.ts +167 -0
- package/Server/Utils/AI/Toolbox/IncidentTools.ts +183 -1
- package/Server/Utils/AI/Toolbox/Index.ts +15 -1
- package/Server/Utils/AI/Toolbox/RecentChangesTools.ts +208 -0
- package/Server/Utils/AI/Toolbox/SentinelActionTools.ts +577 -0
- package/Server/Utils/AI/Toolbox/Serializer.ts +42 -0
- package/Server/Utils/Database/MigrationFailureLog.ts +240 -0
- package/Server/Utils/IncomingCallPhoneNumber.ts +41 -0
- package/Server/Utils/LLM/LLMService.ts +110 -11
- package/Server/Utils/Telemetry/IoTSnapshotScan.ts +12 -9
- package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +12 -9
- package/Tests/Server/API/BaseAPI.test.ts +1 -0
- 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/LLMServiceToolCalling.test.ts +78 -0
- package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +68 -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/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/ScheduleShiftUtil.test.ts +260 -0
- package/Tests/Types/OnCallDutyPolicy/UserOverrideUtil.test.ts +255 -0
- package/Tests/Types/OnCallDutyPolicy/UserOverrideUtilExhaustive.test.ts +1126 -0
- package/Types/Date.ts +246 -15
- package/Types/OnCallDutyPolicy/Layer.ts +790 -260
- package/Types/OnCallDutyPolicy/RestrictionTimes.ts +39 -13
- package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +257 -0
- package/Types/OnCallDutyPolicy/UserOverrideUtil.ts +21 -5
- package/UI/Components/Calendar/Calendar.tsx +8 -2
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +5 -5
- package/UI/Components/Events/RecurringFieldElement.tsx +16 -0
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +8 -0
- package/UI/Components/RadioButtons/BasicRadioButtons.tsx +1 -1
- package/build/dist/Models/DatabaseModels/AIRun.js +52 -0
- package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
- 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 +2 -0
- package/build/dist/Models/DatabaseModels/Index.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/Models/DatabaseModels/Project.js +62 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +107 -0
- package/build/dist/Server/API/AIInvestigationAPI.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 +1 -0
- package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
- package/build/dist/Server/API/ScheduledMaintenanceAPI.js +1 -1
- 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/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -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/AIService.js +53 -2
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +24 -0
- package/build/dist/Server/Services/AlertService.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 +10 -1
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +25 -1
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +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/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/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/Database/QueryUtil.js +45 -0
- package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
- 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 +13 -2
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +59 -11
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
- 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/Sentinel/AlertInvestigationRunner.js +119 -0
- package/build/dist/Server/Utils/AI/Sentinel/AlertInvestigationRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.js +147 -0
- package/build/dist/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.js +93 -0
- package/build/dist/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.js +311 -0
- package/build/dist/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.js.map +1 -0
- package/build/dist/Server/Utils/AI/Sentinel/SentinelMemory.js +143 -0
- package/build/dist/Server/Utils/AI/Sentinel/SentinelMemory.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +152 -1
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +10 -1
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
- 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/SentinelActionTools.js +451 -0
- package/build/dist/Server/Utils/AI/Toolbox/SentinelActionTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +40 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -1
- 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 +81 -5
- 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/Types/Date.js +185 -15
- package/build/dist/Types/Date.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/ScheduleShiftUtil.js +159 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js +20 -5
- package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js.map +1 -1
- package/build/dist/UI/Components/Calendar/Calendar.js +2 -2
- package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.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/ModelFormModal/ModelFormModal.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/package.json +1 -1
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import QueryUtil from "../../../../Server/Types/Database/QueryUtil";
|
|
2
|
+
import TeamMember from "../../../../Models/DatabaseModels/TeamMember";
|
|
3
|
+
import Search from "../../../../Types/BaseDatabase/Search";
|
|
4
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
5
|
+
import { FindOperator } from "typeorm";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* serializeQuery historically only visited top-level query keys. A search
|
|
9
|
+
* operator nested under a single-entity relation (e.g. filtering TeamMember by
|
|
10
|
+
* `user.name`) therefore reached TypeORM as a raw Search instance, which
|
|
11
|
+
* TypeORM silently degrades to an exact equality (`user.name = 'foo'`) instead
|
|
12
|
+
* of an ILIKE. That is what made the Owner / user picker "search" appear to do
|
|
13
|
+
* nothing once a project had more than a page of members. These tests lock in
|
|
14
|
+
* the recursion that converts nested relation operators into real SQL.
|
|
15
|
+
*/
|
|
16
|
+
describe("QueryUtil.serializeQuery — nested relation operators", () => {
|
|
17
|
+
it("converts a Search nested under a single-entity relation into a raw ILIKE", () => {
|
|
18
|
+
const query: Record<string, unknown> = {
|
|
19
|
+
projectId: ObjectID.generate(),
|
|
20
|
+
user: { name: new Search("alice") },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const result: Record<string, any> = QueryUtil.serializeQuery(
|
|
24
|
+
TeamMember,
|
|
25
|
+
query as any,
|
|
26
|
+
) as unknown as Record<string, any>;
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* The nested Search must become a TypeORM Raw ILIKE, never survive as a
|
|
30
|
+
* Search instance.
|
|
31
|
+
*/
|
|
32
|
+
expect(result["user"].name).toBeInstanceOf(FindOperator);
|
|
33
|
+
expect(result["user"].name).not.toBeInstanceOf(Search);
|
|
34
|
+
expect(result["user"].name.type).toBe("raw");
|
|
35
|
+
// QueryHelper.search wraps the (lower-cased) term as an ILIKE parameter.
|
|
36
|
+
expect(Object.values(result["user"].name.objectLiteralParameters)).toEqual([
|
|
37
|
+
"%alice%",
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("also converts a Search on a relation's email column", () => {
|
|
42
|
+
const query: Record<string, unknown> = {
|
|
43
|
+
projectId: ObjectID.generate(),
|
|
44
|
+
user: { email: new Search("alice@example.com") },
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const result: Record<string, any> = QueryUtil.serializeQuery(
|
|
48
|
+
TeamMember,
|
|
49
|
+
query as any,
|
|
50
|
+
) as unknown as Record<string, any>;
|
|
51
|
+
|
|
52
|
+
expect(result["user"].email).toBeInstanceOf(FindOperator);
|
|
53
|
+
expect(result["user"].email.type).toBe("raw");
|
|
54
|
+
expect(Object.values(result["user"].email.objectLiteralParameters)).toEqual(
|
|
55
|
+
["%alice@example.com%"],
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("leaves a plain id equality nested under a relation untouched", () => {
|
|
60
|
+
const userId: ObjectID = ObjectID.generate();
|
|
61
|
+
const query: Record<string, unknown> = {
|
|
62
|
+
projectId: ObjectID.generate(),
|
|
63
|
+
user: { _id: userId.toString() },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const result: Record<string, any> = QueryUtil.serializeQuery(
|
|
67
|
+
TeamMember,
|
|
68
|
+
query as any,
|
|
69
|
+
) as unknown as Record<string, any>;
|
|
70
|
+
|
|
71
|
+
// Plain id equality must NOT be rewritten into an ILIKE — it stays a value.
|
|
72
|
+
expect(result["user"]._id).toBe(userId.toString());
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -306,3 +306,81 @@ describe("LLMService — OpenAI-compatible (generic, e.g. vLLM)", () => {
|
|
|
306
306
|
).rejects.toThrow("Model Name is required");
|
|
307
307
|
});
|
|
308
308
|
});
|
|
309
|
+
|
|
310
|
+
describe("LLMService — prompt caching", () => {
|
|
311
|
+
test("Anthropic marks system and the last tool with an ephemeral cache breakpoint", async () => {
|
|
312
|
+
const spy: PostSpy = mockPostResponse({
|
|
313
|
+
content: [{ type: "text", text: "hi" }],
|
|
314
|
+
stop_reason: "end_turn",
|
|
315
|
+
usage: {
|
|
316
|
+
input_tokens: 12,
|
|
317
|
+
output_tokens: 4,
|
|
318
|
+
cache_read_input_tokens: 100,
|
|
319
|
+
cache_creation_input_tokens: 8,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const response: LLMCompletionResponse = await LLMService.getCompletion({
|
|
324
|
+
llmProviderConfig: { llmType: LlmType.Anthropic, apiKey: "test-key" },
|
|
325
|
+
messages: [
|
|
326
|
+
{ role: "system", content: "be helpful" },
|
|
327
|
+
{ role: "user", content: "hi" },
|
|
328
|
+
],
|
|
329
|
+
tools: [
|
|
330
|
+
{
|
|
331
|
+
name: "a",
|
|
332
|
+
description: "a",
|
|
333
|
+
inputSchema: { type: "object", properties: {} },
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: "b",
|
|
337
|
+
description: "b",
|
|
338
|
+
inputSchema: { type: "object", properties: {} },
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const requestBody: JSONObject = (
|
|
344
|
+
spy.mock.calls[0]![0] as { data: JSONObject }
|
|
345
|
+
).data;
|
|
346
|
+
|
|
347
|
+
// The system prompt is sent as a content-block array with a cache breakpoint.
|
|
348
|
+
const systemBlocks: Array<JSONObject> = requestBody[
|
|
349
|
+
"system"
|
|
350
|
+
] as Array<JSONObject>;
|
|
351
|
+
expect(Array.isArray(systemBlocks)).toBe(true);
|
|
352
|
+
expect(systemBlocks[0]!["cache_control"]).toEqual({ type: "ephemeral" });
|
|
353
|
+
|
|
354
|
+
// Only the last tool carries the breakpoint (it caches the whole block).
|
|
355
|
+
const tools: Array<JSONObject> = requestBody["tools"] as Array<JSONObject>;
|
|
356
|
+
expect(tools[tools.length - 1]!["cache_control"]).toEqual({
|
|
357
|
+
type: "ephemeral",
|
|
358
|
+
});
|
|
359
|
+
expect(tools[0]!["cache_control"]).toBeUndefined();
|
|
360
|
+
|
|
361
|
+
// totalTokens folds in cached + cache-creation input tokens (12+100+8+4).
|
|
362
|
+
expect(response.usage!.totalTokens).toBe(124);
|
|
363
|
+
expect(response.usage!.cachedInputTokens).toBe(100);
|
|
364
|
+
expect(response.usage!.cacheCreationTokens).toBe(8);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("OpenAI surfaces cached prompt tokens from prompt_tokens_details", async () => {
|
|
368
|
+
mockPostResponse({
|
|
369
|
+
choices: [{ message: { content: "ok" } }],
|
|
370
|
+
usage: {
|
|
371
|
+
prompt_tokens: 50,
|
|
372
|
+
completion_tokens: 5,
|
|
373
|
+
total_tokens: 55,
|
|
374
|
+
prompt_tokens_details: { cached_tokens: 40 },
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const response: LLMCompletionResponse = await LLMService.getCompletion({
|
|
379
|
+
llmProviderConfig: { llmType: LlmType.OpenAI, apiKey: "test-key" },
|
|
380
|
+
messages: [{ role: "user", content: "hi" }],
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
expect(response.usage!.cachedInputTokens).toBe(40);
|
|
384
|
+
expect(response.usage!.totalTokens).toBe(55);
|
|
385
|
+
});
|
|
386
|
+
});
|