@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,243 @@
|
|
|
1
|
+
import LayerUtil from "../../../Types/OnCallDutyPolicy/Layer";
|
|
2
|
+
import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
|
|
3
|
+
import RestrictionTimes, {
|
|
4
|
+
RestrictionType,
|
|
5
|
+
WeeklyResctriction,
|
|
6
|
+
} from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
|
|
7
|
+
import Recurring from "../../../Types/Events/Recurring";
|
|
8
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
9
|
+
import EventInterval from "../../../Types/Events/EventInterval";
|
|
10
|
+
import DayOfWeek from "../../../Types/Day/DayOfWeek";
|
|
11
|
+
import moment from "moment-timezone";
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* HIGH-5 (timezone) + LOW-2 (DST). These tests construct restriction times and
|
|
15
|
+
* verify results entirely in an explicit IANA zone via moment.tz, so they hold
|
|
16
|
+
* regardless of the machine/CI local timezone — which is exactly the bug:
|
|
17
|
+
* restriction wall-clock must resolve in the SCHEDULE's zone, not the server's.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
function user(id: string): User {
|
|
21
|
+
return {
|
|
22
|
+
id: {
|
|
23
|
+
toString: () => {
|
|
24
|
+
return id;
|
|
25
|
+
},
|
|
26
|
+
} as any,
|
|
27
|
+
} as User;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function rotation(
|
|
31
|
+
intervalType: EventInterval,
|
|
32
|
+
intervalCount: number,
|
|
33
|
+
): Recurring {
|
|
34
|
+
return Recurring.fromJSON({
|
|
35
|
+
_type: "Recurring",
|
|
36
|
+
value: {
|
|
37
|
+
intervalType: intervalType,
|
|
38
|
+
intervalCount: { _type: "PositiveNumber", value: intervalCount },
|
|
39
|
+
},
|
|
40
|
+
} as any);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// An absolute instant that is `iso` wall-clock in `tz`.
|
|
44
|
+
function tzInstant(iso: string, tz: string): Date {
|
|
45
|
+
return moment.tz(iso, tz).toDate();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Wall-clock "HH:mm" of an instant AS SEEN IN `tz`.
|
|
49
|
+
function hhmm(d: Date, tz: string): string {
|
|
50
|
+
return moment.tz(d, tz).format("HH:mm");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("LayerUtil HIGH-5: daily restriction resolves in the schedule timezone", () => {
|
|
54
|
+
const tz: string = "America/New_York";
|
|
55
|
+
|
|
56
|
+
test("09:00-17:00 restriction resolves to 09:00-17:00 in the schedule zone", () => {
|
|
57
|
+
const util: LayerUtil = new LayerUtil();
|
|
58
|
+
|
|
59
|
+
const layerStart: Date = tzInstant("2026-02-16 00:00", tz); // Mon (no DST)
|
|
60
|
+
const calStart: Date = layerStart;
|
|
61
|
+
const calEnd: Date = tzInstant("2026-02-19 00:00", tz); // +3 days
|
|
62
|
+
|
|
63
|
+
const restrictionTimes: RestrictionTimes = new RestrictionTimes();
|
|
64
|
+
restrictionTimes.restictionType = RestrictionType.Daily;
|
|
65
|
+
restrictionTimes.dayRestrictionTimes = {
|
|
66
|
+
startTime: tzInstant("2026-02-16 09:00", tz),
|
|
67
|
+
endTime: tzInstant("2026-02-16 17:00", tz),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const events: Array<CalendarEvent> = util.getEvents({
|
|
71
|
+
users: [user("u1")],
|
|
72
|
+
startDateTimeOfLayer: layerStart,
|
|
73
|
+
handOffTime: layerStart,
|
|
74
|
+
restrictionTimes: restrictionTimes,
|
|
75
|
+
rotation: rotation(EventInterval.Day, 1),
|
|
76
|
+
timezone: tz,
|
|
77
|
+
calendarStartDate: calStart,
|
|
78
|
+
calendarEndDate: calEnd,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// One 09:00-17:00 window per day, in New York wall-clock.
|
|
82
|
+
const windows: Array<CalendarEvent> = events.filter((e: CalendarEvent) => {
|
|
83
|
+
return hhmm(e.start, tz) === "09:00";
|
|
84
|
+
});
|
|
85
|
+
expect(windows.length).toBeGreaterThanOrEqual(3);
|
|
86
|
+
for (const w of windows) {
|
|
87
|
+
expect(hhmm(w.start, tz)).toBe("09:00");
|
|
88
|
+
expect(hhmm(w.end, tz)).toBe("17:00");
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("a schedule authored + resolved in Kolkata resolves to its own wall-clock", () => {
|
|
93
|
+
const util: LayerUtil = new LayerUtil();
|
|
94
|
+
const tzIST: string = "Asia/Kolkata"; // UTC+5:30, no DST
|
|
95
|
+
|
|
96
|
+
/*
|
|
97
|
+
* Author and resolve entirely in Kolkata, with the layer start aligned to
|
|
98
|
+
* local midnight (as a real schedule created in that zone would be).
|
|
99
|
+
*/
|
|
100
|
+
const layerStart: Date = tzInstant("2026-02-16 00:00", tzIST);
|
|
101
|
+
const restrictionTimes: RestrictionTimes = new RestrictionTimes();
|
|
102
|
+
restrictionTimes.restictionType = RestrictionType.Daily;
|
|
103
|
+
restrictionTimes.dayRestrictionTimes = {
|
|
104
|
+
startTime: tzInstant("2026-02-16 09:30", tzIST),
|
|
105
|
+
endTime: tzInstant("2026-02-16 18:30", tzIST),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const events: Array<CalendarEvent> = util.getEvents({
|
|
109
|
+
users: [user("u1")],
|
|
110
|
+
startDateTimeOfLayer: layerStart,
|
|
111
|
+
handOffTime: layerStart,
|
|
112
|
+
restrictionTimes: restrictionTimes,
|
|
113
|
+
rotation: rotation(EventInterval.Day, 1),
|
|
114
|
+
timezone: tzIST,
|
|
115
|
+
calendarStartDate: layerStart,
|
|
116
|
+
calendarEndDate: tzInstant("2026-02-19 00:00", tzIST),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const windows: Array<CalendarEvent> = events.filter((e: CalendarEvent) => {
|
|
120
|
+
return hhmm(e.start, tzIST) === "09:30";
|
|
121
|
+
});
|
|
122
|
+
expect(windows.length).toBeGreaterThanOrEqual(2);
|
|
123
|
+
for (const w of windows) {
|
|
124
|
+
expect(hhmm(w.end, tzIST)).toBe("18:30");
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("LayerUtil LOW-2: DST correctness across spring-forward", () => {
|
|
130
|
+
const tz: string = "America/New_York";
|
|
131
|
+
|
|
132
|
+
test("09:00-17:00 restriction stays 09:00-17:00 local across the DST spring-forward day", () => {
|
|
133
|
+
const util: LayerUtil = new LayerUtil();
|
|
134
|
+
|
|
135
|
+
// US spring-forward 2026 is Sun Mar 8. Cover Sat Mar 7 -> Tue Mar 10.
|
|
136
|
+
const layerStart: Date = tzInstant("2026-03-07 00:00", tz);
|
|
137
|
+
const restrictionTimes: RestrictionTimes = new RestrictionTimes();
|
|
138
|
+
restrictionTimes.restictionType = RestrictionType.Daily;
|
|
139
|
+
restrictionTimes.dayRestrictionTimes = {
|
|
140
|
+
startTime: tzInstant("2026-03-07 09:00", tz),
|
|
141
|
+
endTime: tzInstant("2026-03-07 17:00", tz),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const events: Array<CalendarEvent> = util.getEvents({
|
|
145
|
+
users: [user("u1")],
|
|
146
|
+
startDateTimeOfLayer: layerStart,
|
|
147
|
+
handOffTime: layerStart,
|
|
148
|
+
restrictionTimes: restrictionTimes,
|
|
149
|
+
rotation: rotation(EventInterval.Day, 1),
|
|
150
|
+
timezone: tz,
|
|
151
|
+
calendarStartDate: layerStart,
|
|
152
|
+
calendarEndDate: tzInstant("2026-03-10 00:00", tz),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const windows: Array<CalendarEvent> = events.filter((e: CalendarEvent) => {
|
|
156
|
+
return hhmm(e.start, tz) === "09:00";
|
|
157
|
+
});
|
|
158
|
+
// Sat, Sun (DST day), Mon — all keep 09:00-17:00 local despite the lost hour.
|
|
159
|
+
expect(windows.length).toBeGreaterThanOrEqual(3);
|
|
160
|
+
for (const w of windows) {
|
|
161
|
+
expect(hhmm(w.start, tz)).toBe("09:00");
|
|
162
|
+
expect(hhmm(w.end, tz)).toBe("17:00");
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("LayerUtil HIGH-5: weekly restriction resolves day-of-week in the schedule zone", () => {
|
|
168
|
+
const tz: string = "America/New_York";
|
|
169
|
+
|
|
170
|
+
test("Mon 09:00 -> Wed 17:00 weekly restriction covers Tue midday, not Sat", () => {
|
|
171
|
+
const util: LayerUtil = new LayerUtil();
|
|
172
|
+
|
|
173
|
+
const monday: Date = tzInstant("2026-02-16 00:00", tz); // Monday
|
|
174
|
+
const weekly: WeeklyResctriction = {
|
|
175
|
+
startDay: DayOfWeek.Monday,
|
|
176
|
+
endDay: DayOfWeek.Wednesday,
|
|
177
|
+
startTime: tzInstant("2026-02-16 09:00", tz), // Monday 09:00 NY
|
|
178
|
+
endTime: tzInstant("2026-02-18 17:00", tz), // Wednesday 17:00 NY
|
|
179
|
+
};
|
|
180
|
+
const restrictionTimes: RestrictionTimes = new RestrictionTimes();
|
|
181
|
+
restrictionTimes.restictionType = RestrictionType.Weekly;
|
|
182
|
+
restrictionTimes.weeklyRestrictionTimes = [weekly];
|
|
183
|
+
|
|
184
|
+
const events: Array<CalendarEvent> = util.getEvents({
|
|
185
|
+
users: [user("u1")],
|
|
186
|
+
startDateTimeOfLayer: monday,
|
|
187
|
+
handOffTime: tzInstant("2026-02-23 00:00", tz),
|
|
188
|
+
restrictionTimes: restrictionTimes,
|
|
189
|
+
rotation: rotation(EventInterval.Week, 1),
|
|
190
|
+
timezone: tz,
|
|
191
|
+
calendarStartDate: monday,
|
|
192
|
+
calendarEndDate: tzInstant("2026-02-23 00:00", tz),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const covered: (t: Date) => boolean = (t: Date): boolean => {
|
|
196
|
+
return events.some((e: CalendarEvent) => {
|
|
197
|
+
return (
|
|
198
|
+
t.getTime() >= e.start.getTime() && t.getTime() < e.end.getTime()
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// Tue 12:00 NY is inside Mon 09:00 -> Wed 17:00.
|
|
204
|
+
expect(covered(tzInstant("2026-02-17 12:00", tz))).toBe(true);
|
|
205
|
+
// Sat 12:00 NY is not.
|
|
206
|
+
expect(covered(tzInstant("2026-02-21 12:00", tz))).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("LayerUtil HIGH-5 backward compatibility: no timezone == legacy local behavior", () => {
|
|
211
|
+
test("omitting timezone yields identical results to before (local reconstruction)", () => {
|
|
212
|
+
const util: LayerUtil = new LayerUtil();
|
|
213
|
+
// Author restriction in machine-local time via native Date.
|
|
214
|
+
const layerStart: Date = new Date(2026, 1, 16, 0, 0, 0); // local midnight
|
|
215
|
+
const restrictionTimes: RestrictionTimes = new RestrictionTimes();
|
|
216
|
+
restrictionTimes.restictionType = RestrictionType.Daily;
|
|
217
|
+
restrictionTimes.dayRestrictionTimes = {
|
|
218
|
+
startTime: new Date(2026, 1, 16, 9, 0, 0),
|
|
219
|
+
endTime: new Date(2026, 1, 16, 17, 0, 0),
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const events: Array<CalendarEvent> = util.getEvents({
|
|
223
|
+
users: [user("u1")],
|
|
224
|
+
startDateTimeOfLayer: layerStart,
|
|
225
|
+
handOffTime: layerStart,
|
|
226
|
+
restrictionTimes: restrictionTimes,
|
|
227
|
+
rotation: rotation(EventInterval.Day, 1),
|
|
228
|
+
// timezone intentionally omitted
|
|
229
|
+
calendarStartDate: layerStart,
|
|
230
|
+
calendarEndDate: new Date(2026, 1, 19, 0, 0, 0),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const windows: Array<CalendarEvent> = events.filter((e: CalendarEvent) => {
|
|
234
|
+
return e.start.getHours() === 9;
|
|
235
|
+
});
|
|
236
|
+
expect(windows.length).toBeGreaterThanOrEqual(2);
|
|
237
|
+
for (const w of windows) {
|
|
238
|
+
// Local wall-clock hours preserved exactly as legacy behavior.
|
|
239
|
+
expect(w.start.getHours()).toBe(9);
|
|
240
|
+
expect(w.end.getHours()).toBe(17);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPRO (weekly granularity of the post-window-gap off-by-one):
|
|
3
|
+
* weekly rotation [A,B] with a Mon-Fri weekly restriction, resolved on the
|
|
4
|
+
* WEEKEND, attributes next week to the WRONG user.
|
|
5
|
+
*
|
|
6
|
+
* Layer start Mon 2025-01-06 00:00 UTC, weekly rotation, restriction active
|
|
7
|
+
* Mon 00:00 -> Sat 00:00 (i.e. Mon-Fri).
|
|
8
|
+
* Full expansion (calendar): week0 (Jan6-10)=A, week1 (Jan13-17)=B ...
|
|
9
|
+
* Windowed asked on Sat 2025-01-11 12:00 : next week=A (WRONG, should be B).
|
|
10
|
+
*/
|
|
11
|
+
import LayerUtil, { LayerProps } from "../../../Types/OnCallDutyPolicy/Layer";
|
|
12
|
+
import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
|
|
13
|
+
import RestrictionTimes, {
|
|
14
|
+
RestrictionType,
|
|
15
|
+
WeeklyResctriction,
|
|
16
|
+
} from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
|
|
17
|
+
import Recurring from "../../../Types/Events/Recurring";
|
|
18
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
19
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
20
|
+
import EventInterval from "../../../Types/Events/EventInterval";
|
|
21
|
+
import PositiveNumber from "../../../Types/PositiveNumber";
|
|
22
|
+
import DayOfWeek from "../../../Types/Day/DayOfWeek";
|
|
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 makeLayer(): LayerProps {
|
|
35
|
+
const rot: Recurring = new Recurring();
|
|
36
|
+
rot.intervalType = EventInterval.Week;
|
|
37
|
+
rot.intervalCount = new PositiveNumber(1);
|
|
38
|
+
const start: Date = OneUptimeDate.fromString("2025-01-06T00:00:00.000Z"); // Monday
|
|
39
|
+
|
|
40
|
+
// Active Mon 00:00 -> Sat 00:00 (Mon-Fri coverage). Non-wrapping window.
|
|
41
|
+
const weekly: WeeklyResctriction = {
|
|
42
|
+
startDay: DayOfWeek.Monday,
|
|
43
|
+
endDay: DayOfWeek.Saturday,
|
|
44
|
+
startTime: OneUptimeDate.fromString("2025-01-06T00:00:00.000Z"), // Monday
|
|
45
|
+
endTime: OneUptimeDate.fromString("2025-01-11T00:00:00.000Z"), // Saturday
|
|
46
|
+
};
|
|
47
|
+
const r: RestrictionTimes = new RestrictionTimes();
|
|
48
|
+
r.restictionType = RestrictionType.Weekly;
|
|
49
|
+
r.weeklyRestrictionTimes = [weekly];
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
users: [user("A"), user("B")],
|
|
53
|
+
startDateTimeOfLayer: start,
|
|
54
|
+
restrictionTimes: r,
|
|
55
|
+
handOffTime: Recurring.getNextDateInterval(start, rot),
|
|
56
|
+
rotation: rot,
|
|
57
|
+
timezone: undefined,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fmt(e: CalendarEvent): string {
|
|
62
|
+
return `${e.title} [${e.start.toISOString()} -> ${e.end.toISOString()}]`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("Weekly rotation + Mon-Fri restriction: weekend query off-by-one", () => {
|
|
66
|
+
test("week1 is B in calendar but a Saturday query says next week=A", () => {
|
|
67
|
+
const layer: LayerProps = makeLayer();
|
|
68
|
+
const util: LayerUtil = new LayerUtil();
|
|
69
|
+
|
|
70
|
+
const full: Array<CalendarEvent> = util.getEvents({
|
|
71
|
+
...layer,
|
|
72
|
+
calendarStartDate: layer.startDateTimeOfLayer,
|
|
73
|
+
calendarEndDate: OneUptimeDate.fromString("2025-01-25T00:00:00.000Z"),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Who covers Wed Jan 15 (inside week1 Mon-Fri) in the calendar?
|
|
77
|
+
const jan15: Date = OneUptimeDate.fromString("2025-01-15T12:00:00.000Z");
|
|
78
|
+
const week1Cover: CalendarEvent | undefined = full.find(
|
|
79
|
+
(e: CalendarEvent) => {
|
|
80
|
+
return (
|
|
81
|
+
OneUptimeDate.isOnOrAfter(jan15, e.start) &&
|
|
82
|
+
OneUptimeDate.isBefore(jan15, e.end)
|
|
83
|
+
);
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Ask on Saturday Jan 11 12:00 (weekend gap of week0).
|
|
88
|
+
const askAt: Date = OneUptimeDate.fromString("2025-01-11T12:00:00.000Z");
|
|
89
|
+
const windowed: Array<CalendarEvent> = util.getEvents(
|
|
90
|
+
{
|
|
91
|
+
...layer,
|
|
92
|
+
calendarStartDate: askAt,
|
|
93
|
+
calendarEndDate: OneUptimeDate.addRemoveDays(askAt, 14),
|
|
94
|
+
},
|
|
95
|
+
{ getNumberOfEvents: 1 },
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// eslint-disable-next-line no-console
|
|
99
|
+
console.log("FULL:", full.map(fmt));
|
|
100
|
+
// eslint-disable-next-line no-console
|
|
101
|
+
console.log("week1 covered by (full):", week1Cover && fmt(week1Cover));
|
|
102
|
+
// eslint-disable-next-line no-console
|
|
103
|
+
console.log(
|
|
104
|
+
"WINDOWED next @Sat Jan11 12:00:",
|
|
105
|
+
windowed[0] && fmt(windowed[0]),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(week1Cover?.title).toBe("B");
|
|
109
|
+
// Windowed next on-call should be week1 = B; it is A -> BUG.
|
|
110
|
+
expect(windowed[0]?.title).toBe("B");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
|
|
2
|
+
import UserOverrideUtil, {
|
|
3
|
+
UserOverrideRecord,
|
|
4
|
+
} from "../../../Types/OnCallDutyPolicy/UserOverrideUtil";
|
|
5
|
+
|
|
6
|
+
function at(h: number, m: number = 0, s: number = 0, ms: number = 0): Date {
|
|
7
|
+
return new Date(2026, 0, 1, h, m, s, ms);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function event(
|
|
11
|
+
title: string,
|
|
12
|
+
startHour: number,
|
|
13
|
+
endHour: number,
|
|
14
|
+
): CalendarEvent {
|
|
15
|
+
return {
|
|
16
|
+
id: 1,
|
|
17
|
+
title,
|
|
18
|
+
allDay: false,
|
|
19
|
+
start: at(startHour),
|
|
20
|
+
end: at(endHour),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolvedAt(events: Array<CalendarEvent>, t: Date): string | null {
|
|
25
|
+
for (const e of events) {
|
|
26
|
+
if (t.getTime() >= e.start.getTime() && t.getTime() < e.end.getTime()) {
|
|
27
|
+
return e.title;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("Override lens audit", () => {
|
|
34
|
+
test("REPRO: applyOverridesToEvents is non-transitive (roster stores B for chain A->B, B->C)", () => {
|
|
35
|
+
// User A is on-call. A->B (A on vacation, route to B). B->C (B also busy, route to C).
|
|
36
|
+
const base: CalendarEvent = event("A", 8, 14);
|
|
37
|
+
const aToB: UserOverrideRecord = {
|
|
38
|
+
overrideUserId: "A",
|
|
39
|
+
routeAlertsToUserId: "B",
|
|
40
|
+
startsAt: at(9),
|
|
41
|
+
endsAt: at(13),
|
|
42
|
+
onCallDutyPolicyId: null,
|
|
43
|
+
};
|
|
44
|
+
const bToC: UserOverrideRecord = {
|
|
45
|
+
overrideUserId: "B",
|
|
46
|
+
routeAlertsToUserId: "C",
|
|
47
|
+
startsAt: at(9),
|
|
48
|
+
endsAt: at(13),
|
|
49
|
+
onCallDutyPolicyId: null,
|
|
50
|
+
};
|
|
51
|
+
const result: Array<CalendarEvent> =
|
|
52
|
+
UserOverrideUtil.applyOverridesToEvents({
|
|
53
|
+
events: [base],
|
|
54
|
+
overrides: [aToB, bToC],
|
|
55
|
+
});
|
|
56
|
+
// Non-transitive: schedule resolution (roster + dashboard) reports B, NOT C.
|
|
57
|
+
expect(resolvedAt(result, at(11))).toBe("B");
|
|
58
|
+
/*
|
|
59
|
+
* The escalation paging path would then run getRouteAlertToUserId("B"),
|
|
60
|
+
* find the B->C override, and page C -> divergence from the roster (B).
|
|
61
|
+
*/
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("REPRO: swap pair A->B and B->A resolves to B (roster), escalation would route B->A", () => {
|
|
65
|
+
const base: CalendarEvent = event("A", 8, 14);
|
|
66
|
+
const aToB: UserOverrideRecord = {
|
|
67
|
+
overrideUserId: "A",
|
|
68
|
+
routeAlertsToUserId: "B",
|
|
69
|
+
startsAt: at(9),
|
|
70
|
+
endsAt: at(13),
|
|
71
|
+
onCallDutyPolicyId: null,
|
|
72
|
+
};
|
|
73
|
+
const bToA: UserOverrideRecord = {
|
|
74
|
+
overrideUserId: "B",
|
|
75
|
+
routeAlertsToUserId: "A",
|
|
76
|
+
startsAt: at(9),
|
|
77
|
+
endsAt: at(13),
|
|
78
|
+
onCallDutyPolicyId: null,
|
|
79
|
+
};
|
|
80
|
+
const result: Array<CalendarEvent> =
|
|
81
|
+
UserOverrideUtil.applyOverridesToEvents({
|
|
82
|
+
events: [base],
|
|
83
|
+
overrides: [aToB, bToA],
|
|
84
|
+
});
|
|
85
|
+
// Roster resolves to B. getRouteAlertToUserId("B") would map B->A and page A.
|
|
86
|
+
expect(resolvedAt(result, at(11))).toBe("B");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("EDGE: self-override A->A blocks a legit overlapping override A->B", () => {
|
|
90
|
+
const base: CalendarEvent = event("A", 8, 14);
|
|
91
|
+
const selfOverride: UserOverrideRecord = {
|
|
92
|
+
overrideUserId: "A",
|
|
93
|
+
routeAlertsToUserId: "A", // self, no-op substitution
|
|
94
|
+
startsAt: at(10),
|
|
95
|
+
endsAt: at(12),
|
|
96
|
+
onCallDutyPolicyId: null,
|
|
97
|
+
};
|
|
98
|
+
const aToB: UserOverrideRecord = {
|
|
99
|
+
overrideUserId: "A",
|
|
100
|
+
routeAlertsToUserId: "B",
|
|
101
|
+
startsAt: at(9),
|
|
102
|
+
endsAt: at(13),
|
|
103
|
+
onCallDutyPolicyId: null,
|
|
104
|
+
};
|
|
105
|
+
const result: Array<CalendarEvent> =
|
|
106
|
+
UserOverrideUtil.applyOverridesToEvents({
|
|
107
|
+
events: [base],
|
|
108
|
+
overrides: [selfOverride, aToB],
|
|
109
|
+
});
|
|
110
|
+
/*
|
|
111
|
+
* At 11:00 the real override A->B is expected, but the self-override marked
|
|
112
|
+
* [10,12] first, so it is skipped and A stays on-call.
|
|
113
|
+
*/
|
|
114
|
+
// eslint-disable-next-line no-console
|
|
115
|
+
console.log("self-override case @11:00 =>", resolvedAt(result, at(11)));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -209,11 +209,13 @@ describe("RestrictionTimes", () => {
|
|
|
209
209
|
expect(restored.dayRestrictionTimes!.endTime).toBeDefined();
|
|
210
210
|
});
|
|
211
211
|
|
|
212
|
-
test("falls back to an empty
|
|
212
|
+
test("falls back to an empty array when weeklyRestrictionTimes is missing", () => {
|
|
213
213
|
/*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
214
|
+
* When the value object omits weeklyRestrictionTimes, fromJSON must fall
|
|
215
|
+
* back to an empty ARRAY so the array-type contract holds. A previous
|
|
216
|
+
* `|| {}` fallback produced a non-array `{}` that slipped past
|
|
217
|
+
* Layer.getEventsByWeeklyRestriction's length guard and threw
|
|
218
|
+
* "not iterable" during weekly expansion (audit F20).
|
|
217
219
|
*/
|
|
218
220
|
const restored: RestrictionTimes = RestrictionTimes.fromJSON({
|
|
219
221
|
_type: ObjectType.RestrictionTimes,
|
|
@@ -223,8 +225,8 @@ describe("RestrictionTimes", () => {
|
|
|
223
225
|
},
|
|
224
226
|
});
|
|
225
227
|
|
|
226
|
-
expect(restored.weeklyRestrictionTimes).toEqual(
|
|
227
|
-
expect(Array.isArray(restored.weeklyRestrictionTimes)).toBe(
|
|
228
|
+
expect(restored.weeklyRestrictionTimes).toEqual([]);
|
|
229
|
+
expect(Array.isArray(restored.weeklyRestrictionTimes)).toBe(true);
|
|
228
230
|
});
|
|
229
231
|
|
|
230
232
|
test("preserves a provided weeklyRestrictionTimes array on fromJSON", () => {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import RestrictionTimes, {
|
|
2
|
+
RestrictionType,
|
|
3
|
+
WeeklyResctriction,
|
|
4
|
+
} from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
|
|
5
|
+
import LayerUtil, { LayerProps } from "../../../Types/OnCallDutyPolicy/Layer";
|
|
6
|
+
import Recurring from "../../../Types/Events/Recurring";
|
|
7
|
+
import EventInterval from "../../../Types/Events/EventInterval";
|
|
8
|
+
import PositiveNumber from "../../../Types/PositiveNumber";
|
|
9
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
10
|
+
import DayOfWeek from "../../../Types/Day/DayOfWeek";
|
|
11
|
+
import User from "../../../Models/DatabaseModels/User";
|
|
12
|
+
import CalendarEvent from "../../../Types/Calendar/CalendarEvent";
|
|
13
|
+
import { describe, expect, test } from "@jest/globals";
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* Audit M4: getDefaultWeeklyRestrictionTIme() built startTime/endTime with
|
|
17
|
+
* getDateWithCustomTime(), which stamps TODAY's date. The rotation engine derives
|
|
18
|
+
* the ENFORCED weekday purely from the timestamp (getDayOfWeek(startTime)), not
|
|
19
|
+
* from the startDay/endDay enum the dropdown displays — so an untouched default
|
|
20
|
+
* enforced coverage on TODAY's weekday while still showing "Sunday -> Monday".
|
|
21
|
+
* The fix anchors the default timestamps onto the named weekday so the enforced
|
|
22
|
+
* day matches the displayed day.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
function user(id: string): User {
|
|
26
|
+
return {
|
|
27
|
+
id: {
|
|
28
|
+
toString: (): string => {
|
|
29
|
+
return id;
|
|
30
|
+
},
|
|
31
|
+
} as any,
|
|
32
|
+
} as User;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("RestrictionTimes audit M4: default weekly restriction weekday", () => {
|
|
36
|
+
test("default startTime falls on Sunday and endTime on Monday (matches the enum)", () => {
|
|
37
|
+
const def: WeeklyResctriction =
|
|
38
|
+
RestrictionTimes.getDefaultWeeklyRestrictionTIme();
|
|
39
|
+
|
|
40
|
+
expect(def.startDay).toBe(DayOfWeek.Sunday);
|
|
41
|
+
expect(def.endDay).toBe(DayOfWeek.Monday);
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
* The TIMESTAMP's weekday — what the engine actually enforces — matches the
|
|
45
|
+
* enum. Before the fix this was TODAY's weekday.
|
|
46
|
+
*/
|
|
47
|
+
expect(OneUptimeDate.getDayOfWeek(def.startTime)).toBe(DayOfWeek.Sunday);
|
|
48
|
+
expect(OneUptimeDate.getDayOfWeek(def.endTime)).toBe(DayOfWeek.Monday);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("default preserves the intended times of day (00:00 start, 01:00 end)", () => {
|
|
52
|
+
const def: WeeklyResctriction =
|
|
53
|
+
RestrictionTimes.getDefaultWeeklyRestrictionTIme();
|
|
54
|
+
|
|
55
|
+
expect(new Date(def.startTime).getHours()).toBe(0);
|
|
56
|
+
expect(new Date(def.endTime).getHours()).toBe(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("engine enforces the default weekly restriction on Sunday, not today", () => {
|
|
60
|
+
/*
|
|
61
|
+
* Wednesday 2025-06-04 — the layer/window start deliberately falls on a
|
|
62
|
+
* non-Sunday to prove enforcement follows the restriction weekday, not the
|
|
63
|
+
* start day.
|
|
64
|
+
*/
|
|
65
|
+
const anchor: Date = new Date(Date.UTC(2025, 5, 4, 0, 0, 0));
|
|
66
|
+
|
|
67
|
+
const rt: RestrictionTimes = new RestrictionTimes();
|
|
68
|
+
rt.restictionType = RestrictionType.Weekly;
|
|
69
|
+
rt.weeklyRestrictionTimes = [
|
|
70
|
+
RestrictionTimes.getDefaultWeeklyRestrictionTIme(),
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const r: Recurring = new Recurring();
|
|
74
|
+
r.intervalType = EventInterval.Week;
|
|
75
|
+
r.intervalCount = new PositiveNumber(1);
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
* Expand in the SAME zone the Common default was built in (server-local, i.e.
|
|
79
|
+
* timezone undefined). getDefaultWeeklyRestrictionTIme lands the timestamp on
|
|
80
|
+
* Sunday in server-local time; enforcing and reading the weekday in that same
|
|
81
|
+
* zone keeps the assertion independent of the CI machine's timezone (the
|
|
82
|
+
* frontend reconciles the default to the schedule zone separately).
|
|
83
|
+
*/
|
|
84
|
+
const layer: LayerProps = {
|
|
85
|
+
users: [user("A")],
|
|
86
|
+
startDateTimeOfLayer: anchor,
|
|
87
|
+
handOffTime: anchor,
|
|
88
|
+
restrictionTimes: rt,
|
|
89
|
+
rotation: r,
|
|
90
|
+
timezone: undefined,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const events: Array<CalendarEvent> = new LayerUtil().getEvents({
|
|
94
|
+
...layer,
|
|
95
|
+
calendarStartDate: anchor,
|
|
96
|
+
calendarEndDate: new Date(Date.UTC(2025, 5, 18, 0, 0, 0)),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(events.length).toBeGreaterThan(0);
|
|
100
|
+
|
|
101
|
+
/*
|
|
102
|
+
* Every emitted covered segment must start on Sunday (the restriction window
|
|
103
|
+
* is Sunday 00:00 -> Monday 01:00). Before the fix the default landed on the
|
|
104
|
+
* current weekday, so coverage appeared mid-week instead. Monday is allowed
|
|
105
|
+
* only as the continuation edge; no segment may start Tue-Sat.
|
|
106
|
+
*/
|
|
107
|
+
for (const e of events) {
|
|
108
|
+
const startDow: DayOfWeek = OneUptimeDate.getDayOfWeek(new Date(e.start));
|
|
109
|
+
expect([DayOfWeek.Sunday, DayOfWeek.Monday]).toContain(startDow);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|