@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,300 @@
|
|
|
1
|
+
import LayerUtil, { LayerProps } from "../../../Types/OnCallDutyPolicy/Layer";
|
|
2
|
+
import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
|
|
3
|
+
import RestrictionTimes, {
|
|
4
|
+
RestrictionType,
|
|
5
|
+
} from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
|
|
6
|
+
import Recurring from "../../../Types/Events/Recurring";
|
|
7
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
8
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
9
|
+
import EventInterval from "../../../Types/Events/EventInterval";
|
|
10
|
+
|
|
11
|
+
function user(id: string): User {
|
|
12
|
+
return {
|
|
13
|
+
id: {
|
|
14
|
+
toString: () => {
|
|
15
|
+
return id;
|
|
16
|
+
},
|
|
17
|
+
} as any,
|
|
18
|
+
} as User;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function rotation(t: EventInterval, c: number): Recurring {
|
|
22
|
+
return Recurring.fromJSON({
|
|
23
|
+
_type: "Recurring",
|
|
24
|
+
value: {
|
|
25
|
+
intervalType: t,
|
|
26
|
+
intervalCount: { _type: "PositiveNumber", value: c },
|
|
27
|
+
},
|
|
28
|
+
} as any);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function noRestriction(): RestrictionTimes {
|
|
32
|
+
const r: RestrictionTimes = new RestrictionTimes();
|
|
33
|
+
r.restictionType = RestrictionType.None;
|
|
34
|
+
r.dayRestrictionTimes = null;
|
|
35
|
+
return r;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function dailyRestriction(
|
|
39
|
+
startHour: number,
|
|
40
|
+
endHour: number,
|
|
41
|
+
): RestrictionTimes {
|
|
42
|
+
const r: RestrictionTimes = new RestrictionTimes();
|
|
43
|
+
r.restictionType = RestrictionType.Daily;
|
|
44
|
+
r.dayRestrictionTimes = {
|
|
45
|
+
startTime: OneUptimeDate.getDateWithCustomTime({
|
|
46
|
+
hours: startHour,
|
|
47
|
+
minutes: 0,
|
|
48
|
+
seconds: 0,
|
|
49
|
+
}),
|
|
50
|
+
endTime: OneUptimeDate.getDateWithCustomTime({
|
|
51
|
+
hours: endHour,
|
|
52
|
+
minutes: 0,
|
|
53
|
+
seconds: 0,
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
return r;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function coveringUser(events: Array<CalendarEvent>, t: Date): string | null {
|
|
60
|
+
for (const e of events) {
|
|
61
|
+
if (e.start.getTime() <= t.getTime() && e.end.getTime() > t.getTime()) {
|
|
62
|
+
return e.title;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function oracleUser(
|
|
69
|
+
perLayerEvents: Array<Array<CalendarEvent>>,
|
|
70
|
+
t: Date,
|
|
71
|
+
): string | null {
|
|
72
|
+
for (const layerEvents of perLayerEvents) {
|
|
73
|
+
const u: string | null = coveringUser(layerEvents, t);
|
|
74
|
+
if (u !== null) {
|
|
75
|
+
return u;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function overlappingPairs(
|
|
82
|
+
events: Array<CalendarEvent>,
|
|
83
|
+
): Array<[CalendarEvent, CalendarEvent]> {
|
|
84
|
+
const pairs: Array<[CalendarEvent, CalendarEvent]> = [];
|
|
85
|
+
for (let i: number = 0; i < events.length; i++) {
|
|
86
|
+
for (let j: number = i + 1; j < events.length; j++) {
|
|
87
|
+
const a: CalendarEvent = events[i]!;
|
|
88
|
+
const b: CalendarEvent = events[j]!;
|
|
89
|
+
const start: number = Math.max(a.start.getTime(), b.start.getTime());
|
|
90
|
+
const end: number = Math.min(a.end.getTime(), b.end.getTime());
|
|
91
|
+
if (end - start > 0) {
|
|
92
|
+
pairs.push([a, b]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return pairs;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe("Multi-layer edge cases", () => {
|
|
100
|
+
const util: LayerUtil = new LayerUtil();
|
|
101
|
+
|
|
102
|
+
function checkMerge(
|
|
103
|
+
layers: Array<LayerProps>,
|
|
104
|
+
calStart: Date,
|
|
105
|
+
calEnd: Date,
|
|
106
|
+
stepMin: number = 15,
|
|
107
|
+
): {
|
|
108
|
+
mismatches: Array<string>;
|
|
109
|
+
overlaps: number;
|
|
110
|
+
merged: Array<CalendarEvent>;
|
|
111
|
+
} {
|
|
112
|
+
const merged: Array<CalendarEvent> = util.getMultiLayerEvents({
|
|
113
|
+
layers: layers,
|
|
114
|
+
calendarStartDate: calStart,
|
|
115
|
+
calendarEndDate: calEnd,
|
|
116
|
+
});
|
|
117
|
+
const perLayerEvents: Array<Array<CalendarEvent>> = layers.map(
|
|
118
|
+
(layer: LayerProps): Array<CalendarEvent> => {
|
|
119
|
+
return util.getEvents({
|
|
120
|
+
...layer,
|
|
121
|
+
calendarStartDate: calStart,
|
|
122
|
+
calendarEndDate: calEnd,
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
const mismatches: Array<string> = [];
|
|
127
|
+
for (
|
|
128
|
+
let ms: number = calStart.getTime() + 17000;
|
|
129
|
+
ms < calEnd.getTime();
|
|
130
|
+
ms += stepMin * 60 * 1000
|
|
131
|
+
) {
|
|
132
|
+
const t: Date = new Date(ms);
|
|
133
|
+
const expected: string | null = oracleUser(perLayerEvents, t);
|
|
134
|
+
const actual: string | null = coveringUser(merged, t);
|
|
135
|
+
if (expected !== actual) {
|
|
136
|
+
mismatches.push(
|
|
137
|
+
`${OneUptimeDate.toString(t)} expected=${expected} actual=${actual}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
mismatches: mismatches,
|
|
143
|
+
overlaps: overlappingPairs(merged).length,
|
|
144
|
+
merged: merged,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
test("G: high-priority layer starts 2 days into a 5-day window; fallback fills the head", () => {
|
|
149
|
+
const calStart: Date = new Date(2026, 0, 5, 0, 0, 0);
|
|
150
|
+
const primaryStart: Date = new Date(2026, 0, 7, 0, 0, 0); // 2 days later
|
|
151
|
+
const calEnd: Date = new Date(2026, 0, 10, 0, 0, 0);
|
|
152
|
+
|
|
153
|
+
const layers: Array<LayerProps> = [
|
|
154
|
+
{
|
|
155
|
+
users: [user("P1"), user("P2")],
|
|
156
|
+
startDateTimeOfLayer: primaryStart,
|
|
157
|
+
restrictionTimes: noRestriction(),
|
|
158
|
+
handOffTime: primaryStart,
|
|
159
|
+
rotation: rotation(EventInterval.Day, 1),
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
users: [user("F1")],
|
|
163
|
+
startDateTimeOfLayer: calStart,
|
|
164
|
+
restrictionTimes: noRestriction(),
|
|
165
|
+
handOffTime: calStart,
|
|
166
|
+
rotation: rotation(EventInterval.Week, 1),
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
const { mismatches, overlaps } = checkMerge(layers, calStart, calEnd);
|
|
171
|
+
// First 2 days: fallback F1. After primaryStart: P1/P2 daily.
|
|
172
|
+
expect(mismatches).toEqual([]);
|
|
173
|
+
expect(overlaps).toBe(0);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("H: three layers with different start offsets and rotations", () => {
|
|
177
|
+
const calStart: Date = new Date(2026, 0, 5, 0, 0, 0);
|
|
178
|
+
const calEnd: Date = new Date(2026, 0, 9, 0, 0, 0);
|
|
179
|
+
|
|
180
|
+
const layers: Array<LayerProps> = [
|
|
181
|
+
{
|
|
182
|
+
users: [user("A")],
|
|
183
|
+
startDateTimeOfLayer: new Date(2026, 0, 6, 12, 0, 0),
|
|
184
|
+
restrictionTimes: dailyRestriction(0, 6),
|
|
185
|
+
handOffTime: new Date(2026, 0, 6, 12, 0, 0),
|
|
186
|
+
rotation: rotation(EventInterval.Day, 1),
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
users: [user("B1"), user("B2")],
|
|
190
|
+
startDateTimeOfLayer: new Date(2026, 0, 5, 6, 0, 0),
|
|
191
|
+
restrictionTimes: dailyRestriction(8, 20),
|
|
192
|
+
handOffTime: new Date(2026, 0, 5, 12, 0, 0),
|
|
193
|
+
rotation: rotation(EventInterval.Hour, 6),
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
users: [user("Z")],
|
|
197
|
+
startDateTimeOfLayer: calStart,
|
|
198
|
+
restrictionTimes: noRestriction(),
|
|
199
|
+
handOffTime: calStart,
|
|
200
|
+
rotation: rotation(EventInterval.Week, 1),
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
const { mismatches, overlaps } = checkMerge(layers, calStart, calEnd, 10);
|
|
205
|
+
expect(mismatches).toEqual([]);
|
|
206
|
+
expect(overlaps).toBe(0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("I: three layers all starting at window start (equal starts) -> priority order", () => {
|
|
210
|
+
const calStart: Date = new Date(2026, 0, 5, 0, 0, 0);
|
|
211
|
+
const calEnd: Date = new Date(2026, 0, 12, 0, 0, 0);
|
|
212
|
+
|
|
213
|
+
const layers: Array<LayerProps> = [
|
|
214
|
+
{
|
|
215
|
+
users: [user("A")],
|
|
216
|
+
startDateTimeOfLayer: calStart,
|
|
217
|
+
restrictionTimes: noRestriction(),
|
|
218
|
+
handOffTime: calStart,
|
|
219
|
+
rotation: rotation(EventInterval.Week, 1),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
users: [user("B")],
|
|
223
|
+
startDateTimeOfLayer: calStart,
|
|
224
|
+
restrictionTimes: noRestriction(),
|
|
225
|
+
handOffTime: calStart,
|
|
226
|
+
rotation: rotation(EventInterval.Day, 1),
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
users: [user("C")],
|
|
230
|
+
startDateTimeOfLayer: calStart,
|
|
231
|
+
restrictionTimes: noRestriction(),
|
|
232
|
+
handOffTime: calStart,
|
|
233
|
+
rotation: rotation(EventInterval.Hour, 1),
|
|
234
|
+
},
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
const { mismatches, overlaps } = checkMerge(layers, calStart, calEnd, 13);
|
|
238
|
+
// Layer 0 (A) is 24/7 highest priority -> A everywhere.
|
|
239
|
+
expect(mismatches).toEqual([]);
|
|
240
|
+
expect(overlaps).toBe(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("J: high-priority restricted layer over a RESTRICTED (non-24/7) fallback leaving true gaps", () => {
|
|
244
|
+
// Both layers restricted; there are genuine uncovered periods (oracle=null).
|
|
245
|
+
const calStart: Date = new Date(2026, 0, 5, 0, 0, 0);
|
|
246
|
+
const calEnd: Date = new Date(2026, 0, 9, 0, 0, 0);
|
|
247
|
+
|
|
248
|
+
const layers: Array<LayerProps> = [
|
|
249
|
+
{
|
|
250
|
+
users: [user("A")],
|
|
251
|
+
startDateTimeOfLayer: calStart,
|
|
252
|
+
restrictionTimes: dailyRestriction(9, 12),
|
|
253
|
+
handOffTime: calStart,
|
|
254
|
+
rotation: rotation(EventInterval.Week, 1),
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
users: [user("B")],
|
|
258
|
+
startDateTimeOfLayer: calStart,
|
|
259
|
+
restrictionTimes: dailyRestriction(10, 17), // overlaps A at 10-12
|
|
260
|
+
handOffTime: calStart,
|
|
261
|
+
rotation: rotation(EventInterval.Week, 1),
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
const { mismatches, overlaps } = checkMerge(layers, calStart, calEnd, 11);
|
|
266
|
+
// Expect: A owns 9-12, B owns 12:00:01-17, nobody owns nights.
|
|
267
|
+
expect(mismatches).toEqual([]);
|
|
268
|
+
expect(overlaps).toBe(0);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("K: primary rotation window exactly abuts fallback stitch boundary", () => {
|
|
272
|
+
/*
|
|
273
|
+
* Primary hourly restricted 10-14; fallback 24/7. Check the exact 10:00 and
|
|
274
|
+
* 14:00 boundaries via the merged output shape (no overlaps, contiguous).
|
|
275
|
+
*/
|
|
276
|
+
const calStart: Date = new Date(2026, 0, 5, 0, 0, 0);
|
|
277
|
+
const calEnd: Date = new Date(2026, 0, 7, 0, 0, 0);
|
|
278
|
+
|
|
279
|
+
const layers: Array<LayerProps> = [
|
|
280
|
+
{
|
|
281
|
+
users: [user("A"), user("B")],
|
|
282
|
+
startDateTimeOfLayer: calStart,
|
|
283
|
+
restrictionTimes: dailyRestriction(10, 14),
|
|
284
|
+
handOffTime: new Date(2026, 0, 5, 12, 0, 0),
|
|
285
|
+
rotation: rotation(EventInterval.Hour, 2),
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
users: [user("Z")],
|
|
289
|
+
startDateTimeOfLayer: calStart,
|
|
290
|
+
restrictionTimes: noRestriction(),
|
|
291
|
+
handOffTime: calStart,
|
|
292
|
+
rotation: rotation(EventInterval.Week, 1),
|
|
293
|
+
},
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
const { mismatches, overlaps } = checkMerge(layers, calStart, calEnd, 5);
|
|
297
|
+
expect(mismatches).toEqual([]);
|
|
298
|
+
expect(overlaps).toBe(0);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import LayerUtil, { LayerProps } from "../../../Types/OnCallDutyPolicy/Layer";
|
|
2
|
+
import RestrictionTimes, {
|
|
3
|
+
RestrictionType,
|
|
4
|
+
} from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
|
|
5
|
+
import Recurring from "../../../Types/Events/Recurring";
|
|
6
|
+
import EventInterval from "../../../Types/Events/EventInterval";
|
|
7
|
+
import PositiveNumber from "../../../Types/PositiveNumber";
|
|
8
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
9
|
+
import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
|
|
10
|
+
import { describe, expect, test } from "@jest/globals";
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Audit L1: getEventsByDailyRestriction's overnight (end < start) branch stepped
|
|
14
|
+
* its day-by-day iterator with addRemoveDays(currentDayStart, 1) WITHOUT the
|
|
15
|
+
* schedule timezone, i.e. a fixed 24h step. Every segment-shaping call
|
|
16
|
+
* (getStartOfDay / keepTimeButMoveDay) IS timezone-aware, so on a fall-back DST
|
|
17
|
+
* date (the schedule-zone day is 25h long) the 24h step landed the iterator back
|
|
18
|
+
* inside the SAME schedule-zone calendar day and emitted that day's night +
|
|
19
|
+
* morning segments a SECOND time — duplicate, identical on-call events for the
|
|
20
|
+
* same user. Coverage stays continuous (no gap), so this is a duplicate-segment
|
|
21
|
+
* defect. The fix passes this.timezone to both day-step calls.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
function user(id: string): User {
|
|
25
|
+
return {
|
|
26
|
+
id: {
|
|
27
|
+
toString: (): string => {
|
|
28
|
+
return id;
|
|
29
|
+
},
|
|
30
|
+
} as any,
|
|
31
|
+
} as User;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function overnightRestriction(): RestrictionTimes {
|
|
35
|
+
const rt: RestrictionTimes = new RestrictionTimes();
|
|
36
|
+
rt.restictionType = RestrictionType.Daily;
|
|
37
|
+
// 22:00 -> 06:00 (next day). end < start within a day => overnight branch.
|
|
38
|
+
rt.dayRestrictionTimes = {
|
|
39
|
+
startTime: new Date(Date.UTC(2025, 0, 1, 22, 0, 0)),
|
|
40
|
+
endTime: new Date(Date.UTC(2025, 0, 1, 6, 0, 0)),
|
|
41
|
+
};
|
|
42
|
+
return rt;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function weeklyRotation(): Recurring {
|
|
46
|
+
/*
|
|
47
|
+
* One long period so a single rotation event spans the whole DST window and
|
|
48
|
+
* the daily overnight restriction tiles nightly across it.
|
|
49
|
+
*/
|
|
50
|
+
const r: Recurring = new Recurring();
|
|
51
|
+
r.intervalType = EventInterval.Week;
|
|
52
|
+
r.intervalCount = new PositiveNumber(1);
|
|
53
|
+
return r;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function duplicateKeys(events: Array<CalendarEvent>): Array<string> {
|
|
57
|
+
const seen: Set<string> = new Set<string>();
|
|
58
|
+
const dupes: Array<string> = [];
|
|
59
|
+
for (const e of events) {
|
|
60
|
+
const key: string = `${e.title}|${new Date(e.start).toISOString()}|${new Date(
|
|
61
|
+
e.end,
|
|
62
|
+
).toISOString()}`;
|
|
63
|
+
if (seen.has(key)) {
|
|
64
|
+
dupes.push(key);
|
|
65
|
+
}
|
|
66
|
+
seen.add(key);
|
|
67
|
+
}
|
|
68
|
+
return dupes;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe("LayerUtil audit L1: overnight restriction across DST fall-back", () => {
|
|
72
|
+
test("no duplicate segments across the 2025-11-02 fall-back (America/New_York)", () => {
|
|
73
|
+
const anchor: Date = new Date(Date.UTC(2025, 9, 27, 0, 0, 0)); // Oct 27
|
|
74
|
+
const layer: LayerProps = {
|
|
75
|
+
users: [user("A")],
|
|
76
|
+
startDateTimeOfLayer: anchor,
|
|
77
|
+
handOffTime: anchor,
|
|
78
|
+
restrictionTimes: overnightRestriction(),
|
|
79
|
+
rotation: weeklyRotation(),
|
|
80
|
+
timezone: "America/New_York",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const events: Array<CalendarEvent> = new LayerUtil().getEvents({
|
|
84
|
+
...layer,
|
|
85
|
+
calendarStartDate: new Date(Date.UTC(2025, 10, 1, 0, 0, 0)), // Nov 1
|
|
86
|
+
calendarEndDate: new Date(Date.UTC(2025, 10, 6, 0, 0, 0)), // Nov 6
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(events.length).toBeGreaterThan(0); // coverage still exists (no gap)
|
|
90
|
+
expect(duplicateKeys(events)).toEqual([]); // and no duplicate on-call blocks
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("no duplicate segments across the 2025-03-09 spring-forward (America/New_York)", () => {
|
|
94
|
+
const anchor: Date = new Date(Date.UTC(2025, 2, 2, 0, 0, 0)); // Mar 2
|
|
95
|
+
const layer: LayerProps = {
|
|
96
|
+
users: [user("A")],
|
|
97
|
+
startDateTimeOfLayer: anchor,
|
|
98
|
+
handOffTime: anchor,
|
|
99
|
+
restrictionTimes: overnightRestriction(),
|
|
100
|
+
rotation: weeklyRotation(),
|
|
101
|
+
timezone: "America/New_York",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const events: Array<CalendarEvent> = new LayerUtil().getEvents({
|
|
105
|
+
...layer,
|
|
106
|
+
calendarStartDate: new Date(Date.UTC(2025, 2, 7, 0, 0, 0)), // Mar 7
|
|
107
|
+
calendarEndDate: new Date(Date.UTC(2025, 2, 12, 0, 0, 0)), // Mar 12
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(events.length).toBeGreaterThan(0);
|
|
111
|
+
expect(duplicateKeys(events)).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("control: same overnight restriction on a non-DST week has no duplicates", () => {
|
|
115
|
+
const anchor: Date = new Date(Date.UTC(2025, 6, 6, 0, 0, 0)); // Jul 6
|
|
116
|
+
const layer: LayerProps = {
|
|
117
|
+
users: [user("A")],
|
|
118
|
+
startDateTimeOfLayer: anchor,
|
|
119
|
+
handOffTime: anchor,
|
|
120
|
+
restrictionTimes: overnightRestriction(),
|
|
121
|
+
rotation: weeklyRotation(),
|
|
122
|
+
timezone: "America/New_York",
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const events: Array<CalendarEvent> = new LayerUtil().getEvents({
|
|
126
|
+
...layer,
|
|
127
|
+
calendarStartDate: new Date(Date.UTC(2025, 6, 7, 0, 0, 0)),
|
|
128
|
+
calendarEndDate: new Date(Date.UTC(2025, 6, 12, 0, 0, 0)),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(events.length).toBeGreaterThan(0);
|
|
132
|
+
expect(duplicateKeys(events)).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("no event has zero or negative length (start < end) across DST", () => {
|
|
136
|
+
const anchor: Date = new Date(Date.UTC(2025, 9, 27, 0, 0, 0));
|
|
137
|
+
const layer: LayerProps = {
|
|
138
|
+
users: [user("A")],
|
|
139
|
+
startDateTimeOfLayer: anchor,
|
|
140
|
+
handOffTime: anchor,
|
|
141
|
+
restrictionTimes: overnightRestriction(),
|
|
142
|
+
rotation: weeklyRotation(),
|
|
143
|
+
timezone: "America/New_York",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const events: Array<CalendarEvent> = new LayerUtil().getEvents({
|
|
147
|
+
...layer,
|
|
148
|
+
calendarStartDate: new Date(Date.UTC(2025, 10, 1, 0, 0, 0)),
|
|
149
|
+
calendarEndDate: new Date(Date.UTC(2025, 10, 6, 0, 0, 0)),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
for (const e of events) {
|
|
153
|
+
expect(new Date(e.end).getTime()).toBeGreaterThan(
|
|
154
|
+
new Date(e.start).getTime(),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REGRESSION SUITE for the restricted-rotation post-window-gap off-by-one fix
|
|
3
|
+
* (audit F2).
|
|
4
|
+
*
|
|
5
|
+
* When the roster is resolved partway through a rotation period, AFTER that
|
|
6
|
+
* period's restriction window has already closed (the live "who is on call
|
|
7
|
+
* now / next" path resolving in a daily or weekend off-hours gap), the windowed
|
|
8
|
+
* expansion used to carry the current period's user into the next period,
|
|
9
|
+
* paging/notifying the WRONG "next" user for every subsequent shift.
|
|
10
|
+
*
|
|
11
|
+
* The fix decides the first-period rotation advance on the period's FULL span
|
|
12
|
+
* rather than the clamped [now, periodEnd] slice. These tests lock in that
|
|
13
|
+
* behavior AND assert the #2413 "fully-restricted period keeps its turn skipped"
|
|
14
|
+
* behavior is preserved (the guard the fix touches).
|
|
15
|
+
*/
|
|
16
|
+
import LayerUtil, { LayerProps } from "../../../Types/OnCallDutyPolicy/Layer";
|
|
17
|
+
import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
|
|
18
|
+
import RestrictionTimes, {
|
|
19
|
+
RestrictionType,
|
|
20
|
+
WeeklyResctriction,
|
|
21
|
+
} from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
|
|
22
|
+
import Recurring from "../../../Types/Events/Recurring";
|
|
23
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
24
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
25
|
+
import EventInterval from "../../../Types/Events/EventInterval";
|
|
26
|
+
import PositiveNumber from "../../../Types/PositiveNumber";
|
|
27
|
+
import DayOfWeek from "../../../Types/Day/DayOfWeek";
|
|
28
|
+
|
|
29
|
+
function user(id: string): User {
|
|
30
|
+
return {
|
|
31
|
+
id: {
|
|
32
|
+
toString: (): string => {
|
|
33
|
+
return id;
|
|
34
|
+
},
|
|
35
|
+
} as any,
|
|
36
|
+
} as User;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function dailyRestriction(sh: number, eh: number): RestrictionTimes {
|
|
40
|
+
const r: RestrictionTimes = new RestrictionTimes();
|
|
41
|
+
r.restictionType = RestrictionType.Daily;
|
|
42
|
+
r.dayRestrictionTimes = {
|
|
43
|
+
startTime: OneUptimeDate.getDateWithCustomTime({
|
|
44
|
+
hours: sh,
|
|
45
|
+
minutes: 0,
|
|
46
|
+
seconds: 0,
|
|
47
|
+
}),
|
|
48
|
+
endTime: OneUptimeDate.getDateWithCustomTime({
|
|
49
|
+
hours: eh,
|
|
50
|
+
minutes: 0,
|
|
51
|
+
seconds: 0,
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
return r;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function dailyLayer(data: {
|
|
58
|
+
users: string[];
|
|
59
|
+
intervalCount: number;
|
|
60
|
+
restriction: RestrictionTimes;
|
|
61
|
+
start: Date;
|
|
62
|
+
}): LayerProps {
|
|
63
|
+
const rot: Recurring = new Recurring();
|
|
64
|
+
rot.intervalType = EventInterval.Day;
|
|
65
|
+
rot.intervalCount = new PositiveNumber(data.intervalCount);
|
|
66
|
+
return {
|
|
67
|
+
users: data.users.map(user),
|
|
68
|
+
startDateTimeOfLayer: data.start,
|
|
69
|
+
restrictionTimes: data.restriction,
|
|
70
|
+
handOffTime: Recurring.getNextDateInterval(data.start, rot),
|
|
71
|
+
rotation: rot,
|
|
72
|
+
timezone: undefined,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
* Who does the LIVE roster path (windowed, getNumberOfEvents:1) resolve as the
|
|
78
|
+
* first covered event at instant `at`.
|
|
79
|
+
*/
|
|
80
|
+
function windowedNextAt(
|
|
81
|
+
layer: LayerProps,
|
|
82
|
+
at: Date,
|
|
83
|
+
): CalendarEvent | undefined {
|
|
84
|
+
const util: LayerUtil = new LayerUtil();
|
|
85
|
+
return util.getMultiLayerEvents(
|
|
86
|
+
{
|
|
87
|
+
layers: [layer],
|
|
88
|
+
calendarStartDate: at,
|
|
89
|
+
calendarEndDate: OneUptimeDate.addRemoveDays(at, 20),
|
|
90
|
+
},
|
|
91
|
+
{ getNumberOfEvents: 1 },
|
|
92
|
+
)[0];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Ground-truth covering (or next) user from a full expansion.
|
|
96
|
+
function fullExpand(
|
|
97
|
+
layer: LayerProps,
|
|
98
|
+
from: Date,
|
|
99
|
+
to: Date,
|
|
100
|
+
): Array<CalendarEvent> {
|
|
101
|
+
const util: LayerUtil = new LayerUtil();
|
|
102
|
+
return util.getEvents({
|
|
103
|
+
...layer,
|
|
104
|
+
calendarStartDate: from,
|
|
105
|
+
calendarEndDate: to,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const MON_JAN6: Date = OneUptimeDate.fromString("2025-01-06T00:00:00.000Z");
|
|
110
|
+
|
|
111
|
+
describe("F2 fix: daily rotation post-window gap resolves the correct next user", () => {
|
|
112
|
+
test("x1 daily, [A,B], 09-17: evening gaps never go off-by-one over 10 days", () => {
|
|
113
|
+
const layer: LayerProps = dailyLayer({
|
|
114
|
+
users: ["A", "B"],
|
|
115
|
+
intervalCount: 1,
|
|
116
|
+
restriction: dailyRestriction(9, 17),
|
|
117
|
+
start: MON_JAN6,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const full: Array<CalendarEvent> = fullExpand(
|
|
121
|
+
layer,
|
|
122
|
+
MON_JAN6,
|
|
123
|
+
OneUptimeDate.addRemoveDays(MON_JAN6, 12),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
/*
|
|
127
|
+
* For each day, the evening (post-17:00) gap query's "next" must equal the
|
|
128
|
+
* NEXT day's covered user in the full expansion.
|
|
129
|
+
*/
|
|
130
|
+
for (let day: number = 0; day < 9; day++) {
|
|
131
|
+
const eveningGap: Date = OneUptimeDate.addRemoveHours(
|
|
132
|
+
OneUptimeDate.addRemoveDays(MON_JAN6, day),
|
|
133
|
+
20, // 20:00, after the 09-17 window closed
|
|
134
|
+
);
|
|
135
|
+
const nextCovered: CalendarEvent | undefined = full.find(
|
|
136
|
+
(e: CalendarEvent) => {
|
|
137
|
+
return OneUptimeDate.isAfter(e.start, eveningGap);
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
const windowed: CalendarEvent | undefined = windowedNextAt(
|
|
141
|
+
layer,
|
|
142
|
+
eveningGap,
|
|
143
|
+
);
|
|
144
|
+
expect(windowed?.title).toBe(nextCovered?.title);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("x1 daily, 3 users [A,B,C], 09-17: evening gap next-user matches calendar", () => {
|
|
149
|
+
const layer: LayerProps = dailyLayer({
|
|
150
|
+
users: ["A", "B", "C"],
|
|
151
|
+
intervalCount: 1,
|
|
152
|
+
restriction: dailyRestriction(9, 17),
|
|
153
|
+
start: MON_JAN6,
|
|
154
|
+
});
|
|
155
|
+
const full: Array<CalendarEvent> = fullExpand(
|
|
156
|
+
layer,
|
|
157
|
+
MON_JAN6,
|
|
158
|
+
OneUptimeDate.addRemoveDays(MON_JAN6, 12),
|
|
159
|
+
);
|
|
160
|
+
for (let day: number = 0; day < 9; day++) {
|
|
161
|
+
const eveningGap: Date = OneUptimeDate.addRemoveHours(
|
|
162
|
+
OneUptimeDate.addRemoveDays(MON_JAN6, day),
|
|
163
|
+
20,
|
|
164
|
+
);
|
|
165
|
+
const nextCovered: CalendarEvent | undefined = full.find(
|
|
166
|
+
(e: CalendarEvent) => {
|
|
167
|
+
return OneUptimeDate.isAfter(e.start, eveningGap);
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
expect(windowedNextAt(layer, eveningGap)?.title).toBe(nextCovered?.title);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("x2 daily, [A,B], 09-17: last-day evening gap advances to next user", () => {
|
|
175
|
+
const layer: LayerProps = dailyLayer({
|
|
176
|
+
users: ["A", "B"],
|
|
177
|
+
intervalCount: 2,
|
|
178
|
+
restriction: dailyRestriction(9, 17),
|
|
179
|
+
start: MON_JAN6,
|
|
180
|
+
});
|
|
181
|
+
/*
|
|
182
|
+
* Period 1 = [Jan6,Jan8) = A (covers Jan6 & Jan7). Period 2 = [Jan8,Jan10) = B.
|
|
183
|
+
* Querying on Jan7 evening (last covered day of period 1, after 17:00) must
|
|
184
|
+
* resolve the NEXT covered shift = Jan8 = B, not A.
|
|
185
|
+
*/
|
|
186
|
+
const jan7evening: Date = OneUptimeDate.fromString(
|
|
187
|
+
"2025-01-07T20:00:00.000Z",
|
|
188
|
+
);
|
|
189
|
+
const next: CalendarEvent | undefined = windowedNextAt(layer, jan7evening);
|
|
190
|
+
expect(next?.title).toBe("B");
|
|
191
|
+
expect(next?.start.getUTCDate()).toBe(8);
|
|
192
|
+
|
|
193
|
+
// Querying on Jan6 evening (still period 1, Jan7 window ahead) resolves A/Jan7.
|
|
194
|
+
const jan6evening: Date = OneUptimeDate.fromString(
|
|
195
|
+
"2025-01-06T20:00:00.000Z",
|
|
196
|
+
);
|
|
197
|
+
const next2: CalendarEvent | undefined = windowedNextAt(layer, jan6evening);
|
|
198
|
+
expect(next2?.title).toBe("A");
|
|
199
|
+
expect(next2?.start.getUTCDate()).toBe(7);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("F2 fix preserves #2413: fully-restricted periods still skip their turn", () => {
|
|
204
|
+
test("daily [A,B], Mon-Fri only (weekend fully off): weekend query resolves next Monday's user", () => {
|
|
205
|
+
/*
|
|
206
|
+
* Restriction Mon 00:00 -> Sat 00:00 via a weekly restriction => Sat & Sun
|
|
207
|
+
* are fully restricted (no coverage). Rotation must NOT advance across the
|
|
208
|
+
* dead weekend, so the sequence of covered days stays a clean A,B,A,B...
|
|
209
|
+
*/
|
|
210
|
+
const weekly: WeeklyResctriction = {
|
|
211
|
+
startDay: DayOfWeek.Monday,
|
|
212
|
+
endDay: DayOfWeek.Saturday,
|
|
213
|
+
startTime: OneUptimeDate.fromString("2025-01-06T00:00:00.000Z"),
|
|
214
|
+
endTime: OneUptimeDate.fromString("2025-01-11T00:00:00.000Z"),
|
|
215
|
+
};
|
|
216
|
+
const r: RestrictionTimes = new RestrictionTimes();
|
|
217
|
+
r.restictionType = RestrictionType.Weekly;
|
|
218
|
+
r.weeklyRestrictionTimes = [weekly];
|
|
219
|
+
|
|
220
|
+
const rot: Recurring = new Recurring();
|
|
221
|
+
rot.intervalType = EventInterval.Day;
|
|
222
|
+
rot.intervalCount = new PositiveNumber(1);
|
|
223
|
+
const layer: LayerProps = {
|
|
224
|
+
users: [user("A"), user("B")],
|
|
225
|
+
startDateTimeOfLayer: MON_JAN6,
|
|
226
|
+
restrictionTimes: r,
|
|
227
|
+
handOffTime: Recurring.getNextDateInterval(MON_JAN6, rot),
|
|
228
|
+
rotation: rot,
|
|
229
|
+
timezone: undefined,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const full: Array<CalendarEvent> = fullExpand(
|
|
233
|
+
layer,
|
|
234
|
+
MON_JAN6,
|
|
235
|
+
OneUptimeDate.addRemoveDays(MON_JAN6, 14),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
/*
|
|
239
|
+
* A Saturday-gap query (Jan 11) must resolve to the next covered day
|
|
240
|
+
* (Monday Jan 13) with the SAME user the calendar assigns it.
|
|
241
|
+
*/
|
|
242
|
+
const saturdayGap: Date = OneUptimeDate.fromString(
|
|
243
|
+
"2025-01-11T12:00:00.000Z",
|
|
244
|
+
);
|
|
245
|
+
const nextCovered: CalendarEvent | undefined = full.find(
|
|
246
|
+
(e: CalendarEvent) => {
|
|
247
|
+
return OneUptimeDate.isAfter(e.start, saturdayGap);
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
expect(nextCovered).toBeTruthy();
|
|
251
|
+
expect(windowedNextAt(layer, saturdayGap)?.title).toBe(nextCovered?.title);
|
|
252
|
+
});
|
|
253
|
+
});
|