@oneuptime/common 12.0.31 → 12.0.33

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 (747) hide show
  1. package/Models/AnalyticsModels/Span.ts +14 -0
  2. package/Models/DatabaseModels/AlertState.ts +0 -2
  3. package/Models/DatabaseModels/Domain.ts +1 -1
  4. package/Models/DatabaseModels/EnterpriseLicense.ts +56 -0
  5. package/Models/DatabaseModels/Incident.ts +1 -1
  6. package/Models/DatabaseModels/IncidentTemplate.ts +1 -1
  7. package/Models/DatabaseModels/Index.ts +12 -0
  8. package/Models/DatabaseModels/MetricType.ts +0 -2
  9. package/Models/DatabaseModels/Monitor.ts +76 -0
  10. package/Models/DatabaseModels/MonitorStatusTimeline.ts +0 -2
  11. package/Models/DatabaseModels/MonitorTest.ts +0 -2
  12. package/Models/DatabaseModels/NetworkAlertPolicy.ts +690 -0
  13. package/Models/DatabaseModels/NetworkDevice.ts +297 -3
  14. package/Models/DatabaseModels/NetworkDeviceOidTemplate.ts +3 -1
  15. package/Models/DatabaseModels/NetworkDeviceRole.ts +648 -0
  16. package/Models/DatabaseModels/NetworkSite.ts +201 -2
  17. package/Models/DatabaseModels/NetworkSiteType.ts +102 -3
  18. package/Models/DatabaseModels/NetworkSnmpCredentialProfile.ts +841 -0
  19. package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -1
  20. package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -1
  21. package/Models/DatabaseModels/StatusPageAnnouncement.ts +0 -2
  22. package/Models/DatabaseModels/StatusPageAnnouncementTemplate.ts +0 -2
  23. package/Models/DatabaseModels/StatusPagePrivateUser.ts +0 -2
  24. package/Models/DatabaseModels/StatusPageResource.ts +0 -2
  25. package/Models/DatabaseModels/StatusPageSubscriber.ts +0 -2
  26. package/Models/DatabaseModels/TelemetryException.ts +119 -0
  27. package/Models/DatabaseModels/UserNotificationEmailRollupBatch.ts +334 -0
  28. package/Models/DatabaseModels/UserNotificationEmailRollupItem.ts +370 -0
  29. package/Models/DatabaseModels/UserNotificationEmailRollupSetting.ts +282 -0
  30. package/Server/API/EnterpriseLicenseAPI.ts +391 -64
  31. package/Server/API/LlmProviderAPI.ts +137 -22
  32. package/Server/EnvironmentConfig.ts +14 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/1790700000000-AddNetworkSiteTypeParentHierarchy.ts +55 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.ts +72 -0
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/1790900000000-AddEnterpriseLicenseUsageProvenance.ts +31 -0
  36. package/Server/Infrastructure/Postgres/SchemaMigrations/1790900000000-AddTelemetryExceptionErrorClass.ts +124 -0
  37. package/Server/Infrastructure/Postgres/SchemaMigrations/1791000000000-AddUserNotificationEmailRollup.ts +132 -0
  38. package/Server/Infrastructure/Postgres/SchemaMigrations/1791100000000-AddUserNotificationEmailRollupSetting.ts +79 -0
  39. package/Server/Infrastructure/Postgres/SchemaMigrations/1791200000000-AddNetworkSnmpCredentialProfilesAndAlertPolicies.ts +191 -0
  40. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -0
  41. package/Server/Infrastructure/QueueWorker.ts +38 -8
  42. package/Server/Middleware/BearerTokenAuthorization.ts +12 -0
  43. package/Server/Middleware/ProjectAuthorization.ts +11 -0
  44. package/Server/Middleware/SCIMAuthorization.ts +12 -0
  45. package/Server/Middleware/TelemetryIngest.ts +11 -0
  46. package/Server/Services/AIBillingService.ts +101 -27
  47. package/Server/Services/DatabaseService.ts +39 -9
  48. package/Server/Services/EnterpriseLicenseInstanceService.ts +197 -0
  49. package/Server/Services/EnterpriseLicenseService.ts +55 -0
  50. package/Server/Services/Index.ts +12 -0
  51. package/Server/Services/MonitorService.ts +143 -21
  52. package/Server/Services/MonitorStatusTimelineService.ts +150 -8
  53. package/Server/Services/MonitorTemplateService.ts +217 -1
  54. package/Server/Services/NetworkAlertPolicyEngineService.ts +2159 -0
  55. package/Server/Services/NetworkAlertPolicyService.ts +944 -0
  56. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +9 -10
  57. package/Server/Services/NetworkDeviceAutoImportRuleService.ts +60 -15
  58. package/Server/Services/NetworkDeviceRoleService.ts +90 -0
  59. package/Server/Services/NetworkDeviceService.ts +1032 -73
  60. package/Server/Services/NetworkSiteService.ts +1502 -129
  61. package/Server/Services/NetworkSiteTypeService.ts +1216 -0
  62. package/Server/Services/NetworkSnmpCredentialProfileService.ts +238 -0
  63. package/Server/Services/NotificationService.ts +118 -28
  64. package/Server/Services/ProjectService.ts +186 -44
  65. package/Server/Services/StatusPagePrivateUserService.ts +103 -3
  66. package/Server/Services/UserNotificationEmailRollupBatchService.ts +78 -0
  67. package/Server/Services/UserNotificationEmailRollupItemService.ts +79 -0
  68. package/Server/Services/UserNotificationEmailRollupSettingService.ts +113 -0
  69. package/Server/Services/UserNotificationSettingService.ts +62 -31
  70. package/Server/Services/UserService.ts +95 -0
  71. package/Server/Types/Database/QueryHelper.ts +2 -2
  72. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +14 -3
  73. package/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.ts +10 -0
  74. package/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.ts +10 -0
  75. package/Server/Utils/AI/SRE/Insights/InsightTriageRunner.ts +62 -1
  76. package/Server/Utils/AI/Toolbox/AlertTools.ts +15 -1
  77. package/Server/Utils/Billing/BillingFailureNoticeThrottle.ts +80 -0
  78. package/Server/Utils/DataSource/HttpFetch.ts +9 -2
  79. package/Server/Utils/EmailRollup/EmailRollupConstants.ts +98 -0
  80. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +872 -0
  81. package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +781 -0
  82. package/Server/Utils/EmailRollup/EmailRollupWriter.ts +368 -0
  83. package/Server/Utils/LLM/LLMService.ts +78 -0
  84. package/Server/Utils/LogRedaction.ts +8 -0
  85. package/Server/Utils/Logger.ts +79 -1
  86. package/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.ts +190 -0
  87. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +9 -2
  88. package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +61 -1
  89. package/Server/Utils/Monitor/MonitorCriteriaDataExtractor.ts +14 -0
  90. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +49 -0
  91. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +255 -4
  92. package/Server/Utils/Monitor/MonitorMetricUtil.ts +41 -0
  93. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +43 -0
  94. package/Server/Utils/Monitor/NetworkDeviceHydrationUtil.ts +499 -19
  95. package/Server/Utils/Monitor/NetworkDeviceMetricUtil.ts +66 -10
  96. package/Server/Utils/Monitor/NetworkDeviceWalkUtil.ts +238 -44
  97. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +146 -39
  98. package/Server/Utils/NetworkDevice/DeviceHealthAggregation.ts +31 -18
  99. package/Server/Utils/NetworkSite/NetworkSiteHierarchyLock.ts +213 -0
  100. package/Server/Utils/OutboundUserAgent.ts +152 -0
  101. package/Server/Utils/StartServer.ts +51 -19
  102. package/Server/Utils/Telemetry/CaptureSpan.ts +32 -8
  103. package/Server/Utils/Telemetry/ErrorClassResolver.ts +120 -0
  104. package/Server/Utils/Telemetry/SpanUtil.ts +33 -0
  105. package/Server/Utils/Telemetry.ts +198 -51
  106. package/Server/Views/Partials/SensitiveUrlToken.ejs +140 -0
  107. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +158 -27
  108. package/Tests/App/Dashboard/AskAiSuggestedPrompts.test.ts +279 -0
  109. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +35 -8
  110. package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +107 -3
  111. package/Tests/App/Dashboard/DashboardEditPermissions.test.tsx +517 -0
  112. package/Tests/App/Dashboard/DashboardVariablesDiscard.test.tsx +26 -0
  113. package/Tests/App/Dashboard/DashboardWideChartZoom.test.tsx +401 -0
  114. package/Tests/App/Dashboard/DeviceStatusHero.test.tsx +588 -0
  115. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +18 -9
  116. package/Tests/App/Dashboard/MonitorCreateFromMonitorBackedDevice.test.tsx +632 -0
  117. package/Tests/App/Dashboard/MonitorRecommendationCreateProgress.test.tsx +682 -0
  118. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +318 -0
  119. package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +4 -2
  120. package/Tests/App/Dashboard/NetworkDeviceBulkDelete.test.tsx +357 -0
  121. package/Tests/App/Dashboard/NetworkDeviceCreateFormMonitorBinding.test.tsx +676 -0
  122. package/Tests/App/Dashboard/NetworkDeviceCreateFormPingMonitor.test.tsx +791 -0
  123. package/Tests/App/Dashboard/NetworkDeviceCreateFormProbeAndSnmp.test.tsx +833 -0
  124. package/Tests/App/Dashboard/NetworkDeviceCriteriaFilter.test.ts +161 -0
  125. package/Tests/App/Dashboard/NetworkDeviceSettingsMonitoringSection.test.tsx +458 -0
  126. package/Tests/App/Dashboard/NetworkSideMenu.test.tsx +626 -0
  127. package/Tests/App/Dashboard/PingMonitorProvisioning.test.ts +368 -0
  128. package/Tests/App/Dashboard/RecommendationCard.test.tsx +796 -0
  129. package/Tests/App/Dashboard/RecommendationToolbar.test.tsx +519 -0
  130. package/Tests/App/Dashboard/RecommendationsList.test.tsx +661 -0
  131. package/Tests/App/Dashboard/SideMenuHarness.tsx +11 -3
  132. package/Tests/App/Dashboard/UseBulkCreatePingMonitors.test.tsx +899 -0
  133. package/Tests/App/Dashboard/UseBulkSnmpCredentialProfileActions.test.tsx +732 -0
  134. package/Tests/App/SensitiveUrlTokenBootstrap.test.ts +256 -0
  135. package/Tests/App/StatusPage/StatusPageIndexPageRobotsMeta.test.ts +11 -1
  136. package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +9 -4
  137. package/Tests/Models/DatabaseModels/ModelRegistryInvariants.test.ts +115 -41
  138. package/Tests/Models/NetworkSiteHierarchy.test.ts +41 -4
  139. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +951 -7
  140. package/Tests/Server/API/LlmProviderConnectionTest.test.ts +522 -0
  141. package/Tests/Server/Infrastructure/Postgres/NetworkDeviceRoleTableMigration.test.ts +476 -0
  142. package/Tests/Server/Infrastructure/Postgres/NetworkSnmpCredentialProfilesAndAlertPoliciesMigration.test.ts +854 -0
  143. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +18 -0
  144. package/Tests/Server/Infrastructure/Postgres/UserNotificationEmailRollupTableMigration.test.ts +535 -0
  145. package/Tests/Server/Services/AIChatPrivacyPin.test.ts +560 -0
  146. package/Tests/Server/Services/BillingRechargeOwnerEmailGuards.test.ts +783 -0
  147. package/Tests/Server/Services/DatabaseServiceGenerateSlug.test.ts +215 -0
  148. package/Tests/Server/Services/EnterpriseLicenseInstanceServiceDeletionUsage.test.ts +707 -0
  149. package/Tests/Server/Services/EnterpriseLicenseServiceUsageAggregationLock.test.ts +329 -0
  150. package/Tests/Server/Services/MonitorServiceDeleteNetworkDeviceCleanup.test.ts +294 -0
  151. package/Tests/Server/Services/MonitorStatusBridgeProbePath.test.ts +802 -0
  152. package/Tests/Server/Services/MonitorStatusTimelineService.test.ts +72 -2
  153. package/Tests/Server/Services/NetworkAlertPolicyEngineHooks.test.ts +752 -0
  154. package/Tests/Server/Services/NetworkAlertPolicyEngineService.test.ts +1849 -0
  155. package/Tests/Server/Services/NetworkAlertPolicyModel.test.ts +1793 -0
  156. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +47 -4
  157. package/Tests/Server/Services/NetworkDeviceAutoImportRuleService.test.ts +97 -12
  158. package/Tests/Server/Services/NetworkDeviceAutoMonitorLifecycle.test.ts +148 -3
  159. package/Tests/Server/Services/NetworkDeviceMonitorBindingUpdateGuard.test.ts +514 -0
  160. package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +447 -58
  161. package/Tests/Server/Services/NetworkDeviceMonitoringMethodTransition.test.ts +739 -0
  162. package/Tests/Server/Services/NetworkDevicePollingTenancy.test.ts +501 -0
  163. package/Tests/Server/Services/NetworkDeviceRoleModel.test.ts +672 -0
  164. package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +1 -1
  165. package/Tests/Server/Services/NetworkDeviceSiteDefaultProbe.test.ts +432 -0
  166. package/Tests/Server/Services/NetworkSiteMonitoringDefaults.test.ts +498 -0
  167. package/Tests/Server/Services/NetworkSiteRollupPolicyAndMaintenance.test.ts +337 -55
  168. package/Tests/Server/Services/NetworkSiteService.test.ts +1868 -131
  169. package/Tests/Server/Services/NetworkSiteTypeService.test.ts +1109 -0
  170. package/Tests/Server/Services/NetworkSnmpCredentialProfileModel.test.ts +926 -0
  171. package/Tests/Server/Services/NetworkSnmpCredentialProfileService.test.ts +796 -0
  172. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +67 -4
  173. package/Tests/Server/Services/ProjectServiceNetworkSiteTypeDefaults.test.ts +214 -0
  174. package/Tests/Server/Services/ScheduledMaintenanceSubscriberEndTime.test.ts +369 -0
  175. package/Tests/Server/Services/StatusPagePrivateUserEmailChangePasswordResetExpiry.test.ts +269 -0
  176. package/Tests/Server/Services/UserEmailChangePasswordResetExpiry.test.ts +472 -0
  177. package/Tests/Server/Services/UserNotificationEmailRollupModels.test.ts +569 -0
  178. package/Tests/Server/Services/UserNotificationEmailRollupSettingModel.test.ts +314 -0
  179. package/Tests/Server/Services/UserNotificationEmailRollupSettingService.test.ts +409 -0
  180. package/Tests/Server/Services/UserNotificationSettingMonitorDefaults.test.ts +248 -0
  181. package/Tests/Server/Services/UserNotificationSettingRollupRouting.test.ts +856 -0
  182. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +145 -3
  183. package/Tests/Server/Types/Database/Permissions/DashboardAccessPermission.test.ts +321 -0
  184. package/Tests/Server/Types/Database/QueryHelperOperators.test.ts +17 -0
  185. package/Tests/Server/Utils/AI/AlertMonitorFilters.test.ts +124 -0
  186. package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +208 -0
  187. package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +235 -0
  188. package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +253 -0
  189. package/Tests/Server/Utils/BasicCron.test.ts +283 -0
  190. package/Tests/Server/Utils/Captcha.test.ts +422 -0
  191. package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +96 -2
  192. package/Tests/Server/Utils/Database/MigrationFailureLog.test.ts +466 -0
  193. package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1096 -0
  194. package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerClaim.test.ts +466 -0
  195. package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +1428 -0
  196. package/Tests/Server/Utils/EmailRollup/EmailRollupTestHarness.ts +752 -0
  197. package/Tests/Server/Utils/EmailRollup/EmailRollupWriter.test.ts +864 -0
  198. package/Tests/Server/Utils/IncomingCallPhoneNumber.test.ts +174 -0
  199. package/Tests/Server/Utils/LoggerFaultDemotion.test.ts +346 -0
  200. package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +633 -0
  201. package/Tests/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.test.ts +196 -0
  202. package/Tests/Server/Utils/Monitor/DatabaseMetricWrite.test.ts +367 -0
  203. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +100 -0
  204. package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +5 -0
  205. package/Tests/Server/Utils/Monitor/NetworkDeviceHydrationUtil.test.ts +512 -31
  206. package/Tests/Server/Utils/Monitor/NetworkDeviceMetricUtil.test.ts +252 -0
  207. package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +479 -42
  208. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +351 -5
  209. package/Tests/Server/Utils/NetworkDevice/DeviceHealthAggregation.test.ts +136 -12
  210. package/Tests/Server/Utils/NetworkSite/NetworkSiteHierarchyLock.test.ts +306 -0
  211. package/Tests/Server/Utils/OutboundUserAgent.test.ts +266 -0
  212. package/Tests/Server/Utils/SecurityEvent/ThreatIntel/TaxiiClientUserAgent.test.ts +141 -0
  213. package/Tests/Server/Utils/Telemetry/ErrorClassResolver.test.ts +298 -0
  214. package/Tests/Server/Utils/Telemetry.test.ts +456 -66
  215. package/Tests/Types/Monitor/DatabaseMetricCatalog.test.ts +388 -0
  216. package/Tests/Types/Monitor/MonitorStepDatabaseMonitor.test.ts +243 -0
  217. package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +1 -0
  218. package/Tests/Types/Monitor/MonitorType.test.ts +1 -0
  219. package/Tests/Types/Monitor/MonitorTypeKeywords.test.ts +16 -7
  220. package/Tests/Types/Monitor/NetworkDeviceAlertPack.test.ts +60 -0
  221. package/Tests/Types/NetworkDevice/DefaultNetworkDeviceRole.test.ts +417 -0
  222. package/Tests/Types/NetworkDevice/NetworkAlertPolicyScope.test.ts +655 -0
  223. package/Tests/Types/NetworkDevice/NetworkDeviceMonitoringMethod.test.ts +115 -26
  224. package/Tests/Types/NetworkSite/DefaultNetworkSiteType.test.ts +39 -0
  225. package/Tests/Types/NotificationSetting/NotificationEmailRollupPolicy.test.ts +299 -0
  226. package/Tests/Types/Telemetry/ErrorClass.test.ts +483 -0
  227. package/Tests/UI/Components/BasicRadioButtons.test.tsx +336 -0
  228. package/Tests/UI/Components/Charts/ChartClickDoubleClickDisambiguation.test.tsx +221 -0
  229. package/Tests/UI/Components/Charts/ChartDoubleClickReset.test.tsx +220 -0
  230. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +751 -0
  231. package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +475 -43
  232. package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +89 -0
  233. package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +15 -0
  234. package/Tests/UI/Components/NavBarCommandKGating.test.tsx +103 -8
  235. package/Tests/UI/Components/ProgressBar.test.tsx +38 -2
  236. package/Tests/UI/Components/SideMenuItem.test.tsx +256 -4
  237. package/Tests/UI/Components/SideMenuSection.test.tsx +310 -0
  238. package/Tests/UI/Components/SideOverSubmitState.test.tsx +335 -0
  239. package/Tests/UI/Utils/PageScrollLock.test.tsx +377 -0
  240. package/Tests/UI/Utils/PermissionGate.test.ts +78 -0
  241. package/Tests/UI/Utils/SensitiveUrlToken.test.ts +140 -0
  242. package/Tests/UI/Utils/TableColumnsToCsv.test.ts +30 -0
  243. package/Tests/UI/Utils/UseDashboardTimeRangeZoom.test.tsx +187 -0
  244. package/Tests/Utils/Dashboard/DashboardResourceListSharedArgs.test.ts +302 -0
  245. package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +242 -0
  246. package/Tests/Utils/EnterpriseLicenseSeats.test.ts +69 -0
  247. package/Tests/Utils/EnterpriseLicenseSync.test.ts +7 -1
  248. package/Tests/Utils/EnterpriseLicenseUsage.test.ts +433 -2
  249. package/Tests/Utils/Monitor/MonitorMetricType.test.ts +280 -1
  250. package/Tests/Utils/Monitor/MonitorSummarySnapshotUtil.test.ts +1 -0
  251. package/Tests/Utils/Monitor/NetworkDeviceRoleCatalog.test.ts +981 -0
  252. package/Tests/Utils/Monitor/NetworkTopologyRoleCatalog.test.ts +865 -0
  253. package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +38 -0
  254. package/Tests/Utils/NetworkDevice/DeviceHealthStateUtil.test.ts +284 -23
  255. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +177 -0
  256. package/Tests/Utils/NetworkDevice/DeviceRoleKeyUtil.test.ts +538 -0
  257. package/Tests/Utils/NetworkDevice/MonitoringMethodThreadedClassifiers.test.ts +992 -0
  258. package/Tests/Utils/NetworkDevice/NetworkAlertPolicyBootstrapUtil.test.ts +427 -0
  259. package/Tests/Utils/NetworkDevice/ReachabilityStampConsistency.test.ts +185 -0
  260. package/Tests/Utils/NetworkDevice/SnmpCredentialUtil.test.ts +268 -0
  261. package/Tests/Utils/NetworkDiscovery/AutoImportRuleMatcher.test.ts +55 -5
  262. package/Tests/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.test.ts +98 -21
  263. package/Tests/Utils/NetworkDiscovery/DiscoveryImportEligibility.test.ts +98 -20
  264. package/Tests/Utils/NetworkDiscovery/DiscoveryReverseDnsChain.test.ts +8 -7
  265. package/Tests/Utils/NetworkDiscovery/PingMonitorBuilderForAddress.test.ts +274 -0
  266. package/Tests/Utils/NetworkSite/SiteStatusRollupUtil.test.ts +230 -51
  267. package/Tests/Utils/NetworkSite/TypeHierarchyUtil.test.ts +209 -0
  268. package/Tests/Utils/Slo/SloEvaluation.test.ts +171 -0
  269. package/Tests/Utils/Slug.test.ts +61 -1
  270. package/Tests/Utils/Telemetry/CaptureSpanExportGating.test.ts +145 -0
  271. package/Tests/jest.setup.ts +19 -0
  272. package/Types/AI/ExceptionAIClassification.ts +10 -24
  273. package/Types/Email/EmailTemplateType.ts +2 -0
  274. package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +5 -0
  275. package/Types/EnterpriseLicense/EnterpriseLicenseUsageSnapshot.ts +13 -0
  276. package/Types/EnterpriseLicense/EnterpriseLicenseUserCountSource.ts +6 -0
  277. package/Types/Exception/ApiException.ts +10 -0
  278. package/Types/Exception/BadDataException.ts +11 -0
  279. package/Types/Exception/BadRequestException.ts +11 -0
  280. package/Types/Exception/Exception.ts +65 -0
  281. package/Types/Exception/ForbiddenException.ts +10 -0
  282. package/Types/Exception/NotAuthenticatedException.ts +10 -0
  283. package/Types/Exception/NotAuthorizedException.ts +10 -0
  284. package/Types/Exception/NotFoundException.ts +11 -0
  285. package/Types/Exception/PayloadTooLargeException.ts +11 -0
  286. package/Types/Exception/PaymentRequiredException.ts +10 -0
  287. package/Types/Exception/ServiceUnavailableException.ts +10 -0
  288. package/Types/Exception/SsoAuthorizationException.ts +10 -0
  289. package/Types/Exception/TenantNotFoundException.ts +11 -0
  290. package/Types/Exception/TimeoutException.ts +10 -0
  291. package/Types/Exception/TooManyRequestsException.ts +10 -0
  292. package/Types/Exception/UnableToReachServer.ts +10 -0
  293. package/Types/Exception/WebsiteRequestException.ts +10 -0
  294. package/Types/Monitor/CriteriaFilter.ts +60 -2
  295. package/Types/Monitor/DatabaseMetricCatalog.ts +693 -0
  296. package/Types/Monitor/DatabaseMonitor/DatabaseMonitorResponse.ts +83 -0
  297. package/Types/Monitor/MonitorCriteriaInstance.ts +76 -0
  298. package/Types/Monitor/MonitorMetricType.ts +71 -0
  299. package/Types/Monitor/MonitorStep.ts +76 -0
  300. package/Types/Monitor/MonitorStepDatabaseMonitor.ts +171 -0
  301. package/Types/Monitor/MonitorStepSqlMonitor.ts +2 -20
  302. package/Types/Monitor/MonitorType.ts +47 -1
  303. package/Types/Monitor/SnmpMonitor/NetworkDeviceAlertPack.ts +22 -1
  304. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +61 -0
  305. package/Types/Monitor/SqlConnectionConfig.ts +31 -0
  306. package/Types/NetworkDevice/DefaultNetworkDeviceRole.ts +145 -0
  307. package/Types/NetworkDevice/NetworkAlertPolicyScope.ts +343 -0
  308. package/Types/NetworkDevice/NetworkDeviceMonitoringMethod.ts +42 -22
  309. package/Types/NetworkSite/DefaultNetworkSiteTypeHierarchy.ts +30 -0
  310. package/Types/NotificationSetting/NotificationEmailRollupCategory.ts +255 -0
  311. package/Types/NotificationSetting/NotificationEmailRollupPolicy.ts +104 -0
  312. package/Types/Permission.ts +135 -0
  313. package/Types/Probe/ProbeMonitorResponse.ts +16 -0
  314. package/Types/Telemetry/ErrorClass.ts +274 -0
  315. package/Types/Telemetry/UnitOfWork.ts +59 -0
  316. package/UI/Components/Charts/Area/AreaChart.tsx +7 -0
  317. package/UI/Components/Charts/Bar/BarChart.tsx +6 -0
  318. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +9 -1
  319. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +97 -35
  320. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +14 -0
  321. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +97 -35
  322. package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +13 -0
  323. package/UI/Components/Charts/Line/LineChart.tsx +7 -0
  324. package/UI/Components/CommandPalette/CommandPalette.tsx +2 -8
  325. package/UI/Components/EditionLabel/EditionLabel.tsx +4 -0
  326. package/UI/Components/Modal/Modal.tsx +2 -8
  327. package/UI/Components/ModelTable/BaseModelTable.tsx +50 -15
  328. package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +44 -0
  329. package/UI/Components/Navbar/NavBar.tsx +62 -13
  330. package/UI/Components/Navbar/NavBarMenuModal.tsx +7 -2
  331. package/UI/Components/ProgressBar/ProgressBar.tsx +12 -1
  332. package/UI/Components/RadioButtons/BasicRadioButtons.tsx +37 -7
  333. package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +2 -0
  334. package/UI/Components/SideMenu/SideMenu.tsx +135 -7
  335. package/UI/Components/SideMenu/SideMenuItem.tsx +80 -85
  336. package/UI/Components/SideMenu/SideMenuSection.tsx +69 -25
  337. package/UI/Components/SideOver/SideOver.tsx +21 -20
  338. package/UI/Utils/PageScrollLock.ts +79 -3
  339. package/UI/Utils/PermissionGate.ts +61 -0
  340. package/UI/Utils/SensitiveUrlToken.ts +112 -0
  341. package/UI/Utils/TestLLMProvider.ts +11 -0
  342. package/UI/Utils/UseDashboardTimeRangeZoom.ts +77 -0
  343. package/Utils/Dashboard/DashboardTimeRangeZoom.ts +118 -0
  344. package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +16 -0
  345. package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +191 -11
  346. package/Utils/Monitor/MonitorMetricType.ts +153 -1
  347. package/Utils/Monitor/NetworkDeviceRoleCatalog.ts +232 -0
  348. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +2 -1
  349. package/Utils/Monitor/NetworkTopologyUtil.ts +139 -36
  350. package/Utils/NetworkDevice/DeviceHealthStateUtil.ts +65 -14
  351. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +43 -14
  352. package/Utils/NetworkDevice/DeviceRoleKeyUtil.ts +137 -0
  353. package/Utils/NetworkDevice/NetworkAlertPolicyBootstrapUtil.ts +247 -0
  354. package/Utils/NetworkDevice/SnmpCredentialUtil.ts +68 -0
  355. package/Utils/NetworkDiscovery/AutoImportRuleMatcher.ts +15 -7
  356. package/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.ts +52 -17
  357. package/Utils/NetworkDiscovery/DiscoveryImportEligibility.ts +55 -23
  358. package/Utils/NetworkDiscovery/PingMonitorBuilder.ts +115 -13
  359. package/Utils/NetworkSite/SiteStatusRollupUtil.ts +66 -20
  360. package/Utils/NetworkSite/TypeHierarchyUtil.ts +313 -0
  361. package/Utils/Rules/RulePatternMatchUtil.ts +1 -1
  362. package/Utils/Slug.ts +34 -1
  363. package/build/dist/Models/AnalyticsModels/Span.js +14 -0
  364. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  365. package/build/dist/Models/DatabaseModels/AlertState.js +0 -2
  366. package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
  367. package/build/dist/Models/DatabaseModels/Domain.js +1 -1
  368. package/build/dist/Models/DatabaseModels/Domain.js.map +1 -1
  369. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +59 -0
  370. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  371. package/build/dist/Models/DatabaseModels/Incident.js +1 -1
  372. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  373. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +1 -1
  374. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  375. package/build/dist/Models/DatabaseModels/Index.js +12 -0
  376. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  377. package/build/dist/Models/DatabaseModels/MetricType.js +0 -2
  378. package/build/dist/Models/DatabaseModels/MetricType.js.map +1 -1
  379. package/build/dist/Models/DatabaseModels/Monitor.js +75 -0
  380. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  381. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +0 -2
  382. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  383. package/build/dist/Models/DatabaseModels/MonitorTest.js +0 -2
  384. package/build/dist/Models/DatabaseModels/MonitorTest.js.map +1 -1
  385. package/build/dist/Models/DatabaseModels/NetworkAlertPolicy.js +713 -0
  386. package/build/dist/Models/DatabaseModels/NetworkAlertPolicy.js.map +1 -0
  387. package/build/dist/Models/DatabaseModels/NetworkDevice.js +304 -4
  388. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  389. package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js +3 -1
  390. package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js.map +1 -1
  391. package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js +677 -0
  392. package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js.map +1 -0
  393. package/build/dist/Models/DatabaseModels/NetworkSite.js +200 -2
  394. package/build/dist/Models/DatabaseModels/NetworkSite.js.map +1 -1
  395. package/build/dist/Models/DatabaseModels/NetworkSiteType.js +102 -3
  396. package/build/dist/Models/DatabaseModels/NetworkSiteType.js.map +1 -1
  397. package/build/dist/Models/DatabaseModels/NetworkSnmpCredentialProfile.js +878 -0
  398. package/build/dist/Models/DatabaseModels/NetworkSnmpCredentialProfile.js.map +1 -0
  399. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -1
  400. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  401. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -1
  402. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
  403. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +0 -2
  404. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  405. package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js +0 -2
  406. package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js.map +1 -1
  407. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +0 -2
  408. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
  409. package/build/dist/Models/DatabaseModels/StatusPageResource.js +0 -2
  410. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  411. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +0 -2
  412. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  413. package/build/dist/Models/DatabaseModels/TelemetryException.js +123 -0
  414. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  415. package/build/dist/Models/DatabaseModels/UserNotificationEmailRollupBatch.js +363 -0
  416. package/build/dist/Models/DatabaseModels/UserNotificationEmailRollupBatch.js.map +1 -0
  417. package/build/dist/Models/DatabaseModels/UserNotificationEmailRollupItem.js +402 -0
  418. package/build/dist/Models/DatabaseModels/UserNotificationEmailRollupItem.js.map +1 -0
  419. package/build/dist/Models/DatabaseModels/UserNotificationEmailRollupSetting.js +296 -0
  420. package/build/dist/Models/DatabaseModels/UserNotificationEmailRollupSetting.js.map +1 -0
  421. package/build/dist/Server/API/EnterpriseLicenseAPI.js +273 -60
  422. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  423. package/build/dist/Server/API/LlmProviderAPI.js +108 -13
  424. package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
  425. package/build/dist/Server/EnvironmentConfig.js +12 -0
  426. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  427. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790700000000-AddNetworkSiteTypeParentHierarchy.js +24 -0
  428. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790700000000-AddNetworkSiteTypeParentHierarchy.js.map +1 -0
  429. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js +39 -0
  430. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js.map +1 -0
  431. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790900000000-AddEnterpriseLicenseUsageProvenance.js +16 -0
  432. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790900000000-AddEnterpriseLicenseUsageProvenance.js.map +1 -0
  433. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790900000000-AddTelemetryExceptionErrorClass.js +111 -0
  434. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790900000000-AddTelemetryExceptionErrorClass.js.map +1 -0
  435. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791000000000-AddUserNotificationEmailRollup.js +77 -0
  436. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791000000000-AddUserNotificationEmailRollup.js.map +1 -0
  437. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791100000000-AddUserNotificationEmailRollupSetting.js +50 -0
  438. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791100000000-AddUserNotificationEmailRollupSetting.js.map +1 -0
  439. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791200000000-AddNetworkSnmpCredentialProfilesAndAlertPolicies.js +83 -0
  440. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791200000000-AddNetworkSnmpCredentialProfilesAndAlertPolicies.js.map +1 -0
  441. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -0
  442. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  443. package/build/dist/Server/Infrastructure/QueueWorker.js +33 -5
  444. package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
  445. package/build/dist/Server/Middleware/BearerTokenAuthorization.js +12 -0
  446. package/build/dist/Server/Middleware/BearerTokenAuthorization.js.map +1 -1
  447. package/build/dist/Server/Middleware/ProjectAuthorization.js +11 -0
  448. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  449. package/build/dist/Server/Middleware/SCIMAuthorization.js +12 -0
  450. package/build/dist/Server/Middleware/SCIMAuthorization.js.map +1 -1
  451. package/build/dist/Server/Middleware/TelemetryIngest.js +11 -0
  452. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  453. package/build/dist/Server/Services/AIBillingService.js +70 -16
  454. package/build/dist/Server/Services/AIBillingService.js.map +1 -1
  455. package/build/dist/Server/Services/DatabaseService.js +31 -5
  456. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  457. package/build/dist/Server/Services/EnterpriseLicenseInstanceService.js +167 -0
  458. package/build/dist/Server/Services/EnterpriseLicenseInstanceService.js.map +1 -1
  459. package/build/dist/Server/Services/EnterpriseLicenseService.js +39 -0
  460. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  461. package/build/dist/Server/Services/Index.js +12 -0
  462. package/build/dist/Server/Services/Index.js.map +1 -1
  463. package/build/dist/Server/Services/MonitorService.js +119 -21
  464. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  465. package/build/dist/Server/Services/MonitorStatusTimelineService.js +127 -8
  466. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  467. package/build/dist/Server/Services/MonitorTemplateService.js +162 -0
  468. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  469. package/build/dist/Server/Services/NetworkAlertPolicyEngineService.js +1683 -0
  470. package/build/dist/Server/Services/NetworkAlertPolicyEngineService.js.map +1 -0
  471. package/build/dist/Server/Services/NetworkAlertPolicyService.js +732 -0
  472. package/build/dist/Server/Services/NetworkAlertPolicyService.js.map +1 -0
  473. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +10 -9
  474. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  475. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +54 -11
  476. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -1
  477. package/build/dist/Server/Services/NetworkDeviceRoleService.js +70 -0
  478. package/build/dist/Server/Services/NetworkDeviceRoleService.js.map +1 -0
  479. package/build/dist/Server/Services/NetworkDeviceService.js +841 -70
  480. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  481. package/build/dist/Server/Services/NetworkSiteService.js +1145 -107
  482. package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
  483. package/build/dist/Server/Services/NetworkSiteTypeService.js +863 -0
  484. package/build/dist/Server/Services/NetworkSiteTypeService.js.map +1 -1
  485. package/build/dist/Server/Services/NetworkSnmpCredentialProfileService.js +217 -0
  486. package/build/dist/Server/Services/NetworkSnmpCredentialProfileService.js.map +1 -0
  487. package/build/dist/Server/Services/NotificationService.js +80 -17
  488. package/build/dist/Server/Services/NotificationService.js.map +1 -1
  489. package/build/dist/Server/Services/ProjectService.js +131 -30
  490. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  491. package/build/dist/Server/Services/StatusPagePrivateUserService.js +92 -2
  492. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  493. package/build/dist/Server/Services/UserNotificationEmailRollupBatchService.js +76 -0
  494. package/build/dist/Server/Services/UserNotificationEmailRollupBatchService.js.map +1 -0
  495. package/build/dist/Server/Services/UserNotificationEmailRollupItemService.js +77 -0
  496. package/build/dist/Server/Services/UserNotificationEmailRollupItemService.js.map +1 -0
  497. package/build/dist/Server/Services/UserNotificationEmailRollupSettingService.js +111 -0
  498. package/build/dist/Server/Services/UserNotificationEmailRollupSettingService.js.map +1 -0
  499. package/build/dist/Server/Services/UserNotificationSettingService.js +46 -21
  500. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  501. package/build/dist/Server/Services/UserService.js +90 -0
  502. package/build/dist/Server/Services/UserService.js.map +1 -1
  503. package/build/dist/Server/Types/Database/QueryHelper.js +1 -1
  504. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  505. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +14 -3
  506. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -1
  507. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js +10 -0
  508. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js.map +1 -1
  509. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js +10 -0
  510. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js.map +1 -1
  511. package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js +54 -1
  512. package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js.map +1 -1
  513. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +18 -4
  514. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -1
  515. package/build/dist/Server/Utils/Billing/BillingFailureNoticeThrottle.js +59 -0
  516. package/build/dist/Server/Utils/Billing/BillingFailureNoticeThrottle.js.map +1 -0
  517. package/build/dist/Server/Utils/DataSource/HttpFetch.js +2 -1
  518. package/build/dist/Server/Utils/DataSource/HttpFetch.js.map +1 -1
  519. package/build/dist/Server/Utils/EmailRollup/EmailRollupConstants.js +91 -0
  520. package/build/dist/Server/Utils/EmailRollup/EmailRollupConstants.js.map +1 -0
  521. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +691 -0
  522. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -0
  523. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +497 -0
  524. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -0
  525. package/build/dist/Server/Utils/EmailRollup/EmailRollupWriter.js +257 -0
  526. package/build/dist/Server/Utils/EmailRollup/EmailRollupWriter.js.map +1 -0
  527. package/build/dist/Server/Utils/LLM/LLMService.js +64 -1
  528. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  529. package/build/dist/Server/Utils/LogRedaction.js +8 -0
  530. package/build/dist/Server/Utils/LogRedaction.js.map +1 -1
  531. package/build/dist/Server/Utils/Logger.js +63 -1
  532. package/build/dist/Server/Utils/Logger.js.map +1 -1
  533. package/build/dist/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.js +141 -0
  534. package/build/dist/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.js.map +1 -0
  535. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +8 -1
  536. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  537. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +51 -1
  538. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
  539. package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js +7 -0
  540. package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js.map +1 -1
  541. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +37 -0
  542. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  543. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +158 -6
  544. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  545. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +33 -0
  546. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
  547. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +32 -0
  548. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  549. package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js +330 -21
  550. package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js.map +1 -1
  551. package/build/dist/Server/Utils/Monitor/NetworkDeviceMetricUtil.js +55 -10
  552. package/build/dist/Server/Utils/Monitor/NetworkDeviceMetricUtil.js.map +1 -1
  553. package/build/dist/Server/Utils/Monitor/NetworkDeviceWalkUtil.js +178 -49
  554. package/build/dist/Server/Utils/Monitor/NetworkDeviceWalkUtil.js.map +1 -1
  555. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +119 -29
  556. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  557. package/build/dist/Server/Utils/NetworkDevice/DeviceHealthAggregation.js +30 -17
  558. package/build/dist/Server/Utils/NetworkDevice/DeviceHealthAggregation.js.map +1 -1
  559. package/build/dist/Server/Utils/NetworkSite/NetworkSiteHierarchyLock.js +158 -0
  560. package/build/dist/Server/Utils/NetworkSite/NetworkSiteHierarchyLock.js.map +1 -0
  561. package/build/dist/Server/Utils/OutboundUserAgent.js +123 -0
  562. package/build/dist/Server/Utils/OutboundUserAgent.js.map +1 -0
  563. package/build/dist/Server/Utils/StartServer.js +38 -14
  564. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  565. package/build/dist/Server/Utils/Telemetry/CaptureSpan.js +28 -8
  566. package/build/dist/Server/Utils/Telemetry/CaptureSpan.js.map +1 -1
  567. package/build/dist/Server/Utils/Telemetry/ErrorClassResolver.js +95 -0
  568. package/build/dist/Server/Utils/Telemetry/ErrorClassResolver.js.map +1 -0
  569. package/build/dist/Server/Utils/Telemetry/SpanUtil.js +29 -0
  570. package/build/dist/Server/Utils/Telemetry/SpanUtil.js.map +1 -1
  571. package/build/dist/Server/Utils/Telemetry.js +162 -46
  572. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  573. package/build/dist/Types/AI/ExceptionAIClassification.js +10 -24
  574. package/build/dist/Types/AI/ExceptionAIClassification.js.map +1 -1
  575. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  576. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  577. package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseUsageSnapshot.js +2 -0
  578. package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseUsageSnapshot.js.map +1 -0
  579. package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseUserCountSource.js +7 -0
  580. package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseUserCountSource.js.map +1 -0
  581. package/build/dist/Types/Exception/ApiException.js +9 -0
  582. package/build/dist/Types/Exception/ApiException.js.map +1 -1
  583. package/build/dist/Types/Exception/BadDataException.js +10 -0
  584. package/build/dist/Types/Exception/BadDataException.js.map +1 -1
  585. package/build/dist/Types/Exception/BadRequestException.js +10 -0
  586. package/build/dist/Types/Exception/BadRequestException.js.map +1 -1
  587. package/build/dist/Types/Exception/Exception.js +56 -0
  588. package/build/dist/Types/Exception/Exception.js.map +1 -1
  589. package/build/dist/Types/Exception/ForbiddenException.js +9 -0
  590. package/build/dist/Types/Exception/ForbiddenException.js.map +1 -1
  591. package/build/dist/Types/Exception/NotAuthenticatedException.js +9 -0
  592. package/build/dist/Types/Exception/NotAuthenticatedException.js.map +1 -1
  593. package/build/dist/Types/Exception/NotAuthorizedException.js +9 -0
  594. package/build/dist/Types/Exception/NotAuthorizedException.js.map +1 -1
  595. package/build/dist/Types/Exception/NotFoundException.js +10 -0
  596. package/build/dist/Types/Exception/NotFoundException.js.map +1 -1
  597. package/build/dist/Types/Exception/PayloadTooLargeException.js +10 -0
  598. package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -1
  599. package/build/dist/Types/Exception/PaymentRequiredException.js +9 -0
  600. package/build/dist/Types/Exception/PaymentRequiredException.js.map +1 -1
  601. package/build/dist/Types/Exception/ServiceUnavailableException.js +9 -0
  602. package/build/dist/Types/Exception/ServiceUnavailableException.js.map +1 -1
  603. package/build/dist/Types/Exception/SsoAuthorizationException.js +9 -0
  604. package/build/dist/Types/Exception/SsoAuthorizationException.js.map +1 -1
  605. package/build/dist/Types/Exception/TenantNotFoundException.js +10 -0
  606. package/build/dist/Types/Exception/TenantNotFoundException.js.map +1 -1
  607. package/build/dist/Types/Exception/TimeoutException.js +9 -0
  608. package/build/dist/Types/Exception/TimeoutException.js.map +1 -1
  609. package/build/dist/Types/Exception/TooManyRequestsException.js +9 -0
  610. package/build/dist/Types/Exception/TooManyRequestsException.js.map +1 -1
  611. package/build/dist/Types/Exception/UnableToReachServer.js +9 -0
  612. package/build/dist/Types/Exception/UnableToReachServer.js.map +1 -1
  613. package/build/dist/Types/Exception/WebsiteRequestException.js +9 -0
  614. package/build/dist/Types/Exception/WebsiteRequestException.js.map +1 -1
  615. package/build/dist/Types/Monitor/CriteriaFilter.js +45 -2
  616. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  617. package/build/dist/Types/Monitor/DatabaseMetricCatalog.js +594 -0
  618. package/build/dist/Types/Monitor/DatabaseMetricCatalog.js.map +1 -0
  619. package/build/dist/Types/Monitor/DatabaseMonitor/DatabaseMonitorResponse.js +21 -0
  620. package/build/dist/Types/Monitor/DatabaseMonitor/DatabaseMonitorResponse.js.map +1 -0
  621. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +70 -0
  622. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  623. package/build/dist/Types/Monitor/MonitorMetricType.js +61 -0
  624. package/build/dist/Types/Monitor/MonitorMetricType.js.map +1 -1
  625. package/build/dist/Types/Monitor/MonitorStep.js +54 -0
  626. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  627. package/build/dist/Types/Monitor/MonitorStepDatabaseMonitor.js +122 -0
  628. package/build/dist/Types/Monitor/MonitorStepDatabaseMonitor.js.map +1 -0
  629. package/build/dist/Types/Monitor/MonitorStepSqlMonitor.js.map +1 -1
  630. package/build/dist/Types/Monitor/MonitorType.js +45 -1
  631. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  632. package/build/dist/Types/Monitor/SnmpMonitor/NetworkDeviceAlertPack.js +21 -1
  633. package/build/dist/Types/Monitor/SnmpMonitor/NetworkDeviceAlertPack.js.map +1 -1
  634. package/build/dist/Types/Monitor/SqlConnectionConfig.js +2 -0
  635. package/build/dist/Types/Monitor/SqlConnectionConfig.js.map +1 -0
  636. package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js +105 -0
  637. package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js.map +1 -0
  638. package/build/dist/Types/NetworkDevice/NetworkAlertPolicyScope.js +172 -0
  639. package/build/dist/Types/NetworkDevice/NetworkAlertPolicyScope.js.map +1 -0
  640. package/build/dist/Types/NetworkDevice/NetworkDeviceMonitoringMethod.js +40 -22
  641. package/build/dist/Types/NetworkDevice/NetworkDeviceMonitoringMethod.js.map +1 -1
  642. package/build/dist/Types/NetworkSite/DefaultNetworkSiteTypeHierarchy.js +26 -0
  643. package/build/dist/Types/NetworkSite/DefaultNetworkSiteTypeHierarchy.js.map +1 -0
  644. package/build/dist/Types/NotificationSetting/NotificationEmailRollupCategory.js +186 -0
  645. package/build/dist/Types/NotificationSetting/NotificationEmailRollupCategory.js.map +1 -0
  646. package/build/dist/Types/NotificationSetting/NotificationEmailRollupPolicy.js +87 -0
  647. package/build/dist/Types/NotificationSetting/NotificationEmailRollupPolicy.js.map +1 -0
  648. package/build/dist/Types/Permission.js +120 -0
  649. package/build/dist/Types/Permission.js.map +1 -1
  650. package/build/dist/Types/Telemetry/ErrorClass.js +216 -0
  651. package/build/dist/Types/Telemetry/ErrorClass.js.map +1 -0
  652. package/build/dist/Types/Telemetry/UnitOfWork.js +59 -0
  653. package/build/dist/Types/Telemetry/UnitOfWork.js.map +1 -0
  654. package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
  655. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  656. package/build/dist/UI/Components/Charts/Bar/BarChart.js +1 -1
  657. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  658. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +6 -1
  659. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  660. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +79 -31
  661. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  662. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +7 -1
  663. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  664. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +79 -31
  665. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  666. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +14 -0
  667. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -0
  668. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  669. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  670. package/build/dist/UI/Components/CommandPalette/CommandPalette.js +2 -7
  671. package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -1
  672. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +3 -0
  673. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  674. package/build/dist/UI/Components/Modal/Modal.js +2 -7
  675. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  676. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +39 -13
  677. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  678. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +39 -0
  679. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -1
  680. package/build/dist/UI/Components/Navbar/NavBar.js +44 -9
  681. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  682. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
  683. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  684. package/build/dist/UI/Components/ProgressBar/ProgressBar.js +12 -1
  685. package/build/dist/UI/Components/ProgressBar/ProgressBar.js.map +1 -1
  686. package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js +6 -6
  687. package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js.map +1 -1
  688. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +1 -1
  689. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
  690. package/build/dist/UI/Components/SideMenu/SideMenu.js +76 -11
  691. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  692. package/build/dist/UI/Components/SideMenu/SideMenuItem.js +50 -54
  693. package/build/dist/UI/Components/SideMenu/SideMenuItem.js.map +1 -1
  694. package/build/dist/UI/Components/SideMenu/SideMenuSection.js +32 -9
  695. package/build/dist/UI/Components/SideMenu/SideMenuSection.js.map +1 -1
  696. package/build/dist/UI/Components/SideOver/SideOver.js +5 -20
  697. package/build/dist/UI/Components/SideOver/SideOver.js.map +1 -1
  698. package/build/dist/UI/Utils/PageScrollLock.js +31 -3
  699. package/build/dist/UI/Utils/PageScrollLock.js.map +1 -1
  700. package/build/dist/UI/Utils/PermissionGate.js +42 -1
  701. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  702. package/build/dist/UI/Utils/SensitiveUrlToken.js +97 -0
  703. package/build/dist/UI/Utils/SensitiveUrlToken.js.map +1 -0
  704. package/build/dist/UI/Utils/TestLLMProvider.js +4 -5
  705. package/build/dist/UI/Utils/TestLLMProvider.js.map +1 -1
  706. package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js +42 -0
  707. package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js.map +1 -0
  708. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +74 -0
  709. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -0
  710. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +15 -0
  711. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +1 -1
  712. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +111 -9
  713. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -1
  714. package/build/dist/Utils/Monitor/MonitorMetricType.js +109 -1
  715. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  716. package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js +126 -0
  717. package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js.map +1 -0
  718. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +2 -1
  719. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
  720. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +75 -35
  721. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  722. package/build/dist/Utils/NetworkDevice/DeviceHealthStateUtil.js +27 -7
  723. package/build/dist/Utils/NetworkDevice/DeviceHealthStateUtil.js.map +1 -1
  724. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +7 -5
  725. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
  726. package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js +114 -0
  727. package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js.map +1 -0
  728. package/build/dist/Utils/NetworkDevice/NetworkAlertPolicyBootstrapUtil.js +180 -0
  729. package/build/dist/Utils/NetworkDevice/NetworkAlertPolicyBootstrapUtil.js.map +1 -0
  730. package/build/dist/Utils/NetworkDevice/SnmpCredentialUtil.js +12 -0
  731. package/build/dist/Utils/NetworkDevice/SnmpCredentialUtil.js.map +1 -0
  732. package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js +15 -5
  733. package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js.map +1 -1
  734. package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js +44 -15
  735. package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js.map +1 -1
  736. package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js +52 -23
  737. package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js.map +1 -1
  738. package/build/dist/Utils/NetworkDiscovery/PingMonitorBuilder.js +85 -13
  739. package/build/dist/Utils/NetworkDiscovery/PingMonitorBuilder.js.map +1 -1
  740. package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js +30 -15
  741. package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js.map +1 -1
  742. package/build/dist/Utils/NetworkSite/TypeHierarchyUtil.js +191 -0
  743. package/build/dist/Utils/NetworkSite/TypeHierarchyUtil.js.map +1 -0
  744. package/build/dist/Utils/Rules/RulePatternMatchUtil.js +1 -1
  745. package/build/dist/Utils/Slug.js +31 -1
  746. package/build/dist/Utils/Slug.js.map +1 -1
  747. package/package.json +2 -2
@@ -0,0 +1,1849 @@
1
+ /*
2
+ * Contract under test — the Network Alert Policy engine, the ONE place in the
3
+ * product that creates or deletes a policy-owned Monitor.
4
+ *
5
+ * Monitors are billed and they own incident history, so nearly every case
6
+ * here is a case about NOT doing something. The engine is reached from a
7
+ * dozen directions — a device created, re-sited, re-labelled, archived,
8
+ * switched to monitor-backed, handed a probe; a policy saved, enabled,
9
+ * re-scoped, re-pointed, deleted; a five-minute sweep — and each of them
10
+ * ends in `reconcileDevice`, which computes DESIRED against ACTUAL for one
11
+ * device under that device's own Redis lock. What this file pins is the
12
+ * behaviour that would otherwise fail silently and expensively:
13
+ *
14
+ * - TWO POLICIES ON ONE DEVICE each get their own monitor, and neither
15
+ * pass touches the other's row. Ownership is the PAIR (policy,
16
+ * template), not the device.
17
+ * - A SCOPE THAT SHRINKS deletes only monitors stamped with THAT policy.
18
+ * A hand-made Network Device monitor on the same device, and an
19
+ * auto-import rule's, carry no networkAlertPolicyId and are therefore
20
+ * not even in ACTUAL — the query, not a filter afterwards, is what makes
21
+ * that true.
22
+ * - AN UNOWNED (device, template) MONITOR IS ADOPTED, never duplicated.
23
+ * Monitor's partial unique index on (autoProvisionedNetworkDeviceId,
24
+ * monitorTemplateId) would refuse the duplicate on this pass and on
25
+ * every pass after it, so the policy would report a failure forever
26
+ * beside a perfectly good monitor.
27
+ * - A DEVICE THAT STOPS BEING PROVISIONABLE — archived, monitor-backed, or
28
+ * with no probe — loses every policy monitor. A monitor-backed device's
29
+ * health is a bound monitor's; a Network Device monitor on it is billed
30
+ * for a poll that will never happen.
31
+ * - A DISABLED POLICY PAUSES its monitors instead of deleting them, and
32
+ * enabling reverses it. Deleting would lose the incident history and
33
+ * re-bill the fleet on the way back.
34
+ * - THE PLAN IS ASKED ONCE PER RUN and the run stops at the first refusal,
35
+ * so a project that cannot hold another monitor gets one sentence in
36
+ * lastSyncError rather than five hundred identical failures.
37
+ * - THE LOCK is per device, taken before anything is read, released even
38
+ * when the body throws, and a lock somebody else holds means this pass
39
+ * does nothing at all.
40
+ *
41
+ * The collaborating singleton services — and the Semaphore module, which
42
+ * would otherwise reach Redis — are stubbed at the MODULE level before the
43
+ * engine is imported: their real files reach Postgres through
44
+ * DatabaseService, and nothing here should touch any of it.
45
+ *
46
+ * MonitorService.findBy and NetworkDeviceService.findBy are backed by small
47
+ * in-memory tables rather than by call-order mocks, because the engine asks
48
+ * them several different questions per pass and a `mockResolvedValueOnce`
49
+ * chain would pass for the wrong reason the moment the query order changed.
50
+ */
51
+
52
+ jest.mock("../../../Server/EnvironmentConfig", () => {
53
+ return {
54
+ ...(jest.requireActual("../../../Server/EnvironmentConfig") as Record<
55
+ string,
56
+ unknown
57
+ >),
58
+ IsBillingEnabled: true,
59
+ AllowedActiveMonitorCountInFreePlan: 10,
60
+ };
61
+ });
62
+
63
+ jest.mock("../../../Server/Services/MonitorService", () => {
64
+ return {
65
+ __esModule: true,
66
+ default: {
67
+ create: jest.fn(),
68
+ findBy: jest.fn(),
69
+ findOneBy: jest.fn(),
70
+ countBy: jest.fn(),
71
+ deleteBy: jest.fn(),
72
+ updateBy: jest.fn(),
73
+ updateColumnsByIdWithoutHooks: jest.fn(),
74
+ },
75
+ };
76
+ });
77
+
78
+ jest.mock("../../../Server/Services/MonitorTemplateService", () => {
79
+ return {
80
+ __esModule: true,
81
+ default: {
82
+ findOneBy: jest.fn(),
83
+ },
84
+ };
85
+ });
86
+
87
+ jest.mock("../../../Server/Services/NetworkAlertPolicyService", () => {
88
+ return {
89
+ __esModule: true,
90
+ default: {
91
+ findBy: jest.fn(),
92
+ findOneBy: jest.fn(),
93
+ updateColumnsByIdWithoutHooks: jest.fn(),
94
+ },
95
+ };
96
+ });
97
+
98
+ jest.mock("../../../Server/Services/NetworkDeviceService", () => {
99
+ return {
100
+ __esModule: true,
101
+ default: {
102
+ findBy: jest.fn(),
103
+ findOneBy: jest.fn(),
104
+ },
105
+ };
106
+ });
107
+
108
+ jest.mock("../../../Server/Services/ProjectService", () => {
109
+ return {
110
+ __esModule: true,
111
+ default: {
112
+ getCurrentPlan: jest.fn(),
113
+ },
114
+ };
115
+ });
116
+
117
+ jest.mock("../../../Server/Infrastructure/Semaphore", () => {
118
+ return {
119
+ __esModule: true,
120
+ default: {
121
+ lock: jest.fn(),
122
+ release: jest.fn(),
123
+ },
124
+ };
125
+ });
126
+
127
+ jest.mock("../../../Server/Utils/Logger", () => {
128
+ return {
129
+ __esModule: true,
130
+ default: {
131
+ debug: jest.fn(),
132
+ info: jest.fn(),
133
+ warn: jest.fn(),
134
+ error: jest.fn(),
135
+ trace: jest.fn(),
136
+ },
137
+ };
138
+ });
139
+
140
+ import NetworkAlertPolicyEngineService, {
141
+ MAX_MONITORS_PER_POLICY_SYNC,
142
+ POLICY_DEVICE_LOCK_NAMESPACE,
143
+ PolicyRunContext,
144
+ policyDeviceLockKey,
145
+ } from "../../../Server/Services/NetworkAlertPolicyEngineService";
146
+ import MonitorService from "../../../Server/Services/MonitorService";
147
+ import MonitorTemplateService from "../../../Server/Services/MonitorTemplateService";
148
+ import NetworkAlertPolicyService from "../../../Server/Services/NetworkAlertPolicyService";
149
+ import NetworkDeviceService from "../../../Server/Services/NetworkDeviceService";
150
+ import ProjectService from "../../../Server/Services/ProjectService";
151
+ import Semaphore from "../../../Server/Infrastructure/Semaphore";
152
+ import Label from "../../../Models/DatabaseModels/Label";
153
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
154
+ import MonitorTemplate from "../../../Models/DatabaseModels/MonitorTemplate";
155
+ import NetworkAlertPolicy from "../../../Models/DatabaseModels/NetworkAlertPolicy";
156
+ import NetworkDevice from "../../../Models/DatabaseModels/NetworkDevice";
157
+ import FindBy from "../../../Server/Types/Database/FindBy";
158
+ import FindOneBy from "../../../Server/Types/Database/FindOneBy";
159
+ import { PlanType } from "../../../Types/Billing/SubscriptionPlan";
160
+ import BadDataException from "../../../Types/Exception/BadDataException";
161
+ import MonitorStep from "../../../Types/Monitor/MonitorStep";
162
+ import MonitorSteps from "../../../Types/Monitor/MonitorSteps";
163
+ import MonitorType from "../../../Types/Monitor/MonitorType";
164
+ import NetworkDeviceMonitoringMethod from "../../../Types/NetworkDevice/NetworkDeviceMonitoringMethod";
165
+ import NetworkAlertPolicyScope from "../../../Types/NetworkDevice/NetworkAlertPolicyScope";
166
+ import ObjectID from "../../../Types/ObjectID";
167
+ import PositiveNumber from "../../../Types/PositiveNumber";
168
+ import { beforeEach, describe, expect, it, jest } from "@jest/globals";
169
+
170
+ const PROJECT_ID: ObjectID = new ObjectID(
171
+ "11111111-1111-4111-8111-111111111111",
172
+ );
173
+ const DEVICE_ID: ObjectID = new ObjectID(
174
+ "22222222-2222-4222-8222-222222222222",
175
+ );
176
+ const OTHER_DEVICE_ID: ObjectID = new ObjectID(
177
+ "2b2b2b2b-2b2b-4b2b-8b2b-2b2b2b2b2b2b",
178
+ );
179
+ const POLICY_ID: ObjectID = new ObjectID(
180
+ "33333333-3333-4333-8333-333333333333",
181
+ );
182
+ const OTHER_POLICY_ID: ObjectID = new ObjectID(
183
+ "3b3b3b3b-3b3b-4b3b-8b3b-3b3b3b3b3b3b",
184
+ );
185
+ const TEMPLATE_ID: ObjectID = new ObjectID(
186
+ "44444444-4444-4444-8444-444444444444",
187
+ );
188
+ const OTHER_TEMPLATE_ID: ObjectID = new ObjectID(
189
+ "4b4b4b4b-4b4b-4b4b-8b4b-4b4b4b4b4b4b",
190
+ );
191
+ const PROBE_ID: ObjectID = new ObjectID("55555555-5555-4555-8555-555555555555");
192
+ const SITE_ID: ObjectID = new ObjectID("66666666-6666-4666-8666-666666666666");
193
+ const OTHER_SITE_ID: ObjectID = new ObjectID(
194
+ "6b6b6b6b-6b6b-4b6b-8b6b-6b6b6b6b6b6b",
195
+ );
196
+ const LABEL_ID: ObjectID = new ObjectID("77777777-7777-4777-8777-777777777777");
197
+
198
+ // What the mocked Semaphore.lock hands back and release must get back.
199
+ const FAKE_MUTEX: { id: string } = { id: "fake-device-mutex" };
200
+
201
+ const monitorCreateMock: jest.Mock =
202
+ MonitorService.create as unknown as jest.Mock;
203
+ const monitorFindByMock: jest.Mock =
204
+ MonitorService.findBy as unknown as jest.Mock;
205
+ const monitorFindOneByMock: jest.Mock =
206
+ MonitorService.findOneBy as unknown as jest.Mock;
207
+ const monitorCountByMock: jest.Mock =
208
+ MonitorService.countBy as unknown as jest.Mock;
209
+ const monitorDeleteByMock: jest.Mock =
210
+ MonitorService.deleteBy as unknown as jest.Mock;
211
+ const monitorUpdateByMock: jest.Mock =
212
+ MonitorService.updateBy as unknown as jest.Mock;
213
+ const monitorStampMock: jest.Mock =
214
+ MonitorService.updateColumnsByIdWithoutHooks as unknown as jest.Mock;
215
+ const templateFindOneByMock: jest.Mock =
216
+ MonitorTemplateService.findOneBy as unknown as jest.Mock;
217
+ const policyFindByMock: jest.Mock =
218
+ NetworkAlertPolicyService.findBy as unknown as jest.Mock;
219
+ const policyFindOneByMock: jest.Mock =
220
+ NetworkAlertPolicyService.findOneBy as unknown as jest.Mock;
221
+ const policyStampMock: jest.Mock =
222
+ NetworkAlertPolicyService.updateColumnsByIdWithoutHooks as unknown as jest.Mock;
223
+ const deviceFindByMock: jest.Mock =
224
+ NetworkDeviceService.findBy as unknown as jest.Mock;
225
+ const deviceFindOneByMock: jest.Mock =
226
+ NetworkDeviceService.findOneBy as unknown as jest.Mock;
227
+ const currentPlanMock: jest.Mock =
228
+ ProjectService.getCurrentPlan as unknown as jest.Mock;
229
+ const semaphoreLockMock: jest.Mock = Semaphore.lock as unknown as jest.Mock;
230
+ const semaphoreReleaseMock: jest.Mock =
231
+ Semaphore.release as unknown as jest.Mock;
232
+
233
+ /*
234
+ * A query value the engine could have written. Understanding the raw
235
+ * operators (rather than only exact values) is what lets the fake tables
236
+ * answer the SAME questions the database would — including the two that
237
+ * carry the whole safety story here, `networkAlertPolicyId IS NOT NULL` and
238
+ * `IS NULL`.
239
+ */
240
+ function matchesQueryValue(actual: unknown, expected: unknown): boolean {
241
+ const findOperator: {
242
+ getSql?: (alias: string) => string;
243
+ objectLiteralParameters?: Record<string, unknown>;
244
+ } = expected as {
245
+ getSql?: (alias: string) => string;
246
+ objectLiteralParameters?: Record<string, unknown>;
247
+ };
248
+
249
+ if (findOperator && typeof findOperator.getSql === "function") {
250
+ const sql: string = findOperator.getSql("column");
251
+ const parameters: Array<unknown> = Object.values(
252
+ findOperator.objectLiteralParameters || {},
253
+ ).flat();
254
+
255
+ if (sql.includes("IS NOT NULL")) {
256
+ return actual !== null && actual !== undefined;
257
+ }
258
+
259
+ if (sql.includes("IS NULL")) {
260
+ return actual === null || actual === undefined;
261
+ }
262
+
263
+ if (sql.includes("IN (")) {
264
+ return parameters.some((parameter: unknown): boolean => {
265
+ return parameter?.toString() === actual?.toString();
266
+ });
267
+ }
268
+
269
+ if (sql.includes(">")) {
270
+ return (
271
+ actual !== null &&
272
+ actual !== undefined &&
273
+ String(actual) > String(parameters[0])
274
+ );
275
+ }
276
+
277
+ throw new Error(`Unhandled query operator in test matcher: ${sql}`);
278
+ }
279
+
280
+ if (expected === null || expected === undefined) {
281
+ return actual === null || actual === undefined;
282
+ }
283
+
284
+ if (typeof expected === "boolean") {
285
+ return Boolean(actual) === expected;
286
+ }
287
+
288
+ return actual?.toString() === expected.toString();
289
+ }
290
+
291
+ function matchesQuery(
292
+ row: Record<string, unknown>,
293
+ query: Record<string, unknown>,
294
+ ): boolean {
295
+ for (const [column, expected] of Object.entries(query)) {
296
+ if (column === "labels") {
297
+ /*
298
+ * The many-to-many spelling: `labels: [id, id]` is "carries any of
299
+ * these labels", which is exactly what the scope means by a label list.
300
+ */
301
+ const wanted: Array<unknown> = expected as Array<unknown>;
302
+ const carried: Array<Label> = (row["labels"] as Array<Label>) || [];
303
+
304
+ const hasAny: boolean = carried.some((label: Label): boolean => {
305
+ return wanted.some((id: unknown): boolean => {
306
+ return id?.toString() === label.id?.toString();
307
+ });
308
+ });
309
+
310
+ if (!hasAny) {
311
+ return false;
312
+ }
313
+
314
+ continue;
315
+ }
316
+
317
+ const actual: unknown = column === "_id" ? row["_id"] : row[column];
318
+
319
+ if (!matchesQueryValue(actual, expected)) {
320
+ return false;
321
+ }
322
+ }
323
+
324
+ return true;
325
+ }
326
+
327
+ // The fake tables. Rebuilt per test.
328
+ let monitorTable: Array<Monitor> = [];
329
+ let deviceTable: Array<NetworkDevice> = [];
330
+ let policyTable: Array<NetworkAlertPolicy> = [];
331
+
332
+ function sortAndPage<TRow extends { _id?: string }>(
333
+ rows: Array<TRow>,
334
+ findBy: FindBy<never>,
335
+ ): Array<TRow> {
336
+ const sorted: Array<TRow> = [...rows].sort(
337
+ (left: TRow, right: TRow): number => {
338
+ return (left._id || "").localeCompare(right._id || "");
339
+ },
340
+ );
341
+
342
+ const limit: number =
343
+ typeof findBy.limit === "number" ? findBy.limit : sorted.length;
344
+
345
+ return sorted.slice(0, limit);
346
+ }
347
+
348
+ function installFakeTables(): void {
349
+ monitorFindByMock.mockImplementation(
350
+ async (findBy: FindBy<Monitor>): Promise<Array<Monitor>> => {
351
+ return sortAndPage(
352
+ monitorTable.filter((monitor: Monitor): boolean => {
353
+ return matchesQuery(
354
+ monitor as unknown as Record<string, unknown>,
355
+ findBy.query as Record<string, unknown>,
356
+ );
357
+ }),
358
+ findBy as unknown as FindBy<never>,
359
+ );
360
+ },
361
+ );
362
+
363
+ monitorFindOneByMock.mockImplementation(
364
+ async (findOneBy: FindOneBy<Monitor>): Promise<Monitor | null> => {
365
+ return (
366
+ monitorTable.find((monitor: Monitor): boolean => {
367
+ return matchesQuery(
368
+ monitor as unknown as Record<string, unknown>,
369
+ findOneBy.query as Record<string, unknown>,
370
+ );
371
+ }) || null
372
+ );
373
+ },
374
+ );
375
+
376
+ deviceFindByMock.mockImplementation(
377
+ async (findBy: FindBy<NetworkDevice>): Promise<Array<NetworkDevice>> => {
378
+ return sortAndPage(
379
+ deviceTable.filter((device: NetworkDevice): boolean => {
380
+ return matchesQuery(
381
+ device as unknown as Record<string, unknown>,
382
+ findBy.query as Record<string, unknown>,
383
+ );
384
+ }),
385
+ findBy as unknown as FindBy<never>,
386
+ );
387
+ },
388
+ );
389
+
390
+ deviceFindOneByMock.mockImplementation(
391
+ async (
392
+ findOneBy: FindOneBy<NetworkDevice>,
393
+ ): Promise<NetworkDevice | null> => {
394
+ return (
395
+ deviceTable.find((device: NetworkDevice): boolean => {
396
+ return matchesQuery(
397
+ device as unknown as Record<string, unknown>,
398
+ findOneBy.query as Record<string, unknown>,
399
+ );
400
+ }) || null
401
+ );
402
+ },
403
+ );
404
+
405
+ policyFindByMock.mockImplementation(
406
+ async (
407
+ findBy: FindBy<NetworkAlertPolicy>,
408
+ ): Promise<Array<NetworkAlertPolicy>> => {
409
+ return policyTable.filter((policy: NetworkAlertPolicy): boolean => {
410
+ return matchesQuery(
411
+ policy as unknown as Record<string, unknown>,
412
+ findBy.query as Record<string, unknown>,
413
+ );
414
+ });
415
+ },
416
+ );
417
+
418
+ policyFindOneByMock.mockImplementation(
419
+ async (
420
+ findOneBy: FindOneBy<NetworkAlertPolicy>,
421
+ ): Promise<NetworkAlertPolicy | null> => {
422
+ return (
423
+ policyTable.find((policy: NetworkAlertPolicy): boolean => {
424
+ return matchesQuery(
425
+ policy as unknown as Record<string, unknown>,
426
+ findOneBy.query as Record<string, unknown>,
427
+ );
428
+ }) || null
429
+ );
430
+ },
431
+ );
432
+
433
+ /*
434
+ * Writes land back in the fake tables so a second pass in the same test
435
+ * sees what the first one did — which is the only way to assert that
436
+ * reconciliation is idempotent.
437
+ */
438
+ monitorCreateMock.mockImplementation(
439
+ async (createBy: { data: Monitor }): Promise<Monitor> => {
440
+ const monitor: Monitor = createBy.data;
441
+ monitor.id = ObjectID.generate();
442
+ monitorTable.push(monitor);
443
+
444
+ return monitor;
445
+ },
446
+ );
447
+
448
+ monitorDeleteByMock.mockImplementation(
449
+ async (deleteBy: { query: Record<string, unknown> }): Promise<number> => {
450
+ const remaining: Array<Monitor> = monitorTable.filter(
451
+ (monitor: Monitor): boolean => {
452
+ return !matchesQuery(
453
+ monitor as unknown as Record<string, unknown>,
454
+ deleteBy.query,
455
+ );
456
+ },
457
+ );
458
+
459
+ const deleted: number = monitorTable.length - remaining.length;
460
+ monitorTable = remaining;
461
+
462
+ return deleted;
463
+ },
464
+ );
465
+
466
+ monitorUpdateByMock.mockImplementation(
467
+ async (updateBy: {
468
+ query: Record<string, unknown>;
469
+ data: Record<string, unknown>;
470
+ }): Promise<number> => {
471
+ let updated: number = 0;
472
+
473
+ for (const monitor of monitorTable) {
474
+ if (
475
+ matchesQuery(
476
+ monitor as unknown as Record<string, unknown>,
477
+ updateBy.query,
478
+ )
479
+ ) {
480
+ Object.assign(monitor, updateBy.data);
481
+ updated++;
482
+ }
483
+ }
484
+
485
+ return updated;
486
+ },
487
+ );
488
+
489
+ monitorStampMock.mockImplementation(
490
+ async (input: {
491
+ id: ObjectID;
492
+ data: Record<string, unknown>;
493
+ }): Promise<void> => {
494
+ const monitor: Monitor | undefined = monitorTable.find(
495
+ (candidate: Monitor): boolean => {
496
+ return candidate.id?.toString() === input.id.toString();
497
+ },
498
+ );
499
+
500
+ if (monitor) {
501
+ Object.assign(monitor, input.data);
502
+ }
503
+ },
504
+ );
505
+
506
+ policyStampMock.mockResolvedValue(undefined);
507
+ templateFindOneByMock.mockImplementation(
508
+ async (
509
+ findOneBy: FindOneBy<MonitorTemplate>,
510
+ ): Promise<MonitorTemplate | null> => {
511
+ const queriedId: unknown = (findOneBy.query as Record<string, unknown>)[
512
+ "_id"
513
+ ];
514
+
515
+ return queriedId?.toString() === TEMPLATE_ID.toString()
516
+ ? monitorTemplate(TEMPLATE_ID, "Reachability")
517
+ : queriedId?.toString() === OTHER_TEMPLATE_ID.toString()
518
+ ? monitorTemplate(OTHER_TEMPLATE_ID, "Interfaces")
519
+ : null;
520
+ },
521
+ );
522
+
523
+ semaphoreLockMock.mockResolvedValue(FAKE_MUTEX);
524
+ semaphoreReleaseMock.mockResolvedValue(undefined);
525
+ currentPlanMock.mockResolvedValue({
526
+ plan: PlanType.Growth,
527
+ isSubscriptionUnpaid: false,
528
+ });
529
+ monitorCountByMock.mockResolvedValue(new PositiveNumber(0));
530
+ }
531
+
532
+ function monitorTemplate(id: ObjectID, name: string): MonitorTemplate {
533
+ const template: MonitorTemplate = new MonitorTemplate();
534
+ template.id = id;
535
+ template.projectId = PROJECT_ID;
536
+ template.monitorType = MonitorType.NetworkDevice;
537
+ template.monitorName = name;
538
+
539
+ const step: MonitorStep = new MonitorStep();
540
+ step.data!.networkDeviceMonitor = {
541
+ // A design-time placeholder; buildMonitor rebinds it to the real device.
542
+ networkDeviceId: ObjectID.generate().toString(),
543
+ monitorInterfaces: false,
544
+ oids: [],
545
+ };
546
+ template.monitorSteps = new MonitorSteps();
547
+ template.monitorSteps.data = { monitorStepsInstanceArray: [step] };
548
+
549
+ return template;
550
+ }
551
+
552
+ function device(
553
+ overrides: Partial<NetworkDevice> = {},
554
+ id: ObjectID = DEVICE_ID,
555
+ ): NetworkDevice {
556
+ const networkDevice: NetworkDevice = new NetworkDevice();
557
+ networkDevice.id = id;
558
+ networkDevice.projectId = PROJECT_ID;
559
+ networkDevice.name = "Core switch";
560
+ networkDevice.isArchived = false;
561
+ networkDevice.monitoringMethod = NetworkDeviceMonitoringMethod.Probe;
562
+ networkDevice.probeId = PROBE_ID;
563
+ networkDevice.siteId = SITE_ID;
564
+ Object.assign(networkDevice, overrides);
565
+
566
+ return networkDevice;
567
+ }
568
+
569
+ function policy(
570
+ overrides: Partial<NetworkAlertPolicy> = {},
571
+ id: ObjectID = POLICY_ID,
572
+ ): NetworkAlertPolicy {
573
+ const alertPolicy: NetworkAlertPolicy = new NetworkAlertPolicy();
574
+ alertPolicy.id = id;
575
+ alertPolicy.projectId = PROJECT_ID;
576
+ alertPolicy.name = "Warehouse switches";
577
+ alertPolicy.isEnabled = true;
578
+ alertPolicy.monitorTemplateId = TEMPLATE_ID;
579
+ alertPolicy.scope = {} as NetworkAlertPolicyScope;
580
+ Object.assign(alertPolicy, overrides);
581
+
582
+ return alertPolicy;
583
+ }
584
+
585
+ function monitorRow(overrides: Partial<Monitor> = {}): Monitor {
586
+ const monitor: Monitor = new Monitor();
587
+ monitor.id = ObjectID.generate();
588
+ monitor.projectId = PROJECT_ID;
589
+ monitor.monitorType = MonitorType.NetworkDevice;
590
+ monitor.disableActiveMonitoring = false;
591
+ Object.assign(monitor, overrides);
592
+
593
+ return monitor;
594
+ }
595
+
596
+ function labelled(labelId: ObjectID): Array<Label> {
597
+ const label: Label = new Label();
598
+ label.id = labelId;
599
+
600
+ return [label];
601
+ }
602
+
603
+ function monitorsOfDevice(deviceId: ObjectID = DEVICE_ID): Array<Monitor> {
604
+ return monitorTable.filter((monitor: Monitor): boolean => {
605
+ return (
606
+ monitor.autoProvisionedNetworkDeviceId?.toString() === deviceId.toString()
607
+ );
608
+ });
609
+ }
610
+
611
+ beforeEach(() => {
612
+ jest.clearAllMocks();
613
+ monitorTable = [];
614
+ deviceTable = [];
615
+ policyTable = [];
616
+ installFakeTables();
617
+ });
618
+
619
+ describe("reconcileDevice provisions what the policies ask for", () => {
620
+ it("creates one monitor for a device an enabled policy covers", async () => {
621
+ deviceTable = [device()];
622
+ policyTable = [policy()];
623
+
624
+ await NetworkAlertPolicyEngineService.reconcileDevice({
625
+ projectId: PROJECT_ID,
626
+ deviceId: DEVICE_ID,
627
+ });
628
+
629
+ expect(monitorsOfDevice()).toHaveLength(1);
630
+ expect(monitorsOfDevice()[0]!.networkAlertPolicyId?.toString()).toBe(
631
+ POLICY_ID.toString(),
632
+ );
633
+ expect(monitorsOfDevice()[0]!.monitorTemplateId?.toString()).toBe(
634
+ TEMPLATE_ID.toString(),
635
+ );
636
+ });
637
+
638
+ /*
639
+ * The stamp goes on the CREATE, not in a follow-up update. A monitor that
640
+ * exists for a moment without its owner is one a concurrent pass of a
641
+ * different policy could adopt.
642
+ */
643
+ it("stamps the owning policy in the same write that creates the monitor", async () => {
644
+ deviceTable = [device()];
645
+ policyTable = [policy()];
646
+
647
+ await NetworkAlertPolicyEngineService.reconcileDevice({
648
+ projectId: PROJECT_ID,
649
+ deviceId: DEVICE_ID,
650
+ });
651
+
652
+ const created: { data: Monitor } = monitorCreateMock.mock.calls[0]![0] as {
653
+ data: Monitor;
654
+ };
655
+
656
+ expect(created.data.networkAlertPolicyId?.toString()).toBe(
657
+ POLICY_ID.toString(),
658
+ );
659
+ });
660
+
661
+ it("is idempotent: a second pass over an already-covered device writes nothing", async () => {
662
+ deviceTable = [device()];
663
+ policyTable = [policy()];
664
+
665
+ await NetworkAlertPolicyEngineService.reconcileDevice({
666
+ projectId: PROJECT_ID,
667
+ deviceId: DEVICE_ID,
668
+ });
669
+ monitorCreateMock.mockClear();
670
+ monitorDeleteByMock.mockClear();
671
+
672
+ await NetworkAlertPolicyEngineService.reconcileDevice({
673
+ projectId: PROJECT_ID,
674
+ deviceId: DEVICE_ID,
675
+ });
676
+
677
+ expect(monitorCreateMock).not.toHaveBeenCalled();
678
+ expect(monitorDeleteByMock).not.toHaveBeenCalled();
679
+ expect(monitorsOfDevice()).toHaveLength(1);
680
+ });
681
+
682
+ it("provisions nothing for a device outside every policy's scope", async () => {
683
+ deviceTable = [device({ siteId: OTHER_SITE_ID })];
684
+ policyTable = [policy({ scope: { siteIds: [SITE_ID.toString()] } })];
685
+
686
+ await NetworkAlertPolicyEngineService.reconcileDevice({
687
+ projectId: PROJECT_ID,
688
+ deviceId: DEVICE_ID,
689
+ });
690
+
691
+ expect(monitorCreateMock).not.toHaveBeenCalled();
692
+ });
693
+
694
+ it("matches a device by label as well as by site", async () => {
695
+ deviceTable = [device({ labels: labelled(LABEL_ID) })];
696
+ policyTable = [policy({ scope: { labelIds: [LABEL_ID.toString()] } })];
697
+
698
+ await NetworkAlertPolicyEngineService.reconcileDevice({
699
+ projectId: PROJECT_ID,
700
+ deviceId: DEVICE_ID,
701
+ });
702
+
703
+ expect(monitorsOfDevice()).toHaveLength(1);
704
+ });
705
+ });
706
+
707
+ /*
708
+ * THE DANGEROUS CASE. Two policies covering one device is the ordinary
709
+ * situation ("everything gets reachability" plus "warehouse switches get
710
+ * interface alerts"), and it is the one where a wrong ownership key destroys
711
+ * a monitor: if the engine keyed on the DEVICE rather than on (policy,
712
+ * template), each pass would see a monitor that is "not mine" and delete it,
713
+ * and the two policies would take turns removing each other's alerting.
714
+ */
715
+ describe("two policies on one device", () => {
716
+ beforeEach(() => {
717
+ deviceTable = [device()];
718
+ policyTable = [
719
+ policy(),
720
+ policy(
721
+ {
722
+ name: "Interface alerts",
723
+ monitorTemplateId: OTHER_TEMPLATE_ID,
724
+ },
725
+ OTHER_POLICY_ID,
726
+ ),
727
+ ];
728
+ });
729
+
730
+ it("gives the device one monitor per policy", async () => {
731
+ await NetworkAlertPolicyEngineService.reconcileDevice({
732
+ projectId: PROJECT_ID,
733
+ deviceId: DEVICE_ID,
734
+ });
735
+
736
+ const owners: Array<string> = monitorsOfDevice().map(
737
+ (monitor: Monitor): string => {
738
+ return monitor.networkAlertPolicyId!.toString();
739
+ },
740
+ );
741
+
742
+ expect(owners.sort()).toEqual(
743
+ [POLICY_ID.toString(), OTHER_POLICY_ID.toString()].sort(),
744
+ );
745
+ });
746
+
747
+ it("leaves the other policy's monitor alone when one policy's scope stops matching", async () => {
748
+ await NetworkAlertPolicyEngineService.reconcileDevice({
749
+ projectId: PROJECT_ID,
750
+ deviceId: DEVICE_ID,
751
+ });
752
+
753
+ // The first policy narrows to a site this device is not in.
754
+ policyTable[0]!.scope = {
755
+ siteIds: [OTHER_SITE_ID.toString()],
756
+ } as NetworkAlertPolicyScope;
757
+
758
+ await NetworkAlertPolicyEngineService.reconcileDevice({
759
+ projectId: PROJECT_ID,
760
+ deviceId: DEVICE_ID,
761
+ });
762
+
763
+ const survivors: Array<Monitor> = monitorsOfDevice();
764
+
765
+ expect(survivors).toHaveLength(1);
766
+ expect(survivors[0]!.networkAlertPolicyId?.toString()).toBe(
767
+ OTHER_POLICY_ID.toString(),
768
+ );
769
+ });
770
+ });
771
+
772
+ /*
773
+ * THE OTHER DANGEROUS CASE. A shrinking scope is the ordinary way a policy
774
+ * stops covering a device, and it is the moment the engine deletes. What it
775
+ * must delete is exactly the rows stamped with the policy that shrank —
776
+ * never the monitor a person built by hand, and never the one an auto-import
777
+ * rule provisioned, both of which sit on the same device and carry the same
778
+ * `autoProvisionedNetworkDeviceId`.
779
+ */
780
+ describe("a scope that shrinks", () => {
781
+ it("deletes only the monitors the shrinking policy owns", async () => {
782
+ const handMade: Monitor = monitorRow({
783
+ autoProvisionedNetworkDeviceId: DEVICE_ID,
784
+ // No networkAlertPolicyId: nobody's policy made this.
785
+ monitorTemplateId: OTHER_TEMPLATE_ID,
786
+ });
787
+
788
+ monitorTable = [handMade];
789
+ deviceTable = [device()];
790
+ policyTable = [policy()];
791
+
792
+ await NetworkAlertPolicyEngineService.reconcileDevice({
793
+ projectId: PROJECT_ID,
794
+ deviceId: DEVICE_ID,
795
+ });
796
+
797
+ expect(monitorsOfDevice()).toHaveLength(2);
798
+
799
+ policyTable[0]!.scope = {
800
+ siteIds: [OTHER_SITE_ID.toString()],
801
+ } as NetworkAlertPolicyScope;
802
+
803
+ await NetworkAlertPolicyEngineService.reconcileDevice({
804
+ projectId: PROJECT_ID,
805
+ deviceId: DEVICE_ID,
806
+ });
807
+
808
+ const survivors: Array<Monitor> = monitorsOfDevice();
809
+
810
+ expect(survivors).toHaveLength(1);
811
+ expect(survivors[0]!.id?.toString()).toBe(handMade.id?.toString());
812
+ });
813
+
814
+ /*
815
+ * ...and the hand-made monitor is not merely filtered out afterwards: the
816
+ * QUERY that reads ACTUAL asks for `networkAlertPolicyId IS NOT NULL`, so
817
+ * a monitor with no owner is never even a candidate for deletion.
818
+ */
819
+ it("never reads an unowned monitor into the set it may delete", async () => {
820
+ deviceTable = [device()];
821
+ policyTable = [policy()];
822
+
823
+ await NetworkAlertPolicyEngineService.reconcileDevice({
824
+ projectId: PROJECT_ID,
825
+ deviceId: DEVICE_ID,
826
+ });
827
+
828
+ const actualQuery: Record<string, unknown> = (
829
+ monitorFindByMock.mock.calls[0]![0] as {
830
+ query: Record<string, unknown>;
831
+ }
832
+ ).query;
833
+
834
+ expect(
835
+ (
836
+ actualQuery["networkAlertPolicyId"] as {
837
+ getSql: (alias: string) => string;
838
+ }
839
+ ).getSql("column"),
840
+ ).toContain("IS NOT NULL");
841
+ expect(actualQuery["autoProvisionedNetworkDeviceId"]?.toString()).toBe(
842
+ DEVICE_ID.toString(),
843
+ );
844
+ });
845
+ });
846
+
847
+ /*
848
+ * Adoption. A (device, template) monitor that already exists without an owner
849
+ * occupies the pair Monitor's partial unique index protects, so creating a
850
+ * second one is impossible — not slow, impossible, on this pass and on every
851
+ * pass after it.
852
+ */
853
+ describe("an unowned monitor for the same (device, template)", () => {
854
+ it("is adopted by stamping the policy, not duplicated", async () => {
855
+ const unowned: Monitor = monitorRow({
856
+ autoProvisionedNetworkDeviceId: DEVICE_ID,
857
+ monitorTemplateId: TEMPLATE_ID,
858
+ });
859
+
860
+ monitorTable = [unowned];
861
+ deviceTable = [device()];
862
+ policyTable = [policy()];
863
+
864
+ await NetworkAlertPolicyEngineService.reconcileDevice({
865
+ projectId: PROJECT_ID,
866
+ deviceId: DEVICE_ID,
867
+ });
868
+
869
+ expect(monitorCreateMock).not.toHaveBeenCalled();
870
+ expect(monitorsOfDevice()).toHaveLength(1);
871
+ expect(unowned.networkAlertPolicyId?.toString()).toBe(POLICY_ID.toString());
872
+ });
873
+
874
+ /*
875
+ * The stamp is a provenance marker, not an operational change: no hooks,
876
+ * no workflow, no realtime event, no audit row. The monitor keeps doing
877
+ * exactly what it was doing.
878
+ */
879
+ it("is adopted without running the monitor update pipeline", async () => {
880
+ monitorTable = [
881
+ monitorRow({
882
+ autoProvisionedNetworkDeviceId: DEVICE_ID,
883
+ monitorTemplateId: TEMPLATE_ID,
884
+ }),
885
+ ];
886
+ deviceTable = [device()];
887
+ policyTable = [policy()];
888
+
889
+ await NetworkAlertPolicyEngineService.reconcileDevice({
890
+ projectId: PROJECT_ID,
891
+ deviceId: DEVICE_ID,
892
+ });
893
+
894
+ expect(monitorStampMock).toHaveBeenCalledWith(
895
+ expect.objectContaining({
896
+ data: expect.objectContaining({
897
+ networkAlertPolicyId: expect.anything(),
898
+ }),
899
+ }),
900
+ );
901
+ expect(monitorUpdateByMock).not.toHaveBeenCalled();
902
+ });
903
+
904
+ /*
905
+ * A monitor another policy owns is not adoptable and not deletable. The
906
+ * one-policy-per-template rule makes this unreachable through the API, but
907
+ * the engine must never be the thing that turns a data anomaly into a lost
908
+ * monitor.
909
+ */
910
+ it("is left alone when a different policy already owns it", async () => {
911
+ const foreign: Monitor = monitorRow({
912
+ autoProvisionedNetworkDeviceId: DEVICE_ID,
913
+ monitorTemplateId: TEMPLATE_ID,
914
+ networkAlertPolicyId: OTHER_POLICY_ID,
915
+ });
916
+
917
+ monitorTable = [foreign];
918
+ deviceTable = [device()];
919
+ policyTable = [
920
+ policy(),
921
+ policy(
922
+ { name: "Other", scope: { siteIds: [SITE_ID.toString()] } },
923
+ OTHER_POLICY_ID,
924
+ ),
925
+ ];
926
+
927
+ await NetworkAlertPolicyEngineService.reconcileDevice({
928
+ projectId: PROJECT_ID,
929
+ deviceId: DEVICE_ID,
930
+ });
931
+
932
+ expect(foreign.networkAlertPolicyId?.toString()).toBe(
933
+ OTHER_POLICY_ID.toString(),
934
+ );
935
+ expect(monitorTable).toContain(foreign);
936
+ });
937
+ });
938
+
939
+ /*
940
+ * Provisionability is a property of the DEVICE, and losing it removes every
941
+ * policy monitor. The monitor-backed case is the one that matters most: the
942
+ * device's health is now a bound monitor's, nothing polls it, and a Network
943
+ * Device monitor left behind is billed for a verdict that will never arrive.
944
+ */
945
+ describe("a device that stops being provisionable", () => {
946
+ async function reconcileAfter(
947
+ change: Partial<NetworkDevice>,
948
+ ): Promise<Array<Monitor>> {
949
+ deviceTable = [device()];
950
+ policyTable = [policy()];
951
+
952
+ await NetworkAlertPolicyEngineService.reconcileDevice({
953
+ projectId: PROJECT_ID,
954
+ deviceId: DEVICE_ID,
955
+ });
956
+
957
+ expect(monitorsOfDevice()).toHaveLength(1);
958
+
959
+ Object.assign(deviceTable[0]!, change);
960
+
961
+ await NetworkAlertPolicyEngineService.reconcileDevice({
962
+ projectId: PROJECT_ID,
963
+ deviceId: DEVICE_ID,
964
+ });
965
+
966
+ return monitorsOfDevice();
967
+ }
968
+
969
+ it("loses its policy monitors when it becomes monitor-backed", async () => {
970
+ expect(
971
+ await reconcileAfter({
972
+ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor,
973
+ }),
974
+ ).toHaveLength(0);
975
+ });
976
+
977
+ it("loses its policy monitors when it is archived", async () => {
978
+ expect(await reconcileAfter({ isArchived: true })).toHaveLength(0);
979
+ });
980
+
981
+ /*
982
+ * A device with no probe is polled by nothing, so its monitor would sit
983
+ * Pending forever — and still be billed.
984
+ */
985
+ it("loses its policy monitors when its probe is cleared", async () => {
986
+ expect(
987
+ await reconcileAfter({ probeId: undefined as unknown as ObjectID }),
988
+ ).toHaveLength(0);
989
+ });
990
+
991
+ it("gets them back when it becomes probe-polled again", async () => {
992
+ deviceTable = [
993
+ device({ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor }),
994
+ ];
995
+ policyTable = [policy()];
996
+
997
+ await NetworkAlertPolicyEngineService.reconcileDevice({
998
+ projectId: PROJECT_ID,
999
+ deviceId: DEVICE_ID,
1000
+ });
1001
+
1002
+ expect(monitorsOfDevice()).toHaveLength(0);
1003
+
1004
+ deviceTable[0]!.monitoringMethod = NetworkDeviceMonitoringMethod.Probe;
1005
+
1006
+ await NetworkAlertPolicyEngineService.reconcileDevice({
1007
+ projectId: PROJECT_ID,
1008
+ deviceId: DEVICE_ID,
1009
+ });
1010
+
1011
+ expect(monitorsOfDevice()).toHaveLength(1);
1012
+ });
1013
+
1014
+ /*
1015
+ * The legacy free-text values — NULL, "" and "SNMP" — all mean "the probe
1016
+ * polls it". A device written before ping-first polling must not be read
1017
+ * as monitor-backed and quietly stripped of its alerting.
1018
+ */
1019
+ it("treats a legacy SNMP monitoringMethod as probe-polled", async () => {
1020
+ deviceTable = [device({ monitoringMethod: "SNMP" })];
1021
+ policyTable = [policy()];
1022
+
1023
+ await NetworkAlertPolicyEngineService.reconcileDevice({
1024
+ projectId: PROJECT_ID,
1025
+ deviceId: DEVICE_ID,
1026
+ });
1027
+
1028
+ expect(monitorsOfDevice()).toHaveLength(1);
1029
+ });
1030
+ });
1031
+
1032
+ describe("a disabled policy", () => {
1033
+ it("keeps its monitors and pauses them rather than deleting them", async () => {
1034
+ deviceTable = [device()];
1035
+ policyTable = [policy()];
1036
+
1037
+ await NetworkAlertPolicyEngineService.reconcileDevice({
1038
+ projectId: PROJECT_ID,
1039
+ deviceId: DEVICE_ID,
1040
+ });
1041
+
1042
+ policyTable[0]!.isEnabled = false;
1043
+
1044
+ await NetworkAlertPolicyEngineService.reconcileDevice({
1045
+ projectId: PROJECT_ID,
1046
+ deviceId: DEVICE_ID,
1047
+ });
1048
+
1049
+ /*
1050
+ * The row survives — with its id, its incident history and its
1051
+ * dependencies — which is the whole difference between pausing a policy
1052
+ * and deleting one.
1053
+ */
1054
+ expect(monitorsOfDevice()).toHaveLength(1);
1055
+ expect(monitorsOfDevice()[0]!.disableActiveMonitoring).toBe(true);
1056
+ });
1057
+
1058
+ it("provisions nothing new while it is off", async () => {
1059
+ deviceTable = [device()];
1060
+ policyTable = [policy({ isEnabled: false })];
1061
+
1062
+ await NetworkAlertPolicyEngineService.reconcileDevice({
1063
+ projectId: PROJECT_ID,
1064
+ deviceId: DEVICE_ID,
1065
+ });
1066
+
1067
+ expect(monitorCreateMock).not.toHaveBeenCalled();
1068
+ });
1069
+
1070
+ /*
1071
+ * A template-less policy — only reachable through the FK's SET NULL
1072
+ * backstop, because MonitorTemplateService refuses to delete a template a
1073
+ * policy uses — is treated exactly like a disabled one. Nothing to
1074
+ * provision from, so nothing is provisioned and nothing is destroyed.
1075
+ */
1076
+ it("behaves the same when its template was deleted out from under it", async () => {
1077
+ deviceTable = [device()];
1078
+ policyTable = [policy()];
1079
+
1080
+ await NetworkAlertPolicyEngineService.reconcileDevice({
1081
+ projectId: PROJECT_ID,
1082
+ deviceId: DEVICE_ID,
1083
+ });
1084
+
1085
+ delete policyTable[0]!.monitorTemplateId;
1086
+
1087
+ await NetworkAlertPolicyEngineService.reconcileDevice({
1088
+ projectId: PROJECT_ID,
1089
+ deviceId: DEVICE_ID,
1090
+ });
1091
+
1092
+ expect(monitorsOfDevice()).toHaveLength(1);
1093
+ });
1094
+ });
1095
+
1096
+ /*
1097
+ * Re-pointing a policy at a different template is a re-clone: the monitors
1098
+ * from the old template are the old alerting configuration and must go, and
1099
+ * new ones are provisioned from the template the policy names now. This is
1100
+ * why ownership is the pair, not the policy id alone.
1101
+ */
1102
+ describe("a policy re-pointed at a different template", () => {
1103
+ it("replaces the monitors made from the old template", async () => {
1104
+ deviceTable = [device()];
1105
+ policyTable = [policy()];
1106
+
1107
+ await NetworkAlertPolicyEngineService.reconcileDevice({
1108
+ projectId: PROJECT_ID,
1109
+ deviceId: DEVICE_ID,
1110
+ });
1111
+
1112
+ const originalMonitorId: string = monitorsOfDevice()[0]!.id!.toString();
1113
+
1114
+ policyTable[0]!.monitorTemplateId = OTHER_TEMPLATE_ID;
1115
+
1116
+ await NetworkAlertPolicyEngineService.reconcileDevice({
1117
+ projectId: PROJECT_ID,
1118
+ deviceId: DEVICE_ID,
1119
+ });
1120
+
1121
+ const survivors: Array<Monitor> = monitorsOfDevice();
1122
+
1123
+ expect(survivors).toHaveLength(1);
1124
+ expect(survivors[0]!.id!.toString()).not.toBe(originalMonitorId);
1125
+ expect(survivors[0]!.monitorTemplateId?.toString()).toBe(
1126
+ OTHER_TEMPLATE_ID.toString(),
1127
+ );
1128
+ });
1129
+ });
1130
+
1131
+ /*
1132
+ * A monitor whose owning policy row is gone is nobody's. In practice the
1133
+ * policy delete path removed it already; this is the backstop for the row
1134
+ * that outlived it, and without it that monitor would be billed forever with
1135
+ * nothing left to explain it.
1136
+ */
1137
+ describe("a monitor whose policy no longer exists", () => {
1138
+ it("is removed on the next pass", async () => {
1139
+ monitorTable = [
1140
+ monitorRow({
1141
+ autoProvisionedNetworkDeviceId: DEVICE_ID,
1142
+ monitorTemplateId: TEMPLATE_ID,
1143
+ networkAlertPolicyId: OTHER_POLICY_ID,
1144
+ }),
1145
+ ];
1146
+ deviceTable = [device()];
1147
+ policyTable = [];
1148
+
1149
+ await NetworkAlertPolicyEngineService.reconcileDevice({
1150
+ projectId: PROJECT_ID,
1151
+ deviceId: DEVICE_ID,
1152
+ });
1153
+
1154
+ expect(monitorsOfDevice()).toHaveLength(0);
1155
+ });
1156
+ });
1157
+
1158
+ describe("the per-device lock", () => {
1159
+ it("is taken on the device, by name, before anything is read", async () => {
1160
+ deviceTable = [device()];
1161
+ policyTable = [policy()];
1162
+
1163
+ await NetworkAlertPolicyEngineService.reconcileDevice({
1164
+ projectId: PROJECT_ID,
1165
+ deviceId: DEVICE_ID,
1166
+ });
1167
+
1168
+ expect(semaphoreLockMock).toHaveBeenCalledWith(
1169
+ expect.objectContaining({
1170
+ key: policyDeviceLockKey(DEVICE_ID),
1171
+ namespace: POLICY_DEVICE_LOCK_NAMESPACE,
1172
+ /*
1173
+ * One attempt, never a queue: whoever holds it is computing the same
1174
+ * difference from fresher data than this pass would.
1175
+ */
1176
+ acquireAttemptsLimit: 1,
1177
+ }),
1178
+ );
1179
+ expect(policyDeviceLockKey(DEVICE_ID)).toBe(
1180
+ `NetworkAlertPolicy:Device:${DEVICE_ID.toString()}`,
1181
+ );
1182
+ });
1183
+
1184
+ /*
1185
+ * A pass that cannot take the lock does NOTHING — it does not read, and
1186
+ * above all it does not write. That is what makes "two policies racing on
1187
+ * one device" safe: the loser of the race is not a second writer, it is a
1188
+ * no-op.
1189
+ */
1190
+ it("makes a pass that cannot acquire it a complete no-op", async () => {
1191
+ deviceTable = [device()];
1192
+ policyTable = [policy()];
1193
+ semaphoreLockMock.mockRejectedValue(new Error("lock held"));
1194
+
1195
+ await NetworkAlertPolicyEngineService.reconcileDevice({
1196
+ projectId: PROJECT_ID,
1197
+ deviceId: DEVICE_ID,
1198
+ });
1199
+
1200
+ expect(deviceFindOneByMock).not.toHaveBeenCalled();
1201
+ expect(monitorCreateMock).not.toHaveBeenCalled();
1202
+ expect(monitorDeleteByMock).not.toHaveBeenCalled();
1203
+ });
1204
+
1205
+ it("is released even when the reconciliation throws", async () => {
1206
+ deviceTable = [device()];
1207
+ policyTable = [policy()];
1208
+ deviceFindOneByMock.mockRejectedValue(new Error("database is on fire"));
1209
+
1210
+ await NetworkAlertPolicyEngineService.reconcileDevice({
1211
+ projectId: PROJECT_ID,
1212
+ deviceId: DEVICE_ID,
1213
+ });
1214
+
1215
+ expect(semaphoreReleaseMock).toHaveBeenCalledWith(FAKE_MUTEX);
1216
+ });
1217
+
1218
+ /*
1219
+ * ...and the throw is recorded rather than propagated. reconcileDevice is
1220
+ * called from a device create's detached chain; a template that stopped
1221
+ * validating must not become a failed device import.
1222
+ */
1223
+ it("records the failure instead of throwing it at the caller", async () => {
1224
+ deviceTable = [device()];
1225
+ policyTable = [policy()];
1226
+ deviceFindOneByMock.mockRejectedValue(new Error("database is on fire"));
1227
+
1228
+ const context: PolicyRunContext =
1229
+ await NetworkAlertPolicyEngineService.reconcileDevice({
1230
+ projectId: PROJECT_ID,
1231
+ deviceId: DEVICE_ID,
1232
+ });
1233
+
1234
+ expect(context.failures[0]).toContain("database is on fire");
1235
+ });
1236
+ });
1237
+
1238
+ /*
1239
+ * Prerequisite 5. A project whose plan cannot hold another monitor must
1240
+ * produce ONE message an operator can act on, not five hundred identical
1241
+ * create failures — and the run must stop, because every remaining create
1242
+ * would fail the same way.
1243
+ */
1244
+ describe("the plan check", () => {
1245
+ it("stops the run before the first create when the subscription is unpaid", async () => {
1246
+ deviceTable = [device()];
1247
+ policyTable = [policy()];
1248
+ currentPlanMock.mockResolvedValue({
1249
+ plan: PlanType.Growth,
1250
+ isSubscriptionUnpaid: true,
1251
+ });
1252
+
1253
+ const context: PolicyRunContext =
1254
+ await NetworkAlertPolicyEngineService.reconcileDevice({
1255
+ projectId: PROJECT_ID,
1256
+ deviceId: DEVICE_ID,
1257
+ });
1258
+
1259
+ expect(monitorCreateMock).not.toHaveBeenCalled();
1260
+ expect(context.isStopped).toBe(true);
1261
+ expect(context.planException).toContain("subscription is unpaid");
1262
+ });
1263
+
1264
+ it("stops the run when the free plan's monitor limit is already reached", async () => {
1265
+ deviceTable = [device()];
1266
+ policyTable = [policy()];
1267
+ currentPlanMock.mockResolvedValue({
1268
+ plan: PlanType.Free,
1269
+ isSubscriptionUnpaid: false,
1270
+ });
1271
+ monitorCountByMock.mockResolvedValue(new PositiveNumber(10));
1272
+
1273
+ const context: PolicyRunContext =
1274
+ await NetworkAlertPolicyEngineService.reconcileDevice({
1275
+ projectId: PROJECT_ID,
1276
+ deviceId: DEVICE_ID,
1277
+ });
1278
+
1279
+ expect(monitorCreateMock).not.toHaveBeenCalled();
1280
+ expect(context.planException).toContain("free plan");
1281
+ });
1282
+
1283
+ /*
1284
+ * Asked ONCE per run. Fifty devices in one sweep must not be fifty billing
1285
+ * lookups; that is the difference between a five-minute sweep and a
1286
+ * five-minute outage.
1287
+ */
1288
+ it("asks the plan once for a whole run", async () => {
1289
+ deviceTable = [device(), device({}, OTHER_DEVICE_ID)];
1290
+ policyTable = [policy()];
1291
+
1292
+ const context: PolicyRunContext =
1293
+ NetworkAlertPolicyEngineService.createRunContext(PROJECT_ID);
1294
+
1295
+ await NetworkAlertPolicyEngineService.reconcileDevices({
1296
+ projectId: PROJECT_ID,
1297
+ deviceIds: [DEVICE_ID, OTHER_DEVICE_ID],
1298
+ context: context,
1299
+ });
1300
+
1301
+ expect(monitorCreateMock).toHaveBeenCalledTimes(2);
1302
+ expect(currentPlanMock).toHaveBeenCalledTimes(1);
1303
+ });
1304
+
1305
+ /*
1306
+ * The ceiling can be crossed DURING a run: the precheck passed at nine of
1307
+ * ten monitors and the tenth create is the one that is refused. The verdict
1308
+ * is re-asked after a failed create — never pattern-matched out of the
1309
+ * error text, which would drift the moment MonitorService reworded its
1310
+ * refusal — and the run stops at the first refusal instead of the five
1311
+ * hundredth.
1312
+ */
1313
+ it("stops mid-run when a create fails and the plan now refuses another monitor", async () => {
1314
+ deviceTable = [device(), device({}, OTHER_DEVICE_ID)];
1315
+ policyTable = [policy()];
1316
+ currentPlanMock.mockResolvedValue({
1317
+ plan: PlanType.Free,
1318
+ isSubscriptionUnpaid: false,
1319
+ });
1320
+ monitorCountByMock
1321
+ .mockResolvedValueOnce(new PositiveNumber(9))
1322
+ .mockResolvedValue(new PositiveNumber(10));
1323
+ monitorCreateMock.mockRejectedValueOnce(
1324
+ new BadDataException("monitor limit reached"),
1325
+ );
1326
+
1327
+ const context: PolicyRunContext =
1328
+ NetworkAlertPolicyEngineService.createRunContext(PROJECT_ID);
1329
+
1330
+ await NetworkAlertPolicyEngineService.reconcileDevices({
1331
+ projectId: PROJECT_ID,
1332
+ deviceIds: [DEVICE_ID, OTHER_DEVICE_ID],
1333
+ context: context,
1334
+ });
1335
+
1336
+ expect(context.isStopped).toBe(true);
1337
+ expect(context.planException).toContain("free plan");
1338
+ // The second device was never attempted.
1339
+ expect(monitorCreateMock).toHaveBeenCalledTimes(1);
1340
+ });
1341
+
1342
+ /*
1343
+ * A billing lookup that FAILS is not a plan refusal. A Stripe blip must not
1344
+ * stop a fleet from being provisioned; it is recorded and the run carries
1345
+ * on.
1346
+ */
1347
+ it("treats a billing lookup failure as an ordinary failure, not a refusal", async () => {
1348
+ deviceTable = [device()];
1349
+ policyTable = [policy()];
1350
+ currentPlanMock.mockRejectedValue(new Error("stripe timed out"));
1351
+
1352
+ const context: PolicyRunContext =
1353
+ await NetworkAlertPolicyEngineService.reconcileDevice({
1354
+ projectId: PROJECT_ID,
1355
+ deviceId: DEVICE_ID,
1356
+ });
1357
+
1358
+ expect(context.isStopped).toBe(false);
1359
+ expect(context.failures[0]).toContain("stripe timed out");
1360
+ expect(monitorCreateMock).toHaveBeenCalledTimes(1);
1361
+ });
1362
+ });
1363
+
1364
+ describe("the run's monitor budget", () => {
1365
+ it("stops writing once MAX_MONITORS_PER_POLICY_SYNC monitors have been written", async () => {
1366
+ deviceTable = [device(), device({}, OTHER_DEVICE_ID)];
1367
+ policyTable = [policy()];
1368
+
1369
+ const context: PolicyRunContext =
1370
+ NetworkAlertPolicyEngineService.createRunContext(PROJECT_ID, 1);
1371
+
1372
+ await NetworkAlertPolicyEngineService.reconcileDevices({
1373
+ projectId: PROJECT_ID,
1374
+ deviceIds: [DEVICE_ID, OTHER_DEVICE_ID],
1375
+ context: context,
1376
+ });
1377
+
1378
+ /*
1379
+ * One monitor, and the run marked truncated rather than failed: nothing
1380
+ * is half-written, and the next sweep computes the same difference and
1381
+ * continues.
1382
+ */
1383
+ expect(monitorCreateMock).toHaveBeenCalledTimes(1);
1384
+ expect(context.isTruncated).toBe(true);
1385
+ expect(context.failures).toHaveLength(0);
1386
+ });
1387
+
1388
+ it("defaults to the documented cap", () => {
1389
+ expect(
1390
+ NetworkAlertPolicyEngineService.createRunContext(PROJECT_ID)
1391
+ .monitorBudget,
1392
+ ).toBe(MAX_MONITORS_PER_POLICY_SYNC);
1393
+ });
1394
+ });
1395
+
1396
+ describe("syncPolicy over a policy's whole fleet", () => {
1397
+ it("provisions every matching device and stamps the covered count", async () => {
1398
+ deviceTable = [device(), device({}, OTHER_DEVICE_ID)];
1399
+ policyTable = [policy()];
1400
+
1401
+ await NetworkAlertPolicyEngineService.syncPolicy({ policyId: POLICY_ID });
1402
+
1403
+ expect(monitorTable).toHaveLength(2);
1404
+ expect(policyStampMock).toHaveBeenCalledWith(
1405
+ expect.objectContaining({
1406
+ id: POLICY_ID,
1407
+ data: expect.objectContaining({
1408
+ coveredDeviceCount: 2,
1409
+ lastSyncError: null,
1410
+ }),
1411
+ }),
1412
+ );
1413
+ });
1414
+
1415
+ /*
1416
+ * The covered count is what the settings table prints beside the scope
1417
+ * sentence, so it counts the devices the policy actually provisions for —
1418
+ * not every device the scope's SQL half matches. An archived or
1419
+ * monitor-backed device is in the site, and gets no monitor.
1420
+ */
1421
+ it("counts only the devices it can actually provision for", async () => {
1422
+ deviceTable = [
1423
+ device(),
1424
+ device(
1425
+ { monitoringMethod: NetworkDeviceMonitoringMethod.Monitor },
1426
+ OTHER_DEVICE_ID,
1427
+ ),
1428
+ ];
1429
+ policyTable = [policy()];
1430
+
1431
+ await NetworkAlertPolicyEngineService.syncPolicy({ policyId: POLICY_ID });
1432
+
1433
+ expect(policyStampMock).toHaveBeenCalledWith(
1434
+ expect.objectContaining({
1435
+ data: expect.objectContaining({ coveredDeviceCount: 1 }),
1436
+ }),
1437
+ );
1438
+ });
1439
+
1440
+ it("removes the monitors of devices that have left the scope", async () => {
1441
+ deviceTable = [device(), device({}, OTHER_DEVICE_ID)];
1442
+ policyTable = [policy()];
1443
+
1444
+ await NetworkAlertPolicyEngineService.syncPolicy({ policyId: POLICY_ID });
1445
+ expect(monitorTable).toHaveLength(2);
1446
+
1447
+ // The second device moves to a site the policy does not cover.
1448
+ deviceTable[1]!.siteId = OTHER_SITE_ID;
1449
+ policyTable[0]!.scope = {
1450
+ siteIds: [SITE_ID.toString()],
1451
+ } as NetworkAlertPolicyScope;
1452
+
1453
+ await NetworkAlertPolicyEngineService.syncPolicy({ policyId: POLICY_ID });
1454
+
1455
+ expect(monitorTable).toHaveLength(1);
1456
+ expect(monitorTable[0]!.autoProvisionedNetworkDeviceId?.toString()).toBe(
1457
+ DEVICE_ID.toString(),
1458
+ );
1459
+ });
1460
+
1461
+ it("records a failure in lastSyncError where the operator will see it", async () => {
1462
+ deviceTable = [device()];
1463
+ policyTable = [policy()];
1464
+ // The template vanished between the policy's save and this pass.
1465
+ templateFindOneByMock.mockResolvedValue(null);
1466
+
1467
+ await NetworkAlertPolicyEngineService.syncPolicy({ policyId: POLICY_ID });
1468
+
1469
+ expect(policyStampMock).toHaveBeenCalledWith(
1470
+ expect.objectContaining({
1471
+ data: expect.objectContaining({
1472
+ lastSyncError: expect.stringContaining("Monitor template"),
1473
+ }),
1474
+ }),
1475
+ );
1476
+ });
1477
+
1478
+ /*
1479
+ * A disabled policy is looked at and found to have nothing to do. Stamping
1480
+ * lastSyncAt for it is honest — and leaving lastSyncError null is what
1481
+ * stops the settings table showing a red row for a policy somebody
1482
+ * deliberately switched off.
1483
+ */
1484
+ it("stamps a disabled policy without touching a single monitor", async () => {
1485
+ deviceTable = [device()];
1486
+ policyTable = [policy({ isEnabled: false })];
1487
+
1488
+ await NetworkAlertPolicyEngineService.syncPolicy({ policyId: POLICY_ID });
1489
+
1490
+ expect(monitorCreateMock).not.toHaveBeenCalled();
1491
+ expect(monitorDeleteByMock).not.toHaveBeenCalled();
1492
+ expect(policyStampMock).toHaveBeenCalledWith(
1493
+ expect.objectContaining({
1494
+ data: expect.objectContaining({ lastSyncError: null }),
1495
+ }),
1496
+ );
1497
+ });
1498
+
1499
+ it("does nothing at all for a policy that has been deleted", async () => {
1500
+ policyTable = [];
1501
+
1502
+ await expect(
1503
+ NetworkAlertPolicyEngineService.syncPolicy({ policyId: POLICY_ID }),
1504
+ ).resolves.toBeNull();
1505
+
1506
+ expect(policyStampMock).not.toHaveBeenCalled();
1507
+ });
1508
+
1509
+ /*
1510
+ * templateSyncedAt means "every monitor this policy owns is running the
1511
+ * template's current configuration". A sweep that changed nothing has
1512
+ * re-synced nothing, so it must not stamp it — otherwise an operator would
1513
+ * read "Template Synced: 2 minutes ago" and believe a criteria edit they
1514
+ * made an hour ago had landed.
1515
+ */
1516
+ it("does not stamp templateSyncedAt on an ordinary sweep", async () => {
1517
+ deviceTable = [device()];
1518
+ policyTable = [policy()];
1519
+
1520
+ await NetworkAlertPolicyEngineService.syncPolicy({ policyId: POLICY_ID });
1521
+
1522
+ const stamped: Record<string, unknown> = (
1523
+ policyStampMock.mock.calls[0]![0] as { data: Record<string, unknown> }
1524
+ ).data;
1525
+
1526
+ expect(stamped["templateSyncedAt"]).toBeUndefined();
1527
+ });
1528
+
1529
+ it("stamps templateSyncedAt when the fleet was rebuilt from a new template", async () => {
1530
+ deviceTable = [device()];
1531
+ policyTable = [policy()];
1532
+
1533
+ await NetworkAlertPolicyEngineService.syncPolicy({
1534
+ policyId: POLICY_ID,
1535
+ stampTemplateSyncedOnCleanPass: true,
1536
+ });
1537
+
1538
+ const stamped: Record<string, unknown> = (
1539
+ policyStampMock.mock.calls[0]![0] as { data: Record<string, unknown> }
1540
+ ).data;
1541
+
1542
+ expect(stamped["templateSyncedAt"]).toBeInstanceOf(Date);
1543
+ });
1544
+
1545
+ /*
1546
+ * ...but not when the pass ADOPTED a monitor. An adopted monitor was built
1547
+ * by somebody else and its criteria are not this template's, so claiming
1548
+ * the fleet is in step with the template would be false.
1549
+ */
1550
+ it("does not stamp templateSyncedAt for a pass that adopted a monitor", async () => {
1551
+ monitorTable = [
1552
+ monitorRow({
1553
+ autoProvisionedNetworkDeviceId: DEVICE_ID,
1554
+ monitorTemplateId: TEMPLATE_ID,
1555
+ }),
1556
+ ];
1557
+ deviceTable = [device()];
1558
+ policyTable = [policy()];
1559
+
1560
+ await NetworkAlertPolicyEngineService.syncPolicy({
1561
+ policyId: POLICY_ID,
1562
+ stampTemplateSyncedOnCleanPass: true,
1563
+ });
1564
+
1565
+ const stamped: Record<string, unknown> = (
1566
+ policyStampMock.mock.calls[0]![0] as { data: Record<string, unknown> }
1567
+ ).data;
1568
+
1569
+ expect(stamped["templateSyncedAt"]).toBeUndefined();
1570
+ });
1571
+ });
1572
+
1573
+ /*
1574
+ * Prerequisite 9's engine half. A scope is validated at write, so a stale or
1575
+ * malformed id only ever arrives here through a hand-edited row — and the
1576
+ * engine's answer is "match nothing", never "throw" and never "match
1577
+ * everything".
1578
+ */
1579
+ describe("scope ids the engine cannot use", () => {
1580
+ it("matches nothing when a kind holds only unusable ids", async () => {
1581
+ deviceTable = [device()];
1582
+ policyTable = [
1583
+ policy({
1584
+ scope: { siteIds: ["not-a-uuid"] } as NetworkAlertPolicyScope,
1585
+ }),
1586
+ ];
1587
+
1588
+ await NetworkAlertPolicyEngineService.syncPolicy({ policyId: POLICY_ID });
1589
+
1590
+ /*
1591
+ * Dropping the last unusable id and leaving the kind EMPTY would be the
1592
+ * dangerous bug: an empty kind matches every device, so a typo'd scope
1593
+ * would silently provision the whole estate.
1594
+ */
1595
+ expect(monitorCreateMock).not.toHaveBeenCalled();
1596
+ });
1597
+
1598
+ it("adds no clause at all for a kind that is genuinely empty", async () => {
1599
+ deviceTable = [device()];
1600
+ policyTable = [policy({ scope: {} as NetworkAlertPolicyScope })];
1601
+
1602
+ await NetworkAlertPolicyEngineService.syncPolicy({ policyId: POLICY_ID });
1603
+
1604
+ const deviceQuery: Record<string, unknown> = (
1605
+ deviceFindByMock.mock.calls[0]![0] as { query: Record<string, unknown> }
1606
+ ).query;
1607
+
1608
+ expect(deviceQuery["siteId"]).toBeUndefined();
1609
+ expect(deviceQuery["networkDeviceRoleId"]).toBeUndefined();
1610
+ expect(deviceQuery["labels"]).toBeUndefined();
1611
+ expect(monitorCreateMock).toHaveBeenCalledTimes(1);
1612
+ });
1613
+
1614
+ // The engine reads a scope. It never writes one back.
1615
+ it("never rewrites the policy's scope", async () => {
1616
+ deviceTable = [device()];
1617
+ policyTable = [
1618
+ policy({
1619
+ scope: { siteIds: ["not-a-uuid"] } as NetworkAlertPolicyScope,
1620
+ }),
1621
+ ];
1622
+
1623
+ await NetworkAlertPolicyEngineService.syncPolicy({ policyId: POLICY_ID });
1624
+
1625
+ for (const call of policyStampMock.mock.calls) {
1626
+ expect(
1627
+ (call[0] as { data: Record<string, unknown> }).data["scope"],
1628
+ ).toBeUndefined();
1629
+ }
1630
+
1631
+ expect(policyTable[0]!.scope).toEqual({ siteIds: ["not-a-uuid"] });
1632
+ });
1633
+ });
1634
+
1635
+ describe("deleting a policy", () => {
1636
+ it("removes exactly the monitors it owns, as root", async () => {
1637
+ const ownMonitor: Monitor = monitorRow({
1638
+ autoProvisionedNetworkDeviceId: DEVICE_ID,
1639
+ monitorTemplateId: TEMPLATE_ID,
1640
+ networkAlertPolicyId: POLICY_ID,
1641
+ });
1642
+ const otherPolicyMonitor: Monitor = monitorRow({
1643
+ autoProvisionedNetworkDeviceId: OTHER_DEVICE_ID,
1644
+ monitorTemplateId: OTHER_TEMPLATE_ID,
1645
+ networkAlertPolicyId: OTHER_POLICY_ID,
1646
+ });
1647
+ const handMade: Monitor = monitorRow({
1648
+ autoProvisionedNetworkDeviceId: DEVICE_ID,
1649
+ monitorTemplateId: OTHER_TEMPLATE_ID,
1650
+ });
1651
+
1652
+ monitorTable = [ownMonitor, otherPolicyMonitor, handMade];
1653
+
1654
+ const deleted: number =
1655
+ await NetworkAlertPolicyEngineService.deleteMonitorsOwnedByPolicy({
1656
+ projectId: PROJECT_ID,
1657
+ policyId: POLICY_ID,
1658
+ });
1659
+
1660
+ expect(deleted).toBe(1);
1661
+ expect(monitorTable).toEqual([otherPolicyMonitor, handMade]);
1662
+ expect(monitorDeleteByMock).toHaveBeenCalledWith(
1663
+ expect.objectContaining({ props: { isRoot: true } }),
1664
+ );
1665
+ });
1666
+
1667
+ it("does nothing when the policy owns no monitors", async () => {
1668
+ monitorTable = [
1669
+ monitorRow({
1670
+ autoProvisionedNetworkDeviceId: DEVICE_ID,
1671
+ monitorTemplateId: TEMPLATE_ID,
1672
+ }),
1673
+ ];
1674
+
1675
+ expect(
1676
+ await NetworkAlertPolicyEngineService.deleteMonitorsOwnedByPolicy({
1677
+ projectId: PROJECT_ID,
1678
+ policyId: POLICY_ID,
1679
+ }),
1680
+ ).toBe(0);
1681
+ expect(monitorDeleteByMock).not.toHaveBeenCalled();
1682
+ });
1683
+ });
1684
+
1685
+ describe("deleting devices", () => {
1686
+ it("removes their policy-owned monitors and leaves the rest to the caller", async () => {
1687
+ const policyMonitor: Monitor = monitorRow({
1688
+ autoProvisionedNetworkDeviceId: DEVICE_ID,
1689
+ monitorTemplateId: TEMPLATE_ID,
1690
+ networkAlertPolicyId: POLICY_ID,
1691
+ });
1692
+ const ruleMonitor: Monitor = monitorRow({
1693
+ autoProvisionedNetworkDeviceId: DEVICE_ID,
1694
+ monitorTemplateId: OTHER_TEMPLATE_ID,
1695
+ });
1696
+
1697
+ monitorTable = [policyMonitor, ruleMonitor];
1698
+
1699
+ const deleted: number =
1700
+ await NetworkAlertPolicyEngineService.deletePolicyMonitorsForDevices({
1701
+ projectId: PROJECT_ID,
1702
+ deviceIds: [DEVICE_ID],
1703
+ });
1704
+
1705
+ expect(deleted).toBe(1);
1706
+ /*
1707
+ * The auto-import rule's monitor survives this call: it is removed by
1708
+ * NetworkDeviceService's own preflight, under the caller's permissions,
1709
+ * because somebody chose that rule.
1710
+ */
1711
+ expect(monitorTable).toEqual([ruleMonitor]);
1712
+ });
1713
+
1714
+ it("asks nothing when there are no devices", async () => {
1715
+ expect(
1716
+ await NetworkAlertPolicyEngineService.deletePolicyMonitorsForDevices({
1717
+ projectId: PROJECT_ID,
1718
+ deviceIds: [],
1719
+ }),
1720
+ ).toBe(0);
1721
+ expect(monitorFindByMock).not.toHaveBeenCalled();
1722
+ });
1723
+ });
1724
+
1725
+ describe("pausing and resuming a policy's monitors", () => {
1726
+ beforeEach(() => {
1727
+ monitorTable = [
1728
+ monitorRow({
1729
+ autoProvisionedNetworkDeviceId: DEVICE_ID,
1730
+ monitorTemplateId: TEMPLATE_ID,
1731
+ networkAlertPolicyId: POLICY_ID,
1732
+ }),
1733
+ monitorRow({
1734
+ autoProvisionedNetworkDeviceId: OTHER_DEVICE_ID,
1735
+ monitorTemplateId: TEMPLATE_ID,
1736
+ networkAlertPolicyId: POLICY_ID,
1737
+ }),
1738
+ // Another policy's monitor: never touched by this policy's pause.
1739
+ monitorRow({
1740
+ autoProvisionedNetworkDeviceId: DEVICE_ID,
1741
+ monitorTemplateId: OTHER_TEMPLATE_ID,
1742
+ networkAlertPolicyId: OTHER_POLICY_ID,
1743
+ }),
1744
+ ];
1745
+ });
1746
+
1747
+ it("pauses only its own, and resuming reverses it exactly", async () => {
1748
+ await NetworkAlertPolicyEngineService.setPolicyMonitorsPaused({
1749
+ projectId: PROJECT_ID,
1750
+ policyId: POLICY_ID,
1751
+ isPaused: true,
1752
+ });
1753
+
1754
+ expect(monitorTable[0]!.disableActiveMonitoring).toBe(true);
1755
+ expect(monitorTable[1]!.disableActiveMonitoring).toBe(true);
1756
+ expect(monitorTable[2]!.disableActiveMonitoring).toBe(false);
1757
+
1758
+ await NetworkAlertPolicyEngineService.setPolicyMonitorsPaused({
1759
+ projectId: PROJECT_ID,
1760
+ policyId: POLICY_ID,
1761
+ isPaused: false,
1762
+ });
1763
+
1764
+ expect(monitorTable[0]!.disableActiveMonitoring).toBe(false);
1765
+ expect(monitorTable[1]!.disableActiveMonitoring).toBe(false);
1766
+ });
1767
+
1768
+ /*
1769
+ * The pause goes through updateBy, not a raw column write, so workflows,
1770
+ * realtime and audit see it exactly as they would an operator's — and the
1771
+ * walk fan-out, which reads this flag, stops feeding the monitor.
1772
+ */
1773
+ it("writes disableActiveMonitoring through the monitor service", async () => {
1774
+ await NetworkAlertPolicyEngineService.setPolicyMonitorsPaused({
1775
+ projectId: PROJECT_ID,
1776
+ policyId: POLICY_ID,
1777
+ isPaused: true,
1778
+ });
1779
+
1780
+ expect(monitorUpdateByMock).toHaveBeenCalledWith(
1781
+ expect.objectContaining({
1782
+ data: { disableActiveMonitoring: true },
1783
+ props: { isRoot: true },
1784
+ }),
1785
+ );
1786
+ });
1787
+
1788
+ /*
1789
+ * The page is re-read on the flag it is about to write, so a write that
1790
+ * lands on nothing would be read again forever. It stops and says so
1791
+ * instead of spinning the worker.
1792
+ */
1793
+ it("gives up rather than looping when the write lands on nothing", async () => {
1794
+ monitorUpdateByMock.mockResolvedValue(0);
1795
+
1796
+ await expect(
1797
+ NetworkAlertPolicyEngineService.setPolicyMonitorsPaused({
1798
+ projectId: PROJECT_ID,
1799
+ policyId: POLICY_ID,
1800
+ isPaused: true,
1801
+ }),
1802
+ ).resolves.toBe(2);
1803
+ });
1804
+ });
1805
+
1806
+ describe("onMonitorTemplateSynced", () => {
1807
+ it("stamps templateSyncedAt on the policy that provisions from the template", async () => {
1808
+ policyTable = [policy()];
1809
+
1810
+ await NetworkAlertPolicyEngineService.onMonitorTemplateSynced({
1811
+ monitorTemplateId: TEMPLATE_ID,
1812
+ projectId: PROJECT_ID,
1813
+ });
1814
+
1815
+ expect(policyStampMock).toHaveBeenCalledWith(
1816
+ expect.objectContaining({
1817
+ id: POLICY_ID,
1818
+ data: { templateSyncedAt: expect.any(Date) },
1819
+ }),
1820
+ );
1821
+ });
1822
+
1823
+ it("stamps nothing when no policy uses the template", async () => {
1824
+ policyTable = [policy({ monitorTemplateId: OTHER_TEMPLATE_ID })];
1825
+
1826
+ await NetworkAlertPolicyEngineService.onMonitorTemplateSynced({
1827
+ monitorTemplateId: TEMPLATE_ID,
1828
+ projectId: PROJECT_ID,
1829
+ });
1830
+
1831
+ expect(policyStampMock).not.toHaveBeenCalled();
1832
+ });
1833
+
1834
+ /*
1835
+ * Never throws. A bookkeeping column must not fail the template sync the
1836
+ * operator actually asked for.
1837
+ */
1838
+ it("swallows a stamping failure", async () => {
1839
+ policyTable = [policy()];
1840
+ policyStampMock.mockRejectedValue(new Error("no"));
1841
+
1842
+ await expect(
1843
+ NetworkAlertPolicyEngineService.onMonitorTemplateSynced({
1844
+ monitorTemplateId: TEMPLATE_ID,
1845
+ projectId: PROJECT_ID,
1846
+ }),
1847
+ ).resolves.toBeUndefined();
1848
+ });
1849
+ });