@oneuptime/common 11.3.26 → 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/IncidentEpisodeRoleMember.ts +1 -1
- package/Models/DatabaseModels/IncidentMember.ts +1 -1
- package/Models/DatabaseModels/IncomingCallPolicy.ts +13 -0
- package/Models/DatabaseModels/Index.ts +3 -0
- package/Models/DatabaseModels/MigrationFailure.ts +170 -0
- package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +51 -2
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +44 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +6 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.ts +6 -0
- package/Models/DatabaseModels/OnCallDutyPolicyTimeLog.ts +1 -1
- package/Models/DatabaseModels/OnCallDutyPolicyUserOverride.ts +2 -2
- package/Server/API/AlertAPI.ts +1 -1
- package/Server/API/IncidentAPI.ts +2 -2
- package/Server/API/IncidentEpisodeAPI.ts +1 -1
- package/Server/API/LlmProviderAPI.ts +1 -0
- package/Server/API/ScheduledMaintenanceAPI.ts +1 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.ts +81 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.ts +35 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Infrastructure/PostgresDatabase.ts +11 -0
- package/Server/Services/AIService.ts +74 -2
- package/Server/Services/AnalyticsDatabaseService.ts +10 -1
- package/Server/Services/DatabaseService.ts +10 -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/MigrationFailureService.ts +9 -0
- package/Server/Services/MonitorProbeService.ts +33 -7
- package/Server/Services/MonitorService.ts +19 -7
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleService.ts +185 -41
- package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +23 -15
- package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +39 -0
- package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +148 -0
- package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +11 -2
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +502 -26
- package/Server/Services/OnCallDutyPolicyTimeLogService.ts +49 -7
- package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +218 -4
- package/Server/Services/ProjectCallSMSConfigService.ts +82 -1
- package/Server/Services/TeamMemberService.ts +6 -0
- package/Server/Services/UserNotificationRuleService.ts +13 -42
- package/Server/Services/UserOnCallLogService.ts +90 -0
- package/Server/Services/UserOnCallLogTimelineService.ts +43 -1
- package/Server/Types/Database/QueryUtil.ts +57 -0
- package/Server/Utils/AI/AlertAIContextBuilder.ts +6 -1
- package/Server/Utils/AI/Chat/ChatAgentRunner.ts +14 -2
- package/Server/Utils/AI/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/IncidentTools.ts +183 -1
- package/Server/Utils/AI/Toolbox/Index.ts +4 -1
- package/Server/Utils/AI/Toolbox/RecentChangesTools.ts +208 -0
- package/Server/Utils/AI/Toolbox/Serializer.ts +42 -0
- package/Server/Utils/Database/MigrationFailureLog.ts +240 -0
- package/Server/Utils/IncomingCallPhoneNumber.ts +41 -0
- package/Server/Utils/LLM/LLMService.ts +110 -11
- package/Server/Utils/Telemetry/IoTSnapshotScan.ts +12 -9
- package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +12 -9
- package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +0 -8
- package/Tests/Server/Services/OnCallDutyPolicyScheduleLayerUserReorder.test.ts +233 -0
- package/Tests/Server/Services/OnCallDutyPolicyTimeLogServiceScoping.test.ts +242 -0
- package/Tests/Server/Services/OnCallDutyPolicyUserOverrideEdit.test.ts +198 -0
- package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +495 -0
- package/Tests/Server/Types/Database/QueryUtil.test.ts +74 -0
- package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +78 -0
- package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +68 -0
- package/Tests/Types/DateExhaustiveTimezone.test.ts +700 -0
- package/Tests/Types/DateTimezoneWallClock.test.ts +99 -0
- package/Tests/Types/Events/Recurring.test.ts +22 -9
- package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixes.test.ts +291 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixesRound2.test.ts +398 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeekendGap.test.ts +114 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeeklyDayShift.test.ts +76 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyMutation.test.ts +128 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyProbe.test.ts +203 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDiffFuzz.test.ts +322 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilEdgeCases.test.ts +280 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveCurrentUser.test.ts +863 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveDaily.test.ts +900 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveMultiLayer.test.ts +1104 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +957 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveWeekly.test.ts +1095 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilInvariants.test.ts +232 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +227 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMonthYearAudit.test.ts +255 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerAudit.test.ts +388 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerEdge.test.ts +300 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilOvernightDSTAudit.test.ts +158 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapFix.test.ts +253 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapRepro.test.ts +157 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilRotationFixes.test.ts +414 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilTimezone.test.ts +243 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilWeekendGapRepro.test.ts +112 -0
- package/Tests/Types/OnCallDutyPolicy/OverrideLensAudit.test.ts +117 -0
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimes.test.ts +8 -6
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimesDefaultDayAudit.test.ts +112 -0
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimesExhaustive.test.ts +821 -0
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimesMutationInvariant.test.ts +886 -0
- package/Tests/Types/OnCallDutyPolicy/UserOverrideUtil.test.ts +255 -0
- package/Tests/Types/OnCallDutyPolicy/UserOverrideUtilExhaustive.test.ts +1126 -0
- package/Types/Date.ts +200 -15
- 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/ModelFormModal/ModelFormModal.tsx +8 -0
- package/UI/Components/RadioButtons/BasicRadioButtons.tsx +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentMember.js +1 -1
- package/build/dist/Models/DatabaseModels/IncidentMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js +10 -0
- package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MigrationFailure.js +196 -0
- package/build/dist/Models/DatabaseModels/MigrationFailure.js.map +1 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +52 -2
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +45 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +6 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js +6 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js +2 -2
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js.map +1 -1
- package/build/dist/Server/API/AlertAPI.js +1 -1
- package/build/dist/Server/API/IncidentAPI.js +2 -2
- package/build/dist/Server/API/IncidentEpisodeAPI.js +1 -1
- package/build/dist/Server/API/LlmProviderAPI.js +1 -0
- package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
- package/build/dist/Server/API/ScheduledMaintenanceAPI.js +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js +38 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js +16 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/PostgresDatabase.js +9 -0
- package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
- package/build/dist/Server/Services/AIService.js +53 -2
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +10 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +10 -1
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +1 -1
- package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js +55 -2
- package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js.map +1 -1
- package/build/dist/Server/Services/IncomingCallPolicyService.js +40 -0
- package/build/dist/Server/Services/IncomingCallPolicyService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/MigrationFailureService.js +9 -0
- package/build/dist/Server/Services/MigrationFailureService.js.map +1 -0
- package/build/dist/Server/Services/MonitorProbeService.js +33 -7
- package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +12 -5
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +81 -22
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js +134 -31
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +21 -13
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +36 -0
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +121 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +11 -2
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +409 -36
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js +52 -8
- package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +170 -3
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
- package/build/dist/Server/Services/ProjectCallSMSConfigService.js +72 -0
- package/build/dist/Server/Services/ProjectCallSMSConfigService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +6 -0
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +12 -30
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallLogService.js +75 -0
- package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallLogTimelineService.js +31 -1
- package/build/dist/Server/Services/UserOnCallLogTimelineService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryUtil.js +45 -0
- package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
- package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js +6 -1
- package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +13 -2
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/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/IncidentTools.js +152 -1
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +4 -1
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js +165 -0
- package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +40 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -1
- package/build/dist/Server/Utils/Database/MigrationFailureLog.js +174 -0
- package/build/dist/Server/Utils/Database/MigrationFailureLog.js.map +1 -0
- package/build/dist/Server/Utils/IncomingCallPhoneNumber.js +30 -0
- package/build/dist/Server/Utils/IncomingCallPhoneNumber.js.map +1 -0
- package/build/dist/Server/Utils/LLM/LLMService.js +81 -5
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +11 -8
- package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +11 -8
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
- package/build/dist/Types/Date.js +153 -15
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +602 -171
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js +27 -13
- package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/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/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js +1 -1
- package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EMPIRICAL invariant fuzzing (audit lens).
|
|
3
|
+
*
|
|
4
|
+
* For unrestricted rotations over long horizons:
|
|
5
|
+
* (A) CONTIGUITY: sorted events cover [start, end] with no gap > 2s and no
|
|
6
|
+
* overlap (events are stitched with a +1s seam, so a 1s gap is expected).
|
|
7
|
+
* (B) FAIRNESS: over many periods each user's shift count is within 1.
|
|
8
|
+
* (C) ORDER: users cycle A,B,C,A,B,C... with no skip/repeat, including across
|
|
9
|
+
* DST transitions.
|
|
10
|
+
*/
|
|
11
|
+
import LayerUtil, { LayerProps } from "../../../Types/OnCallDutyPolicy/Layer";
|
|
12
|
+
import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
|
|
13
|
+
import RestrictionTimes, {
|
|
14
|
+
RestrictionType,
|
|
15
|
+
} from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
|
|
16
|
+
import Recurring from "../../../Types/Events/Recurring";
|
|
17
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
18
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
19
|
+
import EventInterval from "../../../Types/Events/EventInterval";
|
|
20
|
+
import PositiveNumber from "../../../Types/PositiveNumber";
|
|
21
|
+
|
|
22
|
+
function user(id: string): User {
|
|
23
|
+
return {
|
|
24
|
+
id: {
|
|
25
|
+
toString: (): string => {
|
|
26
|
+
return id;
|
|
27
|
+
},
|
|
28
|
+
} as any,
|
|
29
|
+
} as User;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function noRestriction(): RestrictionTimes {
|
|
33
|
+
const r: RestrictionTimes = new RestrictionTimes();
|
|
34
|
+
r.restictionType = RestrictionType.None;
|
|
35
|
+
r.dayRestrictionTimes = null;
|
|
36
|
+
return r;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Config {
|
|
40
|
+
name: string;
|
|
41
|
+
intervalType: EventInterval;
|
|
42
|
+
intervalCount: number;
|
|
43
|
+
userIds: string[];
|
|
44
|
+
timezone?: string | undefined;
|
|
45
|
+
start: Date;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function layerOf(c: Config): LayerProps {
|
|
49
|
+
const rot: Recurring = new Recurring();
|
|
50
|
+
rot.intervalType = c.intervalType;
|
|
51
|
+
rot.intervalCount = new PositiveNumber(c.intervalCount);
|
|
52
|
+
const handoff: Date = Recurring.getNextDateInterval(c.start, rot);
|
|
53
|
+
return {
|
|
54
|
+
users: c.userIds.map(user),
|
|
55
|
+
startDateTimeOfLayer: c.start,
|
|
56
|
+
restrictionTimes: noRestriction(),
|
|
57
|
+
handOffTime: handoff,
|
|
58
|
+
rotation: rot,
|
|
59
|
+
timezone: c.timezone,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function fullExpand(layer: LayerProps, to: Date): Array<CalendarEvent> {
|
|
64
|
+
const util: LayerUtil = new LayerUtil();
|
|
65
|
+
const events: Array<CalendarEvent> = util.getEvents({
|
|
66
|
+
...layer,
|
|
67
|
+
calendarStartDate: layer.startDateTimeOfLayer,
|
|
68
|
+
calendarEndDate: to,
|
|
69
|
+
});
|
|
70
|
+
events.sort((a: CalendarEvent, b: CalendarEvent) => {
|
|
71
|
+
return a.start.getTime() - b.start.getTime();
|
|
72
|
+
});
|
|
73
|
+
return events;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const START: Date = OneUptimeDate.fromString("2025-01-06T00:00:00.000Z"); // Monday
|
|
77
|
+
|
|
78
|
+
describe("INVARIANT: 24/7 coverage contiguity (no gap > 2s, no overlap)", () => {
|
|
79
|
+
const configs: Config[] = [];
|
|
80
|
+
const intervals: EventInterval[] = [
|
|
81
|
+
EventInterval.Hour,
|
|
82
|
+
EventInterval.Day,
|
|
83
|
+
EventInterval.Week,
|
|
84
|
+
];
|
|
85
|
+
const counts: number[] = [1, 2, 3];
|
|
86
|
+
const userSets: string[][] = [
|
|
87
|
+
["A", "B"],
|
|
88
|
+
["A", "B", "C"],
|
|
89
|
+
];
|
|
90
|
+
const timezones: (string | undefined)[] = [
|
|
91
|
+
undefined,
|
|
92
|
+
"America/New_York",
|
|
93
|
+
"Asia/Kolkata",
|
|
94
|
+
];
|
|
95
|
+
for (const it of intervals) {
|
|
96
|
+
for (const count of counts) {
|
|
97
|
+
for (const users of userSets) {
|
|
98
|
+
for (const tz of timezones) {
|
|
99
|
+
configs.push({
|
|
100
|
+
name: `${count}x${it} users=${users.length} tz=${tz ?? "local"}`,
|
|
101
|
+
intervalType: it,
|
|
102
|
+
intervalCount: count,
|
|
103
|
+
userIds: users,
|
|
104
|
+
timezone: tz,
|
|
105
|
+
start: START,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const horizonMs: Record<string, number> = {
|
|
113
|
+
[EventInterval.Hour]: 60 * 3600000, // 60 hours
|
|
114
|
+
[EventInterval.Day]: 60 * 86400000, // 60 days (crosses spring DST)
|
|
115
|
+
[EventInterval.Week]: 40 * 604800000, // 40 weeks (crosses DST both ways)
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
for (const c of configs) {
|
|
119
|
+
it(`${c.name}`, () => {
|
|
120
|
+
const layer: LayerProps = layerOf(c);
|
|
121
|
+
const to: Date = new Date(
|
|
122
|
+
START.getTime() + (horizonMs[c.intervalType] || 60 * 86400000),
|
|
123
|
+
);
|
|
124
|
+
const events: Array<CalendarEvent> = fullExpand(layer, to);
|
|
125
|
+
|
|
126
|
+
const problems: string[] = [];
|
|
127
|
+
for (let i: number = 1; i < events.length; i++) {
|
|
128
|
+
const prev: CalendarEvent = events[i - 1]!;
|
|
129
|
+
const cur: CalendarEvent = events[i]!;
|
|
130
|
+
const gapMs: number = cur.start.getTime() - prev.end.getTime();
|
|
131
|
+
// Expected seam is +1s. Flag gaps > 2s (coverage hole) or overlaps (<0).
|
|
132
|
+
if (gapMs > 2000) {
|
|
133
|
+
problems.push(
|
|
134
|
+
`GAP ${gapMs}ms between ${prev.title}@${prev.end.toISOString()} and ${cur.title}@${cur.start.toISOString()}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (gapMs < 0) {
|
|
138
|
+
problems.push(
|
|
139
|
+
`OVERLAP ${gapMs}ms between ${prev.title}@${prev.end.toISOString()} and ${cur.title}@${cur.start.toISOString()}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
expect({ config: c.name, problems }).toEqual({
|
|
145
|
+
config: c.name,
|
|
146
|
+
problems: [],
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("INVARIANT: rotation order + fairness over many periods", () => {
|
|
153
|
+
const configs: Config[] = [];
|
|
154
|
+
const intervals: EventInterval[] = [
|
|
155
|
+
EventInterval.Hour,
|
|
156
|
+
EventInterval.Day,
|
|
157
|
+
EventInterval.Week,
|
|
158
|
+
];
|
|
159
|
+
const counts: number[] = [1, 2, 3];
|
|
160
|
+
const userSets: string[][] = [
|
|
161
|
+
["A", "B", "C"],
|
|
162
|
+
["A", "B", "C", "D", "E"],
|
|
163
|
+
];
|
|
164
|
+
const timezones: (string | undefined)[] = [undefined, "America/New_York"];
|
|
165
|
+
for (const it of intervals) {
|
|
166
|
+
for (const count of counts) {
|
|
167
|
+
for (const users of userSets) {
|
|
168
|
+
for (const tz of timezones) {
|
|
169
|
+
configs.push({
|
|
170
|
+
name: `${count}x${it} users=${users.length} tz=${tz ?? "local"}`,
|
|
171
|
+
intervalType: it,
|
|
172
|
+
intervalCount: count,
|
|
173
|
+
userIds: users,
|
|
174
|
+
timezone: tz,
|
|
175
|
+
start: START,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const horizonMs: Record<string, number> = {
|
|
183
|
+
[EventInterval.Hour]: 120 * 3600000,
|
|
184
|
+
[EventInterval.Day]: 120 * 86400000,
|
|
185
|
+
[EventInterval.Week]: 60 * 604800000,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
for (const c of configs) {
|
|
189
|
+
it(`${c.name}`, () => {
|
|
190
|
+
const layer: LayerProps = layerOf(c);
|
|
191
|
+
const to: Date = new Date(
|
|
192
|
+
START.getTime() + (horizonMs[c.intervalType] || 120 * 86400000),
|
|
193
|
+
);
|
|
194
|
+
const events: Array<CalendarEvent> = fullExpand(layer, to);
|
|
195
|
+
|
|
196
|
+
// ORDER: consecutive events cycle in userIds order.
|
|
197
|
+
const orderProblems: string[] = [];
|
|
198
|
+
const n: number = c.userIds.length;
|
|
199
|
+
for (let i: number = 0; i < events.length; i++) {
|
|
200
|
+
const expected: string = c.userIds[i % n]!;
|
|
201
|
+
if (events[i]!.title !== expected) {
|
|
202
|
+
orderProblems.push(
|
|
203
|
+
`event#${i} start=${events[i]!.start.toISOString()} expected=${expected} actual=${events[i]!.title}`,
|
|
204
|
+
);
|
|
205
|
+
break; // first divergence is enough
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// FAIRNESS: drop the final (possibly clipped) event, count shifts.
|
|
210
|
+
const counts: Record<string, number> = {};
|
|
211
|
+
const usable: Array<CalendarEvent> = events.slice(0, -1);
|
|
212
|
+
for (const e of usable) {
|
|
213
|
+
counts[e.title] = (counts[e.title] || 0) + 1;
|
|
214
|
+
}
|
|
215
|
+
const values: number[] = c.userIds.map((u: string) => {
|
|
216
|
+
return counts[u] || 0;
|
|
217
|
+
});
|
|
218
|
+
const max: number = Math.max(...values);
|
|
219
|
+
const min: number = Math.min(...values);
|
|
220
|
+
|
|
221
|
+
expect({
|
|
222
|
+
config: c.name,
|
|
223
|
+
orderProblems,
|
|
224
|
+
fairnessSpread: max - min,
|
|
225
|
+
}).toEqual({
|
|
226
|
+
config: c.name,
|
|
227
|
+
orderProblems: [],
|
|
228
|
+
fairnessSpread: max - min <= 1 ? max - min : "TOO_WIDE:" + (max - min),
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import LayerUtil, { LayerProps } from "../../../Types/OnCallDutyPolicy/Layer";
|
|
2
|
+
import RestrictionTimes, {
|
|
3
|
+
RestrictionType,
|
|
4
|
+
} from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
|
|
5
|
+
import Recurring from "../../../Types/Events/Recurring";
|
|
6
|
+
import EventInterval from "../../../Types/Events/EventInterval";
|
|
7
|
+
import PositiveNumber from "../../../Types/PositiveNumber";
|
|
8
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
9
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
10
|
+
import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
|
|
11
|
+
import { describe, expect, test } from "@jest/globals";
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* Audit H2: removeOverlappingEvents ran a full Array.sort() INSIDE its per-event
|
|
15
|
+
* loop (O(n^2 log n)); getEvents rebuilt its array with [...events, ...] each
|
|
16
|
+
* period (O(n^2)). The fix hoists the sort to a single post-loop sort and pushes
|
|
17
|
+
* per-period segments, and makes the lower-priority "trim current event" step a
|
|
18
|
+
* monotonic max so it is independent of finalEvents ordering (which is what made
|
|
19
|
+
* hoisting the sort safe). These tests lock in the merge INVARIANTS the
|
|
20
|
+
* optimization must preserve: no overlaps, sorted, positive length, and correct
|
|
21
|
+
* priority/fallback coverage with no gaps under a 24/7 fallback.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
function user(id: string): User {
|
|
25
|
+
return {
|
|
26
|
+
id: {
|
|
27
|
+
toString: (): string => {
|
|
28
|
+
return id;
|
|
29
|
+
},
|
|
30
|
+
} as any,
|
|
31
|
+
} as User;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function rotation(type: EventInterval, count: number): Recurring {
|
|
35
|
+
const r: Recurring = new Recurring();
|
|
36
|
+
r.intervalType = type;
|
|
37
|
+
r.intervalCount = new PositiveNumber(count);
|
|
38
|
+
return r;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function businessHoursRestriction(): RestrictionTimes {
|
|
42
|
+
const rt: RestrictionTimes = new RestrictionTimes();
|
|
43
|
+
rt.restictionType = RestrictionType.Daily;
|
|
44
|
+
rt.dayRestrictionTimes = {
|
|
45
|
+
startTime: new Date(Date.UTC(2025, 0, 1, 9, 0, 0)),
|
|
46
|
+
endTime: new Date(Date.UTC(2025, 0, 1, 17, 0, 0)),
|
|
47
|
+
};
|
|
48
|
+
return rt;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assertSortedNonOverlappingPositive(
|
|
52
|
+
events: Array<CalendarEvent>,
|
|
53
|
+
): void {
|
|
54
|
+
for (let i: number = 0; i < events.length; i++) {
|
|
55
|
+
// positive length
|
|
56
|
+
expect(new Date(events[i]!.end).getTime()).toBeGreaterThan(
|
|
57
|
+
new Date(events[i]!.start).getTime(),
|
|
58
|
+
);
|
|
59
|
+
if (i > 0) {
|
|
60
|
+
// sorted by start
|
|
61
|
+
expect(new Date(events[i]!.start).getTime()).toBeGreaterThanOrEqual(
|
|
62
|
+
new Date(events[i - 1]!.start).getTime(),
|
|
63
|
+
);
|
|
64
|
+
// non-overlapping (previous end is at/before this start)
|
|
65
|
+
expect(new Date(events[i - 1]!.end).getTime()).toBeLessThanOrEqual(
|
|
66
|
+
new Date(events[i]!.start).getTime(),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe("LayerUtil audit H2: multi-layer merge invariants preserved", () => {
|
|
73
|
+
const windowStart: Date = new Date(Date.UTC(2025, 5, 2, 0, 0, 0));
|
|
74
|
+
const windowEnd: Date = new Date(Date.UTC(2025, 5, 6, 0, 0, 0));
|
|
75
|
+
|
|
76
|
+
test("high-priority business-hours layer over a 24/7 fallback: no overlaps, no gaps, both users present", () => {
|
|
77
|
+
const layers: Array<LayerProps> = [
|
|
78
|
+
{
|
|
79
|
+
// priority 1 (higher): business-hours coverage by X
|
|
80
|
+
users: [user("X")],
|
|
81
|
+
startDateTimeOfLayer: new Date(Date.UTC(2025, 5, 1, 0, 0, 0)),
|
|
82
|
+
handOffTime: new Date(Date.UTC(2025, 5, 1, 0, 0, 0)),
|
|
83
|
+
restrictionTimes: businessHoursRestriction(),
|
|
84
|
+
rotation: rotation(EventInterval.Day, 1),
|
|
85
|
+
timezone: "UTC",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
// priority 2 (fallback): 24/7 coverage by Y
|
|
89
|
+
users: [user("Y")],
|
|
90
|
+
startDateTimeOfLayer: new Date(Date.UTC(2025, 5, 1, 0, 0, 0)),
|
|
91
|
+
handOffTime: new Date(Date.UTC(2025, 5, 1, 0, 0, 0)),
|
|
92
|
+
restrictionTimes: RestrictionTimes.getDefault(),
|
|
93
|
+
rotation: rotation(EventInterval.Week, 1),
|
|
94
|
+
timezone: "UTC",
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const events: Array<CalendarEvent> = new LayerUtil().getMultiLayerEvents({
|
|
99
|
+
layers,
|
|
100
|
+
calendarStartDate: windowStart,
|
|
101
|
+
calendarEndDate: windowEnd,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
assertSortedNonOverlappingPositive(events);
|
|
105
|
+
|
|
106
|
+
const titles: Set<string> = new Set(
|
|
107
|
+
events.map((e: CalendarEvent) => {
|
|
108
|
+
return e.title;
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
expect(titles.has("X")).toBe(true); // higher priority appears
|
|
112
|
+
expect(titles.has("Y")).toBe(true); // fallback fills the gaps
|
|
113
|
+
|
|
114
|
+
/*
|
|
115
|
+
* With a 24/7 fallback the merged coverage must be CONTIGUOUS across the
|
|
116
|
+
* window (no uncovered gap). Allow the ~1s boundary seam the engine leaves
|
|
117
|
+
* between adjacent events.
|
|
118
|
+
*/
|
|
119
|
+
for (let i: number = 1; i < events.length; i++) {
|
|
120
|
+
const gapSeconds: number =
|
|
121
|
+
(new Date(events[i]!.start).getTime() -
|
|
122
|
+
new Date(events[i - 1]!.end).getTime()) /
|
|
123
|
+
1000;
|
|
124
|
+
expect(gapSeconds).toBeLessThanOrEqual(2);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("two back-to-back higher-priority windows over a fallback preserve the fallback's trailing tail (max-trim path)", () => {
|
|
129
|
+
/*
|
|
130
|
+
* Higher-priority daily rotation with two users creates adjacent daily
|
|
131
|
+
* windows; the fallback must survive as gaps/tail with no overlap.
|
|
132
|
+
*/
|
|
133
|
+
const layers: Array<LayerProps> = [
|
|
134
|
+
{
|
|
135
|
+
users: [user("P"), user("Q")],
|
|
136
|
+
startDateTimeOfLayer: new Date(Date.UTC(2025, 5, 1, 0, 0, 0)),
|
|
137
|
+
handOffTime: new Date(Date.UTC(2025, 5, 1, 0, 0, 0)),
|
|
138
|
+
restrictionTimes: businessHoursRestriction(),
|
|
139
|
+
rotation: rotation(EventInterval.Day, 1),
|
|
140
|
+
timezone: "UTC",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
users: [user("F")],
|
|
144
|
+
startDateTimeOfLayer: new Date(Date.UTC(2025, 5, 1, 0, 0, 0)),
|
|
145
|
+
handOffTime: new Date(Date.UTC(2025, 5, 1, 0, 0, 0)),
|
|
146
|
+
restrictionTimes: RestrictionTimes.getDefault(),
|
|
147
|
+
rotation: rotation(EventInterval.Week, 1),
|
|
148
|
+
timezone: "UTC",
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const events: Array<CalendarEvent> = new LayerUtil().getMultiLayerEvents({
|
|
153
|
+
layers,
|
|
154
|
+
calendarStartDate: windowStart,
|
|
155
|
+
calendarEndDate: windowEnd,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
assertSortedNonOverlappingPositive(events);
|
|
159
|
+
|
|
160
|
+
/*
|
|
161
|
+
* The fallback F must appear (it owns the nights/weekends the higher layer
|
|
162
|
+
* does not cover) — the trailing-tail reconstruction must not delete it.
|
|
163
|
+
*/
|
|
164
|
+
const titles: Set<string> = new Set(
|
|
165
|
+
events.map((e: CalendarEvent) => {
|
|
166
|
+
return e.title;
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
expect(titles.has("F")).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("single unrestricted layer: getNumberOfEvents returns exactly the requested current+next", () => {
|
|
173
|
+
const layer: LayerProps = {
|
|
174
|
+
users: [user("A"), user("B")],
|
|
175
|
+
startDateTimeOfLayer: new Date(Date.UTC(2025, 5, 1, 0, 0, 0)),
|
|
176
|
+
handOffTime: new Date(Date.UTC(2025, 5, 1, 0, 0, 0)),
|
|
177
|
+
restrictionTimes: RestrictionTimes.getDefault(),
|
|
178
|
+
rotation: rotation(EventInterval.Day, 1),
|
|
179
|
+
timezone: "UTC",
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const events: Array<CalendarEvent> = new LayerUtil().getMultiLayerEvents(
|
|
183
|
+
{
|
|
184
|
+
layers: [layer],
|
|
185
|
+
calendarStartDate: new Date(Date.UTC(2025, 5, 3, 12, 0, 0)),
|
|
186
|
+
calendarEndDate: new Date(Date.UTC(2025, 5, 10, 0, 0, 0)),
|
|
187
|
+
},
|
|
188
|
+
{ getNumberOfEvents: 2 },
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(events.length).toBe(2);
|
|
192
|
+
assertSortedNonOverlappingPositive(events);
|
|
193
|
+
// current and next are different users in a 2-user daily rotation.
|
|
194
|
+
expect(events[0]!.title).not.toBe(events[1]!.title);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("hourly layer over a multi-day window resolves quickly (push not O(n^2) spread)", () => {
|
|
198
|
+
const layer: LayerProps = {
|
|
199
|
+
users: [user("A"), user("B")],
|
|
200
|
+
startDateTimeOfLayer: new Date(Date.UTC(2025, 5, 1, 0, 0, 0)),
|
|
201
|
+
handOffTime: new Date(Date.UTC(2025, 5, 1, 0, 0, 0)),
|
|
202
|
+
restrictionTimes: RestrictionTimes.getDefault(),
|
|
203
|
+
rotation: rotation(EventInterval.Hour, 1),
|
|
204
|
+
timezone: "UTC",
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const start: number = OneUptimeDate.getCurrentDate().getTime();
|
|
208
|
+
const events: Array<CalendarEvent> = new LayerUtil().getMultiLayerEvents({
|
|
209
|
+
layers: [layer],
|
|
210
|
+
/*
|
|
211
|
+
* ~20 days of hourly events (~480) — the merge must stay well under any
|
|
212
|
+
* pathological quadratic blowup.
|
|
213
|
+
*/
|
|
214
|
+
calendarStartDate: new Date(Date.UTC(2025, 5, 1, 0, 0, 0)),
|
|
215
|
+
calendarEndDate: new Date(Date.UTC(2025, 5, 21, 0, 0, 0)),
|
|
216
|
+
});
|
|
217
|
+
const elapsedMs: number = OneUptimeDate.getCurrentDate().getTime() - start;
|
|
218
|
+
|
|
219
|
+
expect(events.length).toBeGreaterThan(0);
|
|
220
|
+
assertSortedNonOverlappingPositive(events);
|
|
221
|
+
/*
|
|
222
|
+
* Generous ceiling — the pre-fix quadratic path took tens of seconds for far
|
|
223
|
+
* fewer events; this must complete in a small fraction of that.
|
|
224
|
+
*/
|
|
225
|
+
expect(elapsedMs).toBeLessThan(15000);
|
|
226
|
+
});
|
|
227
|
+
});
|