@oneuptime/common 12.0.29 → 12.0.30
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/Index.ts +11 -0
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +43 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +662 -0
- package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +584 -0
- package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +546 -0
- package/Models/DatabaseModels/UserOnCallShiftReminder.ts +231 -0
- package/Models/DatabaseModels/UserOnCallShiftReminderLog.ts +331 -0
- package/Server/API/OnCallCalendarAPI.ts +2194 -0
- package/Server/EnvironmentConfig.ts +138 -0
- package/Server/Infrastructure/OnCallCalendarFeedCache.ts +1050 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.ts +253 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Middleware/OnCallCalendarFeedRateLimit.ts +487 -0
- package/Server/Services/Index.ts +12 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +32 -0
- package/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.ts +260 -0
- package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +70 -3
- package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +138 -3
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +1465 -134
- package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +167 -31
- package/Server/Services/ProjectOnCallCalendarFeedService.ts +192 -0
- package/Server/Services/TeamMemberService.ts +489 -1
- package/Server/Services/TeamService.ts +50 -0
- package/Server/Services/UserNotificationSettingService.ts +44 -1
- package/Server/Services/UserOnCallCalendarFeedService.ts +159 -0
- package/Server/Services/UserOnCallShiftReminderLogService.ts +151 -0
- package/Server/Services/UserOnCallShiftReminderService.ts +112 -0
- package/Server/Utils/OnCall/CalendarFeedToken.ts +241 -0
- package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1488 -0
- package/Server/Utils/OnCall/OnCallCalendarFeedUrls.ts +230 -0
- package/Server/Utils/OnCall/OnCallShiftChangeListeners.ts +227 -0
- package/Server/Utils/OnCall/OnCallShiftMaterializer.ts +1127 -0
- package/Server/Utils/OnCall/OnCallShiftReminderListener.ts +174 -0
- package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +2584 -0
- package/Server/Utils/Response.ts +134 -0
- package/Server/Utils/StartServer.ts +10 -0
- package/Server/Utils/Telemetry/AppMetrics.ts +51 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedLinks.test.tsx +229 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +489 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +149 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedUtil.test.ts +855 -0
- package/Tests/App/Dashboard/OnCallPersonalCalendarFeed.test.tsx +1209 -0
- package/Tests/App/Dashboard/OnCallSharedCalendarFeedCard.test.tsx +1019 -0
- package/Tests/App/Dashboard/OnCallShiftRemindersAndUpcomingShifts.test.tsx +655 -0
- package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +73 -0
- package/Tests/Models/OnCallCalendarFeedModels.test.ts +642 -0
- package/Tests/Models/OnCallDutyPolicyScheduleCalendarFeed.test.ts +288 -0
- package/Tests/Models/OnCallDutyPolicyScheduleShiftConfigVersion.test.ts +88 -0
- package/Tests/Models/UserOnCallShiftReminderModels.test.ts +368 -0
- package/Tests/Server/API/Helpers.ts +8 -0
- package/Tests/Server/API/OnCallCalendarAPI.test.ts +4461 -0
- package/Tests/Server/EnvironmentConfigOnCallCalendarFeed.test.ts +357 -0
- package/Tests/Server/Infrastructure/OnCallCalendarFeedCache.test.ts +1589 -0
- package/Tests/Server/Infrastructure/Postgres/AddOnCallCalendarFeedsMigration.test.ts +369 -0
- package/Tests/Server/Middleware/OnCallCalendarFeedRateLimit.test.ts +1365 -0
- package/Tests/Server/Services/FeedRetentionConsistency.test.ts +70 -0
- package/Tests/Server/Services/OnCallCalendarFeedServices.test.ts +1103 -0
- package/Tests/Server/Services/OnCallDutyPolicyScheduleResolver.test.ts +1037 -0
- package/Tests/Server/Services/OnCallShiftConfigPropagation.test.ts +1270 -0
- package/Tests/Server/Services/TeamDeleteMemberCleanup.test.ts +156 -0
- package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +755 -0
- package/Tests/Server/Services/UserNotificationSettingShiftReminderDefaults.test.ts +267 -0
- package/Tests/Server/Services/UserOnCallShiftReminderServices.test.ts +583 -0
- package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +254 -0
- package/Tests/Server/Utils/OnCall/CalendarFeedToken.test.ts +467 -0
- package/Tests/Server/Utils/OnCall/OnCallCalendarFeedRenderer.test.ts +2142 -0
- package/Tests/Server/Utils/OnCall/OnCallCalendarFeedUrls.test.ts +278 -0
- package/Tests/Server/Utils/OnCall/OnCallResolverTestHarness.ts +440 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftChangeListeners.test.ts +314 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftMaterializer.test.ts +1502 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderListener.test.ts +491 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderRunner.test.ts +3088 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderTestHarness.ts +910 -0
- package/Tests/Server/Utils/ResponseSendCalendarResponse.test.ts +561 -0
- package/Tests/Server/Utils/StartServerEncryptionSecretWarning.test.ts +64 -0
- package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +29 -0
- package/Tests/Types/Calendar/ICalendar.test.ts +488 -0
- package/Tests/Types/NotificationSetting/NotificationSettingEventType.test.ts +53 -0
- package/Tests/Types/OnCallDutyPolicy/CalendarFeedTestFixtures.ts +310 -0
- package/Tests/Types/OnCallDutyPolicy/CalendarFeedWindow.test.ts +205 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilLayerMetaAndIterationCap.test.ts +458 -0
- package/Tests/Types/OnCallDutyPolicy/MaterializedShift.test.ts +197 -0
- package/Tests/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.test.ts +1986 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtilGroupKey.test.ts +678 -0
- package/Tests/Types/OnCallDutyPolicy/ShiftSeamUtil.test.ts +417 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Types/Calendar/ICalendar.ts +442 -0
- package/Types/Email/EmailTemplateType.ts +2 -0
- package/Types/NotificationSetting/NotificationSettingEventType.ts +4 -0
- package/Types/OnCallDutyPolicy/CalendarFeedWindow.ts +140 -0
- package/Types/OnCallDutyPolicy/Layer.ts +190 -35
- package/Types/OnCallDutyPolicy/MaterializedShift.ts +274 -0
- package/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.ts +1415 -0
- package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +159 -4
- package/Types/OnCallDutyPolicy/ShiftSeamUtil.ts +106 -0
- package/build/dist/Models/DatabaseModels/Index.js +11 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +44 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +699 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +620 -0
- package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +585 -0
- package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js +247 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js +355 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js.map +1 -0
- package/build/dist/Server/API/OnCallCalendarAPI.js +1371 -0
- package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -0
- package/build/dist/Server/EnvironmentConfig.js +102 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js +663 -0
- package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js +103 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js +342 -0
- package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js.map +1 -0
- package/build/dist/Server/Services/Index.js +12 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +20 -0
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js +219 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +47 -3
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +112 -3
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +955 -9
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +93 -6
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
- package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js +157 -0
- package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/TeamMemberService.js +396 -1
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/TeamService.js +43 -0
- package/build/dist/Server/Services/TeamService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +26 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallCalendarFeedService.js +140 -0
- package/build/dist/Server/Services/UserOnCallCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js +116 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js.map +1 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderService.js +92 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderService.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js +161 -0
- package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js +983 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js +155 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js +145 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js +752 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js +92 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1786 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -0
- package/build/dist/Server/Utils/Response.js +97 -0
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +9 -1
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/AppMetrics.js +41 -0
- package/build/dist/Server/Utils/Telemetry/AppMetrics.js.map +1 -1
- package/build/dist/Types/Calendar/ICalendar.js +290 -0
- package/build/dist/Types/Calendar/ICalendar.js.map +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js +2 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js +97 -0
- package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +113 -19
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js +134 -0
- package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js +800 -0
- package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +97 -3
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js +77 -0
- package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import OnCallShiftChangeListeners, {
|
|
2
|
+
OnCallShiftChangeEvent,
|
|
3
|
+
OnCallShiftChangeReason,
|
|
4
|
+
} from "../../../../Server/Utils/OnCall/OnCallShiftChangeListeners";
|
|
5
|
+
import OnCallShiftReminderRunner, {
|
|
6
|
+
SHIFT_REMINDER_LISTENER_NAME,
|
|
7
|
+
ShiftReminderChangePassStats,
|
|
8
|
+
} from "../../../../Server/Utils/OnCall/OnCallShiftReminderRunner";
|
|
9
|
+
import logger from "../../../../Server/Utils/Logger";
|
|
10
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import {
|
|
14
|
+
afterEach,
|
|
15
|
+
beforeEach,
|
|
16
|
+
describe,
|
|
17
|
+
expect,
|
|
18
|
+
jest,
|
|
19
|
+
test,
|
|
20
|
+
} from "@jest/globals";
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* OnCallShiftReminderListener wires the change pass (catch-up + reassigned
|
|
24
|
+
* notices) into the on-call configuration hooks through
|
|
25
|
+
* OnCallShiftChangeListeners. Pinned here:
|
|
26
|
+
*
|
|
27
|
+
* 1. importing the module registers the listener under the runner's name
|
|
28
|
+
* (a side effect, like a RunCron job file) and doing so twice never
|
|
29
|
+
* stacks a second listener;
|
|
30
|
+
* 2. the registered function hands the event to
|
|
31
|
+
* OnCallShiftReminderRunner.runChangePass;
|
|
32
|
+
* 3. a change pass that throws is caught and logged — the hook that
|
|
33
|
+
* published the event can neither fail nor be delayed by it;
|
|
34
|
+
* 4. BOTH App/Index.ts and App/FeatureSet/Workers/Index.ts import the
|
|
35
|
+
* module, because the hooks run in whichever process serves the CRUD
|
|
36
|
+
* request (the api role for a dashboard edit);
|
|
37
|
+
* 5. bursts are coalesced: one pass per project at a time, everything that
|
|
38
|
+
* arrives while it runs merged into ONE follow-up, so bulk edits cannot
|
|
39
|
+
* pile N synchronous schedule expansions onto the api event loop.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
// Imported last so the registration side effect is what the tests observe.
|
|
43
|
+
import registerOnCallShiftReminderListener, {
|
|
44
|
+
mergeOnCallShiftChangeEvents,
|
|
45
|
+
onCallShiftReminderListener,
|
|
46
|
+
resetOnCallShiftReminderCoalescing,
|
|
47
|
+
} from "../../../../Server/Utils/OnCall/OnCallShiftReminderListener";
|
|
48
|
+
|
|
49
|
+
const COMMON_DIR: string = path.resolve(__dirname, "../../../..");
|
|
50
|
+
const APP_DIR: string = path.resolve(COMMON_DIR, "../App");
|
|
51
|
+
const LISTENER_PATH: string = path.join(
|
|
52
|
+
COMMON_DIR,
|
|
53
|
+
"Server",
|
|
54
|
+
"Utils",
|
|
55
|
+
"OnCall",
|
|
56
|
+
"OnCallShiftReminderListener.ts",
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const LISTENER_SPECIFIER: string =
|
|
60
|
+
"Common/Server/Utils/OnCall/OnCallShiftReminderListener";
|
|
61
|
+
|
|
62
|
+
function escapeForRegExp(value: string): string {
|
|
63
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const SIDE_EFFECT_IMPORT: RegExp = new RegExp(
|
|
67
|
+
`^\\s*import\\s+["']${escapeForRegExp(LISTENER_SPECIFIER)}["']\\s*;?\\s*$`,
|
|
68
|
+
"m",
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
function event(): OnCallShiftChangeEvent {
|
|
72
|
+
return OnCallShiftChangeListeners.buildEvent({
|
|
73
|
+
projectId: new ObjectID("project-1"),
|
|
74
|
+
scheduleIds: [new ObjectID("schedule-1")],
|
|
75
|
+
userIds: [new ObjectID("user-a")],
|
|
76
|
+
reason: OnCallShiftChangeReason.OverrideChanged,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function eventFor(data: {
|
|
81
|
+
projectId: string | null;
|
|
82
|
+
scheduleId: string;
|
|
83
|
+
userId: string;
|
|
84
|
+
reason?: OnCallShiftChangeReason | undefined;
|
|
85
|
+
occurredAt?: Date | undefined;
|
|
86
|
+
}): OnCallShiftChangeEvent {
|
|
87
|
+
return OnCallShiftChangeListeners.buildEvent({
|
|
88
|
+
projectId: data.projectId ? new ObjectID(data.projectId) : null,
|
|
89
|
+
scheduleIds: [new ObjectID(data.scheduleId)],
|
|
90
|
+
userIds: [new ObjectID(data.userId)],
|
|
91
|
+
reason: data.reason ?? OnCallShiftChangeReason.LayerUserChanged,
|
|
92
|
+
occurredAt: data.occurredAt,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function idsOf(ids: Array<ObjectID>): Array<string> {
|
|
97
|
+
return ids
|
|
98
|
+
.map((id: ObjectID) => {
|
|
99
|
+
return id.toString();
|
|
100
|
+
})
|
|
101
|
+
.sort();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function emptyStats(): ShiftReminderChangePassStats {
|
|
105
|
+
return {
|
|
106
|
+
now: new Date(),
|
|
107
|
+
projectId: "project-1",
|
|
108
|
+
users: 0,
|
|
109
|
+
catchUpsSent: 0,
|
|
110
|
+
reassignedSent: 0,
|
|
111
|
+
claimCollisions: 0,
|
|
112
|
+
sendFailures: 0,
|
|
113
|
+
missingSettings: 0,
|
|
114
|
+
errors: 0,
|
|
115
|
+
skippedReason: null,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
describe("OnCallShiftReminderListener", () => {
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
resetOnCallShiftReminderCoalescing();
|
|
122
|
+
jest.spyOn(logger, "error").mockImplementation((): void => {
|
|
123
|
+
return undefined;
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
afterEach(() => {
|
|
128
|
+
resetOnCallShiftReminderCoalescing();
|
|
129
|
+
jest.restoreAllMocks();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("importing the module registers the change pass under the runner's listener name", () => {
|
|
133
|
+
expect(SHIFT_REMINDER_LISTENER_NAME).toBe("shift-reminders");
|
|
134
|
+
expect(OnCallShiftChangeListeners.getRegisteredNames()).toContain(
|
|
135
|
+
SHIFT_REMINDER_LISTENER_NAME,
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("registering again (a second import, another role) never stacks a second listener", () => {
|
|
140
|
+
const before: number = OnCallShiftChangeListeners.getCount();
|
|
141
|
+
|
|
142
|
+
registerOnCallShiftReminderListener();
|
|
143
|
+
registerOnCallShiftReminderListener();
|
|
144
|
+
|
|
145
|
+
expect(OnCallShiftChangeListeners.getCount()).toBe(before);
|
|
146
|
+
expect(
|
|
147
|
+
OnCallShiftChangeListeners.getRegisteredNames().filter(
|
|
148
|
+
(name: string | undefined) => {
|
|
149
|
+
return name === SHIFT_REMINDER_LISTENER_NAME;
|
|
150
|
+
},
|
|
151
|
+
),
|
|
152
|
+
).toHaveLength(1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("the listener hands the event to OnCallShiftReminderRunner.runChangePass", async () => {
|
|
156
|
+
const runChangePass: any = jest
|
|
157
|
+
.spyOn(OnCallShiftReminderRunner, "runChangePass")
|
|
158
|
+
.mockResolvedValue(emptyStats());
|
|
159
|
+
|
|
160
|
+
const published: OnCallShiftChangeEvent = event();
|
|
161
|
+
|
|
162
|
+
await onCallShiftReminderListener(published);
|
|
163
|
+
|
|
164
|
+
expect(runChangePass).toHaveBeenCalledTimes(1);
|
|
165
|
+
expect(runChangePass.mock.calls[0]![0]).toBe(published);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("a change pass reached through notify() runs for a published event", async () => {
|
|
169
|
+
const runChangePass: any = jest
|
|
170
|
+
.spyOn(OnCallShiftReminderRunner, "runChangePass")
|
|
171
|
+
.mockResolvedValue(emptyStats());
|
|
172
|
+
|
|
173
|
+
await OnCallShiftChangeListeners.notify(event());
|
|
174
|
+
|
|
175
|
+
expect(runChangePass).toHaveBeenCalledTimes(1);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("a throwing change pass is caught and logged; the listener itself resolves", async () => {
|
|
179
|
+
jest
|
|
180
|
+
.spyOn(OnCallShiftReminderRunner, "runChangePass")
|
|
181
|
+
.mockRejectedValue(new Error("boom"));
|
|
182
|
+
|
|
183
|
+
await expect(onCallShiftReminderListener(event())).resolves.toBeUndefined();
|
|
184
|
+
|
|
185
|
+
const errorLog: any = logger.error as any;
|
|
186
|
+
|
|
187
|
+
expect(errorLog).toHaveBeenCalled();
|
|
188
|
+
expect(
|
|
189
|
+
errorLog.mock.calls.some((call: Array<unknown>) => {
|
|
190
|
+
return String(call[0]).includes(SHIFT_REMINDER_LISTENER_NAME);
|
|
191
|
+
}),
|
|
192
|
+
).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("a synchronous throw inside runChangePass is caught too", async () => {
|
|
196
|
+
jest
|
|
197
|
+
.spyOn(OnCallShiftReminderRunner, "runChangePass")
|
|
198
|
+
.mockImplementation((): Promise<ShiftReminderChangePassStats> => {
|
|
199
|
+
throw new TypeError("sync boom");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await expect(onCallShiftReminderListener(event())).resolves.toBeUndefined();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
/*
|
|
206
|
+
* Coalescing. The hooks run on the api tier, and every pass re-materializes
|
|
207
|
+
* the affected schedules synchronously, so a burst of edits (ten users
|
|
208
|
+
* added to a layer is ten create hooks) must not become ten passes.
|
|
209
|
+
*/
|
|
210
|
+
describe("coalescing bursts", () => {
|
|
211
|
+
interface GatedRun {
|
|
212
|
+
events: Array<OnCallShiftChangeEvent>;
|
|
213
|
+
release: () => void;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** runChangePass that blocks on the FIRST call until released. */
|
|
217
|
+
function gateFirstPass(): GatedRun {
|
|
218
|
+
const events: Array<OnCallShiftChangeEvent> = [];
|
|
219
|
+
let release: () => void = (): void => {
|
|
220
|
+
return undefined;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const gate: Promise<void> = new Promise<void>((resolve: () => void) => {
|
|
224
|
+
release = resolve;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
jest
|
|
228
|
+
.spyOn(OnCallShiftReminderRunner, "runChangePass")
|
|
229
|
+
.mockImplementation((async (
|
|
230
|
+
published: OnCallShiftChangeEvent,
|
|
231
|
+
): Promise<ShiftReminderChangePassStats> => {
|
|
232
|
+
events.push(published);
|
|
233
|
+
|
|
234
|
+
if (events.length === 1) {
|
|
235
|
+
await gate;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return emptyStats();
|
|
239
|
+
}) as never);
|
|
240
|
+
|
|
241
|
+
return { events, release };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
test("a burst for one project costs two passes, and the follow-up carries the union of what queued", async () => {
|
|
245
|
+
const run: GatedRun = gateFirstPass();
|
|
246
|
+
|
|
247
|
+
const first: Promise<void> = onCallShiftReminderListener(
|
|
248
|
+
eventFor({
|
|
249
|
+
projectId: "project-1",
|
|
250
|
+
scheduleId: "schedule-1",
|
|
251
|
+
userId: "user-a",
|
|
252
|
+
}),
|
|
253
|
+
);
|
|
254
|
+
const second: Promise<void> = onCallShiftReminderListener(
|
|
255
|
+
eventFor({
|
|
256
|
+
projectId: "project-1",
|
|
257
|
+
scheduleId: "schedule-1",
|
|
258
|
+
userId: "user-b",
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
const third: Promise<void> = onCallShiftReminderListener(
|
|
262
|
+
eventFor({
|
|
263
|
+
projectId: "project-1",
|
|
264
|
+
scheduleId: "schedule-2",
|
|
265
|
+
userId: "user-c",
|
|
266
|
+
reason: OnCallShiftChangeReason.LayerChanged,
|
|
267
|
+
}),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
run.release();
|
|
271
|
+
|
|
272
|
+
await Promise.all([first, second, third]);
|
|
273
|
+
|
|
274
|
+
expect(run.events).toHaveLength(2);
|
|
275
|
+
expect(idsOf(run.events[0]!.userIds)).toEqual(["user-a"]);
|
|
276
|
+
|
|
277
|
+
// The merged follow-up: both queued events, nothing lost.
|
|
278
|
+
expect(idsOf(run.events[1]!.userIds)).toEqual(["user-b", "user-c"]);
|
|
279
|
+
expect(idsOf(run.events[1]!.scheduleIds)).toEqual([
|
|
280
|
+
"schedule-1",
|
|
281
|
+
"schedule-2",
|
|
282
|
+
]);
|
|
283
|
+
expect(run.events[1]!.reason).toBe(OnCallShiftChangeReason.LayerChanged);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("every caller's promise settles only after the pass covering its event has run", async () => {
|
|
287
|
+
const run: GatedRun = gateFirstPass();
|
|
288
|
+
|
|
289
|
+
let secondSettled: boolean = false;
|
|
290
|
+
|
|
291
|
+
const first: Promise<void> = onCallShiftReminderListener(
|
|
292
|
+
eventFor({
|
|
293
|
+
projectId: "project-1",
|
|
294
|
+
scheduleId: "schedule-1",
|
|
295
|
+
userId: "user-a",
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
const second: Promise<void> = onCallShiftReminderListener(
|
|
299
|
+
eventFor({
|
|
300
|
+
projectId: "project-1",
|
|
301
|
+
scheduleId: "schedule-1",
|
|
302
|
+
userId: "user-b",
|
|
303
|
+
}),
|
|
304
|
+
).then((): void => {
|
|
305
|
+
secondSettled = true;
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(secondSettled).toBe(false);
|
|
309
|
+
expect(run.events).toHaveLength(1);
|
|
310
|
+
|
|
311
|
+
run.release();
|
|
312
|
+
await Promise.all([first, second]);
|
|
313
|
+
|
|
314
|
+
expect(secondSettled).toBe(true);
|
|
315
|
+
expect(run.events).toHaveLength(2);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("different projects are never queued behind each other", async () => {
|
|
319
|
+
const run: GatedRun = gateFirstPass();
|
|
320
|
+
|
|
321
|
+
const first: Promise<void> = onCallShiftReminderListener(
|
|
322
|
+
eventFor({
|
|
323
|
+
projectId: "project-1",
|
|
324
|
+
scheduleId: "schedule-1",
|
|
325
|
+
userId: "user-a",
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
328
|
+
const other: Promise<void> = onCallShiftReminderListener(
|
|
329
|
+
eventFor({
|
|
330
|
+
projectId: "project-2",
|
|
331
|
+
scheduleId: "schedule-9",
|
|
332
|
+
userId: "user-z",
|
|
333
|
+
}),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
// The second project ran while the first was still blocked.
|
|
337
|
+
await other;
|
|
338
|
+
expect(run.events).toHaveLength(2);
|
|
339
|
+
expect(run.events[1]!.projectId?.toString()).toBe("project-2");
|
|
340
|
+
|
|
341
|
+
run.release();
|
|
342
|
+
await first;
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("an event that names no project is never coalesced (the pass resolves it from the schedules)", async () => {
|
|
346
|
+
const run: GatedRun = gateFirstPass();
|
|
347
|
+
|
|
348
|
+
const first: Promise<void> = onCallShiftReminderListener(
|
|
349
|
+
eventFor({
|
|
350
|
+
projectId: null,
|
|
351
|
+
scheduleId: "schedule-1",
|
|
352
|
+
userId: "user-a",
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
const second: Promise<void> = onCallShiftReminderListener(
|
|
356
|
+
eventFor({
|
|
357
|
+
projectId: null,
|
|
358
|
+
scheduleId: "schedule-2",
|
|
359
|
+
userId: "user-b",
|
|
360
|
+
}),
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
await second;
|
|
364
|
+
expect(run.events).toHaveLength(2);
|
|
365
|
+
|
|
366
|
+
run.release();
|
|
367
|
+
await first;
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("a queued follow-up still runs when the pass before it throws", async () => {
|
|
371
|
+
const events: Array<OnCallShiftChangeEvent> = [];
|
|
372
|
+
let release: () => void = (): void => {
|
|
373
|
+
return undefined;
|
|
374
|
+
};
|
|
375
|
+
const gate: Promise<void> = new Promise<void>((resolve: () => void) => {
|
|
376
|
+
release = resolve;
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
jest
|
|
380
|
+
.spyOn(OnCallShiftReminderRunner, "runChangePass")
|
|
381
|
+
.mockImplementation((async (
|
|
382
|
+
published: OnCallShiftChangeEvent,
|
|
383
|
+
): Promise<ShiftReminderChangePassStats> => {
|
|
384
|
+
events.push(published);
|
|
385
|
+
|
|
386
|
+
if (events.length === 1) {
|
|
387
|
+
await gate;
|
|
388
|
+
throw new Error("boom");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return emptyStats();
|
|
392
|
+
}) as never);
|
|
393
|
+
|
|
394
|
+
const first: Promise<void> = onCallShiftReminderListener(
|
|
395
|
+
eventFor({
|
|
396
|
+
projectId: "project-1",
|
|
397
|
+
scheduleId: "schedule-1",
|
|
398
|
+
userId: "user-a",
|
|
399
|
+
}),
|
|
400
|
+
);
|
|
401
|
+
const second: Promise<void> = onCallShiftReminderListener(
|
|
402
|
+
eventFor({
|
|
403
|
+
projectId: "project-1",
|
|
404
|
+
scheduleId: "schedule-1",
|
|
405
|
+
userId: "user-b",
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
release();
|
|
410
|
+
|
|
411
|
+
await expect(first).resolves.toBeUndefined();
|
|
412
|
+
await expect(second).resolves.toBeUndefined();
|
|
413
|
+
expect(events).toHaveLength(2);
|
|
414
|
+
expect(idsOf(events[1]!.userIds)).toEqual(["user-b"]);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test("a later burst after the queue drained starts a fresh pass", async () => {
|
|
418
|
+
jest
|
|
419
|
+
.spyOn(OnCallShiftReminderRunner, "runChangePass")
|
|
420
|
+
.mockResolvedValue(emptyStats());
|
|
421
|
+
|
|
422
|
+
await onCallShiftReminderListener(event());
|
|
423
|
+
await onCallShiftReminderListener(event());
|
|
424
|
+
|
|
425
|
+
expect(
|
|
426
|
+
(OnCallShiftReminderRunner.runChangePass as any).mock.calls,
|
|
427
|
+
).toHaveLength(2);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("mergeOnCallShiftChangeEvents unions the ids and keeps the newest occurredAt", () => {
|
|
431
|
+
const older: OnCallShiftChangeEvent = eventFor({
|
|
432
|
+
projectId: "project-1",
|
|
433
|
+
scheduleId: "schedule-1",
|
|
434
|
+
userId: "user-a",
|
|
435
|
+
reason: OnCallShiftChangeReason.LayerUserChanged,
|
|
436
|
+
occurredAt: new Date("2026-09-03T15:00:00Z"),
|
|
437
|
+
});
|
|
438
|
+
const newer: OnCallShiftChangeEvent = eventFor({
|
|
439
|
+
projectId: "project-1",
|
|
440
|
+
scheduleId: "schedule-2",
|
|
441
|
+
userId: "user-a",
|
|
442
|
+
reason: OnCallShiftChangeReason.OverrideChanged,
|
|
443
|
+
occurredAt: new Date("2026-09-03T15:00:05Z"),
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const merged: OnCallShiftChangeEvent = mergeOnCallShiftChangeEvents(
|
|
447
|
+
older,
|
|
448
|
+
newer,
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
expect(merged.projectId?.toString()).toBe("project-1");
|
|
452
|
+
expect(idsOf(merged.scheduleIds)).toEqual(["schedule-1", "schedule-2"]);
|
|
453
|
+
// Deduplicated, not repeated.
|
|
454
|
+
expect(idsOf(merged.userIds)).toEqual(["user-a"]);
|
|
455
|
+
expect(merged.reason).toBe(OnCallShiftChangeReason.OverrideChanged);
|
|
456
|
+
expect(merged.occurredAt.toISOString()).toBe("2026-09-03T15:00:05.000Z");
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
describe("wiring", () => {
|
|
461
|
+
test("the module exists where the two entrypoints import it from", () => {
|
|
462
|
+
expect(fs.existsSync(LISTENER_PATH)).toBe(true);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("App/FeatureSet/Workers/Index.ts imports the listener module as a side effect", () => {
|
|
466
|
+
const source: string = fs.readFileSync(
|
|
467
|
+
path.join(APP_DIR, "FeatureSet", "Workers", "Index.ts"),
|
|
468
|
+
"utf8",
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
expect(source).toMatch(SIDE_EFFECT_IMPORT);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test("App/Index.ts imports the listener module too, so the api role runs the change pass", () => {
|
|
475
|
+
const source: string = fs.readFileSync(
|
|
476
|
+
path.join(APP_DIR, "Index.ts"),
|
|
477
|
+
"utf8",
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
expect(source).toMatch(SIDE_EFFECT_IMPORT);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("the listener module registers at import time (a plain function nobody calls would be dead code)", () => {
|
|
484
|
+
const source: string = fs.readFileSync(LISTENER_PATH, "utf8");
|
|
485
|
+
|
|
486
|
+
expect(source).toMatch(/^registerOnCallShiftReminderListener\(\);\s*$/m);
|
|
487
|
+
expect(source).toContain("OnCallShiftChangeListeners.register(");
|
|
488
|
+
expect(source).toContain("SHIFT_REMINDER_LISTENER_NAME");
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
});
|