@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,233 @@
|
|
|
1
|
+
import OnCallDutyPolicyScheduleLayerUserService from "../../../Server/Services/OnCallDutyPolicyScheduleLayerUserService";
|
|
2
|
+
import Model from "../../../Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser";
|
|
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
|
+
* OnCallDutyPolicyScheduleLayerUserService.onBeforeUpdate re-sequences the
|
|
9
|
+
* `order` of the other rows in a schedule layer when a user is dragged to a new
|
|
10
|
+
* position. Audit finding L3: the "moving down" branch (newOrder > currentOrder)
|
|
11
|
+
* decremented EVERY row with `order <= newOrder`, including rows ABOVE the moved
|
|
12
|
+
* user. Dragging a user downward therefore drove the top row's order to 0 (and
|
|
13
|
+
* negative after repeated down-drags) and opened a gap at 1, breaking the
|
|
14
|
+
* 1-based contiguous invariant that create-default (count+1) and delete
|
|
15
|
+
* re-sequencing depend on.
|
|
16
|
+
*
|
|
17
|
+
* The fix adds the missing lower bound `resource.order > currentOrder`, so only
|
|
18
|
+
* rows strictly between the old and new position shift — mirroring the
|
|
19
|
+
* double-bounded "moving up" branch (`order >= newOrder && order < currentOrder`).
|
|
20
|
+
*
|
|
21
|
+
* These tests stub the three persistence helpers onBeforeUpdate calls
|
|
22
|
+
* (findOneBy -> moved row's current order + layer id; findBy -> every row in the
|
|
23
|
+
* layer; updateOneBy -> the per-row shift) and CAPTURE each updateOneBy so we can
|
|
24
|
+
* assert exactly which rows moved and to what order. No Postgres involved.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const service: any = OnCallDutyPolicyScheduleLayerUserService as any;
|
|
28
|
+
|
|
29
|
+
// A schedule layer id shared by every row in these single-layer scenarios.
|
|
30
|
+
const LAYER_ID: ObjectID = new ObjectID("layer1");
|
|
31
|
+
|
|
32
|
+
const USER_A: ObjectID = new ObjectID("A");
|
|
33
|
+
const USER_B: ObjectID = new ObjectID("B");
|
|
34
|
+
const USER_C: ObjectID = new ObjectID("C");
|
|
35
|
+
|
|
36
|
+
// A captured updateOneBy shift: which row and what order it was set to.
|
|
37
|
+
interface CapturedShift {
|
|
38
|
+
id: string;
|
|
39
|
+
order: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Build a fake persisted row for the layer.
|
|
43
|
+
function row(id: ObjectID, order: number): Model {
|
|
44
|
+
return {
|
|
45
|
+
_id: id,
|
|
46
|
+
order,
|
|
47
|
+
onCallDutyPolicyScheduleLayerId: LAYER_ID,
|
|
48
|
+
} as unknown as Model;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Build the UpdateBy passed to onBeforeUpdate for moving `movedId` to `newOrder`.
|
|
52
|
+
function updateBy(movedId: ObjectID, newOrder: number): any {
|
|
53
|
+
return {
|
|
54
|
+
data: { order: newOrder },
|
|
55
|
+
query: { _id: movedId },
|
|
56
|
+
props: {},
|
|
57
|
+
} as any;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
* Stub findOneBy (moved row lookup), findBy (all rows in the layer) and
|
|
62
|
+
* updateOneBy (record the shift). Returns the array captured shifts are pushed
|
|
63
|
+
* into so each test can inspect exactly what moved.
|
|
64
|
+
*/
|
|
65
|
+
function stubService(
|
|
66
|
+
movedRow: Model,
|
|
67
|
+
layerRows: Array<Model>,
|
|
68
|
+
): Array<CapturedShift> {
|
|
69
|
+
const captured: Array<CapturedShift> = [];
|
|
70
|
+
|
|
71
|
+
jest.spyOn(service, "findOneBy").mockResolvedValue(movedRow as never);
|
|
72
|
+
|
|
73
|
+
jest.spyOn(service, "findBy").mockResolvedValue(layerRows as never);
|
|
74
|
+
|
|
75
|
+
jest
|
|
76
|
+
.spyOn(service, "updateOneBy")
|
|
77
|
+
.mockImplementation((...args: Array<unknown>): Promise<void> => {
|
|
78
|
+
const updateArg: {
|
|
79
|
+
query: { _id: ObjectID };
|
|
80
|
+
data: { order: number };
|
|
81
|
+
} = args[0] as {
|
|
82
|
+
query: { _id: ObjectID };
|
|
83
|
+
data: { order: number };
|
|
84
|
+
};
|
|
85
|
+
captured.push({
|
|
86
|
+
id: updateArg.query._id.toString(),
|
|
87
|
+
order: updateArg.data.order,
|
|
88
|
+
});
|
|
89
|
+
return Promise.resolve();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return captured;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe("OnCallDutyPolicyScheduleLayerUserService.onBeforeUpdate reorder", () => {
|
|
96
|
+
afterEach(() => {
|
|
97
|
+
jest.restoreAllMocks();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/*
|
|
101
|
+
* ----------------------------------------------------------------------- *
|
|
102
|
+
* 1. MOVE DOWN B: order 2 -> 3. Only C (3 -> 2) shifts; A is untouched.
|
|
103
|
+
* -----------------------------------------------------------------------
|
|
104
|
+
*/
|
|
105
|
+
test("moving B down (2 -> 3) decrements ONLY C and never touches A", async () => {
|
|
106
|
+
const captured: Array<CapturedShift> = stubService(row(USER_B, 2), [
|
|
107
|
+
row(USER_A, 1),
|
|
108
|
+
row(USER_B, 2),
|
|
109
|
+
row(USER_C, 3),
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
await service.onBeforeUpdate(updateBy(USER_B, 3));
|
|
113
|
+
|
|
114
|
+
// Exactly one row shifted, and it was C going from 3 down to 2.
|
|
115
|
+
expect(captured).toHaveLength(1);
|
|
116
|
+
expect(captured[0]).toEqual({ id: USER_C.toString(), order: 2 });
|
|
117
|
+
|
|
118
|
+
// A (the top row) must never be decremented — the regression.
|
|
119
|
+
const aShifts: Array<CapturedShift> = captured.filter(
|
|
120
|
+
(c: CapturedShift): boolean => {
|
|
121
|
+
return c.id === USER_A.toString();
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
expect(aShifts).toHaveLength(0);
|
|
125
|
+
|
|
126
|
+
/*
|
|
127
|
+
* After the shift, with the outer update setting B = 3, the resulting layer
|
|
128
|
+
* is contiguous {A:1, C:2, B:3}.
|
|
129
|
+
*/
|
|
130
|
+
const finalOrders: Record<string, number> = {
|
|
131
|
+
[USER_A.toString()]: 1, // untouched
|
|
132
|
+
[USER_C.toString()]: 2, // shifted
|
|
133
|
+
[USER_B.toString()]: 3, // set by the outer update
|
|
134
|
+
};
|
|
135
|
+
expect(finalOrders[USER_A.toString()]).toBe(1);
|
|
136
|
+
expect(finalOrders[USER_C.toString()]).toBe(2);
|
|
137
|
+
expect(finalOrders[USER_B.toString()]).toBe(3);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/*
|
|
141
|
+
* ----------------------------------------------------------------------- *
|
|
142
|
+
* 2. MOVE UP C: order 3 -> 1. A (1 -> 2) and B (2 -> 3) increment; C untouched.
|
|
143
|
+
* -----------------------------------------------------------------------
|
|
144
|
+
*/
|
|
145
|
+
test("moving C up (3 -> 1) increments ONLY A and B", async () => {
|
|
146
|
+
const captured: Array<CapturedShift> = stubService(row(USER_C, 3), [
|
|
147
|
+
row(USER_A, 1),
|
|
148
|
+
row(USER_B, 2),
|
|
149
|
+
row(USER_C, 3),
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
await service.onBeforeUpdate(updateBy(USER_C, 1));
|
|
153
|
+
|
|
154
|
+
expect(captured).toHaveLength(2);
|
|
155
|
+
|
|
156
|
+
const byId: Record<string, number> = {};
|
|
157
|
+
for (const shift of captured) {
|
|
158
|
+
byId[shift.id] = shift.order;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
expect(byId[USER_A.toString()]).toBe(2);
|
|
162
|
+
expect(byId[USER_B.toString()]).toBe(3);
|
|
163
|
+
// C itself is not part of the shift loop; the outer update sets it to 1.
|
|
164
|
+
expect(byId[USER_C.toString()]).toBeUndefined();
|
|
165
|
+
|
|
166
|
+
// Contiguous {C:1, A:2, B:3}.
|
|
167
|
+
const finalOrders: Record<string, number> = {
|
|
168
|
+
[USER_C.toString()]: 1,
|
|
169
|
+
[USER_A.toString()]: 2,
|
|
170
|
+
[USER_B.toString()]: 3,
|
|
171
|
+
};
|
|
172
|
+
expect(Object.values(finalOrders).sort()).toEqual([1, 2, 3]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
/*
|
|
176
|
+
* ----------------------------------------------------------------------- *
|
|
177
|
+
* 3. MOVE DOWN A: order 1 -> 3 (dragging the TOP row all the way down).
|
|
178
|
+
* Only B (2 -> 1) and C (3 -> 2) decrement; A is not touched by the loop.
|
|
179
|
+
* -----------------------------------------------------------------------
|
|
180
|
+
*/
|
|
181
|
+
test("moving the top row A down (1 -> 3) decrements ONLY B and C, no row hits 0", async () => {
|
|
182
|
+
const captured: Array<CapturedShift> = stubService(row(USER_A, 1), [
|
|
183
|
+
row(USER_A, 1),
|
|
184
|
+
row(USER_B, 2),
|
|
185
|
+
row(USER_C, 3),
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
await service.onBeforeUpdate(updateBy(USER_A, 3));
|
|
189
|
+
|
|
190
|
+
expect(captured).toHaveLength(2);
|
|
191
|
+
|
|
192
|
+
const byId: Record<string, number> = {};
|
|
193
|
+
for (const shift of captured) {
|
|
194
|
+
byId[shift.id] = shift.order;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
expect(byId[USER_B.toString()]).toBe(1);
|
|
198
|
+
expect(byId[USER_C.toString()]).toBe(2);
|
|
199
|
+
// A is the moved row itself — the shift loop must not decrement it.
|
|
200
|
+
expect(byId[USER_A.toString()]).toBeUndefined();
|
|
201
|
+
|
|
202
|
+
// Nothing collapsed to 0 or negative.
|
|
203
|
+
for (const shift of captured) {
|
|
204
|
+
expect(shift.order).toBeGreaterThan(0);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
/*
|
|
209
|
+
* ----------------------------------------------------------------------- *
|
|
210
|
+
* 4. Regression: in a move-down, NO captured shift may set an order <= 0.
|
|
211
|
+
* Before the fix, dragging B down drove A's order to 0. This asserts the
|
|
212
|
+
* invariant across the whole move-down code path.
|
|
213
|
+
* -----------------------------------------------------------------------
|
|
214
|
+
*/
|
|
215
|
+
test("regression: no move-down shift ever sets order <= 0", async () => {
|
|
216
|
+
const captured: Array<CapturedShift> = stubService(row(USER_B, 2), [
|
|
217
|
+
row(USER_A, 1),
|
|
218
|
+
row(USER_B, 2),
|
|
219
|
+
row(USER_C, 3),
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
await service.onBeforeUpdate(updateBy(USER_B, 3));
|
|
223
|
+
|
|
224
|
+
for (const shift of captured) {
|
|
225
|
+
expect(shift.order).toBeGreaterThan(0);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Keep any incidental logger.error noise out of the test output.
|
|
231
|
+
jest.spyOn(logger, "error").mockImplementation((): void => {
|
|
232
|
+
return undefined;
|
|
233
|
+
});
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import OnCallDutyPolicyTimeLogService from "../../../Server/Services/OnCallDutyPolicyTimeLogService";
|
|
2
|
+
import Model from "../../../Models/DatabaseModels/OnCallDutyPolicyTimeLog";
|
|
3
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
4
|
+
import QueryHelper from "../../../Server/Types/Database/QueryHelper";
|
|
5
|
+
import logger from "../../../Server/Utils/Logger";
|
|
6
|
+
import { describe, expect, test, afterEach } from "@jest/globals";
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* OnCallDutyPolicyTimeLogService scopes the team/schedule "context" columns of
|
|
10
|
+
* both its dedup read (startTimeLogForUser) and its close write
|
|
11
|
+
* (endTimeLogForUser) EXPLICITLY. When a context column is absent the query
|
|
12
|
+
* must constrain it to IS NULL rather than omit it.
|
|
13
|
+
*
|
|
14
|
+
* The old `...(x && { x })` OMITTED an absent column, so the column matched ANY
|
|
15
|
+
* value. Two bugs followed:
|
|
16
|
+
* - audit M3: ending a direct-user assignment (no teamId/scheduleId) matched
|
|
17
|
+
* and closed that user's still-open SCHEDULE and TEAM logs too.
|
|
18
|
+
* - audit L4: a direct-user start deduped against an already-open
|
|
19
|
+
* schedule/team log and never created the direct log.
|
|
20
|
+
*
|
|
21
|
+
* These tests mock the inherited DatabaseService persistence helpers
|
|
22
|
+
* (findOneBy / create / updateBy) so no Postgres is needed, and CAPTURE the
|
|
23
|
+
* query object each method is called with to assert the exact IS-NULL vs
|
|
24
|
+
* ObjectID scoping of the teamId and onCallDutyPolicyScheduleId columns.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* The serialized shape of QueryHelper.isNull() — a TypeORM `Raw` FindOperator.
|
|
29
|
+
* It is stable across calls (the SQL closure is dropped by JSON.stringify), so
|
|
30
|
+
* structural equality identifies an IS-NULL constraint on a column.
|
|
31
|
+
*/
|
|
32
|
+
const IS_NULL_JSON: string = JSON.stringify(QueryHelper.isNull());
|
|
33
|
+
|
|
34
|
+
// A distinct value that is NOT an IS-NULL operator, for negative assertions.
|
|
35
|
+
function isIsNull(value: unknown): boolean {
|
|
36
|
+
return JSON.stringify(value) === IS_NULL_JSON;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Minimal stand-in for a persisted OnCallDutyPolicyTimeLog row.
|
|
40
|
+
function fakeRow(id: ObjectID): Model {
|
|
41
|
+
return { id } as unknown as Model;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Base args shared by every startTimeLogForUser call in these tests.
|
|
45
|
+
function startArgsBase(): {
|
|
46
|
+
projectId: ObjectID;
|
|
47
|
+
onCallDutyPolicyId: ObjectID;
|
|
48
|
+
onCallDutyPolicyEscalationRuleId: ObjectID;
|
|
49
|
+
userId: ObjectID;
|
|
50
|
+
startsAt: Date;
|
|
51
|
+
} {
|
|
52
|
+
return {
|
|
53
|
+
projectId: new ObjectID("11111111-1111-4111-8111-111111111111"),
|
|
54
|
+
onCallDutyPolicyId: new ObjectID("22222222-2222-4222-8222-222222222222"),
|
|
55
|
+
onCallDutyPolicyEscalationRuleId: new ObjectID(
|
|
56
|
+
"33333333-3333-4333-8333-333333333333",
|
|
57
|
+
),
|
|
58
|
+
userId: new ObjectID("44444444-4444-4444-8444-444444444444"),
|
|
59
|
+
startsAt: new Date("2026-07-09T00:00:00.000Z"),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Capture the `query` object handed to findOneBy on the next call.
|
|
64
|
+
function spyFindOneBy(resolve: Model | null): jest.SpyInstance {
|
|
65
|
+
return jest
|
|
66
|
+
.spyOn(OnCallDutyPolicyTimeLogService, "findOneBy")
|
|
67
|
+
.mockResolvedValue(resolve as never);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Stub create so startTimeLogForUser can complete when no existing log is found.
|
|
71
|
+
function spyCreate(): jest.SpyInstance {
|
|
72
|
+
return jest
|
|
73
|
+
.spyOn(OnCallDutyPolicyTimeLogService, "create")
|
|
74
|
+
.mockResolvedValue(
|
|
75
|
+
fakeRow(new ObjectID("99999999-9999-4999-8999-999999999999")) as never,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Stub updateBy (returns a row count) for the endTimeLogForUser tests.
|
|
80
|
+
function spyUpdateBy(): jest.SpyInstance {
|
|
81
|
+
return jest
|
|
82
|
+
.spyOn(OnCallDutyPolicyTimeLogService, "updateBy")
|
|
83
|
+
.mockResolvedValue(1 as never);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function queryOf(spy: jest.SpyInstance): Record<string, unknown> {
|
|
87
|
+
return (spy.mock.calls[0]![0] as { query: Record<string, unknown> }).query;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe("OnCallDutyPolicyTimeLogService context-column scoping", () => {
|
|
91
|
+
afterEach(() => {
|
|
92
|
+
jest.restoreAllMocks();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
* ----------------------------------------------------------------------- *
|
|
97
|
+
* startTimeLogForUser dedup query scoping.
|
|
98
|
+
* ----------------------------------------------------------------------- *
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
test("startTimeLogForUser with NO teamId/scheduleId scopes both columns to IS NULL", async () => {
|
|
102
|
+
const findOneBy: jest.SpyInstance = spyFindOneBy(null);
|
|
103
|
+
const create: jest.SpyInstance = spyCreate();
|
|
104
|
+
|
|
105
|
+
await OnCallDutyPolicyTimeLogService.startTimeLogForUser(startArgsBase());
|
|
106
|
+
|
|
107
|
+
expect(findOneBy).toHaveBeenCalledTimes(1);
|
|
108
|
+
const query: Record<string, unknown> = queryOf(findOneBy);
|
|
109
|
+
|
|
110
|
+
// A direct-user log must only dedup against other direct logs => IS NULL.
|
|
111
|
+
expect(isIsNull(query["teamId"])).toBe(true);
|
|
112
|
+
expect(isIsNull(query["onCallDutyPolicyScheduleId"])).toBe(true);
|
|
113
|
+
// endsAt IS NULL is always part of the open-log scope.
|
|
114
|
+
expect(isIsNull(query["endsAt"])).toBe(true);
|
|
115
|
+
|
|
116
|
+
// Nothing existed => a fresh log row is created.
|
|
117
|
+
expect(create).toHaveBeenCalledTimes(1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("startTimeLogForUser WITH teamId only: teamId matches the passed ObjectID, schedule IS NULL", async () => {
|
|
121
|
+
const teamId: ObjectID = new ObjectID(
|
|
122
|
+
"55555555-5555-4555-8555-555555555555",
|
|
123
|
+
);
|
|
124
|
+
const findOneBy: jest.SpyInstance = spyFindOneBy(null);
|
|
125
|
+
spyCreate();
|
|
126
|
+
|
|
127
|
+
await OnCallDutyPolicyTimeLogService.startTimeLogForUser({
|
|
128
|
+
...startArgsBase(),
|
|
129
|
+
teamId,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const query: Record<string, unknown> = queryOf(findOneBy);
|
|
133
|
+
|
|
134
|
+
// teamId is the concrete value passed in (not an IS-NULL operator).
|
|
135
|
+
expect(isIsNull(query["teamId"])).toBe(false);
|
|
136
|
+
expect((query["teamId"] as ObjectID).toString()).toBe(teamId.toString());
|
|
137
|
+
// No schedule context => that column is scoped to IS NULL.
|
|
138
|
+
expect(isIsNull(query["onCallDutyPolicyScheduleId"])).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("startTimeLogForUser WITH scheduleId only: schedule matches passed ObjectID, teamId IS NULL", async () => {
|
|
142
|
+
const onCallDutyPolicyScheduleId: ObjectID = new ObjectID(
|
|
143
|
+
"66666666-6666-4666-8666-666666666666",
|
|
144
|
+
);
|
|
145
|
+
const findOneBy: jest.SpyInstance = spyFindOneBy(null);
|
|
146
|
+
spyCreate();
|
|
147
|
+
|
|
148
|
+
await OnCallDutyPolicyTimeLogService.startTimeLogForUser({
|
|
149
|
+
...startArgsBase(),
|
|
150
|
+
onCallDutyPolicyScheduleId,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const query: Record<string, unknown> = queryOf(findOneBy);
|
|
154
|
+
|
|
155
|
+
// Schedule column is the concrete value passed in.
|
|
156
|
+
expect(isIsNull(query["onCallDutyPolicyScheduleId"])).toBe(false);
|
|
157
|
+
expect((query["onCallDutyPolicyScheduleId"] as ObjectID).toString()).toBe(
|
|
158
|
+
onCallDutyPolicyScheduleId.toString(),
|
|
159
|
+
);
|
|
160
|
+
// No team context => that column is scoped to IS NULL.
|
|
161
|
+
expect(isIsNull(query["teamId"])).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("startTimeLogForUser is idempotent: an existing open log is returned and create is NOT called", async () => {
|
|
165
|
+
const existingId: ObjectID = new ObjectID(
|
|
166
|
+
"77777777-7777-4777-8777-777777777777",
|
|
167
|
+
);
|
|
168
|
+
const existing: Model = fakeRow(existingId);
|
|
169
|
+
const findOneBy: jest.SpyInstance = spyFindOneBy(existing);
|
|
170
|
+
const create: jest.SpyInstance = spyCreate();
|
|
171
|
+
|
|
172
|
+
const result: Model =
|
|
173
|
+
await OnCallDutyPolicyTimeLogService.startTimeLogForUser(startArgsBase());
|
|
174
|
+
|
|
175
|
+
expect(findOneBy).toHaveBeenCalledTimes(1);
|
|
176
|
+
// Returns the already-open log verbatim (audit M1 relies on this).
|
|
177
|
+
expect(result).toBe(existing);
|
|
178
|
+
// No duplicate stint is created.
|
|
179
|
+
expect(create).not.toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
/*
|
|
183
|
+
* ----------------------------------------------------------------------- *
|
|
184
|
+
* endTimeLogForUser close-query scoping.
|
|
185
|
+
* ----------------------------------------------------------------------- *
|
|
186
|
+
*/
|
|
187
|
+
|
|
188
|
+
test("endTimeLogForUser with NO teamId/scheduleId scopes both columns to IS NULL", async () => {
|
|
189
|
+
const updateBy: jest.SpyInstance = spyUpdateBy();
|
|
190
|
+
|
|
191
|
+
await OnCallDutyPolicyTimeLogService.endTimeLogForUser({
|
|
192
|
+
projectId: new ObjectID("11111111-1111-4111-8111-111111111111"),
|
|
193
|
+
onCallDutyPolicyId: new ObjectID("22222222-2222-4222-8222-222222222222"),
|
|
194
|
+
onCallDutyPolicyEscalationRuleId: new ObjectID(
|
|
195
|
+
"33333333-3333-4333-8333-333333333333",
|
|
196
|
+
),
|
|
197
|
+
userId: new ObjectID("44444444-4444-4444-8444-444444444444"),
|
|
198
|
+
endsAt: new Date("2026-07-09T01:00:00.000Z"),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(updateBy).toHaveBeenCalledTimes(1);
|
|
202
|
+
const query: Record<string, unknown> = queryOf(updateBy);
|
|
203
|
+
|
|
204
|
+
// Ending a direct-user stint must NOT close schedule/team logs (audit M3).
|
|
205
|
+
expect(isIsNull(query["teamId"])).toBe(true);
|
|
206
|
+
expect(isIsNull(query["onCallDutyPolicyScheduleId"])).toBe(true);
|
|
207
|
+
expect(isIsNull(query["endsAt"])).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("endTimeLogForUser WITH scheduleId scopes onCallDutyPolicyScheduleId to the passed ObjectID", async () => {
|
|
211
|
+
const onCallDutyPolicyScheduleId: ObjectID = new ObjectID(
|
|
212
|
+
"88888888-8888-4888-8888-888888888888",
|
|
213
|
+
);
|
|
214
|
+
const updateBy: jest.SpyInstance = spyUpdateBy();
|
|
215
|
+
|
|
216
|
+
await OnCallDutyPolicyTimeLogService.endTimeLogForUser({
|
|
217
|
+
projectId: new ObjectID("11111111-1111-4111-8111-111111111111"),
|
|
218
|
+
onCallDutyPolicyId: new ObjectID("22222222-2222-4222-8222-222222222222"),
|
|
219
|
+
onCallDutyPolicyEscalationRuleId: new ObjectID(
|
|
220
|
+
"33333333-3333-4333-8333-333333333333",
|
|
221
|
+
),
|
|
222
|
+
userId: new ObjectID("44444444-4444-4444-8444-444444444444"),
|
|
223
|
+
onCallDutyPolicyScheduleId,
|
|
224
|
+
endsAt: new Date("2026-07-09T01:00:00.000Z"),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const query: Record<string, unknown> = queryOf(updateBy);
|
|
228
|
+
|
|
229
|
+
// Only THIS schedule's open stint is closed.
|
|
230
|
+
expect(isIsNull(query["onCallDutyPolicyScheduleId"])).toBe(false);
|
|
231
|
+
expect((query["onCallDutyPolicyScheduleId"] as ObjectID).toString()).toBe(
|
|
232
|
+
onCallDutyPolicyScheduleId.toString(),
|
|
233
|
+
);
|
|
234
|
+
// No team context supplied => team column stays IS NULL.
|
|
235
|
+
expect(isIsNull(query["teamId"])).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Silence any incidental logger.error noise from the service under test.
|
|
240
|
+
jest.spyOn(logger, "error").mockImplementation((): void => {
|
|
241
|
+
return undefined;
|
|
242
|
+
});
|
|
@@ -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
|
+
});
|