@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,1104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EXHAUSTIVE multi-layer priority-merge coverage
|
|
3
|
+
* (lens: getMultiLayerEvents + removeOverlappingEvents).
|
|
4
|
+
*
|
|
5
|
+
* The on-call roster stacks layers by priority: the LOWEST layer index wins
|
|
6
|
+
* every instant it covers, and lower-priority layers fill only the instants no
|
|
7
|
+
* higher-priority layer occupies. getMultiLayerEvents expands each layer with
|
|
8
|
+
* getEvents, tags it with a 1-based priority (layer index 0 => priority 1 =
|
|
9
|
+
* highest), then removeOverlappingEvents carves overlaps so the higher-priority
|
|
10
|
+
* window always survives and the lower-priority coverage is split (its trailing
|
|
11
|
+
* tail after a higher window preserved).
|
|
12
|
+
*
|
|
13
|
+
* This suite exhaustively cross-checks the merged output against an independent
|
|
14
|
+
* per-layer PRIORITY ORACLE at a dense grid of instants, for 2/3/4-layer stacks
|
|
15
|
+
* with mixed rotations/intervals/restrictions/start-offsets/timezones, and
|
|
16
|
+
* asserts the three core invariants:
|
|
17
|
+
* (1) highest-priority layer wins each instant,
|
|
18
|
+
* (2) NO coverage the union of layers provides is dropped,
|
|
19
|
+
* (3) contiguous single coverage where a 24/7 fallback exists (no gap > 2s,
|
|
20
|
+
* no real overlaps / double paging).
|
|
21
|
+
*
|
|
22
|
+
* Boundary seams: the trimming uses +/-1s at carve points, so the merged output
|
|
23
|
+
* and the oracle can legitimately disagree only within ~1-2s of a layer-event
|
|
24
|
+
* boundary. Instant comparisons treat a disagreement as a real mismatch ONLY
|
|
25
|
+
* when the sampled instant is farther than a small tolerance from every
|
|
26
|
+
* layer/merged boundary; the whole-timeline gap scan independently bounds every
|
|
27
|
+
* hole to <= 2s so no genuine coverage hole can hide between grid samples.
|
|
28
|
+
*
|
|
29
|
+
* All restriction windows are authored identically for the oracle and the merge
|
|
30
|
+
* (both go through getEvents), so the equality checks are self-consistent under
|
|
31
|
+
* ANY server process timezone — the file is run under the developer zone AND
|
|
32
|
+
* under TZ=UTC to exercise server-vs-schedule divergence.
|
|
33
|
+
*/
|
|
34
|
+
import LayerUtil, { LayerProps } from "../../../Types/OnCallDutyPolicy/Layer";
|
|
35
|
+
import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
|
|
36
|
+
import RestrictionTimes, {
|
|
37
|
+
RestrictionType,
|
|
38
|
+
WeeklyResctriction,
|
|
39
|
+
} from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
|
|
40
|
+
import Recurring from "../../../Types/Events/Recurring";
|
|
41
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
42
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
43
|
+
import EventInterval from "../../../Types/Events/EventInterval";
|
|
44
|
+
|
|
45
|
+
// Documented User mock pattern.
|
|
46
|
+
function user(id: string): User {
|
|
47
|
+
return {
|
|
48
|
+
id: {
|
|
49
|
+
toString: (): string => {
|
|
50
|
+
return id;
|
|
51
|
+
},
|
|
52
|
+
} as any,
|
|
53
|
+
} as User;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function rotation(t: EventInterval, c: number): Recurring {
|
|
57
|
+
return Recurring.fromJSON({
|
|
58
|
+
_type: "Recurring",
|
|
59
|
+
value: {
|
|
60
|
+
intervalType: t,
|
|
61
|
+
intervalCount: { _type: "PositiveNumber", value: c },
|
|
62
|
+
},
|
|
63
|
+
} as any);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function noRestriction(): RestrictionTimes {
|
|
67
|
+
const r: RestrictionTimes = new RestrictionTimes();
|
|
68
|
+
r.restictionType = RestrictionType.None;
|
|
69
|
+
r.dayRestrictionTimes = null;
|
|
70
|
+
return r;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function dailyRestriction(
|
|
74
|
+
startHour: number,
|
|
75
|
+
endHour: number,
|
|
76
|
+
): RestrictionTimes {
|
|
77
|
+
const r: RestrictionTimes = new RestrictionTimes();
|
|
78
|
+
r.restictionType = RestrictionType.Daily;
|
|
79
|
+
r.dayRestrictionTimes = {
|
|
80
|
+
startTime: OneUptimeDate.getDateWithCustomTime({
|
|
81
|
+
hours: startHour,
|
|
82
|
+
minutes: 0,
|
|
83
|
+
seconds: 0,
|
|
84
|
+
}),
|
|
85
|
+
endTime: OneUptimeDate.getDateWithCustomTime({
|
|
86
|
+
hours: endHour,
|
|
87
|
+
minutes: 0,
|
|
88
|
+
seconds: 0,
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
return r;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
* Weekly restriction authored from explicit boundary Dates so the weekday of
|
|
96
|
+
* each window is unambiguous (the expansion reads the day-of-week off the
|
|
97
|
+
* Date itself), letting us build a genuinely "weekday only" window.
|
|
98
|
+
*/
|
|
99
|
+
function weeklyRestrictionByDates(
|
|
100
|
+
windows: Array<{ start: Date; end: Date }>,
|
|
101
|
+
): RestrictionTimes {
|
|
102
|
+
const r: RestrictionTimes = new RestrictionTimes();
|
|
103
|
+
r.restictionType = RestrictionType.Weekly;
|
|
104
|
+
r.dayRestrictionTimes = null;
|
|
105
|
+
r.weeklyRestrictionTimes = windows.map(
|
|
106
|
+
(w: { start: Date; end: Date }): WeeklyResctriction => {
|
|
107
|
+
return {
|
|
108
|
+
startDay: OneUptimeDate.getDayOfWeek(w.start),
|
|
109
|
+
endDay: OneUptimeDate.getDayOfWeek(w.end),
|
|
110
|
+
startTime: w.start,
|
|
111
|
+
endTime: w.end,
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
return r;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface LayerSpec {
|
|
119
|
+
users: string[];
|
|
120
|
+
start: Date;
|
|
121
|
+
restriction: RestrictionTimes;
|
|
122
|
+
handOff: Date;
|
|
123
|
+
rot: Recurring;
|
|
124
|
+
timezone?: string | undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildLayer(s: LayerSpec): LayerProps {
|
|
128
|
+
return {
|
|
129
|
+
users: s.users.map(user),
|
|
130
|
+
startDateTimeOfLayer: s.start,
|
|
131
|
+
restrictionTimes: s.restriction,
|
|
132
|
+
handOffTime: s.handOff,
|
|
133
|
+
rotation: s.rot,
|
|
134
|
+
timezone: s.timezone,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Which user covers instant t in a single event list? half-open [start, end).
|
|
139
|
+
function coveringUser(events: Array<CalendarEvent>, t: Date): string | null {
|
|
140
|
+
const tm: number = t.getTime();
|
|
141
|
+
for (const e of events) {
|
|
142
|
+
if (e.start.getTime() <= tm && e.end.getTime() > tm) {
|
|
143
|
+
return e.title;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/*
|
|
150
|
+
* Oracle: expected on-call user at instant t across layers, resolved by priority
|
|
151
|
+
* (lowest layer index that covers t wins). Uses each layer's own getEvents.
|
|
152
|
+
*/
|
|
153
|
+
function oracleUser(
|
|
154
|
+
perLayerEvents: Array<Array<CalendarEvent>>,
|
|
155
|
+
t: Date,
|
|
156
|
+
): string | null {
|
|
157
|
+
for (const layerEvents of perLayerEvents) {
|
|
158
|
+
const u: string | null = coveringUser(layerEvents, t);
|
|
159
|
+
if (u !== null) {
|
|
160
|
+
return u;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Real (positive-length) overlap between two merged events => double coverage.
|
|
167
|
+
function overlappingPairs(
|
|
168
|
+
events: Array<CalendarEvent>,
|
|
169
|
+
): Array<[CalendarEvent, CalendarEvent]> {
|
|
170
|
+
const pairs: Array<[CalendarEvent, CalendarEvent]> = [];
|
|
171
|
+
for (let i: number = 0; i < events.length; i++) {
|
|
172
|
+
for (let j: number = i + 1; j < events.length; j++) {
|
|
173
|
+
const a: CalendarEvent = events[i]!;
|
|
174
|
+
const b: CalendarEvent = events[j]!;
|
|
175
|
+
const start: number = Math.max(a.start.getTime(), b.start.getTime());
|
|
176
|
+
const end: number = Math.min(a.end.getTime(), b.end.getTime());
|
|
177
|
+
if (end - start > 0) {
|
|
178
|
+
pairs.push([a, b]);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return pairs;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Sorted array of every start/end boundary across a set of event lists.
|
|
186
|
+
function collectBoundaries(lists: Array<Array<CalendarEvent>>): number[] {
|
|
187
|
+
const set: Set<number> = new Set<number>();
|
|
188
|
+
for (const list of lists) {
|
|
189
|
+
for (const e of list) {
|
|
190
|
+
set.add(e.start.getTime());
|
|
191
|
+
set.add(e.end.getTime());
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return Array.from(set).sort((a: number, b: number) => {
|
|
195
|
+
return a - b;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function minDistanceToBoundary(sorted: number[], t: number): number {
|
|
200
|
+
if (sorted.length === 0) {
|
|
201
|
+
return Number.POSITIVE_INFINITY;
|
|
202
|
+
}
|
|
203
|
+
let lo: number = 0;
|
|
204
|
+
let hi: number = sorted.length - 1;
|
|
205
|
+
let best: number = Number.POSITIVE_INFINITY;
|
|
206
|
+
while (lo <= hi) {
|
|
207
|
+
const mid: number = (lo + hi) >> 1;
|
|
208
|
+
const v: number = sorted[mid]!;
|
|
209
|
+
best = Math.min(best, Math.abs(v - t));
|
|
210
|
+
if (v < t) {
|
|
211
|
+
lo = mid + 1;
|
|
212
|
+
} else if (v > t) {
|
|
213
|
+
hi = mid - 1;
|
|
214
|
+
} else {
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return best;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Largest uncovered stretch inside [from, to] treating merged events as a union.
|
|
222
|
+
function maxCoverageGapMs(
|
|
223
|
+
merged: Array<CalendarEvent>,
|
|
224
|
+
from: Date,
|
|
225
|
+
to: Date,
|
|
226
|
+
): number {
|
|
227
|
+
const sorted: Array<CalendarEvent> = [...merged].sort(
|
|
228
|
+
(a: CalendarEvent, b: CalendarEvent) => {
|
|
229
|
+
return a.start.getTime() - b.start.getTime();
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
let cursor: number = from.getTime();
|
|
233
|
+
let maxGap: number = 0;
|
|
234
|
+
for (const e of sorted) {
|
|
235
|
+
const s: number = e.start.getTime();
|
|
236
|
+
const en: number = e.end.getTime();
|
|
237
|
+
if (s > cursor) {
|
|
238
|
+
maxGap = Math.max(maxGap, s - cursor);
|
|
239
|
+
}
|
|
240
|
+
if (en > cursor) {
|
|
241
|
+
cursor = en;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (to.getTime() > cursor) {
|
|
245
|
+
maxGap = Math.max(maxGap, to.getTime() - cursor);
|
|
246
|
+
}
|
|
247
|
+
return maxGap;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const util: LayerUtil = new LayerUtil();
|
|
251
|
+
|
|
252
|
+
function merge(
|
|
253
|
+
layers: Array<LayerProps>,
|
|
254
|
+
calStart: Date,
|
|
255
|
+
calEnd: Date,
|
|
256
|
+
): Array<CalendarEvent> {
|
|
257
|
+
return util.getMultiLayerEvents({
|
|
258
|
+
layers: layers,
|
|
259
|
+
calendarStartDate: calStart,
|
|
260
|
+
calendarEndDate: calEnd,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function expandPerLayer(
|
|
265
|
+
layers: Array<LayerProps>,
|
|
266
|
+
calStart: Date,
|
|
267
|
+
calEnd: Date,
|
|
268
|
+
): Array<Array<CalendarEvent>> {
|
|
269
|
+
return layers.map((layer: LayerProps): Array<CalendarEvent> => {
|
|
270
|
+
return util.getEvents({
|
|
271
|
+
...layer,
|
|
272
|
+
calendarStartDate: calStart,
|
|
273
|
+
calendarEndDate: calEnd,
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
interface AnalyzeResult {
|
|
279
|
+
merged: Array<CalendarEvent>;
|
|
280
|
+
perLayer: Array<Array<CalendarEvent>>;
|
|
281
|
+
mismatches: string[];
|
|
282
|
+
overlaps: number;
|
|
283
|
+
samples: number;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Dense-instant priority-oracle cross check with boundary-seam tolerance.
|
|
287
|
+
function analyze(
|
|
288
|
+
layers: Array<LayerProps>,
|
|
289
|
+
calStart: Date,
|
|
290
|
+
calEnd: Date,
|
|
291
|
+
stepSeconds: number,
|
|
292
|
+
seamToleranceMs: number = 2000,
|
|
293
|
+
): AnalyzeResult {
|
|
294
|
+
const merged: Array<CalendarEvent> = merge(layers, calStart, calEnd);
|
|
295
|
+
const perLayer: Array<Array<CalendarEvent>> = expandPerLayer(
|
|
296
|
+
layers,
|
|
297
|
+
calStart,
|
|
298
|
+
calEnd,
|
|
299
|
+
);
|
|
300
|
+
const boundaries: number[] = collectBoundaries([merged, ...perLayer]);
|
|
301
|
+
|
|
302
|
+
const mismatches: string[] = [];
|
|
303
|
+
const stepMs: number = stepSeconds * 1000;
|
|
304
|
+
const endMs: number = calEnd.getTime();
|
|
305
|
+
let samples: number = 0;
|
|
306
|
+
|
|
307
|
+
// Offset 17s off the window start so grid samples avoid clean boundaries.
|
|
308
|
+
for (let ms: number = calStart.getTime() + 17000; ms < endMs; ms += stepMs) {
|
|
309
|
+
samples++;
|
|
310
|
+
const t: Date = new Date(ms);
|
|
311
|
+
const expected: string | null = oracleUser(perLayer, t);
|
|
312
|
+
const actual: string | null = coveringUser(merged, t);
|
|
313
|
+
if (expected === actual) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
/*
|
|
317
|
+
* Disagreement is acceptable ONLY as a boundary seam (<= tolerance from a
|
|
318
|
+
* real boundary). Anything else is a dropped-coverage / wrong-winner bug.
|
|
319
|
+
*/
|
|
320
|
+
if (minDistanceToBoundary(boundaries, ms) <= seamToleranceMs) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
mismatches.push(
|
|
324
|
+
`${OneUptimeDate.toString(t)} expected=${expected} actual=${actual}`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
merged: merged,
|
|
330
|
+
perLayer: perLayer,
|
|
331
|
+
mismatches: mismatches,
|
|
332
|
+
overlaps: overlappingPairs(merged).length,
|
|
333
|
+
samples: samples,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/*
|
|
338
|
+
* ---- Fixed reference dates (local-time constructor; interpreted in process
|
|
339
|
+
* zone, so behavior is exercised under both the developer zone and TZ=UTC). ----
|
|
340
|
+
*/
|
|
341
|
+
const MON_JAN5: Date = new Date(2026, 0, 5, 0, 0, 0); // Monday Jan 5 2026
|
|
342
|
+
|
|
343
|
+
function d(
|
|
344
|
+
year: number,
|
|
345
|
+
monthIndex: number,
|
|
346
|
+
day: number,
|
|
347
|
+
hour: number = 0,
|
|
348
|
+
minute: number = 0,
|
|
349
|
+
): Date {
|
|
350
|
+
return new Date(year, monthIndex, day, hour, minute, 0);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
interface Scenario {
|
|
354
|
+
name: string;
|
|
355
|
+
layers: Array<LayerProps>;
|
|
356
|
+
calStart: Date;
|
|
357
|
+
calEnd: Date;
|
|
358
|
+
stepSeconds: number;
|
|
359
|
+
fullCoverage: boolean; // union is 24/7 across [calStart, calEnd]
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function scenarios(): Scenario[] {
|
|
363
|
+
const list: Scenario[] = [];
|
|
364
|
+
|
|
365
|
+
// S1: business-hours daily primary over 24/7 weekly fallback (2 weeks).
|
|
366
|
+
list.push({
|
|
367
|
+
name: "S1 2-layer: daily 09-17 primary (2 users) over 24/7 weekly fallback",
|
|
368
|
+
layers: [
|
|
369
|
+
buildLayer({
|
|
370
|
+
users: ["P1", "P2"],
|
|
371
|
+
start: MON_JAN5,
|
|
372
|
+
restriction: dailyRestriction(9, 17),
|
|
373
|
+
handOff: d(2026, 0, 5, 12),
|
|
374
|
+
rot: rotation(EventInterval.Day, 1),
|
|
375
|
+
}),
|
|
376
|
+
buildLayer({
|
|
377
|
+
users: ["F1", "F2"],
|
|
378
|
+
start: MON_JAN5,
|
|
379
|
+
restriction: noRestriction(),
|
|
380
|
+
handOff: MON_JAN5,
|
|
381
|
+
rot: rotation(EventInterval.Week, 1),
|
|
382
|
+
}),
|
|
383
|
+
],
|
|
384
|
+
calStart: MON_JAN5,
|
|
385
|
+
calEnd: d(2026, 0, 19),
|
|
386
|
+
stepSeconds: 300,
|
|
387
|
+
fullCoverage: true,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
/*
|
|
391
|
+
* S2: fast hourly restricted primary => back-to-back windows over ONE long
|
|
392
|
+
* 24/7 weekly fallback event (exercises the trailing-tail reconstruction).
|
|
393
|
+
*/
|
|
394
|
+
list.push({
|
|
395
|
+
name: "S2 2-layer: hourly daily-09-17 primary (3 users) over 24/7 weekly fallback",
|
|
396
|
+
layers: [
|
|
397
|
+
buildLayer({
|
|
398
|
+
users: ["A", "B", "C"],
|
|
399
|
+
start: MON_JAN5,
|
|
400
|
+
restriction: dailyRestriction(9, 17),
|
|
401
|
+
handOff: d(2026, 0, 5, 9),
|
|
402
|
+
rot: rotation(EventInterval.Hour, 1),
|
|
403
|
+
}),
|
|
404
|
+
buildLayer({
|
|
405
|
+
users: ["Z"],
|
|
406
|
+
start: MON_JAN5,
|
|
407
|
+
restriction: noRestriction(),
|
|
408
|
+
handOff: MON_JAN5,
|
|
409
|
+
rot: rotation(EventInterval.Week, 1),
|
|
410
|
+
}),
|
|
411
|
+
],
|
|
412
|
+
calStart: MON_JAN5,
|
|
413
|
+
calEnd: d(2026, 0, 9),
|
|
414
|
+
stepSeconds: 60,
|
|
415
|
+
fullCoverage: true,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// S3: two disjoint restricted primaries over 24/7 fallback (3 layers).
|
|
419
|
+
list.push({
|
|
420
|
+
name: "S3 3-layer: mornings 06-10 + afternoons 14-18 over 24/7 fallback",
|
|
421
|
+
layers: [
|
|
422
|
+
buildLayer({
|
|
423
|
+
users: ["M1", "M2"],
|
|
424
|
+
start: MON_JAN5,
|
|
425
|
+
restriction: dailyRestriction(6, 10),
|
|
426
|
+
handOff: d(2026, 0, 5, 8),
|
|
427
|
+
rot: rotation(EventInterval.Day, 1),
|
|
428
|
+
}),
|
|
429
|
+
buildLayer({
|
|
430
|
+
users: ["N1", "N2"],
|
|
431
|
+
start: MON_JAN5,
|
|
432
|
+
restriction: dailyRestriction(14, 18),
|
|
433
|
+
handOff: d(2026, 0, 5, 16),
|
|
434
|
+
rot: rotation(EventInterval.Day, 1),
|
|
435
|
+
}),
|
|
436
|
+
buildLayer({
|
|
437
|
+
users: ["Z1"],
|
|
438
|
+
start: MON_JAN5,
|
|
439
|
+
restriction: noRestriction(),
|
|
440
|
+
handOff: MON_JAN5,
|
|
441
|
+
rot: rotation(EventInterval.Week, 1),
|
|
442
|
+
}),
|
|
443
|
+
],
|
|
444
|
+
calStart: MON_JAN5,
|
|
445
|
+
calEnd: d(2026, 0, 19),
|
|
446
|
+
stepSeconds: 180,
|
|
447
|
+
fullCoverage: true,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// S4: weekly rotation primary + daily secondary + 24/7 fallback (3 layers).
|
|
451
|
+
list.push({
|
|
452
|
+
name: "S4 3-layer: weekly-rot daily-09-17 + daily-20-23 secondary over 24/7 fallback",
|
|
453
|
+
layers: [
|
|
454
|
+
buildLayer({
|
|
455
|
+
users: ["W1", "W2"],
|
|
456
|
+
start: MON_JAN5,
|
|
457
|
+
restriction: dailyRestriction(9, 17),
|
|
458
|
+
handOff: d(2026, 0, 5, 12),
|
|
459
|
+
rot: rotation(EventInterval.Week, 1),
|
|
460
|
+
}),
|
|
461
|
+
buildLayer({
|
|
462
|
+
users: ["S1u", "S2u"],
|
|
463
|
+
start: MON_JAN5,
|
|
464
|
+
restriction: dailyRestriction(20, 23),
|
|
465
|
+
handOff: d(2026, 0, 5, 21),
|
|
466
|
+
rot: rotation(EventInterval.Day, 2),
|
|
467
|
+
}),
|
|
468
|
+
buildLayer({
|
|
469
|
+
users: ["Z1"],
|
|
470
|
+
start: MON_JAN5,
|
|
471
|
+
restriction: noRestriction(),
|
|
472
|
+
handOff: MON_JAN5,
|
|
473
|
+
rot: rotation(EventInterval.Week, 1),
|
|
474
|
+
}),
|
|
475
|
+
],
|
|
476
|
+
calStart: MON_JAN5,
|
|
477
|
+
calEnd: d(2026, 0, 26),
|
|
478
|
+
stepSeconds: 300,
|
|
479
|
+
fullCoverage: true,
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// S5: FOUR layers, three back-to-back daily windows over a 24/7 fallback.
|
|
483
|
+
list.push({
|
|
484
|
+
name: "S5 4-layer: 06-09 + 09-15 + 15-21 daily over 24/7 fallback",
|
|
485
|
+
layers: [
|
|
486
|
+
buildLayer({
|
|
487
|
+
users: ["E1", "E2"],
|
|
488
|
+
start: MON_JAN5,
|
|
489
|
+
restriction: dailyRestriction(6, 9),
|
|
490
|
+
handOff: d(2026, 0, 5, 7),
|
|
491
|
+
rot: rotation(EventInterval.Day, 1),
|
|
492
|
+
}),
|
|
493
|
+
buildLayer({
|
|
494
|
+
users: ["D1", "D2", "D3"],
|
|
495
|
+
start: MON_JAN5,
|
|
496
|
+
restriction: dailyRestriction(9, 15),
|
|
497
|
+
handOff: d(2026, 0, 5, 12),
|
|
498
|
+
rot: rotation(EventInterval.Day, 1),
|
|
499
|
+
}),
|
|
500
|
+
buildLayer({
|
|
501
|
+
users: ["V1", "V2"],
|
|
502
|
+
start: MON_JAN5,
|
|
503
|
+
restriction: dailyRestriction(15, 21),
|
|
504
|
+
handOff: d(2026, 0, 5, 18),
|
|
505
|
+
rot: rotation(EventInterval.Day, 1),
|
|
506
|
+
}),
|
|
507
|
+
buildLayer({
|
|
508
|
+
users: ["Z1"],
|
|
509
|
+
start: MON_JAN5,
|
|
510
|
+
restriction: noRestriction(),
|
|
511
|
+
handOff: MON_JAN5,
|
|
512
|
+
rot: rotation(EventInterval.Week, 1),
|
|
513
|
+
}),
|
|
514
|
+
],
|
|
515
|
+
calStart: MON_JAN5,
|
|
516
|
+
calEnd: d(2026, 0, 15),
|
|
517
|
+
stepSeconds: 120,
|
|
518
|
+
fullCoverage: true,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// S6: three 24/7 layers, equal starts, different rotations -> layer0 wins.
|
|
522
|
+
list.push({
|
|
523
|
+
name: "S6 3-layer: equal-start 24/7 layers, mixed rotations -> highest wins",
|
|
524
|
+
layers: [
|
|
525
|
+
buildLayer({
|
|
526
|
+
users: ["A1", "A2"],
|
|
527
|
+
start: MON_JAN5,
|
|
528
|
+
restriction: noRestriction(),
|
|
529
|
+
handOff: MON_JAN5,
|
|
530
|
+
rot: rotation(EventInterval.Week, 1),
|
|
531
|
+
}),
|
|
532
|
+
buildLayer({
|
|
533
|
+
users: ["B1", "B2", "B3"],
|
|
534
|
+
start: MON_JAN5,
|
|
535
|
+
restriction: noRestriction(),
|
|
536
|
+
handOff: MON_JAN5,
|
|
537
|
+
rot: rotation(EventInterval.Day, 2),
|
|
538
|
+
}),
|
|
539
|
+
buildLayer({
|
|
540
|
+
users: ["C1"],
|
|
541
|
+
start: MON_JAN5,
|
|
542
|
+
restriction: noRestriction(),
|
|
543
|
+
handOff: MON_JAN5,
|
|
544
|
+
rot: rotation(EventInterval.Hour, 3),
|
|
545
|
+
}),
|
|
546
|
+
],
|
|
547
|
+
calStart: MON_JAN5,
|
|
548
|
+
calEnd: d(2026, 0, 19),
|
|
549
|
+
stepSeconds: 240,
|
|
550
|
+
fullCoverage: true,
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// S7: higher-priority layer fully covers the lower one (both 24/7).
|
|
554
|
+
list.push({
|
|
555
|
+
name: "S7 2-layer: 24/7 primary fully shadows 24/7 secondary",
|
|
556
|
+
layers: [
|
|
557
|
+
buildLayer({
|
|
558
|
+
users: ["A"],
|
|
559
|
+
start: MON_JAN5,
|
|
560
|
+
restriction: noRestriction(),
|
|
561
|
+
handOff: MON_JAN5,
|
|
562
|
+
rot: rotation(EventInterval.Week, 1),
|
|
563
|
+
}),
|
|
564
|
+
buildLayer({
|
|
565
|
+
users: ["B1", "B2"],
|
|
566
|
+
start: MON_JAN5,
|
|
567
|
+
restriction: noRestriction(),
|
|
568
|
+
handOff: MON_JAN5,
|
|
569
|
+
rot: rotation(EventInterval.Day, 1),
|
|
570
|
+
}),
|
|
571
|
+
],
|
|
572
|
+
calStart: MON_JAN5,
|
|
573
|
+
calEnd: d(2026, 0, 19),
|
|
574
|
+
stepSeconds: 300,
|
|
575
|
+
fullCoverage: true,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// S8: nested three-way overlap (11-15 in 10-16 in 09-17) over 24/7 fallback.
|
|
579
|
+
list.push({
|
|
580
|
+
name: "S8 4-layer: nested 11-15 / 10-16 / 09-17 three-way overlap over 24/7 fallback",
|
|
581
|
+
layers: [
|
|
582
|
+
buildLayer({
|
|
583
|
+
users: ["A"],
|
|
584
|
+
start: MON_JAN5,
|
|
585
|
+
restriction: dailyRestriction(11, 15),
|
|
586
|
+
handOff: d(2026, 0, 5, 12),
|
|
587
|
+
rot: rotation(EventInterval.Day, 1),
|
|
588
|
+
}),
|
|
589
|
+
buildLayer({
|
|
590
|
+
users: ["B"],
|
|
591
|
+
start: MON_JAN5,
|
|
592
|
+
restriction: dailyRestriction(10, 16),
|
|
593
|
+
handOff: d(2026, 0, 5, 12),
|
|
594
|
+
rot: rotation(EventInterval.Day, 1),
|
|
595
|
+
}),
|
|
596
|
+
buildLayer({
|
|
597
|
+
users: ["C"],
|
|
598
|
+
start: MON_JAN5,
|
|
599
|
+
restriction: dailyRestriction(9, 17),
|
|
600
|
+
handOff: d(2026, 0, 5, 12),
|
|
601
|
+
rot: rotation(EventInterval.Day, 1),
|
|
602
|
+
}),
|
|
603
|
+
buildLayer({
|
|
604
|
+
users: ["Z"],
|
|
605
|
+
start: MON_JAN5,
|
|
606
|
+
restriction: noRestriction(),
|
|
607
|
+
handOff: MON_JAN5,
|
|
608
|
+
rot: rotation(EventInterval.Week, 1),
|
|
609
|
+
}),
|
|
610
|
+
],
|
|
611
|
+
calStart: MON_JAN5,
|
|
612
|
+
calEnd: d(2026, 0, 12),
|
|
613
|
+
stepSeconds: 90,
|
|
614
|
+
fullCoverage: true,
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
/*
|
|
618
|
+
* S9: fully-restricted (weekday-only) primary over a WEEKEND window => the
|
|
619
|
+
* primary contributes zero coverage and the 24/7 fallback fills 100%.
|
|
620
|
+
*/
|
|
621
|
+
list.push({
|
|
622
|
+
name: "S9 2-layer: weekday-only primary over WEEKEND window -> fallback fills fully",
|
|
623
|
+
layers: [
|
|
624
|
+
buildLayer({
|
|
625
|
+
users: ["P1", "P2"],
|
|
626
|
+
start: MON_JAN5,
|
|
627
|
+
restriction: weeklyRestrictionByDates([
|
|
628
|
+
{ start: d(2026, 0, 5, 0), end: d(2026, 0, 10, 0) }, // Mon 00:00 -> Sat 00:00
|
|
629
|
+
]),
|
|
630
|
+
handOff: d(2026, 0, 5, 12),
|
|
631
|
+
rot: rotation(EventInterval.Day, 1),
|
|
632
|
+
}),
|
|
633
|
+
buildLayer({
|
|
634
|
+
users: ["Z"],
|
|
635
|
+
start: MON_JAN5,
|
|
636
|
+
restriction: noRestriction(),
|
|
637
|
+
handOff: MON_JAN5,
|
|
638
|
+
rot: rotation(EventInterval.Week, 1),
|
|
639
|
+
}),
|
|
640
|
+
],
|
|
641
|
+
calStart: d(2026, 0, 10, 12), // Sat 12:00 (fully inside the weekend gap)
|
|
642
|
+
calEnd: d(2026, 0, 11, 12), // Sun 12:00
|
|
643
|
+
stepSeconds: 60,
|
|
644
|
+
fullCoverage: true,
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
/*
|
|
648
|
+
* S10: schedule authored in America/New_York over a 24/7 fallback; run under
|
|
649
|
+
* a possibly-different server zone (TZ=UTC in CI) to exercise divergence.
|
|
650
|
+
*/
|
|
651
|
+
list.push({
|
|
652
|
+
name: "S10 2-layer(tz): America/New_York daily 09-17 primary over 24/7 fallback",
|
|
653
|
+
layers: [
|
|
654
|
+
buildLayer({
|
|
655
|
+
users: ["NY1", "NY2"],
|
|
656
|
+
start: MON_JAN5,
|
|
657
|
+
restriction: dailyRestriction(9, 17),
|
|
658
|
+
handOff: d(2026, 0, 5, 12),
|
|
659
|
+
rot: rotation(EventInterval.Day, 1),
|
|
660
|
+
timezone: "America/New_York",
|
|
661
|
+
}),
|
|
662
|
+
buildLayer({
|
|
663
|
+
users: ["Z"],
|
|
664
|
+
start: MON_JAN5,
|
|
665
|
+
restriction: noRestriction(),
|
|
666
|
+
handOff: MON_JAN5,
|
|
667
|
+
rot: rotation(EventInterval.Week, 1),
|
|
668
|
+
timezone: "America/New_York",
|
|
669
|
+
}),
|
|
670
|
+
],
|
|
671
|
+
calStart: MON_JAN5,
|
|
672
|
+
calEnd: d(2026, 0, 19),
|
|
673
|
+
stepSeconds: 300,
|
|
674
|
+
fullCoverage: true,
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
/*
|
|
678
|
+
* S11: higher-priority layer starts 2 days into the window; fallback fills
|
|
679
|
+
* the head, then primary takes over (equal-start NOT required).
|
|
680
|
+
*/
|
|
681
|
+
list.push({
|
|
682
|
+
name: "S11 2-layer: primary starts 2 days in, 24/7 fallback fills the head",
|
|
683
|
+
layers: [
|
|
684
|
+
buildLayer({
|
|
685
|
+
users: ["P1", "P2"],
|
|
686
|
+
start: d(2026, 0, 7, 0),
|
|
687
|
+
restriction: noRestriction(),
|
|
688
|
+
handOff: d(2026, 0, 7, 0),
|
|
689
|
+
rot: rotation(EventInterval.Day, 1),
|
|
690
|
+
}),
|
|
691
|
+
buildLayer({
|
|
692
|
+
users: ["F1"],
|
|
693
|
+
start: MON_JAN5,
|
|
694
|
+
restriction: noRestriction(),
|
|
695
|
+
handOff: MON_JAN5,
|
|
696
|
+
rot: rotation(EventInterval.Week, 1),
|
|
697
|
+
}),
|
|
698
|
+
],
|
|
699
|
+
calStart: MON_JAN5,
|
|
700
|
+
calEnd: d(2026, 0, 12),
|
|
701
|
+
stepSeconds: 120,
|
|
702
|
+
fullCoverage: true,
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
/*
|
|
706
|
+
* S12: TWO restricted layers, NO fallback => genuine uncovered gaps
|
|
707
|
+
* (oracle null == merged null). Only priority-agreement + no-overlap hold.
|
|
708
|
+
*/
|
|
709
|
+
list.push({
|
|
710
|
+
name: "S12 2-layer: overlapping restricted primaries, no fallback (genuine gaps)",
|
|
711
|
+
layers: [
|
|
712
|
+
buildLayer({
|
|
713
|
+
users: ["A"],
|
|
714
|
+
start: MON_JAN5,
|
|
715
|
+
restriction: dailyRestriction(9, 12),
|
|
716
|
+
handOff: MON_JAN5,
|
|
717
|
+
rot: rotation(EventInterval.Week, 1),
|
|
718
|
+
}),
|
|
719
|
+
buildLayer({
|
|
720
|
+
users: ["B"],
|
|
721
|
+
start: MON_JAN5,
|
|
722
|
+
restriction: dailyRestriction(10, 17),
|
|
723
|
+
handOff: MON_JAN5,
|
|
724
|
+
rot: rotation(EventInterval.Week, 1),
|
|
725
|
+
}),
|
|
726
|
+
],
|
|
727
|
+
calStart: MON_JAN5,
|
|
728
|
+
calEnd: d(2026, 0, 9),
|
|
729
|
+
stepSeconds: 60,
|
|
730
|
+
fullCoverage: false,
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
/*
|
|
734
|
+
* S13: FOUR mixed-interval layers - hourly, daily, weekly restricted primaries
|
|
735
|
+
* over a 24/7 monthly fallback.
|
|
736
|
+
*/
|
|
737
|
+
list.push({
|
|
738
|
+
name: "S13 4-layer: hourly + daily + weekly restricted over 24/7 monthly fallback",
|
|
739
|
+
layers: [
|
|
740
|
+
buildLayer({
|
|
741
|
+
users: ["H1", "H2"],
|
|
742
|
+
start: MON_JAN5,
|
|
743
|
+
restriction: dailyRestriction(8, 12),
|
|
744
|
+
handOff: d(2026, 0, 5, 8),
|
|
745
|
+
rot: rotation(EventInterval.Hour, 2),
|
|
746
|
+
}),
|
|
747
|
+
buildLayer({
|
|
748
|
+
users: ["G1", "G2"],
|
|
749
|
+
start: MON_JAN5,
|
|
750
|
+
restriction: dailyRestriction(12, 18),
|
|
751
|
+
handOff: d(2026, 0, 5, 15),
|
|
752
|
+
rot: rotation(EventInterval.Day, 1),
|
|
753
|
+
}),
|
|
754
|
+
buildLayer({
|
|
755
|
+
users: ["K1"],
|
|
756
|
+
start: MON_JAN5,
|
|
757
|
+
restriction: dailyRestriction(18, 23),
|
|
758
|
+
handOff: d(2026, 0, 5, 20),
|
|
759
|
+
rot: rotation(EventInterval.Week, 1),
|
|
760
|
+
}),
|
|
761
|
+
buildLayer({
|
|
762
|
+
users: ["Z1"],
|
|
763
|
+
start: MON_JAN5,
|
|
764
|
+
restriction: noRestriction(),
|
|
765
|
+
handOff: MON_JAN5,
|
|
766
|
+
rot: rotation(EventInterval.Month, 1),
|
|
767
|
+
}),
|
|
768
|
+
],
|
|
769
|
+
calStart: MON_JAN5,
|
|
770
|
+
calEnd: d(2026, 0, 12),
|
|
771
|
+
stepSeconds: 60,
|
|
772
|
+
fullCoverage: true,
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// S14: back-to-back DIFFERENT-priority windows (09-13 then 13-17) over 24/7.
|
|
776
|
+
list.push({
|
|
777
|
+
name: "S14 3-layer: back-to-back 09-13 + 13-17 different-priority over 24/7 fallback",
|
|
778
|
+
layers: [
|
|
779
|
+
buildLayer({
|
|
780
|
+
users: ["AM"],
|
|
781
|
+
start: MON_JAN5,
|
|
782
|
+
restriction: dailyRestriction(9, 13),
|
|
783
|
+
handOff: d(2026, 0, 5, 11),
|
|
784
|
+
rot: rotation(EventInterval.Week, 1),
|
|
785
|
+
}),
|
|
786
|
+
buildLayer({
|
|
787
|
+
users: ["PM"],
|
|
788
|
+
start: MON_JAN5,
|
|
789
|
+
restriction: dailyRestriction(13, 17),
|
|
790
|
+
handOff: d(2026, 0, 5, 15),
|
|
791
|
+
rot: rotation(EventInterval.Week, 1),
|
|
792
|
+
}),
|
|
793
|
+
buildLayer({
|
|
794
|
+
users: ["Z"],
|
|
795
|
+
start: MON_JAN5,
|
|
796
|
+
restriction: noRestriction(),
|
|
797
|
+
handOff: MON_JAN5,
|
|
798
|
+
rot: rotation(EventInterval.Week, 1),
|
|
799
|
+
}),
|
|
800
|
+
],
|
|
801
|
+
calStart: MON_JAN5,
|
|
802
|
+
calEnd: d(2026, 0, 12),
|
|
803
|
+
stepSeconds: 90,
|
|
804
|
+
fullCoverage: true,
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
return list;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
describe("Exhaustive multi-layer merge == per-layer priority oracle", () => {
|
|
811
|
+
for (const sc of scenarios()) {
|
|
812
|
+
test(sc.name, () => {
|
|
813
|
+
const res: AnalyzeResult = analyze(
|
|
814
|
+
sc.layers,
|
|
815
|
+
sc.calStart,
|
|
816
|
+
sc.calEnd,
|
|
817
|
+
sc.stepSeconds,
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
/*
|
|
821
|
+
* Invariant (1)+(2): highest priority wins each instant and no covered
|
|
822
|
+
* instant is dropped (mismatches would include both wrong-winner and
|
|
823
|
+
* null-where-oracle-covers).
|
|
824
|
+
*/
|
|
825
|
+
expect({ name: sc.name, mismatches: res.mismatches }).toEqual({
|
|
826
|
+
name: sc.name,
|
|
827
|
+
mismatches: [],
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
// Invariant (3a): never double-cover (no double paging).
|
|
831
|
+
expect(res.overlaps).toBe(0);
|
|
832
|
+
|
|
833
|
+
// Sanity: the grid actually sampled a meaningful number of instants.
|
|
834
|
+
expect(res.samples).toBeGreaterThan(10);
|
|
835
|
+
|
|
836
|
+
/*
|
|
837
|
+
* Invariant (3b): where a 24/7 fallback exists, coverage is contiguous
|
|
838
|
+
* across the entire window with only sub-2s carve seams.
|
|
839
|
+
*/
|
|
840
|
+
if (sc.fullCoverage) {
|
|
841
|
+
const gap: number = maxCoverageGapMs(
|
|
842
|
+
res.merged,
|
|
843
|
+
sc.calStart,
|
|
844
|
+
sc.calEnd,
|
|
845
|
+
);
|
|
846
|
+
expect(gap).toBeLessThanOrEqual(2000);
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
describe("Targeted invariants: winner, tail-preservation, fallback-fill", () => {
|
|
853
|
+
test("S7: higher-priority 24/7 layer fully shadows the lower one (B never appears)", () => {
|
|
854
|
+
const sc: Scenario = scenarios().find((s: Scenario) => {
|
|
855
|
+
return s.name.startsWith("S7");
|
|
856
|
+
})!;
|
|
857
|
+
const merged: Array<CalendarEvent> = merge(
|
|
858
|
+
sc.layers,
|
|
859
|
+
sc.calStart,
|
|
860
|
+
sc.calEnd,
|
|
861
|
+
);
|
|
862
|
+
expect(merged.length).toBeGreaterThan(0);
|
|
863
|
+
for (const e of merged) {
|
|
864
|
+
expect(e.title).toBe("A");
|
|
865
|
+
expect(e.title).not.toBe("B1");
|
|
866
|
+
expect(e.title).not.toBe("B2");
|
|
867
|
+
}
|
|
868
|
+
// Full single coverage across the window.
|
|
869
|
+
expect(
|
|
870
|
+
maxCoverageGapMs(merged, sc.calStart, sc.calEnd),
|
|
871
|
+
).toBeLessThanOrEqual(2000);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
test("S9: weekday-only primary over a weekend window => fallback Z fills 100%, primary absent", () => {
|
|
875
|
+
const sc: Scenario = scenarios().find((s: Scenario) => {
|
|
876
|
+
return s.name.startsWith("S9");
|
|
877
|
+
})!;
|
|
878
|
+
const merged: Array<CalendarEvent> = merge(
|
|
879
|
+
sc.layers,
|
|
880
|
+
sc.calStart,
|
|
881
|
+
sc.calEnd,
|
|
882
|
+
);
|
|
883
|
+
expect(merged.length).toBeGreaterThan(0);
|
|
884
|
+
for (const e of merged) {
|
|
885
|
+
expect(e.title).toBe("Z");
|
|
886
|
+
}
|
|
887
|
+
// The primary users must never surface anywhere in the weekend window.
|
|
888
|
+
const titles: string[] = merged.map((e: CalendarEvent) => {
|
|
889
|
+
return e.title;
|
|
890
|
+
});
|
|
891
|
+
expect(titles).not.toContain("P1");
|
|
892
|
+
expect(titles).not.toContain("P2");
|
|
893
|
+
// No hole: fallback covers the entire weekend window.
|
|
894
|
+
expect(
|
|
895
|
+
maxCoverageGapMs(merged, sc.calStart, sc.calEnd),
|
|
896
|
+
).toBeLessThanOrEqual(2000);
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
test("S2: hourly primary => fallback tail after 17:00 survives every day (no lost tail)", () => {
|
|
900
|
+
const sc: Scenario = scenarios().find((s: Scenario) => {
|
|
901
|
+
return s.name.startsWith("S2");
|
|
902
|
+
})!;
|
|
903
|
+
const merged: Array<CalendarEvent> = merge(
|
|
904
|
+
sc.layers,
|
|
905
|
+
sc.calStart,
|
|
906
|
+
sc.calEnd,
|
|
907
|
+
);
|
|
908
|
+
|
|
909
|
+
/*
|
|
910
|
+
* For each of the 4 days, the post-window evening (20:00) must be the
|
|
911
|
+
* fallback user Z — proving the fallback's trailing coverage after the
|
|
912
|
+
* back-to-back hourly windows was preserved, not deleted by the carve.
|
|
913
|
+
*/
|
|
914
|
+
for (let day: number = 0; day < 4; day++) {
|
|
915
|
+
const evening: Date = d(2026, 0, 5 + day, 20, 0);
|
|
916
|
+
const early: Date = d(2026, 0, 5 + day, 3, 0);
|
|
917
|
+
expect(coveringUser(merged, evening)).toBe("Z");
|
|
918
|
+
expect(coveringUser(merged, early)).toBe("Z");
|
|
919
|
+
}
|
|
920
|
+
// A mid-window hour (e.g. 12:30) must be one of the primary hourly users.
|
|
921
|
+
const midday: string | null = coveringUser(merged, d(2026, 0, 5, 12, 30));
|
|
922
|
+
expect(["A", "B", "C"]).toContain(midday);
|
|
923
|
+
|
|
924
|
+
// And at least one merged event with a primary title exists (windows kept).
|
|
925
|
+
const hasPrimary: boolean = merged.some((e: CalendarEvent) => {
|
|
926
|
+
return e.title === "A" || e.title === "B" || e.title === "C";
|
|
927
|
+
});
|
|
928
|
+
expect(hasPrimary).toBe(true);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
test("S8: nested three-way overlap resolves highest-priority winner at each band", () => {
|
|
932
|
+
const sc: Scenario = scenarios().find((s: Scenario) => {
|
|
933
|
+
return s.name.startsWith("S8");
|
|
934
|
+
})!;
|
|
935
|
+
const merged: Array<CalendarEvent> = merge(
|
|
936
|
+
sc.layers,
|
|
937
|
+
sc.calStart,
|
|
938
|
+
sc.calEnd,
|
|
939
|
+
);
|
|
940
|
+
/*
|
|
941
|
+
* Monday Jan 5 bands: 09-10 C, 10-11 B, 11-15 A, 15-16 B, 16-17 C, else Z.
|
|
942
|
+
* (12:00 is a rotation-handoff seam for all three primaries, so probe 12:30.)
|
|
943
|
+
*/
|
|
944
|
+
expect(coveringUser(merged, d(2026, 0, 5, 3, 0))).toBe("Z");
|
|
945
|
+
expect(coveringUser(merged, d(2026, 0, 5, 9, 30))).toBe("C");
|
|
946
|
+
expect(coveringUser(merged, d(2026, 0, 5, 10, 30))).toBe("B");
|
|
947
|
+
expect(coveringUser(merged, d(2026, 0, 5, 12, 30))).toBe("A");
|
|
948
|
+
expect(coveringUser(merged, d(2026, 0, 5, 14, 59))).toBe("A");
|
|
949
|
+
expect(coveringUser(merged, d(2026, 0, 5, 15, 30))).toBe("B");
|
|
950
|
+
expect(coveringUser(merged, d(2026, 0, 5, 16, 30))).toBe("C");
|
|
951
|
+
expect(coveringUser(merged, d(2026, 0, 5, 20, 0))).toBe("Z");
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
test("S14: back-to-back different-priority windows both present with fallback around", () => {
|
|
955
|
+
const sc: Scenario = scenarios().find((s: Scenario) => {
|
|
956
|
+
return s.name.startsWith("S14");
|
|
957
|
+
})!;
|
|
958
|
+
const merged: Array<CalendarEvent> = merge(
|
|
959
|
+
sc.layers,
|
|
960
|
+
sc.calStart,
|
|
961
|
+
sc.calEnd,
|
|
962
|
+
);
|
|
963
|
+
/*
|
|
964
|
+
* Monday: 03:00 Z, AM owns 09-13, PM owns 13-17, 20:00 Z. Probe interiors
|
|
965
|
+
* that avoid the weekly handoff seams (AM @11:00, PM @15:00).
|
|
966
|
+
*/
|
|
967
|
+
expect(coveringUser(merged, d(2026, 0, 5, 3, 0))).toBe("Z");
|
|
968
|
+
expect(coveringUser(merged, d(2026, 0, 5, 10, 0))).toBe("AM");
|
|
969
|
+
expect(coveringUser(merged, d(2026, 0, 5, 12, 0))).toBe("AM");
|
|
970
|
+
expect(coveringUser(merged, d(2026, 0, 5, 14, 0))).toBe("PM");
|
|
971
|
+
expect(coveringUser(merged, d(2026, 0, 5, 16, 0))).toBe("PM");
|
|
972
|
+
expect(coveringUser(merged, d(2026, 0, 5, 20, 0))).toBe("Z");
|
|
973
|
+
// Contiguous, no overlap.
|
|
974
|
+
expect(overlappingPairs(merged).length).toBe(0);
|
|
975
|
+
expect(
|
|
976
|
+
maxCoverageGapMs(merged, sc.calStart, sc.calEnd),
|
|
977
|
+
).toBeLessThanOrEqual(2000);
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
test("S11: fallback fills head before the delayed primary, primary owns the tail", () => {
|
|
981
|
+
const sc: Scenario = scenarios().find((s: Scenario) => {
|
|
982
|
+
return s.name.startsWith("S11");
|
|
983
|
+
})!;
|
|
984
|
+
const merged: Array<CalendarEvent> = merge(
|
|
985
|
+
sc.layers,
|
|
986
|
+
sc.calStart,
|
|
987
|
+
sc.calEnd,
|
|
988
|
+
);
|
|
989
|
+
// Day 0 (Jan 5) noon: only the fallback F1 exists yet.
|
|
990
|
+
expect(coveringUser(merged, d(2026, 0, 5, 12, 0))).toBe("F1");
|
|
991
|
+
expect(coveringUser(merged, d(2026, 0, 6, 12, 0))).toBe("F1");
|
|
992
|
+
// Day 2 (Jan 7) onward: primary P1/P2 daily rotation takes over.
|
|
993
|
+
expect(["P1", "P2"]).toContain(coveringUser(merged, d(2026, 0, 7, 12, 0)));
|
|
994
|
+
expect(["P1", "P2"]).toContain(coveringUser(merged, d(2026, 0, 8, 12, 0)));
|
|
995
|
+
expect(
|
|
996
|
+
maxCoverageGapMs(merged, sc.calStart, sc.calEnd),
|
|
997
|
+
).toBeLessThanOrEqual(2000);
|
|
998
|
+
});
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
describe("Structural guarantees over the merged output", () => {
|
|
1002
|
+
test("merged events are individually well-formed (start < end) for every scenario", () => {
|
|
1003
|
+
for (const sc of scenarios()) {
|
|
1004
|
+
const merged: Array<CalendarEvent> = merge(
|
|
1005
|
+
sc.layers,
|
|
1006
|
+
sc.calStart,
|
|
1007
|
+
sc.calEnd,
|
|
1008
|
+
);
|
|
1009
|
+
for (const e of merged) {
|
|
1010
|
+
expect(e.end.getTime()).toBeGreaterThan(e.start.getTime());
|
|
1011
|
+
// Events stay within the calendar window (allowing the 1s carve seam).
|
|
1012
|
+
expect(e.start.getTime()).toBeGreaterThanOrEqual(
|
|
1013
|
+
sc.calStart.getTime() - 1000,
|
|
1014
|
+
);
|
|
1015
|
+
expect(e.end.getTime()).toBeLessThanOrEqual(sc.calEnd.getTime() + 1000);
|
|
1016
|
+
}
|
|
1017
|
+
// Unique ids assigned by the merge.
|
|
1018
|
+
const ids: number[] = merged.map((e: CalendarEvent) => {
|
|
1019
|
+
return e.id;
|
|
1020
|
+
});
|
|
1021
|
+
expect(new Set<number>(ids).size).toBe(ids.length);
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
test("layer order matters: swapping priority flips the winner in an overlap", () => {
|
|
1026
|
+
// Two 24/7 layers over the same window: whichever is index 0 wins entirely.
|
|
1027
|
+
const layerA: LayerProps = buildLayer({
|
|
1028
|
+
users: ["A"],
|
|
1029
|
+
start: MON_JAN5,
|
|
1030
|
+
restriction: noRestriction(),
|
|
1031
|
+
handOff: MON_JAN5,
|
|
1032
|
+
rot: rotation(EventInterval.Week, 1),
|
|
1033
|
+
});
|
|
1034
|
+
const layerB: LayerProps = buildLayer({
|
|
1035
|
+
users: ["B"],
|
|
1036
|
+
start: MON_JAN5,
|
|
1037
|
+
restriction: noRestriction(),
|
|
1038
|
+
handOff: MON_JAN5,
|
|
1039
|
+
rot: rotation(EventInterval.Week, 1),
|
|
1040
|
+
});
|
|
1041
|
+
const end: Date = d(2026, 0, 12);
|
|
1042
|
+
|
|
1043
|
+
const abMerged: Array<CalendarEvent> = merge(
|
|
1044
|
+
[layerA, layerB],
|
|
1045
|
+
MON_JAN5,
|
|
1046
|
+
end,
|
|
1047
|
+
);
|
|
1048
|
+
for (const e of abMerged) {
|
|
1049
|
+
expect(e.title).toBe("A");
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const baMerged: Array<CalendarEvent> = merge(
|
|
1053
|
+
[layerB, layerA],
|
|
1054
|
+
MON_JAN5,
|
|
1055
|
+
end,
|
|
1056
|
+
);
|
|
1057
|
+
for (const e of baMerged) {
|
|
1058
|
+
expect(e.title).toBe("B");
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
test("getNumberOfEvents cap is applied only after the merge, not per-layer", () => {
|
|
1063
|
+
/*
|
|
1064
|
+
* Daily 09-17 primary over 24/7 weekly fallback. The first merged event is
|
|
1065
|
+
* the fallback night; capping must still surface later events correctly.
|
|
1066
|
+
*/
|
|
1067
|
+
const layers: Array<LayerProps> = [
|
|
1068
|
+
buildLayer({
|
|
1069
|
+
users: ["P"],
|
|
1070
|
+
start: MON_JAN5,
|
|
1071
|
+
restriction: dailyRestriction(9, 17),
|
|
1072
|
+
handOff: d(2026, 0, 5, 12),
|
|
1073
|
+
rot: rotation(EventInterval.Day, 1),
|
|
1074
|
+
}),
|
|
1075
|
+
buildLayer({
|
|
1076
|
+
users: ["Z"],
|
|
1077
|
+
start: MON_JAN5,
|
|
1078
|
+
restriction: noRestriction(),
|
|
1079
|
+
handOff: MON_JAN5,
|
|
1080
|
+
rot: rotation(EventInterval.Week, 1),
|
|
1081
|
+
}),
|
|
1082
|
+
];
|
|
1083
|
+
const full: Array<CalendarEvent> = util.getMultiLayerEvents({
|
|
1084
|
+
layers: layers,
|
|
1085
|
+
calendarStartDate: MON_JAN5,
|
|
1086
|
+
calendarEndDate: d(2026, 0, 12),
|
|
1087
|
+
});
|
|
1088
|
+
const capped: Array<CalendarEvent> = util.getMultiLayerEvents(
|
|
1089
|
+
{
|
|
1090
|
+
layers: layers,
|
|
1091
|
+
calendarStartDate: MON_JAN5,
|
|
1092
|
+
calendarEndDate: d(2026, 0, 12),
|
|
1093
|
+
},
|
|
1094
|
+
{ getNumberOfEvents: 3 },
|
|
1095
|
+
);
|
|
1096
|
+
expect(capped.length).toBe(3);
|
|
1097
|
+
// The capped list is exactly the prefix of the full merged timeline.
|
|
1098
|
+
for (let i: number = 0; i < capped.length; i++) {
|
|
1099
|
+
expect(capped[i]!.title).toBe(full[i]!.title);
|
|
1100
|
+
expect(capped[i]!.start.getTime()).toBe(full[i]!.start.getTime());
|
|
1101
|
+
expect(capped[i]!.end.getTime()).toBe(full[i]!.end.getTime());
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
});
|