@oneuptime/common 11.7.1 → 11.7.2

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.
Files changed (237) hide show
  1. package/Models/DatabaseModels/DashboardDomain.ts +1 -0
  2. package/Models/DatabaseModels/Incident.ts +2 -0
  3. package/Models/DatabaseModels/IncidentEpisode.ts +1 -0
  4. package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +1 -0
  5. package/Models/DatabaseModels/IncidentEpisodeStateTimeline.ts +1 -0
  6. package/Models/DatabaseModels/IncidentPublicNote.ts +1 -0
  7. package/Models/DatabaseModels/IncidentStateTimeline.ts +1 -0
  8. package/Models/DatabaseModels/Monitor.ts +1 -0
  9. package/Models/DatabaseModels/MonitorStatusTimeline.ts +1 -0
  10. package/Models/DatabaseModels/NetworkSite.ts +1 -0
  11. package/Models/DatabaseModels/NetworkSiteStatusTimeline.ts +1 -0
  12. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +1 -0
  13. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +2 -0
  14. package/Models/DatabaseModels/Probe.ts +13 -13
  15. package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -0
  16. package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +1 -0
  17. package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +1 -0
  18. package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -0
  19. package/Models/DatabaseModels/StatusPage.ts +1 -0
  20. package/Models/DatabaseModels/StatusPageAnnouncement.ts +1 -0
  21. package/Models/DatabaseModels/StatusPageDomain.ts +1 -0
  22. package/Models/DatabaseModels/WorkspaceNotificationSummary.ts +1 -0
  23. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +11 -0
  24. package/Server/API/BaseAPI.ts +17 -1
  25. package/Server/API/CommonAPI.ts +32 -0
  26. package/Server/API/MicrosoftTeamsAPI.ts +99 -2
  27. package/Server/API/ProbeAPI.ts +2 -0
  28. package/Server/API/SlackAPI.ts +62 -0
  29. package/Server/API/StatusPageAPI.ts +157 -192
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.ts +89 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.ts +61 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.ts +280 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  34. package/Server/Services/DatabaseService.ts +11 -3
  35. package/Server/Services/MonitorGroupService.ts +145 -0
  36. package/Server/Services/MonitorService.ts +231 -30
  37. package/Server/Services/ProbeService.ts +319 -16
  38. package/Server/Services/StatusPageService.ts +72 -42
  39. package/Server/Services/WorkspaceNotificationRuleService.ts +232 -2
  40. package/Server/Utils/Database/PostgresErrorTranslator.ts +131 -0
  41. package/Server/Utils/Monitor/MonitorResource.ts +88 -35
  42. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +21 -1
  43. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +158 -0
  44. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +706 -6
  45. package/Tests/Models/DatabaseModels/ProbeColumnAccessControl.test.ts +103 -0
  46. package/Tests/Server/API/BaseAPIUpdateNoOp.test.ts +135 -0
  47. package/Tests/Server/API/CommonAPIAuthGuard.test.ts +317 -0
  48. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +228 -0
  49. package/Tests/Server/API/ProbeAPI.test.ts +6 -0
  50. package/Tests/Server/API/ZZVerifyChatsAuth.test.ts +167 -0
  51. package/Tests/Server/Services/AddHotQueryIndexesMigration.test.ts +255 -0
  52. package/Tests/Server/Services/AddHotQueryIndexesSecondPassMigration.test.ts +258 -0
  53. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +1 -1
  54. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +121 -0
  55. package/Tests/Server/Services/HeartbeatWriteFastPathSafety.test.ts +200 -0
  56. package/Tests/Server/Services/HookFreeWriteSafetySecondPass.test.ts +181 -0
  57. package/Tests/Server/Services/MonitorGroupServiceBatchedStatuses.test.ts +513 -0
  58. package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +492 -0
  59. package/Tests/Server/Services/MonitorServiceProbeSelection.test.ts +401 -0
  60. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +453 -0
  61. package/Tests/Server/Services/ProbeAuthKeyCacheAndHeartbeat.test.ts +679 -0
  62. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +3 -3
  63. package/Tests/Server/Services/StatusPageDomainResolutionCache.test.ts +245 -0
  64. package/Tests/Server/Services/StatusPageOverviewTimelineWindow.test.ts +283 -0
  65. package/Tests/Server/Services/StatusPageServiceMcp.test.ts +17 -1
  66. package/Tests/Server/Services/StatusPageServiceMonitorGroupBatching.test.ts +331 -0
  67. package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +12 -12
  68. package/Tests/Server/Services/WorkspaceNotificationRuleChats.test.ts +1390 -0
  69. package/Tests/Server/Utils/AI/Insights/InsightFixRouting.test.ts +4 -4
  70. package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +2 -2
  71. package/Tests/Server/Utils/AI/Insights/InsightStore.test.ts +1 -3
  72. package/Tests/Server/Utils/AI/Insights/InsightStoreHardening.test.ts +1 -3
  73. package/Tests/Server/Utils/AI/Insights/InsightTriage.test.ts +1 -1
  74. package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +5 -5
  75. package/Tests/Server/Utils/Alert/AlertPrivacyFilter.test.ts +187 -0
  76. package/Tests/Server/Utils/AlertEpisode/AlertEpisodePrivacyFilter.test.ts +170 -0
  77. package/Tests/Server/Utils/Database/PostgresErrorTranslator.test.ts +125 -0
  78. package/Tests/Server/Utils/Incident/IncidentPrivacyFilter.test.ts +264 -0
  79. package/Tests/Server/Utils/IncidentEpisode/IncidentEpisodePrivacyFilter.test.ts +175 -0
  80. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +181 -0
  81. package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +1 -1
  82. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +2 -2
  83. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelsList.test.ts +341 -0
  84. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +2175 -0
  85. package/Tests/Server/Utils/Workspace/SlackChannelsList.test.ts +585 -0
  86. package/Tests/Types/Billing/MeteredPlan.test.ts +46 -0
  87. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +219 -1
  88. package/Tests/UI/Components/BulkUpdateForm.test.tsx +174 -0
  89. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +315 -0
  90. package/Tests/UI/Components/ModelDetailSelect.test.tsx +229 -0
  91. package/Tests/UI/Components/ModelTable/BaseModelTableBulkSelectAll.test.tsx +1258 -0
  92. package/Tests/UI/Components/ModelTableExportFromColumns.test.ts +153 -0
  93. package/Tests/UI/Components/MonacoLoader.test.ts +67 -0
  94. package/Tests/UI/Components/MonacoRuntime.test.ts +121 -0
  95. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +212 -3
  96. package/Tests/UI/Utils/Project.test.ts +809 -0
  97. package/Tests/UI/Utils/TableColumnsToCsv.test.ts +360 -0
  98. package/Tests/Utils/Alerts/AlertMetricType.test.ts +143 -0
  99. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +219 -0
  100. package/Tests/Utils/Incident/IncidentMetricType.test.ts +174 -0
  101. package/Tests/Utils/Monitor/MonitorProbeSelectionUtil.test.ts +145 -0
  102. package/Tests/Utils/StatusPage/ResourceUptime.test.ts +263 -0
  103. package/Types/Workspace/NotificationRules/BaseNotificationRule.ts +4 -0
  104. package/Types/Workspace/NotificationRules/NotificationRuleCondition.ts +25 -10
  105. package/Types/Workspace/WorkspaceMessagePayload.ts +1 -0
  106. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +63 -8
  107. package/UI/Components/CodeEditor/CodeEditor.tsx +3 -0
  108. package/UI/Components/CodeEditor/MonacoLoader.ts +36 -0
  109. package/UI/Components/Forms/ModelForm.tsx +41 -18
  110. package/UI/Components/ModelDetail/CardModelDetail.tsx +23 -1
  111. package/UI/Components/ModelDetail/ModelDetail.tsx +46 -1
  112. package/UI/Components/ModelTable/BaseModelTable.tsx +300 -30
  113. package/UI/Components/ModelTable/Column.ts +12 -0
  114. package/UI/Components/ModelTable/ExportFromColumns.ts +57 -0
  115. package/UI/Components/Navbar/NavBar.tsx +66 -1
  116. package/UI/Components/Table/Table.tsx +52 -13
  117. package/UI/Components/Table/Types/Column.ts +18 -0
  118. package/UI/Utils/Project.ts +214 -18
  119. package/UI/Utils/TableColumnsToCsv.ts +86 -6
  120. package/UI/esbuild-config.js +34 -0
  121. package/Utils/Monitor/MonitorProbeSelectionUtil.ts +72 -0
  122. package/build/dist/Models/DatabaseModels/DashboardDomain.js +1 -0
  123. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  124. package/build/dist/Models/DatabaseModels/Incident.js +2 -0
  125. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  126. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +1 -0
  127. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  128. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +1 -0
  129. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -1
  130. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js +1 -0
  131. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js.map +1 -1
  132. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +1 -0
  133. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
  134. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +1 -0
  135. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
  136. package/build/dist/Models/DatabaseModels/Monitor.js +1 -0
  137. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  138. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +1 -0
  139. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  140. package/build/dist/Models/DatabaseModels/NetworkSite.js +1 -0
  141. package/build/dist/Models/DatabaseModels/NetworkSite.js.map +1 -1
  142. package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js +1 -0
  143. package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js.map +1 -1
  144. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +1 -0
  145. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  146. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +2 -0
  147. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  148. package/build/dist/Models/DatabaseModels/Probe.js +13 -13
  149. package/build/dist/Models/DatabaseModels/Probe.js.map +1 -1
  150. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -0
  151. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  152. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +1 -0
  153. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
  154. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +1 -0
  155. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
  156. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -0
  157. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
  158. package/build/dist/Models/DatabaseModels/StatusPage.js +1 -0
  159. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  160. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +1 -0
  161. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  162. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +1 -0
  163. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  164. package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js +1 -0
  165. package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js.map +1 -1
  166. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  167. package/build/dist/Server/API/BaseAPI.js +12 -1
  168. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  169. package/build/dist/Server/API/CommonAPI.js +21 -0
  170. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  171. package/build/dist/Server/API/MicrosoftTeamsAPI.js +72 -2
  172. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  173. package/build/dist/Server/API/ProbeAPI.js +2 -0
  174. package/build/dist/Server/API/ProbeAPI.js.map +1 -1
  175. package/build/dist/Server/API/SlackAPI.js +44 -0
  176. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  177. package/build/dist/Server/API/StatusPageAPI.js +98 -152
  178. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  179. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js +36 -0
  180. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js.map +1 -0
  181. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js +26 -0
  182. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js.map +1 -0
  183. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js +254 -0
  184. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js.map +1 -0
  185. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  186. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  187. package/build/dist/Server/Services/DatabaseService.js +10 -2
  188. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  189. package/build/dist/Server/Services/MonitorGroupService.js +118 -0
  190. package/build/dist/Server/Services/MonitorGroupService.js.map +1 -1
  191. package/build/dist/Server/Services/MonitorService.js +198 -24
  192. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  193. package/build/dist/Server/Services/ProbeService.js +238 -9
  194. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  195. package/build/dist/Server/Services/StatusPageService.js +46 -39
  196. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  197. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +178 -3
  198. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  199. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js +80 -0
  200. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js.map +1 -0
  201. package/build/dist/Server/Utils/Monitor/MonitorResource.js +67 -29
  202. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  203. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +31 -14
  204. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  205. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +117 -0
  206. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -0
  207. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +549 -7
  208. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  209. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js +17 -7
  210. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js.map +1 -1
  211. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +37 -11
  212. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  213. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +2 -0
  214. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  215. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js +34 -0
  216. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js.map +1 -0
  217. package/build/dist/UI/Components/Forms/ModelForm.js +37 -17
  218. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  219. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +22 -1
  220. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  221. package/build/dist/UI/Components/ModelDetail/ModelDetail.js +31 -1
  222. package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
  223. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +234 -21
  224. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  225. package/build/dist/UI/Components/ModelTable/ExportFromColumns.js +30 -0
  226. package/build/dist/UI/Components/ModelTable/ExportFromColumns.js.map +1 -0
  227. package/build/dist/UI/Components/Navbar/NavBar.js +39 -1
  228. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  229. package/build/dist/UI/Components/Table/Table.js +31 -11
  230. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  231. package/build/dist/UI/Utils/Project.js +161 -15
  232. package/build/dist/UI/Utils/Project.js.map +1 -1
  233. package/build/dist/UI/Utils/TableColumnsToCsv.js +67 -6
  234. package/build/dist/UI/Utils/TableColumnsToCsv.js.map +1 -1
  235. package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js +52 -0
  236. package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js.map +1 -0
  237. package/package.json +4 -3
@@ -0,0 +1,258 @@
1
+ import DashboardDomain from "../../../Models/DatabaseModels/DashboardDomain";
2
+ import NetworkSite from "../../../Models/DatabaseModels/NetworkSite";
3
+ import OnCallDutyPolicySchedule from "../../../Models/DatabaseModels/OnCallDutyPolicySchedule";
4
+ import ScheduledMaintenanceTemplate from "../../../Models/DatabaseModels/ScheduledMaintenanceTemplate";
5
+ import StatusPage from "../../../Models/DatabaseModels/StatusPage";
6
+ import StatusPageDomain from "../../../Models/DatabaseModels/StatusPageDomain";
7
+ import WorkspaceNotificationSummary from "../../../Models/DatabaseModels/WorkspaceNotificationSummary";
8
+ import { AddHotQueryIndexesSecondPass1785148065137 } from "../../../Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass";
9
+ import SchemaMigrations from "../../../Server/Infrastructure/Postgres/SchemaMigrations/Index";
10
+ import { createHash } from "node:crypto";
11
+ import { QueryRunner, getMetadataArgsStorage } from "typeorm";
12
+ import type { IndexMetadataArgs } from "typeorm/metadata-args/IndexMetadataArgs";
13
+ import { describe, expect, test } from "@jest/globals";
14
+
15
+ /*
16
+ * The 8 columns below are the filter columns of hot recurring lookups that were
17
+ * full-table seq scans before AddHotQueryIndexesSecondPass1785148065137:
18
+ * - StatusPageDomain.fullDomain / DashboardDomain.fullDomain: resolved on
19
+ * EVERY public request to a custom-domain status page or dashboard (the
20
+ * Host header is looked up by fullDomain to find the page to serve),
21
+ * - StatusPage.sendNextReportBy: the EVERY_MINUTE status-page report cron
22
+ * sweeps for pages whose next report is due,
23
+ * - OnCallDutyPolicySchedule.rosterHandoffAt / rosterNextStartAt: the
24
+ * EVERY_MINUTE on-call roster crons sweep for schedules due a handoff or
25
+ * a roster start,
26
+ * - WorkspaceNotificationSummary.nextSendAt: the workspace notification
27
+ * digest cron sweeps for summaries due to send,
28
+ * - ScheduledMaintenanceTemplate.scheduleNextEventAt: the recurring
29
+ * scheduled-maintenance cron sweeps for templates due to spawn an event,
30
+ * - NetworkSite.lastRollupAt: the network-site rollup cron sweeps for sites
31
+ * due a metrics rollup.
32
+ *
33
+ * These tests pin the pieces that must all hold for the scans to stay gone —
34
+ * a regression in ANY one silently reintroduces them:
35
+ * 1. the @Index() decorator on each entity property (keeps future
36
+ * schema:generate runs from emitting a DROP INDEX as "drift"),
37
+ * 2. the migration's SQL contract: up() creates exactly these 8 indexes
38
+ * and nothing else (no ALTER TABLE drift statements from a regenerated
39
+ * migration), down() drops exactly the indexes up() created,
40
+ * 3. each index NAME equals TypeORM's deterministic derivation
41
+ * "IDX_" + sha1(tableName + "_" + columnName) truncated to 26 hex chars,
42
+ * so the hand-written migration creates the SAME index a schema:generate
43
+ * run would derive from the decorators (a name mismatch would make a
44
+ * future generated migration drop and recreate the index as drift),
45
+ * 4. the migration's registration in SchemaMigrations/Index.ts (an
46
+ * unregistered migration is dead code and never runs on boot).
47
+ *
48
+ * Pure metadata/mocks — no Postgres connection anywhere.
49
+ */
50
+
51
+ type ModelClass = { new (): unknown };
52
+
53
+ // [entity class, table name (=== class name for all of these), indexed column]
54
+ const HOT_INDEX_PAIRS: Array<[ModelClass, string, string]> = [
55
+ [StatusPageDomain, "StatusPageDomain", "fullDomain"],
56
+ [DashboardDomain, "DashboardDomain", "fullDomain"],
57
+ [StatusPage, "StatusPage", "sendNextReportBy"],
58
+ [OnCallDutyPolicySchedule, "OnCallDutyPolicySchedule", "rosterHandoffAt"],
59
+ [OnCallDutyPolicySchedule, "OnCallDutyPolicySchedule", "rosterNextStartAt"],
60
+ [WorkspaceNotificationSummary, "WorkspaceNotificationSummary", "nextSendAt"],
61
+ [
62
+ ScheduledMaintenanceTemplate,
63
+ "ScheduledMaintenanceTemplate",
64
+ "scheduleNextEventAt",
65
+ ],
66
+ [NetworkSite, "NetworkSite", "lastRollupAt"],
67
+ ];
68
+
69
+ /*
70
+ * IndexMetadataArgs.columns is either an array of property names (property-
71
+ * level @Index()) or a function over the entity's properties map (class-level
72
+ * @Index((o) => [o.a, o.b])). Resolve both to plain property-name arrays; the
73
+ * function form is fed a proxy whose every property reads as its own name,
74
+ * which is exactly how TypeORM's propertiesMap behaves for flat columns.
75
+ */
76
+ function resolveIndexColumns(
77
+ columns: IndexMetadataArgs["columns"],
78
+ ): Array<string> {
79
+ if (!columns) {
80
+ return [];
81
+ }
82
+ if (Array.isArray(columns)) {
83
+ return columns.map(String);
84
+ }
85
+ const propertiesMap: Record<string, string> = new Proxy(
86
+ {} as Record<string, string>,
87
+ {
88
+ get: (_target: Record<string, string>, property: string | symbol) => {
89
+ return String(property);
90
+ },
91
+ },
92
+ );
93
+ const resolved: Array<unknown> | { [key: string]: number } =
94
+ columns(propertiesMap);
95
+ if (Array.isArray(resolved)) {
96
+ return resolved.map(String);
97
+ }
98
+ return Object.keys(resolved);
99
+ }
100
+
101
+ /*
102
+ * TypeORM's DefaultNamingStrategy.indexName: "IDX_" + the first 26 hex chars
103
+ * of sha1(`${tableName}_${columnName}`) for a single-column index.
104
+ */
105
+ function typeormIndexName(tableName: string, columnName: string): string {
106
+ const hash: string = createHash("sha1")
107
+ .update(`${tableName}_${columnName}`)
108
+ .digest("hex");
109
+ return `IDX_${hash.substring(0, 26)}`;
110
+ }
111
+
112
+ interface CreateIndexStatement {
113
+ indexName: string;
114
+ tableName: string;
115
+ columnName: string;
116
+ }
117
+
118
+ const CREATE_INDEX_REGEX: RegExp =
119
+ /^CREATE INDEX "([^"]+)" ON "([^"]+)" \("([^"]+)"\)\s*$/;
120
+ const DROP_INDEX_REGEX: RegExp = /^DROP INDEX "public"\."([^"]+)"$/;
121
+
122
+ function makeQueryRunner(): { runner: QueryRunner; query: jest.Mock } {
123
+ const query: jest.Mock = jest.fn().mockResolvedValue(undefined);
124
+ return { runner: { query } as unknown as QueryRunner, query };
125
+ }
126
+
127
+ function executedSql(query: jest.Mock): Array<string> {
128
+ return query.mock.calls.map((call: Array<unknown>) => {
129
+ return String(call[0]);
130
+ });
131
+ }
132
+
133
+ describe("AddHotQueryIndexesSecondPass: entity @Index() decorators", () => {
134
+ test.each(
135
+ HOT_INDEX_PAIRS.map(
136
+ ([model, table, column]: [ModelClass, string, string]) => {
137
+ return [table, column, model] as [string, string, ModelClass];
138
+ },
139
+ ),
140
+ )(
141
+ "%s.%s has a single-column index decorator",
142
+ (_table: string, column: string, model: ModelClass) => {
143
+ const singleColumnIndexes: Array<string> = getMetadataArgsStorage()
144
+ .indices.filter((index: IndexMetadataArgs) => {
145
+ return index.target === model;
146
+ })
147
+ .map((index: IndexMetadataArgs) => {
148
+ return resolveIndexColumns(index.columns);
149
+ })
150
+ .filter((columns: Array<string>) => {
151
+ return columns.length === 1;
152
+ })
153
+ .map((columns: Array<string>) => {
154
+ return columns[0] as string;
155
+ });
156
+
157
+ expect(singleColumnIndexes).toContain(column);
158
+ },
159
+ );
160
+ });
161
+
162
+ describe("AddHotQueryIndexesSecondPass1785148065137 migration SQL contract", () => {
163
+ const migration: AddHotQueryIndexesSecondPass1785148065137 =
164
+ new AddHotQueryIndexesSecondPass1785148065137();
165
+
166
+ test("up() issues exactly 8 CREATE INDEX statements covering exactly the 8 hot columns — and nothing else", async () => {
167
+ const { runner, query } = makeQueryRunner();
168
+ await migration.up(runner);
169
+
170
+ const statements: Array<string> = executedSql(query);
171
+ expect(statements).toHaveLength(8);
172
+
173
+ /*
174
+ * A regenerated migration on a drifted schema would interleave ALTER
175
+ * TABLE / DROP statements with the index creates; none may sneak in.
176
+ */
177
+ for (const sql of statements) {
178
+ expect(sql).not.toContain("ALTER TABLE");
179
+ expect(sql).not.toContain("DROP");
180
+ expect(sql).toMatch(CREATE_INDEX_REGEX);
181
+ }
182
+
183
+ const created: Array<CreateIndexStatement> = statements.map(
184
+ (sql: string) => {
185
+ const match: RegExpMatchArray = sql.match(
186
+ CREATE_INDEX_REGEX,
187
+ ) as RegExpMatchArray;
188
+ return {
189
+ indexName: match[1] as string,
190
+ tableName: match[2] as string,
191
+ columnName: match[3] as string,
192
+ };
193
+ },
194
+ );
195
+
196
+ const createdPairs: Array<string> = created
197
+ .map((statement: CreateIndexStatement) => {
198
+ return `"${statement.tableName}"."${statement.columnName}"`;
199
+ })
200
+ .sort();
201
+ const expectedPairs: Array<string> = HOT_INDEX_PAIRS.map(
202
+ ([, table, column]: [ModelClass, string, string]) => {
203
+ return `"${table}"."${column}"`;
204
+ },
205
+ ).sort();
206
+
207
+ expect(createdPairs).toEqual(expectedPairs);
208
+ });
209
+
210
+ test("every index name matches TypeORM's deterministic sha1 derivation from (table, column)", async () => {
211
+ const { runner, query } = makeQueryRunner();
212
+ await migration.up(runner);
213
+
214
+ const statements: Array<string> = executedSql(query);
215
+ for (const sql of statements) {
216
+ const match: RegExpMatchArray = sql.match(
217
+ CREATE_INDEX_REGEX,
218
+ ) as RegExpMatchArray;
219
+ const indexName: string = match[1] as string;
220
+ const tableName: string = match[2] as string;
221
+ const columnName: string = match[3] as string;
222
+
223
+ expect(indexName).toBe(typeormIndexName(tableName, columnName));
224
+ }
225
+ });
226
+
227
+ test("down() drops exactly the 8 indexes up() created", async () => {
228
+ const upRunner: { runner: QueryRunner; query: jest.Mock } =
229
+ makeQueryRunner();
230
+ await migration.up(upRunner.runner);
231
+ const createdNames: Array<string> = executedSql(upRunner.query).map(
232
+ (sql: string) => {
233
+ return (sql.match(CREATE_INDEX_REGEX) as RegExpMatchArray)[1] as string;
234
+ },
235
+ );
236
+
237
+ const downRunner: { runner: QueryRunner; query: jest.Mock } =
238
+ makeQueryRunner();
239
+ await migration.down(downRunner.runner);
240
+ const statements: Array<string> = executedSql(downRunner.query);
241
+ expect(statements).toHaveLength(8);
242
+
243
+ const droppedNames: Array<string> = statements.map((sql: string) => {
244
+ expect(sql).toMatch(DROP_INDEX_REGEX);
245
+ return (sql.match(DROP_INDEX_REGEX) as RegExpMatchArray)[1] as string;
246
+ });
247
+
248
+ expect([...droppedNames].sort()).toEqual([...createdNames].sort());
249
+ });
250
+ });
251
+
252
+ describe("AddHotQueryIndexesSecondPass1785148065137 registration", () => {
253
+ test("is registered in SchemaMigrations/Index.ts so it actually runs on boot", () => {
254
+ expect(SchemaMigrations).toContain(
255
+ AddHotQueryIndexesSecondPass1785148065137,
256
+ );
257
+ });
258
+ });
@@ -206,7 +206,7 @@ describe("DatabaseService._updateBy — updateOneById with a model instance no l
206
206
  isRoot: true,
207
207
  },
208
208
  }),
209
- ).resolves.toBeUndefined();
209
+ ).resolves.toBe(1);
210
210
 
211
211
  expect(findBySpy).toHaveBeenCalledTimes(1);
212
212
  const findArgs: { select: Select<NetworkDeviceDiscoveryScan> } = (
@@ -0,0 +1,121 @@
1
+ import NetworkDeviceDiscoveryScanService, {
2
+ Service as NetworkDeviceDiscoveryScanServiceClass,
3
+ } from "../../../Server/Services/NetworkDeviceDiscoveryScanService";
4
+ import DatabaseService from "../../../Server/Services/DatabaseService";
5
+ import NetworkDeviceDiscoveryScan from "../../../Models/DatabaseModels/NetworkDeviceDiscoveryScan";
6
+ import { describe, expect, test } from "@jest/globals";
7
+
8
+ /*
9
+ * The /probe-ingest/probe/discovery-scan/list route hands the requesting
10
+ * probe its pending subnet scans and claims them (status "In Progress" +
11
+ * startedAt) via DatabaseService.updateColumnsByIdWithoutHooks — one raw
12
+ * parameterized UPDATE that skips ALL on-update hooks: workflow HTTP
13
+ * triggers, audit-log inserts, realtime events, service
14
+ * onBeforeUpdate/onUpdateSuccess. The probe synchronously waits on this
15
+ * route's response, and it sits behind the same probe-auth middleware whose
16
+ * hang got healthy probes flagged Disconnected — so the claim write must
17
+ * stay a single statement, not the full updateOneById pipeline (permission
18
+ * pre-fetch SELECT + row re-fetch + save() transaction).
19
+ *
20
+ * - App/FeatureSet/Telemetry/API/ProbeIngest/DiscoveryScan.ts
21
+ * NetworkDeviceDiscoveryScan.status/startedAt when a probe claims a
22
+ * Pending scan
23
+ *
24
+ * The conversion dropped NOTHING: the model declares no update workflow, no
25
+ * audit logging and no realtime events, and the service overrides neither
26
+ * update hook — so the old pipeline's hook stages were inert for this
27
+ * write. But the fast path skips hooks UNCONDITIONALLY: if someone later
28
+ * adds a decorator to NetworkDeviceDiscoveryScan or an update-hook override
29
+ * to its service, nothing at the call site fails — the new hook is just
30
+ * silently never fired for the claim write. This suite turns that silent
31
+ * drift into a loud test failure: if any assertion here starts failing, the
32
+ * hookless claim writes in DiscoveryScan.ts silently skip that new hook —
33
+ * revisit the call site before changing the assertion.
34
+ *
35
+ * Pure model-metadata + class-shape tests — no Postgres, no Redis.
36
+ */
37
+
38
+ describe("discovery-scan claim hookless write safety preconditions", () => {
39
+ describe("NetworkDeviceDiscoveryScan model (claim write in DiscoveryScan.ts)", () => {
40
+ /*
41
+ * No @EnableWorkflow decorator AT ALL — the accessor is entirely unset,
42
+ * which resolves to "no update workflow". If this becomes defined,
43
+ * someone added @EnableWorkflow to NetworkDeviceDiscoveryScan and must
44
+ * decide whether the claim write should keep skipping it.
45
+ */
46
+ test("has no @EnableWorkflow metadata at all", () => {
47
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
48
+ expect(scan.enableWorkflowOn).toBeFalsy();
49
+ expect(scan.enableWorkflowOn?.update).toBeFalsy();
50
+ });
51
+
52
+ test("has no on-update audit log", () => {
53
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
54
+ expect(scan.enableAuditLogOn?.update).toBeFalsy();
55
+ });
56
+
57
+ test("has no realtime events", () => {
58
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
59
+ expect(scan.enableRealtimeEventsOn).toBeFalsy();
60
+ });
61
+ });
62
+
63
+ describe("fast-path columns exist on the entity", () => {
64
+ /*
65
+ * updateColumnsByIdWithoutHooks validates column names against entity
66
+ * metadata at runtime and throws BadDataException on an unknown column.
67
+ * Pinning column existence here means a rename breaks this suite at CI
68
+ * time instead of leaving every claimed scan stuck in Pending at
69
+ * runtime.
70
+ */
71
+ test("NetworkDeviceDiscoveryScan has every column the claim write stamps", () => {
72
+ const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
73
+ const fastPathColumns: Array<string> = ["status", "startedAt"];
74
+ for (const column of fastPathColumns) {
75
+ expect(scan.isTableColumn(column)).toBe(true);
76
+ }
77
+ // Negative control: isTableColumn actually discriminates.
78
+ expect(scan.isTableColumn("notARealColumn")).toBe(false);
79
+ });
80
+ });
81
+
82
+ describe("service defines no update hooks of its own", () => {
83
+ /*
84
+ * DatabaseService's base onBeforeUpdate/onUpdateSuccess are no-op
85
+ * pass-throughs; a service only gets update behavior by OVERRIDING
86
+ * them. Own-property checks on the concrete service prototype pin that
87
+ * NetworkDeviceDiscoveryScanService does not — so the fast path skips
88
+ * nothing. If an override appears, these fail loudly and the claim
89
+ * write in DiscoveryScan.ts must be re-evaluated.
90
+ */
91
+ const updateHooks: Array<string> = ["onBeforeUpdate", "onUpdateSuccess"];
92
+
93
+ test("NetworkDeviceDiscoveryScanService does not override onBeforeUpdate/onUpdateSuccess", () => {
94
+ for (const hook of updateHooks) {
95
+ expect(
96
+ Object.prototype.hasOwnProperty.call(
97
+ NetworkDeviceDiscoveryScanServiceClass.prototype,
98
+ hook,
99
+ ),
100
+ ).toBe(false);
101
+ }
102
+ // The default export is an instance of the class checked above.
103
+ expect(NetworkDeviceDiscoveryScanService).toBeInstanceOf(
104
+ NetworkDeviceDiscoveryScanServiceClass,
105
+ );
106
+ });
107
+
108
+ /*
109
+ * Positive control: the hooks DO exist on the DatabaseService base
110
+ * prototype, so the own-property checks above cannot pass vacuously
111
+ * (e.g. after a rename of the hook methods themselves).
112
+ */
113
+ test("the base DatabaseService prototype defines both hooks", () => {
114
+ for (const hook of updateHooks) {
115
+ expect(
116
+ Object.prototype.hasOwnProperty.call(DatabaseService.prototype, hook),
117
+ ).toBe(true);
118
+ }
119
+ });
120
+ });
121
+ });
@@ -0,0 +1,200 @@
1
+ import MonitorProbeService from "../../../Server/Services/MonitorProbeService";
2
+ import MonitorService from "../../../Server/Services/MonitorService";
3
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
4
+ import MonitorProbe from "../../../Models/DatabaseModels/MonitorProbe";
5
+ import ObjectID from "../../../Types/ObjectID";
6
+ import UpdateBy from "../../../Server/Types/Database/UpdateBy";
7
+ import { OnUpdate } from "../../../Server/Types/Database/Hooks";
8
+ import { describe, expect, test, afterEach } from "@jest/globals";
9
+
10
+ /*
11
+ * Four hot bookkeeping writes were moved off the full updateOneById/
12
+ * updateOneBy pipeline onto DatabaseService.updateColumnsByIdWithoutHooks
13
+ * (one raw parameterized UPDATE; skips ALL on-update hooks — workflow HTTP
14
+ * triggers, audit-log inserts, realtime events, service onUpdateSuccess):
15
+ *
16
+ * - Common/Server/Utils/Monitor/MonitorResource.ts
17
+ * MonitorProbe.lastMonitoringLog, on every probe check result
18
+ * - App/FeatureSet/Workers/Jobs/IncomingRequestMonitor/CheckHeartbeat.ts
19
+ * Monitor.incomingRequestMonitorHeartbeatCheckedAt
20
+ * - App/FeatureSet/Workers/Jobs/IncomingEmailMonitor/CheckOnlineStatus.ts
21
+ * Monitor.incomingEmailMonitorHeartbeatCheckedAt
22
+ * - App/FeatureSet/Workers/Jobs/TelemetryMonitor/MonitorTelemetryMonitor.ts
23
+ * Monitor.telemetryMonitorLastMonitorAt + telemetryMonitorNextMonitorAt
24
+ *
25
+ * Those conversions are behavior-preserving only under the model-decorator
26
+ * preconditions pinned below. The fast path skips hooks UNCONDITIONALLY, so
27
+ * if someone later adds a decorator (or a column rename breaks a fast-path
28
+ * write), nothing at the call sites fails — the new hook is just silently
29
+ * never fired for these writes. This suite turns that silent drift into a
30
+ * loud test failure: when one of these assertions starts failing, revisit
31
+ * the corresponding fast-path call site before changing the assertion.
32
+ *
33
+ * Pure model-metadata + mocked-service tests — no Postgres, no Redis.
34
+ */
35
+
36
+ describe("heartbeat write fast-path safety preconditions", () => {
37
+ afterEach(() => {
38
+ jest.restoreAllMocks();
39
+ });
40
+
41
+ describe("MonitorProbe (lastMonitoringLog write in MonitorResource.ts)", () => {
42
+ /*
43
+ * The MonitorProbe conversion dropped NOTHING: the model has no
44
+ * @EnableWorkflow, no @EnableAuditLog and no realtime events, so the
45
+ * old updateOneBy pipeline's hooks were inert for this row anyway.
46
+ * If any assertion here starts failing, a decorator was added to
47
+ * MonitorProbe and the fast-path write in MonitorResource.ts now
48
+ * silently skips it — revisit that call site.
49
+ */
50
+ test("has no on-update workflow trigger", () => {
51
+ const probe: MonitorProbe = new MonitorProbe();
52
+ expect(probe.enableWorkflowOn?.update).toBeFalsy();
53
+ });
54
+
55
+ test("has no on-update audit log", () => {
56
+ const probe: MonitorProbe = new MonitorProbe();
57
+ expect(probe.enableAuditLogOn?.update).toBeFalsy();
58
+ });
59
+
60
+ test("has no realtime events", () => {
61
+ const probe: MonitorProbe = new MonitorProbe();
62
+ expect(probe.enableRealtimeEventsOn).toBeFalsy();
63
+ });
64
+ });
65
+
66
+ describe("Monitor (heartbeat/scheduler timestamp writes in worker jobs)", () => {
67
+ /*
68
+ * Monitor DOES have @EnableWorkflow({ update: true }) and
69
+ * @EnableAuditLog(). The heartbeat/scheduler conversions DELIBERATELY
70
+ * skip both for these machine-stamped bookkeeping columns — firing a
71
+ * workflow HTTP trigger plus an audit-log insert per monitor per 30s/60s
72
+ * tick is exactly the load the fast path removes (documented precedent:
73
+ * the lastMonitoringLog write in MonitorResource.ts). These assertions
74
+ * pin that tradeoff so it stays a documented decision: if Monitor's
75
+ * decorators change shape, re-evaluate the worker-job call sites rather
76
+ * than assuming the skip is still the intended behavior.
77
+ */
78
+ test("still has on-update workflow enabled (the skip is deliberate)", () => {
79
+ const monitor: Monitor = new Monitor();
80
+ expect(monitor.enableWorkflowOn?.update).toBe(true);
81
+ });
82
+
83
+ test("still has on-update audit log enabled (the skip is deliberate)", () => {
84
+ const monitor: Monitor = new Monitor();
85
+ expect(monitor.enableAuditLogOn?.update).toBe(true);
86
+ });
87
+
88
+ /*
89
+ * Nothing ELSE was dropped: Monitor has no realtime events, so
90
+ * workflow + audit are the only hooks the fast path bypasses. If this
91
+ * starts failing, realtime updates on Monitor rows would silently never
92
+ * fire for the heartbeat writes — revisit the worker-job call sites.
93
+ */
94
+ test("has no realtime events (so only workflow+audit are skipped)", () => {
95
+ const monitor: Monitor = new Monitor();
96
+ expect(monitor.enableRealtimeEventsOn).toBeFalsy();
97
+ });
98
+ });
99
+
100
+ describe("fast-path columns exist on their entities", () => {
101
+ /*
102
+ * updateColumnsByIdWithoutHooks validates column names against entity
103
+ * metadata at runtime and throws BadDataException on an unknown column.
104
+ * Pinning column existence here means a rename breaks this suite at CI
105
+ * time instead of breaking every heartbeat write at runtime.
106
+ */
107
+ test("Monitor has every column the worker jobs stamp", () => {
108
+ const monitor: Monitor = new Monitor();
109
+ const fastPathColumns: Array<string> = [
110
+ "incomingRequestMonitorHeartbeatCheckedAt",
111
+ "incomingEmailMonitorHeartbeatCheckedAt",
112
+ "telemetryMonitorLastMonitorAt",
113
+ "telemetryMonitorNextMonitorAt",
114
+ ];
115
+ for (const column of fastPathColumns) {
116
+ expect(monitor.isTableColumn(column)).toBe(true);
117
+ }
118
+ // Negative control: isTableColumn actually discriminates.
119
+ expect(monitor.isTableColumn("notARealColumn")).toBe(false);
120
+ });
121
+
122
+ test("MonitorProbe has the lastMonitoringLog column", () => {
123
+ const probe: MonitorProbe = new MonitorProbe();
124
+ expect(probe.isTableColumn("lastMonitoringLog")).toBe(true);
125
+ expect(probe.isTableColumn("notARealColumn")).toBe(false);
126
+ });
127
+ });
128
+
129
+ describe("MonitorProbeService.onUpdateSuccess early-exit", () => {
130
+ /*
131
+ * The old updateOneBy path for lastMonitoringLog also ran
132
+ * MonitorProbeService.onUpdateSuccess, but that hook early-exits unless
133
+ * updateBy.data.isEnabled is set — so skipping it for a
134
+ * lastMonitoringLog-only write changes nothing. If the early-exit
135
+ * condition ever widens (e.g. the hook starts reacting to other
136
+ * columns), the no-op test below fails and the MonitorResource.ts
137
+ * fast-path write must be revisited.
138
+ */
139
+ test("is a no-op when data lacks isEnabled (the fast-path case)", async () => {
140
+ const findBySpy: jest.SpyInstance = jest
141
+ .spyOn(MonitorProbeService, "findBy")
142
+ .mockResolvedValue([]);
143
+ const refreshSpy: jest.SpyInstance = jest
144
+ .spyOn(MonitorService, "refreshMonitorProbeStatus")
145
+ .mockResolvedValue(undefined as never);
146
+
147
+ const onUpdate: OnUpdate<MonitorProbe> = {
148
+ updateBy: {
149
+ query: {},
150
+ // What the old pipeline would have passed for the converted write.
151
+ data: { lastMonitoringLog: {} },
152
+ props: { isRoot: true },
153
+ } as UpdateBy<MonitorProbe>,
154
+ carryForward: null,
155
+ };
156
+
157
+ const result: OnUpdate<MonitorProbe> = await (MonitorProbeService as any)[
158
+ "onUpdateSuccess"
159
+ ](onUpdate, [ObjectID.generate()]);
160
+
161
+ expect(result).toBe(onUpdate);
162
+ expect(findBySpy).not.toHaveBeenCalled();
163
+ expect(refreshSpy).not.toHaveBeenCalled();
164
+ });
165
+
166
+ /*
167
+ * Positive control: proves the spies above would have observed the hook
168
+ * doing work, so the no-op test cannot pass vacuously.
169
+ */
170
+ test("does refresh monitor status when isEnabled IS in data", async () => {
171
+ const monitorId: ObjectID = ObjectID.generate();
172
+ const probeRow: MonitorProbe = new MonitorProbe();
173
+ probeRow.monitorId = monitorId;
174
+
175
+ const findBySpy: jest.SpyInstance = jest
176
+ .spyOn(MonitorProbeService, "findBy")
177
+ .mockResolvedValue([probeRow]);
178
+ const refreshSpy: jest.SpyInstance = jest
179
+ .spyOn(MonitorService, "refreshMonitorProbeStatus")
180
+ .mockResolvedValue(undefined as never);
181
+
182
+ const onUpdate: OnUpdate<MonitorProbe> = {
183
+ updateBy: {
184
+ query: {},
185
+ data: { isEnabled: false },
186
+ props: { isRoot: true },
187
+ } as UpdateBy<MonitorProbe>,
188
+ carryForward: null,
189
+ };
190
+
191
+ await (MonitorProbeService as any)["onUpdateSuccess"](onUpdate, [
192
+ ObjectID.generate(),
193
+ ]);
194
+
195
+ expect(findBySpy).toHaveBeenCalledTimes(1);
196
+ expect(refreshSpy).toHaveBeenCalledTimes(1);
197
+ expect(refreshSpy).toHaveBeenCalledWith(monitorId);
198
+ });
199
+ });
200
+ });