@oneuptime/common 11.3.25 → 11.3.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/AIConversation.ts +322 -0
- package/Models/DatabaseModels/AIConversationMessage.ts +495 -0
- package/Models/DatabaseModels/AIRun.ts +584 -0
- package/Models/DatabaseModels/AIRunEvent.ts +443 -0
- package/Models/DatabaseModels/IncidentEpisodeRoleMember.ts +1 -1
- package/Models/DatabaseModels/IncidentMember.ts +1 -1
- package/Models/DatabaseModels/IncomingCallPolicy.ts +13 -0
- package/Models/DatabaseModels/Index.ts +11 -0
- package/Models/DatabaseModels/LlmLog.ts +26 -0
- package/Models/DatabaseModels/LlmProvider.ts +2 -2
- package/Models/DatabaseModels/MigrationFailure.ts +170 -0
- package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +51 -2
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +44 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +6 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.ts +6 -0
- package/Models/DatabaseModels/OnCallDutyPolicyTimeLog.ts +1 -1
- package/Models/DatabaseModels/OnCallDutyPolicyUserOverride.ts +2 -2
- package/Server/API/AIChatAPI.ts +693 -0
- package/Server/API/AlertAPI.ts +1 -1
- package/Server/API/IncidentAPI.ts +2 -2
- package/Server/API/IncidentEpisodeAPI.ts +1 -1
- package/Server/API/LlmProviderAPI.ts +170 -0
- package/Server/API/MicrosoftTeamsAPI.ts +5 -0
- package/Server/API/ScheduledMaintenanceAPI.ts +1 -1
- package/Server/API/SlackAPI.ts +663 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.ts +245 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.ts +36 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.ts +46 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.ts +119 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.ts +81 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.ts +35 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +16 -0
- package/Server/Infrastructure/PostgresDatabase.ts +11 -0
- package/Server/Services/AIConversationMessageService.ts +39 -0
- package/Server/Services/AIConversationService.ts +63 -0
- package/Server/Services/AIRunEventService.ts +39 -0
- package/Server/Services/AIRunService.ts +39 -0
- package/Server/Services/AIService.ts +127 -33
- package/Server/Services/AnalyticsDatabaseService.ts +10 -1
- package/Server/Services/DatabaseService.ts +29 -1
- package/Server/Services/IncidentService.ts +1 -1
- package/Server/Services/IncomingCallPolicyEscalationRuleService.ts +75 -2
- package/Server/Services/IncomingCallPolicyService.ts +42 -1
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/LlmProviderService.ts +110 -0
- package/Server/Services/MigrationFailureService.ts +9 -0
- package/Server/Services/MonitorProbeService.ts +33 -7
- package/Server/Services/MonitorService.ts +19 -7
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleService.ts +185 -41
- package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +23 -15
- package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +39 -0
- package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +148 -0
- package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +11 -2
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +502 -26
- package/Server/Services/OnCallDutyPolicyTimeLogService.ts +49 -7
- package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +218 -4
- package/Server/Services/ProjectCallSMSConfigService.ts +82 -1
- package/Server/Services/ProjectService.ts +24 -0
- package/Server/Services/TeamMemberService.ts +6 -0
- package/Server/Services/UserNotificationRuleService.ts +13 -42
- package/Server/Services/UserOnCallLogService.ts +90 -0
- package/Server/Services/UserOnCallLogTimelineService.ts +43 -1
- package/Server/Types/AnalyticsDatabase/ModelPermission.ts +74 -9
- package/Server/Types/Database/QueryUtil.ts +57 -0
- package/Server/Utils/AI/AIChatPrivacyFilter.ts +28 -0
- package/Server/Utils/AI/AlertAIContextBuilder.ts +6 -1
- package/Server/Utils/AI/Chat/ChatAgentRunner.ts +1066 -0
- package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +239 -0
- package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +51 -0
- package/Server/Utils/AI/IncidentAIContextBuilder.ts +18 -3
- package/Server/Utils/AI/IncidentEpisodeAIContextBuilder.ts +6 -1
- package/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.ts +12 -2
- package/Server/Utils/AI/Toolbox/AlertTools.ts +201 -0
- package/Server/Utils/AI/Toolbox/AlertWriteTools.ts +174 -0
- package/Server/Utils/AI/Toolbox/ContextTools.ts +189 -0
- package/Server/Utils/AI/Toolbox/ExceptionTools.ts +149 -0
- package/Server/Utils/AI/Toolbox/IncidentTools.ts +386 -0
- package/Server/Utils/AI/Toolbox/IncidentWriteTools.ts +350 -0
- package/Server/Utils/AI/Toolbox/Index.ts +231 -0
- package/Server/Utils/AI/Toolbox/LogTools.ts +339 -0
- package/Server/Utils/AI/Toolbox/MetricTools.ts +179 -0
- package/Server/Utils/AI/Toolbox/MonitorTools.ts +193 -0
- package/Server/Utils/AI/Toolbox/RecentChangesTools.ts +208 -0
- package/Server/Utils/AI/Toolbox/Serializer.ts +299 -0
- package/Server/Utils/AI/Toolbox/ToolTypes.ts +249 -0
- package/Server/Utils/AI/Toolbox/TraceTools.ts +402 -0
- package/Server/Utils/AI/Toolbox/WidgetBuilder.ts +189 -0
- package/Server/Utils/Database/MigrationFailureLog.ts +240 -0
- package/Server/Utils/IncomingCallPhoneNumber.ts +41 -0
- package/Server/Utils/LLM/LLMService.ts +605 -96
- package/Server/Utils/Telemetry/IoTSnapshotScan.ts +12 -9
- package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +12 -9
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +512 -19
- package/Server/Utils/Workspace/Slack/Slack.ts +80 -0
- package/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
- package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +0 -8
- package/Tests/Server/Services/OnCallDutyPolicyScheduleLayerUserReorder.test.ts +233 -0
- package/Tests/Server/Services/OnCallDutyPolicyTimeLogServiceScoping.test.ts +242 -0
- package/Tests/Server/Services/OnCallDutyPolicyUserOverrideEdit.test.ts +198 -0
- package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +495 -0
- package/Tests/Server/Types/Database/QueryUtil.test.ts +74 -0
- package/Tests/Server/Utils/AI/AIChatModelACL.test.ts +42 -0
- package/Tests/Server/Utils/AI/ChatAgentHelpers.test.ts +89 -0
- package/Tests/Server/Utils/AI/LLMServiceBaseUrl.test.ts +139 -0
- package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +386 -0
- package/Tests/Server/Utils/AI/ToolArgsGetTimeRange.test.ts +62 -0
- package/Tests/Server/Utils/AI/ToolArgsScopeServiceIds.test.ts +79 -0
- package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +223 -0
- package/Tests/Types/Billing/SubscriptionStatus.test.ts +127 -0
- package/Tests/Types/DateExhaustiveTimezone.test.ts +700 -0
- package/Tests/Types/DateTimezoneWallClock.test.ts +99 -0
- package/Tests/Types/Events/Recurring.test.ts +22 -9
- package/Tests/Types/Metrics/RecordingRuleDefinition.test.ts +213 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixes.test.ts +291 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixesRound2.test.ts +398 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeekendGap.test.ts +114 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeeklyDayShift.test.ts +76 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyMutation.test.ts +128 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyProbe.test.ts +203 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDiffFuzz.test.ts +322 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilEdgeCases.test.ts +280 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveCurrentUser.test.ts +863 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveDaily.test.ts +900 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveMultiLayer.test.ts +1104 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +957 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveWeekly.test.ts +1095 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilInvariants.test.ts +232 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +227 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMonthYearAudit.test.ts +255 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerAudit.test.ts +388 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerEdge.test.ts +300 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilOvernightDSTAudit.test.ts +158 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapFix.test.ts +253 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapRepro.test.ts +157 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilRotationFixes.test.ts +414 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilTimezone.test.ts +243 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilWeekendGapRepro.test.ts +112 -0
- package/Tests/Types/OnCallDutyPolicy/OverrideLensAudit.test.ts +117 -0
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimes.test.ts +8 -6
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimesDefaultDayAudit.test.ts +112 -0
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimesExhaustive.test.ts +821 -0
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimesMutationInvariant.test.ts +886 -0
- package/Tests/Types/OnCallDutyPolicy/UserOverrideUtil.test.ts +255 -0
- package/Tests/Types/OnCallDutyPolicy/UserOverrideUtilExhaustive.test.ts +1126 -0
- package/Types/AI/AIChatMessageRole.ts +6 -0
- package/Types/AI/AIChatMessageStatus.ts +32 -0
- package/Types/AI/AIChatPermissionMode.ts +69 -0
- package/Types/AI/AIChatTypes.ts +231 -0
- package/Types/AI/AIRunEventType.ts +17 -0
- package/Types/AI/AIRunStatus.ts +28 -0
- package/Types/AI/AIRunType.ts +6 -0
- package/Types/Date.ts +200 -15
- package/Types/LLM/LlmType.ts +6 -0
- package/Types/OnCallDutyPolicy/Layer.ts +790 -260
- package/Types/OnCallDutyPolicy/RestrictionTimes.ts +39 -13
- package/Types/OnCallDutyPolicy/UserOverrideUtil.ts +21 -5
- package/UI/Components/Events/RecurringFieldElement.tsx +16 -0
- package/UI/Components/Header/HeaderIconDropdownButton.tsx +22 -12
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +35 -3
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +8 -0
- package/UI/Components/Page/Page.tsx +14 -0
- package/UI/Components/RadioButtons/BasicRadioButtons.tsx +1 -1
- package/UI/Utils/LlmTypeDropdownOptions.ts +40 -0
- package/UI/Utils/TestLLMProvider.ts +59 -0
- package/build/dist/Models/DatabaseModels/AIConversation.js +345 -0
- package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AIConversationMessage.js +521 -0
- package/build/dist/Models/DatabaseModels/AIConversationMessage.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AIRun.js +619 -0
- package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AIRunEvent.js +469 -0
- package/build/dist/Models/DatabaseModels/AIRunEvent.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentMember.js +1 -1
- package/build/dist/Models/DatabaseModels/IncidentMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js +10 -0
- package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +10 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LlmLog.js +28 -0
- package/build/dist/Models/DatabaseModels/LlmLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LlmProvider.js +2 -2
- package/build/dist/Models/DatabaseModels/LlmProvider.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MigrationFailure.js +196 -0
- package/build/dist/Models/DatabaseModels/MigrationFailure.js.map +1 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +52 -2
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +45 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +6 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js +6 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js +2 -2
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +498 -0
- package/build/dist/Server/API/AIChatAPI.js.map +1 -0
- package/build/dist/Server/API/AlertAPI.js +1 -1
- package/build/dist/Server/API/IncidentAPI.js +2 -2
- package/build/dist/Server/API/IncidentEpisodeAPI.js +1 -1
- package/build/dist/Server/API/LlmProviderAPI.js +108 -1
- package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +4 -0
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/ScheduledMaintenanceAPI.js +1 -1
- package/build/dist/Server/API/SlackAPI.js +442 -0
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js +96 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783363279075-AddAIChatModels.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js +25 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783443471795-AddLlmProviderToAIConversation.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783453297388-AddAIChatWriteActionsAndWidgets.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js +46 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783461767405-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js +38 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js +16 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +16 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/PostgresDatabase.js +9 -0
- package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
- package/build/dist/Server/Services/AIConversationMessageService.js +34 -0
- package/build/dist/Server/Services/AIConversationMessageService.js.map +1 -0
- package/build/dist/Server/Services/AIConversationService.js +42 -0
- package/build/dist/Server/Services/AIConversationService.js.map +1 -0
- package/build/dist/Server/Services/AIRunEventService.js +34 -0
- package/build/dist/Server/Services/AIRunEventService.js.map +1 -0
- package/build/dist/Server/Services/AIRunService.js +34 -0
- package/build/dist/Server/Services/AIRunService.js.map +1 -0
- package/build/dist/Server/Services/AIService.js +84 -25
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +10 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +20 -1
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +1 -1
- package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js +55 -2
- package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js.map +1 -1
- package/build/dist/Server/Services/IncomingCallPolicyService.js +40 -0
- package/build/dist/Server/Services/IncomingCallPolicyService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/LlmProviderService.js +108 -0
- package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
- package/build/dist/Server/Services/MigrationFailureService.js +9 -0
- package/build/dist/Server/Services/MigrationFailureService.js.map +1 -0
- package/build/dist/Server/Services/MonitorProbeService.js +33 -7
- package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +12 -5
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +81 -22
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js +134 -31
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +21 -13
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +36 -0
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +121 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +11 -2
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +409 -36
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js +52 -8
- package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +170 -3
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
- package/build/dist/Server/Services/ProjectCallSMSConfigService.js +72 -0
- package/build/dist/Server/Services/ProjectCallSMSConfigService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +25 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +6 -0
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +12 -30
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallLogService.js +75 -0
- package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallLogTimelineService.js +31 -1
- package/build/dist/Server/Services/UserOnCallLogTimelineService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +50 -7
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryUtil.js +45 -0
- package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
- package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js +18 -0
- package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js.map +1 -0
- package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js +6 -1
- package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +756 -0
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +165 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +44 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -0
- package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js +18 -3
- package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js.map +1 -1
- package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js +6 -1
- package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js.map +1 -1
- package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js +12 -2
- package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +167 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js +136 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertWriteTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js +141 -0
- package/build/dist/Server/Utils/AI/Toolbox/ContextTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js +117 -0
- package/build/dist/Server/Utils/AI/Toolbox/ExceptionTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +318 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js +280 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentWriteTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +153 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/LogTools.js +246 -0
- package/build/dist/Server/Utils/AI/Toolbox/LogTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +120 -0
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js +158 -0
- package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js +165 -0
- package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +228 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js +142 -0
- package/build/dist/Server/Utils/AI/Toolbox/ToolTypes.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js +309 -0
- package/build/dist/Server/Utils/AI/Toolbox/TraceTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js +120 -0
- package/build/dist/Server/Utils/AI/Toolbox/WidgetBuilder.js.map +1 -0
- package/build/dist/Server/Utils/Database/MigrationFailureLog.js +174 -0
- package/build/dist/Server/Utils/Database/MigrationFailureLog.js.map +1 -0
- package/build/dist/Server/Utils/IncomingCallPhoneNumber.js +30 -0
- package/build/dist/Server/Utils/IncomingCallPhoneNumber.js.map +1 -0
- package/build/dist/Server/Utils/LLM/LLMService.js +452 -92
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +11 -8
- package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +11 -8
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +378 -15
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js +59 -0
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/app-manifest.json +16 -1
- package/build/dist/Types/AI/AIChatMessageRole.js +7 -0
- package/build/dist/Types/AI/AIChatMessageRole.js.map +1 -0
- package/build/dist/Types/AI/AIChatMessageStatus.js +27 -0
- package/build/dist/Types/AI/AIChatMessageStatus.js.map +1 -0
- package/build/dist/Types/AI/AIChatPermissionMode.js +53 -0
- package/build/dist/Types/AI/AIChatPermissionMode.js.map +1 -0
- package/build/dist/Types/AI/AIChatTypes.js +74 -0
- package/build/dist/Types/AI/AIChatTypes.js.map +1 -0
- package/build/dist/Types/AI/AIRunEventType.js +18 -0
- package/build/dist/Types/AI/AIRunEventType.js.map +1 -0
- package/build/dist/Types/AI/AIRunStatus.js +26 -0
- package/build/dist/Types/AI/AIRunStatus.js.map +1 -0
- package/build/dist/Types/AI/AIRunType.js +7 -0
- package/build/dist/Types/AI/AIRunType.js.map +1 -0
- package/build/dist/Types/Date.js +153 -15
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/LLM/LlmType.js +6 -0
- package/build/dist/Types/LLM/LlmType.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +602 -171
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js +27 -13
- package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js +20 -5
- package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js.map +1 -1
- package/build/dist/UI/Components/Events/RecurringFieldElement.js +13 -0
- package/build/dist/UI/Components/Events/RecurringFieldElement.js.map +1 -1
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +10 -5
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +18 -4
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/UI/Components/Page/Page.js +3 -1
- package/build/dist/UI/Components/Page/Page.js.map +1 -1
- package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js +1 -1
- package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js.map +1 -1
- package/build/dist/UI/Utils/LlmTypeDropdownOptions.js +38 -0
- package/build/dist/UI/Utils/LlmTypeDropdownOptions.js.map +1 -0
- package/build/dist/UI/Utils/TestLLMProvider.js +37 -0
- package/build/dist/UI/Utils/TestLLMProvider.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,957 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EXHAUSTIVE unrestricted-rotation correctness matrix for LayerUtil.getEvents
|
|
3
|
+
* and LayerUtil.getMultiLayerEvents.
|
|
4
|
+
*
|
|
5
|
+
* Covers the full cross product:
|
|
6
|
+
* intervalType {Hour, Day, Week, Month, Year}
|
|
7
|
+
* x intervalCount {1, 2, 3}
|
|
8
|
+
* x users {1, 2, 3, 5}
|
|
9
|
+
* x calendar windows {starting AT the layer start, starting PARTWAY through}
|
|
10
|
+
* x timezone {local (undefined), America/New_York, Asia/Kolkata}
|
|
11
|
+
*
|
|
12
|
+
* Invariants asserted per config:
|
|
13
|
+
* (a) 24/7 CONTIGUITY - each event.start == previous.end + exactly 1 second
|
|
14
|
+
* (no gaps, no overlaps, every event has positive length), over a
|
|
15
|
+
* multi-period horizon.
|
|
16
|
+
* (b) ROTATION ORDER - users cycle A,B,C,... with no skips and no repeats
|
|
17
|
+
* (the user index advances by exactly one every period, modulo the roster
|
|
18
|
+
* size), and an at-layer-start expansion begins with the first user.
|
|
19
|
+
* (c) FAIRNESS - shift counts across the roster differ by at most one over
|
|
20
|
+
* many periods, and every user takes a turn.
|
|
21
|
+
* (d) WINDOWED == FULL EXPANSION - the live "who is on call at `at`" resolution
|
|
22
|
+
* (getEvents / getMultiLayerEvents with getNumberOfEvents:1 starting the
|
|
23
|
+
* window at `at`) agrees with the covering event of a full expansion from
|
|
24
|
+
* the layer start, at many OFF-boundary instants; the windowed event is
|
|
25
|
+
* clamped to `at` and runs to the true period boundary.
|
|
26
|
+
* (e) EDGE CASES - single user (always that user), empty users (no events),
|
|
27
|
+
* calendar end before layer start (no events), and grid-aligned degenerate
|
|
28
|
+
* handoffs (handoff == layer start, handoff before layer start) resolving
|
|
29
|
+
* to the exact same rotation as the natural handoff.
|
|
30
|
+
*
|
|
31
|
+
* A dedicated block exercises America/New_York across US spring-forward and
|
|
32
|
+
* fall-back so the schedule-zone (wall-clock) stepping diverges from the
|
|
33
|
+
* process zone (run this file additionally under TZ=UTC).
|
|
34
|
+
*
|
|
35
|
+
* Everything is internally consistent (windowed vs full both flow through the
|
|
36
|
+
* same engine with the same zone), so the timezone/DST assertions hold under
|
|
37
|
+
* any process TZ. Ground-truth boundaries are always read back from the engine
|
|
38
|
+
* rather than recomputed, so the tests never depend on a hand-rolled boundary
|
|
39
|
+
* model.
|
|
40
|
+
*/
|
|
41
|
+
import LayerUtil, { LayerProps } from "../../../Types/OnCallDutyPolicy/Layer";
|
|
42
|
+
import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
|
|
43
|
+
import RestrictionTimes, {
|
|
44
|
+
RestrictionType,
|
|
45
|
+
} from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
|
|
46
|
+
import Recurring from "../../../Types/Events/Recurring";
|
|
47
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
48
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
49
|
+
import EventInterval from "../../../Types/Events/EventInterval";
|
|
50
|
+
import PositiveNumber from "../../../Types/PositiveNumber";
|
|
51
|
+
import moment from "moment-timezone";
|
|
52
|
+
|
|
53
|
+
// Documented User mock pattern (id.toString() returns the title used by events).
|
|
54
|
+
function user(id: string): User {
|
|
55
|
+
return {
|
|
56
|
+
id: {
|
|
57
|
+
toString: (): string => {
|
|
58
|
+
return id;
|
|
59
|
+
},
|
|
60
|
+
} as any,
|
|
61
|
+
} as User;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function noRestriction(): RestrictionTimes {
|
|
65
|
+
const r: RestrictionTimes = new RestrictionTimes();
|
|
66
|
+
r.restictionType = RestrictionType.None;
|
|
67
|
+
r.dayRestrictionTimes = null;
|
|
68
|
+
return r;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const NY: string = "America/New_York";
|
|
72
|
+
const KOLKATA: string = "Asia/Kolkata";
|
|
73
|
+
|
|
74
|
+
// A Monday 00:00 UTC, far from any DST transition for Day/Week horizons.
|
|
75
|
+
const FIXED_START: Date = OneUptimeDate.fromString("2025-01-06T00:00:00.000Z");
|
|
76
|
+
|
|
77
|
+
interface Cfg {
|
|
78
|
+
name: string;
|
|
79
|
+
intervalType: EventInterval;
|
|
80
|
+
count: number;
|
|
81
|
+
userIds: string[];
|
|
82
|
+
timezone: string | undefined;
|
|
83
|
+
start: Date;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/*
|
|
87
|
+
* Calendar-unit stepping that mirrors the engine (Hour absolute; others honor
|
|
88
|
+
* the schedule zone across DST, exactly like moveHandsOffTimeAfterCurrentEventStartTime).
|
|
89
|
+
*/
|
|
90
|
+
function addUnits(
|
|
91
|
+
date: Date,
|
|
92
|
+
units: number,
|
|
93
|
+
intervalType: EventInterval,
|
|
94
|
+
tz: string | undefined,
|
|
95
|
+
): Date {
|
|
96
|
+
switch (intervalType) {
|
|
97
|
+
case EventInterval.Hour:
|
|
98
|
+
return OneUptimeDate.addRemoveHours(date, units);
|
|
99
|
+
case EventInterval.Day:
|
|
100
|
+
return OneUptimeDate.addRemoveDays(date, units, tz);
|
|
101
|
+
case EventInterval.Week:
|
|
102
|
+
return OneUptimeDate.addRemoveWeeks(date, units, tz);
|
|
103
|
+
case EventInterval.Month:
|
|
104
|
+
return OneUptimeDate.addRemoveMonths(date, units, tz);
|
|
105
|
+
case EventInterval.Year:
|
|
106
|
+
return OneUptimeDate.addRemoveYears(date, units, tz);
|
|
107
|
+
default:
|
|
108
|
+
return OneUptimeDate.addRemoveDays(date, units, tz);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function rotationOf(cfg: Cfg): Recurring {
|
|
113
|
+
const rot: Recurring = new Recurring();
|
|
114
|
+
rot.intervalType = cfg.intervalType;
|
|
115
|
+
rot.intervalCount = new PositiveNumber(cfg.count);
|
|
116
|
+
return rot;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function layerOf(cfg: Cfg, handoff?: Date): LayerProps {
|
|
120
|
+
const rot: Recurring = rotationOf(cfg);
|
|
121
|
+
return {
|
|
122
|
+
users: cfg.userIds.map(user),
|
|
123
|
+
startDateTimeOfLayer: cfg.start,
|
|
124
|
+
restrictionTimes: noRestriction(),
|
|
125
|
+
handOffTime: handoff ?? Recurring.getNextDateInterval(cfg.start, rot),
|
|
126
|
+
rotation: rot,
|
|
127
|
+
timezone: cfg.timezone,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Target number of full-period events to expand per interval (bounds horizon).
|
|
132
|
+
const NUM_FULL: Record<EventInterval, number> = {
|
|
133
|
+
[EventInterval.Hour]: 26,
|
|
134
|
+
[EventInterval.Day]: 26,
|
|
135
|
+
[EventInterval.Week]: 20,
|
|
136
|
+
[EventInterval.Month]: 16,
|
|
137
|
+
[EventInterval.Year]: 12,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/*
|
|
141
|
+
* Expand `numFull` full-period events from the layer start. The horizon end is
|
|
142
|
+
* pushed several periods past the target so the returned slice is entirely
|
|
143
|
+
* full periods whose end timestamps are REAL engine boundaries (the trailing
|
|
144
|
+
* artifact event that getEvents can emit when the window end lands on a
|
|
145
|
+
* boundary, if any, falls beyond the slice and is discarded).
|
|
146
|
+
*/
|
|
147
|
+
function expandFull(cfg: Cfg, numFull: number): CalendarEvent[] {
|
|
148
|
+
const layer: LayerProps = layerOf(cfg);
|
|
149
|
+
const util: LayerUtil = new LayerUtil();
|
|
150
|
+
const horizonEnd: Date = addUnits(
|
|
151
|
+
cfg.start,
|
|
152
|
+
(numFull + 6) * cfg.count,
|
|
153
|
+
cfg.intervalType,
|
|
154
|
+
cfg.timezone,
|
|
155
|
+
);
|
|
156
|
+
const events: CalendarEvent[] = util.getEvents({
|
|
157
|
+
...layer,
|
|
158
|
+
calendarStartDate: cfg.start,
|
|
159
|
+
calendarEndDate: horizonEnd,
|
|
160
|
+
});
|
|
161
|
+
return events.slice(0, numFull);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function titleIndex(cfg: Cfg, title: string): number {
|
|
165
|
+
return cfg.userIds.indexOf(title);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// An interior (strictly off-boundary) instant of an event, at fraction f in (0,1).
|
|
169
|
+
function interiorInstant(e: CalendarEvent, f: number): Date {
|
|
170
|
+
const span: number = e.end.getTime() - e.start.getTime();
|
|
171
|
+
return new Date(e.start.getTime() + Math.floor(f * span));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---- invariant checkers (return human-readable problems; empty == pass) ----
|
|
175
|
+
|
|
176
|
+
function contiguityProblems(events: CalendarEvent[]): string[] {
|
|
177
|
+
const problems: string[] = [];
|
|
178
|
+
for (let i: number = 0; i < events.length; i++) {
|
|
179
|
+
const e: CalendarEvent = events[i]!;
|
|
180
|
+
if (!OneUptimeDate.isAfter(e.end, e.start)) {
|
|
181
|
+
problems.push(
|
|
182
|
+
`event ${i} non-positive length ${e.start.toISOString()}->${e.end.toISOString()}`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (i > 0) {
|
|
186
|
+
const prev: CalendarEvent = events[i - 1]!;
|
|
187
|
+
const gapSeconds: number = OneUptimeDate.getSecondsBetweenTwoDates(
|
|
188
|
+
prev.end,
|
|
189
|
+
e.start,
|
|
190
|
+
);
|
|
191
|
+
// exactly 1s handoff gap => no overlap (would be <=0) and no gap (>1).
|
|
192
|
+
if (gapSeconds !== 1) {
|
|
193
|
+
problems.push(
|
|
194
|
+
`event ${i} handoff gap ${gapSeconds}s (expected 1) prevEnd=${prev.end.toISOString()} start=${e.start.toISOString()}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return problems;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function orderProblems(
|
|
203
|
+
events: CalendarEvent[],
|
|
204
|
+
cfg: Cfg,
|
|
205
|
+
expectedFirstIndex: number | undefined,
|
|
206
|
+
): string[] {
|
|
207
|
+
const problems: string[] = [];
|
|
208
|
+
const k: number = cfg.userIds.length;
|
|
209
|
+
if (events.length === 0) {
|
|
210
|
+
return problems;
|
|
211
|
+
}
|
|
212
|
+
const idx0: number = titleIndex(cfg, events[0]!.title);
|
|
213
|
+
if (idx0 < 0) {
|
|
214
|
+
problems.push(`event 0 unknown title ${events[0]!.title}`);
|
|
215
|
+
return problems;
|
|
216
|
+
}
|
|
217
|
+
if (expectedFirstIndex !== undefined && idx0 !== expectedFirstIndex) {
|
|
218
|
+
problems.push(`first user index ${idx0} != expected ${expectedFirstIndex}`);
|
|
219
|
+
}
|
|
220
|
+
for (let i: number = 1; i < events.length; i++) {
|
|
221
|
+
const idx: number = titleIndex(cfg, events[i]!.title);
|
|
222
|
+
const expected: number = (idx0 + i) % k;
|
|
223
|
+
if (idx !== expected) {
|
|
224
|
+
problems.push(
|
|
225
|
+
`event ${i} user index ${idx} != expected ${expected} (title ${events[i]!.title})`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return problems;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function fairnessProblems(events: CalendarEvent[], cfg: Cfg): string[] {
|
|
233
|
+
const problems: string[] = [];
|
|
234
|
+
const counts: Map<string, number> = new Map<string, number>();
|
|
235
|
+
for (const e of events) {
|
|
236
|
+
counts.set(e.title, (counts.get(e.title) || 0) + 1);
|
|
237
|
+
if (titleIndex(cfg, e.title) < 0) {
|
|
238
|
+
problems.push(`unexpected on-call title ${e.title}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const values: number[] = Array.from(counts.values());
|
|
242
|
+
if (values.length > 0) {
|
|
243
|
+
const max: number = Math.max(...values);
|
|
244
|
+
const min: number = Math.min(...values);
|
|
245
|
+
if (max - min > 1) {
|
|
246
|
+
problems.push(`shift-count spread ${min}..${max} exceeds 1`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Over a horizon >= roster size, every user must take a turn.
|
|
250
|
+
if (events.length >= cfg.userIds.length) {
|
|
251
|
+
for (const id of cfg.userIds) {
|
|
252
|
+
if (!counts.has(id)) {
|
|
253
|
+
problems.push(`user ${id} never on-call over ${events.length} periods`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return problems;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---------------------------- matrix ----------------------------
|
|
261
|
+
|
|
262
|
+
const INTERVALS: EventInterval[] = [
|
|
263
|
+
EventInterval.Hour,
|
|
264
|
+
EventInterval.Day,
|
|
265
|
+
EventInterval.Week,
|
|
266
|
+
EventInterval.Month,
|
|
267
|
+
EventInterval.Year,
|
|
268
|
+
];
|
|
269
|
+
const COUNTS: number[] = [1, 2, 3];
|
|
270
|
+
const USER_SETS: string[][] = [
|
|
271
|
+
["A"],
|
|
272
|
+
["A", "B"],
|
|
273
|
+
["A", "B", "C"],
|
|
274
|
+
["A", "B", "C", "D", "E"],
|
|
275
|
+
];
|
|
276
|
+
const TIMEZONES: (string | undefined)[] = [undefined, NY, KOLKATA];
|
|
277
|
+
|
|
278
|
+
function buildMatrix(): Cfg[] {
|
|
279
|
+
const cfgs: Cfg[] = [];
|
|
280
|
+
for (const it of INTERVALS) {
|
|
281
|
+
for (const count of COUNTS) {
|
|
282
|
+
for (const users of USER_SETS) {
|
|
283
|
+
for (const tz of TIMEZONES) {
|
|
284
|
+
cfgs.push({
|
|
285
|
+
name: `${count}x${it} users=${users.length} tz=${tz ?? "local"}`,
|
|
286
|
+
intervalType: it,
|
|
287
|
+
count,
|
|
288
|
+
userIds: users,
|
|
289
|
+
timezone: tz,
|
|
290
|
+
start: FIXED_START,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return cfgs;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const MATRIX: Cfg[] = buildMatrix();
|
|
300
|
+
|
|
301
|
+
/*
|
|
302
|
+
* ================================================================
|
|
303
|
+
* (a)(b)(c) contiguity + rotation order + fairness, window AT layer start.
|
|
304
|
+
* ================================================================
|
|
305
|
+
*/
|
|
306
|
+
describe("Unrestricted rotation: 24/7 contiguity + order + fairness (window AT layer start)", () => {
|
|
307
|
+
for (const cfg of MATRIX) {
|
|
308
|
+
it(`${cfg.name}`, () => {
|
|
309
|
+
const numFull: number = NUM_FULL[cfg.intervalType];
|
|
310
|
+
const events: CalendarEvent[] = expandFull(cfg, numFull);
|
|
311
|
+
|
|
312
|
+
// Sanity: we actually got the requested number of full events.
|
|
313
|
+
expect(events.length).toBe(numFull);
|
|
314
|
+
|
|
315
|
+
// (a) contiguity / positive length / no gaps or overlaps.
|
|
316
|
+
expect({ cfg: cfg.name, problems: contiguityProblems(events) }).toEqual({
|
|
317
|
+
cfg: cfg.name,
|
|
318
|
+
problems: [],
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// (b) rotation order begins at the first user and never skips/repeats.
|
|
322
|
+
expect({
|
|
323
|
+
cfg: cfg.name,
|
|
324
|
+
problems: orderProblems(events, cfg, 0),
|
|
325
|
+
}).toEqual({ cfg: cfg.name, problems: [] });
|
|
326
|
+
|
|
327
|
+
// (c) fairness across the roster.
|
|
328
|
+
expect({
|
|
329
|
+
cfg: cfg.name,
|
|
330
|
+
problems: fairnessProblems(events, cfg),
|
|
331
|
+
}).toEqual({ cfg: cfg.name, problems: [] });
|
|
332
|
+
|
|
333
|
+
// The layer start owns the first period.
|
|
334
|
+
expect(events[0]!.start.getTime()).toBe(cfg.start.getTime());
|
|
335
|
+
// Single-user rotations are always that user.
|
|
336
|
+
if (cfg.userIds.length === 1) {
|
|
337
|
+
expect(
|
|
338
|
+
events.every((e: CalendarEvent) => {
|
|
339
|
+
return e.title === "A";
|
|
340
|
+
}),
|
|
341
|
+
).toBe(true);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
/*
|
|
348
|
+
* ================================================================
|
|
349
|
+
* (a)(b) contiguity + consecutive order for a window that starts PARTWAY
|
|
350
|
+
* through a period (the windowed/live path). First event is clamped to the
|
|
351
|
+
* window start and carries the correct in-progress user.
|
|
352
|
+
* ================================================================
|
|
353
|
+
*/
|
|
354
|
+
describe("Unrestricted rotation: contiguity + order for a PARTWAY (windowed) calendar start", () => {
|
|
355
|
+
for (const cfg of MATRIX) {
|
|
356
|
+
it(`${cfg.name}`, () => {
|
|
357
|
+
const numFull: number = NUM_FULL[cfg.intervalType];
|
|
358
|
+
const full: CalendarEvent[] = expandFull(cfg, numFull);
|
|
359
|
+
|
|
360
|
+
// Start partway through period p; span M subsequent periods.
|
|
361
|
+
const p: number = Math.min(3, numFull - 8);
|
|
362
|
+
const startIdx: number = p < 0 ? 0 : p;
|
|
363
|
+
const spanPeriods: number = Math.min(12, numFull - startIdx - 2);
|
|
364
|
+
|
|
365
|
+
const windowStart: Date = interiorInstant(full[startIdx]!, 0.4);
|
|
366
|
+
const windowEnd: Date = interiorInstant(
|
|
367
|
+
full[startIdx + spanPeriods]!,
|
|
368
|
+
0.6,
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
const util: LayerUtil = new LayerUtil();
|
|
372
|
+
const events: CalendarEvent[] = util.getEvents({
|
|
373
|
+
...layerOf(cfg),
|
|
374
|
+
calendarStartDate: windowStart,
|
|
375
|
+
calendarEndDate: windowEnd,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
expect(events.length).toBeGreaterThan(0);
|
|
379
|
+
|
|
380
|
+
// First event is clamped to the window start and is the in-progress user.
|
|
381
|
+
expect(events[0]!.start.getTime()).toBe(windowStart.getTime());
|
|
382
|
+
expect(events[0]!.title).toBe(full[startIdx]!.title);
|
|
383
|
+
// ...and runs to that period's true boundary.
|
|
384
|
+
expect(events[0]!.end.getTime()).toBe(full[startIdx]!.end.getTime());
|
|
385
|
+
// Last event is clamped to the window end.
|
|
386
|
+
expect(events[events.length - 1]!.end.getTime()).toBe(
|
|
387
|
+
windowEnd.getTime(),
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
// (a) contiguity.
|
|
391
|
+
expect({ cfg: cfg.name, problems: contiguityProblems(events) }).toEqual({
|
|
392
|
+
cfg: cfg.name,
|
|
393
|
+
problems: [],
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
/*
|
|
397
|
+
* (b) consecutive rotation advancement (no skips/repeats), anchored on the
|
|
398
|
+
* in-progress user (partway start need not begin at index 0).
|
|
399
|
+
*/
|
|
400
|
+
const expectedFirst: number = titleIndex(cfg, full[startIdx]!.title);
|
|
401
|
+
expect({
|
|
402
|
+
cfg: cfg.name,
|
|
403
|
+
problems: orderProblems(events, cfg, expectedFirst),
|
|
404
|
+
}).toEqual({ cfg: cfg.name, problems: [] });
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
/*
|
|
410
|
+
* ================================================================
|
|
411
|
+
* (d) windowed resolution == full-expansion cover at many OFF-boundary instants,
|
|
412
|
+
* through BOTH getEvents and getMultiLayerEvents.
|
|
413
|
+
* ================================================================
|
|
414
|
+
*/
|
|
415
|
+
function sampleIndices(n: number): number[] {
|
|
416
|
+
const raw: number[] = [0, 1, Math.floor(n / 2), n - 2, n - 1];
|
|
417
|
+
const seen: Set<number> = new Set<number>();
|
|
418
|
+
const out: number[] = [];
|
|
419
|
+
for (const i of raw) {
|
|
420
|
+
if (i >= 0 && i < n && !seen.has(i)) {
|
|
421
|
+
seen.add(i);
|
|
422
|
+
out.push(i);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
describe("Unrestricted rotation: windowed current-user == full-expansion cover (OFF-boundary)", () => {
|
|
429
|
+
// Two off-boundary probes per sampled period (early and late in the period).
|
|
430
|
+
const FRACTIONS: number[] = [0.35, 0.72];
|
|
431
|
+
|
|
432
|
+
for (const cfg of MATRIX) {
|
|
433
|
+
it(`${cfg.name}`, () => {
|
|
434
|
+
const numFull: number = NUM_FULL[cfg.intervalType];
|
|
435
|
+
const full: CalendarEvent[] = expandFull(cfg, numFull);
|
|
436
|
+
const util: LayerUtil = new LayerUtil();
|
|
437
|
+
const problems: string[] = [];
|
|
438
|
+
const indices: number[] = sampleIndices(numFull);
|
|
439
|
+
|
|
440
|
+
for (let s: number = 0; s < indices.length; s++) {
|
|
441
|
+
const idx: number = indices[s]!;
|
|
442
|
+
const cover: CalendarEvent = full[idx]!;
|
|
443
|
+
for (const f of FRACTIONS) {
|
|
444
|
+
const at: Date = interiorInstant(cover, f);
|
|
445
|
+
// strictly interior guard.
|
|
446
|
+
if (
|
|
447
|
+
!OneUptimeDate.isAfter(at, cover.start) ||
|
|
448
|
+
!OneUptimeDate.isBefore(at, cover.end)
|
|
449
|
+
) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
const windowEnd: Date = addUnits(
|
|
453
|
+
at,
|
|
454
|
+
4 * cfg.count,
|
|
455
|
+
cfg.intervalType,
|
|
456
|
+
cfg.timezone,
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
// getEvents windowed - the live "who is on call at `at`" path.
|
|
460
|
+
const windowed: CalendarEvent[] = util.getEvents(
|
|
461
|
+
{
|
|
462
|
+
...layerOf(cfg),
|
|
463
|
+
calendarStartDate: at,
|
|
464
|
+
calendarEndDate: windowEnd,
|
|
465
|
+
},
|
|
466
|
+
{ getNumberOfEvents: 1 },
|
|
467
|
+
);
|
|
468
|
+
if (windowed.length !== 1) {
|
|
469
|
+
problems.push(
|
|
470
|
+
`at=${at.toISOString()} getEvents returned ${windowed.length}`,
|
|
471
|
+
);
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if (windowed[0]!.title !== cover.title) {
|
|
475
|
+
problems.push(
|
|
476
|
+
`at=${at.toISOString()} getEvents title ${windowed[0]!.title} != cover ${cover.title}`,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
if (windowed[0]!.start.getTime() !== at.getTime()) {
|
|
480
|
+
problems.push(
|
|
481
|
+
`at=${at.toISOString()} getEvents start not clamped to instant (${windowed[0]!.start.toISOString()})`,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
if (windowed[0]!.end.getTime() !== cover.end.getTime()) {
|
|
485
|
+
problems.push(
|
|
486
|
+
`at=${at.toISOString()} getEvents end ${windowed[0]!.end.toISOString()} != boundary ${cover.end.toISOString()}`,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/*
|
|
491
|
+
* getMultiLayerEvents windowed (single layer) must agree on the user.
|
|
492
|
+
* Checked once per config (first probe) to keep runtime bounded; the
|
|
493
|
+
* dedicated parity/windowed blocks below cover getMultiLayerEvents more.
|
|
494
|
+
*/
|
|
495
|
+
if (s === 0 && f === FRACTIONS[0]) {
|
|
496
|
+
const multi: CalendarEvent[] = util.getMultiLayerEvents(
|
|
497
|
+
{
|
|
498
|
+
layers: [layerOf(cfg)],
|
|
499
|
+
calendarStartDate: at,
|
|
500
|
+
calendarEndDate: windowEnd,
|
|
501
|
+
},
|
|
502
|
+
{ getNumberOfEvents: 1 },
|
|
503
|
+
);
|
|
504
|
+
if (multi.length !== 1 || multi[0]!.title !== cover.title) {
|
|
505
|
+
problems.push(
|
|
506
|
+
`at=${at.toISOString()} getMultiLayerEvents title ${multi[0]?.title} != cover ${cover.title}`,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
expect({ cfg: cfg.name, problems }).toEqual({
|
|
514
|
+
cfg: cfg.name,
|
|
515
|
+
problems: [],
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
/*
|
|
522
|
+
* ================================================================
|
|
523
|
+
* getMultiLayerEvents structural parity + priority shadowing.
|
|
524
|
+
* ================================================================
|
|
525
|
+
*/
|
|
526
|
+
describe("getMultiLayerEvents: single-layer parity with getEvents (interior window)", () => {
|
|
527
|
+
const parityCfgs: Cfg[] = MATRIX.filter((c: Cfg) => {
|
|
528
|
+
return c.timezone === undefined && c.count <= 2;
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
for (const cfg of parityCfgs) {
|
|
532
|
+
it(`${cfg.name}`, () => {
|
|
533
|
+
const numFull: number = NUM_FULL[cfg.intervalType];
|
|
534
|
+
const full: CalendarEvent[] = expandFull(cfg, numFull);
|
|
535
|
+
/*
|
|
536
|
+
* interior window: [start, interior of a later period] => no boundary-aligned
|
|
537
|
+
* trailing artifact, so getEvents and getMultiLayerEvents must match exactly.
|
|
538
|
+
*/
|
|
539
|
+
const endIdx: number = Math.min(10, numFull - 1);
|
|
540
|
+
const calEnd: Date = interiorInstant(full[endIdx]!, 0.5);
|
|
541
|
+
const util: LayerUtil = new LayerUtil();
|
|
542
|
+
|
|
543
|
+
const single: CalendarEvent[] = util.getEvents({
|
|
544
|
+
...layerOf(cfg),
|
|
545
|
+
calendarStartDate: cfg.start,
|
|
546
|
+
calendarEndDate: calEnd,
|
|
547
|
+
});
|
|
548
|
+
const multi: CalendarEvent[] = util.getMultiLayerEvents({
|
|
549
|
+
layers: [layerOf(cfg)],
|
|
550
|
+
calendarStartDate: cfg.start,
|
|
551
|
+
calendarEndDate: calEnd,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
expect(multi.length).toBe(single.length);
|
|
555
|
+
for (let i: number = 0; i < single.length; i++) {
|
|
556
|
+
expect(multi[i]!.title).toBe(single[i]!.title);
|
|
557
|
+
expect(multi[i]!.start.getTime()).toBe(single[i]!.start.getTime());
|
|
558
|
+
expect(multi[i]!.end.getTime()).toBe(single[i]!.end.getTime());
|
|
559
|
+
}
|
|
560
|
+
// And the merged output is itself contiguous.
|
|
561
|
+
expect(contiguityProblems(multi)).toEqual([]);
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
describe("getMultiLayerEvents: a 24/7 higher-priority layer fully shadows a lower-priority layer", () => {
|
|
567
|
+
it("Day x1 primary [A,B] over fallback [X,Y,Z] => only primary users, contiguous", () => {
|
|
568
|
+
const primary: Cfg = {
|
|
569
|
+
name: "primary",
|
|
570
|
+
intervalType: EventInterval.Day,
|
|
571
|
+
count: 1,
|
|
572
|
+
userIds: ["A", "B"],
|
|
573
|
+
timezone: undefined,
|
|
574
|
+
start: FIXED_START,
|
|
575
|
+
};
|
|
576
|
+
const fallback: Cfg = {
|
|
577
|
+
name: "fallback",
|
|
578
|
+
intervalType: EventInterval.Day,
|
|
579
|
+
count: 1,
|
|
580
|
+
userIds: ["X", "Y", "Z"],
|
|
581
|
+
timezone: undefined,
|
|
582
|
+
start: FIXED_START,
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const full: CalendarEvent[] = expandFull(primary, 12);
|
|
586
|
+
const calEnd: Date = interiorInstant(full[10]!, 0.5);
|
|
587
|
+
const util: LayerUtil = new LayerUtil();
|
|
588
|
+
|
|
589
|
+
const merged: CalendarEvent[] = util.getMultiLayerEvents({
|
|
590
|
+
layers: [layerOf(primary), layerOf(fallback)],
|
|
591
|
+
calendarStartDate: FIXED_START,
|
|
592
|
+
calendarEndDate: calEnd,
|
|
593
|
+
});
|
|
594
|
+
const primaryOnly: CalendarEvent[] = util.getMultiLayerEvents({
|
|
595
|
+
layers: [layerOf(primary)],
|
|
596
|
+
calendarStartDate: FIXED_START,
|
|
597
|
+
calendarEndDate: calEnd,
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// Fallback is completely shadowed by the 24/7 primary.
|
|
601
|
+
for (const e of merged) {
|
|
602
|
+
expect(["A", "B"]).toContain(e.title);
|
|
603
|
+
}
|
|
604
|
+
// Merged == primary alone.
|
|
605
|
+
expect(merged.length).toBe(primaryOnly.length);
|
|
606
|
+
for (let i: number = 0; i < merged.length; i++) {
|
|
607
|
+
expect(merged[i]!.title).toBe(primaryOnly[i]!.title);
|
|
608
|
+
expect(merged[i]!.start.getTime()).toBe(primaryOnly[i]!.start.getTime());
|
|
609
|
+
expect(merged[i]!.end.getTime()).toBe(primaryOnly[i]!.end.getTime());
|
|
610
|
+
}
|
|
611
|
+
expect(contiguityProblems(merged)).toEqual([]);
|
|
612
|
+
expect(orderProblems(merged, primary, 0)).toEqual([]);
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
/*
|
|
617
|
+
* ================================================================
|
|
618
|
+
* (e) edge cases.
|
|
619
|
+
* ================================================================
|
|
620
|
+
*/
|
|
621
|
+
describe("Edge cases", () => {
|
|
622
|
+
it("empty users => no events", () => {
|
|
623
|
+
const cfg: Cfg = {
|
|
624
|
+
name: "empty",
|
|
625
|
+
intervalType: EventInterval.Day,
|
|
626
|
+
count: 1,
|
|
627
|
+
userIds: [],
|
|
628
|
+
timezone: undefined,
|
|
629
|
+
start: FIXED_START,
|
|
630
|
+
};
|
|
631
|
+
const util: LayerUtil = new LayerUtil();
|
|
632
|
+
const events: CalendarEvent[] = util.getEvents({
|
|
633
|
+
...layerOf(cfg),
|
|
634
|
+
calendarStartDate: FIXED_START,
|
|
635
|
+
calendarEndDate: OneUptimeDate.addRemoveDays(FIXED_START, 10),
|
|
636
|
+
});
|
|
637
|
+
expect(events).toEqual([]);
|
|
638
|
+
const multi: CalendarEvent[] = util.getMultiLayerEvents({
|
|
639
|
+
layers: [layerOf(cfg)],
|
|
640
|
+
calendarStartDate: FIXED_START,
|
|
641
|
+
calendarEndDate: OneUptimeDate.addRemoveDays(FIXED_START, 10),
|
|
642
|
+
});
|
|
643
|
+
expect(multi).toEqual([]);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it("calendar end strictly before layer start => no events", () => {
|
|
647
|
+
for (const it of INTERVALS) {
|
|
648
|
+
const cfg: Cfg = {
|
|
649
|
+
name: `calEndBefore-${it}`,
|
|
650
|
+
intervalType: it,
|
|
651
|
+
count: 1,
|
|
652
|
+
userIds: ["A", "B"],
|
|
653
|
+
timezone: undefined,
|
|
654
|
+
start: FIXED_START,
|
|
655
|
+
};
|
|
656
|
+
const util: LayerUtil = new LayerUtil();
|
|
657
|
+
const events: CalendarEvent[] = util.getEvents({
|
|
658
|
+
...layerOf(cfg),
|
|
659
|
+
calendarStartDate: OneUptimeDate.addRemoveDays(FIXED_START, -30),
|
|
660
|
+
calendarEndDate: OneUptimeDate.addRemoveDays(FIXED_START, -5),
|
|
661
|
+
});
|
|
662
|
+
expect(events).toEqual([]);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it("calendar window entirely before layer start (both ends before) => no events", () => {
|
|
667
|
+
const cfg: Cfg = {
|
|
668
|
+
name: "before",
|
|
669
|
+
intervalType: EventInterval.Hour,
|
|
670
|
+
count: 2,
|
|
671
|
+
userIds: ["A", "B", "C"],
|
|
672
|
+
timezone: undefined,
|
|
673
|
+
start: FIXED_START,
|
|
674
|
+
};
|
|
675
|
+
const util: LayerUtil = new LayerUtil();
|
|
676
|
+
const events: CalendarEvent[] = util.getEvents({
|
|
677
|
+
...layerOf(cfg),
|
|
678
|
+
calendarStartDate: OneUptimeDate.addRemoveHours(FIXED_START, -50),
|
|
679
|
+
calendarEndDate: OneUptimeDate.addRemoveHours(FIXED_START, -1),
|
|
680
|
+
});
|
|
681
|
+
expect(events).toEqual([]);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it("single user => always that user across every interval/count", () => {
|
|
685
|
+
for (const it of INTERVALS) {
|
|
686
|
+
for (const count of COUNTS) {
|
|
687
|
+
const cfg: Cfg = {
|
|
688
|
+
name: `single-${count}x${it}`,
|
|
689
|
+
intervalType: it,
|
|
690
|
+
count,
|
|
691
|
+
userIds: ["SOLO"],
|
|
692
|
+
timezone: undefined,
|
|
693
|
+
start: FIXED_START,
|
|
694
|
+
};
|
|
695
|
+
const events: CalendarEvent[] = expandFull(cfg, 10);
|
|
696
|
+
expect(events.length).toBe(10);
|
|
697
|
+
expect(
|
|
698
|
+
events.every((e: CalendarEvent) => {
|
|
699
|
+
return e.title === "SOLO";
|
|
700
|
+
}),
|
|
701
|
+
).toBe(true);
|
|
702
|
+
expect(contiguityProblems(events)).toEqual([]);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it("grid-aligned degenerate handoffs (== start, and before start) equal the natural rotation", () => {
|
|
708
|
+
const intervals: EventInterval[] = [
|
|
709
|
+
EventInterval.Hour,
|
|
710
|
+
EventInterval.Day,
|
|
711
|
+
EventInterval.Week,
|
|
712
|
+
EventInterval.Month,
|
|
713
|
+
];
|
|
714
|
+
for (const it of intervals) {
|
|
715
|
+
for (const count of [1, 2]) {
|
|
716
|
+
const cfg: Cfg = {
|
|
717
|
+
name: `${count}x${it}`,
|
|
718
|
+
intervalType: it,
|
|
719
|
+
count,
|
|
720
|
+
userIds: ["A", "B", "C"],
|
|
721
|
+
timezone: undefined,
|
|
722
|
+
start: FIXED_START,
|
|
723
|
+
};
|
|
724
|
+
// interior end derived from the natural expansion (no boundary artifact).
|
|
725
|
+
const naturalFull: CalendarEvent[] = expandFull(cfg, 10);
|
|
726
|
+
const calEnd: Date = interiorInstant(naturalFull[8]!, 0.5);
|
|
727
|
+
const util: LayerUtil = new LayerUtil();
|
|
728
|
+
|
|
729
|
+
const natural: CalendarEvent[] = util.getEvents({
|
|
730
|
+
...layerOf(cfg),
|
|
731
|
+
calendarStartDate: cfg.start,
|
|
732
|
+
calendarEndDate: calEnd,
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
/*
|
|
736
|
+
* handoff == layer start (k=0), and grid-aligned handoffs before the
|
|
737
|
+
* layer start (k = -1, -2, -3 periods) must produce the SAME rotation.
|
|
738
|
+
*/
|
|
739
|
+
for (const k of [0, 1, 2, 3]) {
|
|
740
|
+
const handoff: Date = addUnits(
|
|
741
|
+
cfg.start,
|
|
742
|
+
-k * count,
|
|
743
|
+
it,
|
|
744
|
+
cfg.timezone,
|
|
745
|
+
);
|
|
746
|
+
const events: CalendarEvent[] = util.getEvents({
|
|
747
|
+
...layerOf(cfg, handoff),
|
|
748
|
+
calendarStartDate: cfg.start,
|
|
749
|
+
calendarEndDate: calEnd,
|
|
750
|
+
});
|
|
751
|
+
expect(events.length).toBe(natural.length);
|
|
752
|
+
for (let i: number = 0; i < natural.length; i++) {
|
|
753
|
+
expect(events[i]!.title).toBe(natural[i]!.title);
|
|
754
|
+
expect(events[i]!.start.getTime()).toBe(
|
|
755
|
+
natural[i]!.start.getTime(),
|
|
756
|
+
);
|
|
757
|
+
expect(events[i]!.end.getTime()).toBe(natural[i]!.end.getTime());
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it("NOTE: current behavior - getEvents emits a trailing negative-length event when the window end lands exactly on a rotation boundary; getMultiLayerEvents filters it out", () => {
|
|
765
|
+
const cfg: Cfg = {
|
|
766
|
+
name: "boundary",
|
|
767
|
+
intervalType: EventInterval.Day,
|
|
768
|
+
count: 1,
|
|
769
|
+
userIds: ["A", "B"],
|
|
770
|
+
timezone: undefined,
|
|
771
|
+
start: FIXED_START,
|
|
772
|
+
};
|
|
773
|
+
const util: LayerUtil = new LayerUtil();
|
|
774
|
+
// 3 days == exactly a rotation boundary for a daily x1 rotation.
|
|
775
|
+
const calEnd: Date = OneUptimeDate.addRemoveDays(FIXED_START, 3);
|
|
776
|
+
|
|
777
|
+
const raw: CalendarEvent[] = util.getEvents({
|
|
778
|
+
...layerOf(cfg),
|
|
779
|
+
calendarStartDate: FIXED_START,
|
|
780
|
+
calendarEndDate: calEnd,
|
|
781
|
+
});
|
|
782
|
+
const last: CalendarEvent = raw[raw.length - 1]!;
|
|
783
|
+
/*
|
|
784
|
+
* NOTE: current behavior - the final event is inverted (end 1s before start).
|
|
785
|
+
* This is an internal artifact; both production consumers go through
|
|
786
|
+
* getMultiLayerEvents, which removes it (see assertion below). Not a
|
|
787
|
+
* user-observable defect, so it is documented rather than reported.
|
|
788
|
+
*/
|
|
789
|
+
expect(OneUptimeDate.isBefore(last.end, last.start)).toBe(true);
|
|
790
|
+
|
|
791
|
+
const multi: CalendarEvent[] = util.getMultiLayerEvents({
|
|
792
|
+
layers: [layerOf(cfg)],
|
|
793
|
+
calendarStartDate: FIXED_START,
|
|
794
|
+
calendarEndDate: calEnd,
|
|
795
|
+
});
|
|
796
|
+
// No inverted / zero-length events survive the merge.
|
|
797
|
+
for (const e of multi) {
|
|
798
|
+
expect(OneUptimeDate.isAfter(e.end, e.start)).toBe(true);
|
|
799
|
+
}
|
|
800
|
+
expect(contiguityProblems(multi)).toEqual([]);
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
/*
|
|
805
|
+
* ================================================================
|
|
806
|
+
* DST / server-vs-schedule divergence (America/New_York). Run also under TZ=UTC.
|
|
807
|
+
* ================================================================
|
|
808
|
+
*/
|
|
809
|
+
describe("America/New_York rotation across DST transitions (schedule-zone wall-clock preserved)", () => {
|
|
810
|
+
// 2025-03-08 00:00 New York (EST) - one day before US spring-forward.
|
|
811
|
+
const SPRING_START: Date = moment.tz("2025-03-08 00:00", NY).toDate();
|
|
812
|
+
// 2025-11-01 00:00 New York (EDT) - one day before US fall-back.
|
|
813
|
+
const FALL_START: Date = moment.tz("2025-11-01 00:00", NY).toDate();
|
|
814
|
+
|
|
815
|
+
const scenarios: {
|
|
816
|
+
label: string;
|
|
817
|
+
start: Date;
|
|
818
|
+
it: EventInterval;
|
|
819
|
+
count: number;
|
|
820
|
+
}[] = [
|
|
821
|
+
{
|
|
822
|
+
label: "spring daily x1",
|
|
823
|
+
start: SPRING_START,
|
|
824
|
+
it: EventInterval.Day,
|
|
825
|
+
count: 1,
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
label: "spring daily x2",
|
|
829
|
+
start: SPRING_START,
|
|
830
|
+
it: EventInterval.Day,
|
|
831
|
+
count: 2,
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
label: "spring weekly x1",
|
|
835
|
+
start: SPRING_START,
|
|
836
|
+
it: EventInterval.Week,
|
|
837
|
+
count: 1,
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
label: "spring hourly x1",
|
|
841
|
+
start: SPRING_START,
|
|
842
|
+
it: EventInterval.Hour,
|
|
843
|
+
count: 1,
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
label: "fall daily x1",
|
|
847
|
+
start: FALL_START,
|
|
848
|
+
it: EventInterval.Day,
|
|
849
|
+
count: 1,
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
label: "fall daily x2",
|
|
853
|
+
start: FALL_START,
|
|
854
|
+
it: EventInterval.Day,
|
|
855
|
+
count: 2,
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
label: "fall weekly x1",
|
|
859
|
+
start: FALL_START,
|
|
860
|
+
it: EventInterval.Week,
|
|
861
|
+
count: 1,
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
label: "fall hourly x1",
|
|
865
|
+
start: FALL_START,
|
|
866
|
+
it: EventInterval.Hour,
|
|
867
|
+
count: 1,
|
|
868
|
+
},
|
|
869
|
+
];
|
|
870
|
+
|
|
871
|
+
for (const s of scenarios) {
|
|
872
|
+
it(`${s.label}: contiguous, ordered, and windowed==cover across the transition`, () => {
|
|
873
|
+
const cfg: Cfg = {
|
|
874
|
+
name: s.label,
|
|
875
|
+
intervalType: s.it,
|
|
876
|
+
count: s.count,
|
|
877
|
+
userIds: ["A", "B", "C"],
|
|
878
|
+
timezone: NY,
|
|
879
|
+
start: s.start,
|
|
880
|
+
};
|
|
881
|
+
const numFull: number = 12;
|
|
882
|
+
const full: CalendarEvent[] = expandFull(cfg, numFull);
|
|
883
|
+
|
|
884
|
+
expect(full.length).toBe(numFull);
|
|
885
|
+
// Contiguity holds structurally even across the 23h/25h DST day.
|
|
886
|
+
expect(contiguityProblems(full)).toEqual([]);
|
|
887
|
+
// Rotation order is unaffected by DST.
|
|
888
|
+
expect(orderProblems(full, cfg, 0)).toEqual([]);
|
|
889
|
+
expect(fairnessProblems(full, cfg)).toEqual([]);
|
|
890
|
+
|
|
891
|
+
if (s.it === EventInterval.Day && s.count === 1) {
|
|
892
|
+
/*
|
|
893
|
+
* Prove tz-aware (wall-clock) stepping is in effect rather than fixed
|
|
894
|
+
* 24h arithmetic: the daily rotation period that spans the DST
|
|
895
|
+
* transition has a different absolute length (23h spring / 25h fall)
|
|
896
|
+
* than a normal 24h period. (We do NOT assert the boundary lands on a
|
|
897
|
+
* specific NY wall-clock time: the FIRST handoff is computed zone-naive
|
|
898
|
+
* by Recurring.getNextDateInterval, so the boundary grid may be offset
|
|
899
|
+
* by the DST hour - that is the documented server-vs-schedule behavior.)
|
|
900
|
+
*/
|
|
901
|
+
const daySpans: Set<number> = new Set<number>();
|
|
902
|
+
for (const e of full) {
|
|
903
|
+
daySpans.add(OneUptimeDate.getSecondsBetweenTwoDates(e.start, e.end));
|
|
904
|
+
}
|
|
905
|
+
expect(daySpans.size).toBeGreaterThan(1);
|
|
906
|
+
// Every period is within +/- 1h of a nominal 24h day.
|
|
907
|
+
for (const span of Array.from(daySpans)) {
|
|
908
|
+
expect(Math.abs(span - 86400)).toBeLessThanOrEqual(3600 + 1);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Windowed==cover across the transition (OFF-boundary instants).
|
|
913
|
+
const util: LayerUtil = new LayerUtil();
|
|
914
|
+
const problems: string[] = [];
|
|
915
|
+
for (let idx: number = 0; idx < full.length; idx++) {
|
|
916
|
+
const cover: CalendarEvent = full[idx]!;
|
|
917
|
+
for (const f of [0.25, 0.75]) {
|
|
918
|
+
const at: Date = interiorInstant(cover, f);
|
|
919
|
+
if (
|
|
920
|
+
!OneUptimeDate.isAfter(at, cover.start) ||
|
|
921
|
+
!OneUptimeDate.isBefore(at, cover.end)
|
|
922
|
+
) {
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
const windowEnd: Date = addUnits(
|
|
926
|
+
at,
|
|
927
|
+
4 * cfg.count,
|
|
928
|
+
cfg.intervalType,
|
|
929
|
+
cfg.timezone,
|
|
930
|
+
);
|
|
931
|
+
const windowed: CalendarEvent[] = util.getEvents(
|
|
932
|
+
{
|
|
933
|
+
...layerOf(cfg),
|
|
934
|
+
calendarStartDate: at,
|
|
935
|
+
calendarEndDate: windowEnd,
|
|
936
|
+
},
|
|
937
|
+
{ getNumberOfEvents: 1 },
|
|
938
|
+
);
|
|
939
|
+
if (windowed[0]?.title !== cover.title) {
|
|
940
|
+
problems.push(
|
|
941
|
+
`at=${at.toISOString()} title ${windowed[0]?.title} != ${cover.title}`,
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
if (windowed[0]!.end.getTime() !== cover.end.getTime()) {
|
|
945
|
+
problems.push(
|
|
946
|
+
`at=${at.toISOString()} end mismatch ${windowed[0]!.end.toISOString()} vs ${cover.end.toISOString()}`,
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
expect({ label: s.label, problems }).toEqual({
|
|
952
|
+
label: s.label,
|
|
953
|
+
problems: [],
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
});
|