@oneuptime/common 14.0.10 → 14.0.11

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 (837) hide show
  1. package/Models/DatabaseModels/AIConversation.ts +127 -1
  2. package/Models/DatabaseModels/CephCluster.ts +12 -0
  3. package/Models/DatabaseModels/CloudResource.ts +12 -0
  4. package/Models/DatabaseModels/DatabaseServer.ts +12 -0
  5. package/Models/DatabaseModels/DockerHost.ts +12 -0
  6. package/Models/DatabaseModels/DockerSwarmCluster.ts +12 -0
  7. package/Models/DatabaseModels/GlobalOidc.ts +0 -4
  8. package/Models/DatabaseModels/GlobalOidcProject.ts +0 -4
  9. package/Models/DatabaseModels/GlobalSso.ts +0 -4
  10. package/Models/DatabaseModels/GlobalSsoProject.ts +0 -4
  11. package/Models/DatabaseModels/Host.ts +12 -0
  12. package/Models/DatabaseModels/IncidentForm.ts +594 -0
  13. package/Models/DatabaseModels/IncidentFormSubmission.ts +283 -0
  14. package/Models/DatabaseModels/IncidentTemplate.ts +59 -0
  15. package/Models/DatabaseModels/Index.ts +14 -0
  16. package/Models/DatabaseModels/IoTFleet.ts +12 -0
  17. package/Models/DatabaseModels/KubernetesCluster.ts +12 -0
  18. package/Models/DatabaseModels/MessageQueue.ts +1102 -0
  19. package/Models/DatabaseModels/MessageQueueLabelRule.ts +553 -0
  20. package/Models/DatabaseModels/MessageQueueOwnerRule.ts +633 -0
  21. package/Models/DatabaseModels/MessageQueueOwnerTeam.ts +491 -0
  22. package/Models/DatabaseModels/MessageQueueOwnerUser.ts +490 -0
  23. package/Models/DatabaseModels/PodmanHost.ts +12 -0
  24. package/Models/DatabaseModels/Project.ts +59 -0
  25. package/Models/DatabaseModels/ProjectOidc.ts +0 -4
  26. package/Models/DatabaseModels/ProjectSso.ts +0 -4
  27. package/Models/DatabaseModels/ProxmoxCluster.ts +12 -0
  28. package/Models/DatabaseModels/RumApplication.ts +12 -0
  29. package/Models/DatabaseModels/ServerlessFunction.ts +12 -0
  30. package/Models/DatabaseModels/Service.ts +12 -0
  31. package/Models/DatabaseModels/StatusPageOidc.ts +0 -4
  32. package/Models/DatabaseModels/StatusPageSso.ts +0 -4
  33. package/Models/DatabaseModels/TeamComplianceSetting.ts +47 -2
  34. package/Models/DatabaseModels/VMwareVCenter.ts +12 -0
  35. package/Server/API/AIChatAPI.ts +13 -114
  36. package/Server/API/AIInvestigationAPI.ts +3 -3
  37. package/Server/API/AIInvestigationConversationAPI.ts +266 -0
  38. package/Server/API/IncidentFormAPI.ts +376 -0
  39. package/Server/API/ProjectOIDC.ts +16 -25
  40. package/Server/API/ProjectSSO.ts +16 -24
  41. package/Server/API/StatusPageAPI.ts +42 -77
  42. package/Server/Enterprise/EnterpriseEdition.ts +93 -28
  43. package/Server/Enterprise/EnterpriseFeature.ts +25 -3
  44. package/Server/Enterprise/EnterpriseLicenseSnapshot.ts +1 -1
  45. package/Server/Enterprise/EnterpriseServerModule.ts +4 -1
  46. package/Server/EnvironmentConfig.ts +1 -1
  47. package/Server/Infrastructure/Postgres/SchemaMigrations/1796400000000-AddIncidentForms.ts +129 -0
  48. package/Server/Infrastructure/Postgres/SchemaMigrations/1796500000000-AddTeamComplianceRuleNotificationChannels.ts +52 -0
  49. package/Server/Infrastructure/Postgres/SchemaMigrations/1796600000000-AddAIInvestigationConversationAndTimeLimits.ts +75 -0
  50. package/Server/Infrastructure/Postgres/SchemaMigrations/1796700000000-AddMessageQueueTables.ts +477 -0
  51. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  52. package/Server/Middleware/IncidentFormRateLimit.ts +847 -0
  53. package/Server/Middleware/UserAuthorization.ts +16 -38
  54. package/Server/Services/AIService.ts +10 -13
  55. package/Server/Services/IncidentCustomFieldService.ts +112 -2
  56. package/Server/Services/IncidentEpisodeMemberService.ts +24 -4
  57. package/Server/Services/IncidentEpisodeService.ts +10 -1
  58. package/Server/Services/IncidentFormService.ts +1566 -0
  59. package/Server/Services/IncidentFormSubmissionService.ts +97 -0
  60. package/Server/Services/IncidentGroupingEngineService.ts +45 -8
  61. package/Server/Services/IncidentService.ts +123 -5
  62. package/Server/Services/IncidentTemplateService.ts +22 -0
  63. package/Server/Services/Index.ts +14 -0
  64. package/Server/Services/MessageQueueLabelRuleEngineService.ts +321 -0
  65. package/Server/Services/MessageQueueLabelRuleService.ts +14 -0
  66. package/Server/Services/MessageQueueOwnerRuleEngineService.ts +412 -0
  67. package/Server/Services/MessageQueueOwnerRuleService.ts +14 -0
  68. package/Server/Services/MessageQueueOwnerTeamService.ts +62 -0
  69. package/Server/Services/MessageQueueOwnerUserService.ts +62 -0
  70. package/Server/Services/MessageQueueService.ts +1463 -0
  71. package/Server/Services/OpenTelemetryIngestService.ts +37 -4
  72. package/Server/Services/ProjectService.ts +0 -59
  73. package/Server/Services/StatusPagePrivateUserService.ts +1 -4
  74. package/Server/Services/StatusPageService.ts +1 -51
  75. package/Server/Services/TeamComplianceSettingService.ts +163 -42
  76. package/Server/Services/UserNotificationRuleService.ts +8 -1
  77. package/Server/Services/WorkspaceNotificationSummaryService.ts +12 -3
  78. package/Server/Types/Database/Permissions/CreatePermission.ts +17 -3
  79. package/Server/Types/Database/Permissions/EditionPermission.ts +140 -38
  80. package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +16 -0
  81. package/Server/Types/Database/Permissions/TablePermission.ts +3 -3
  82. package/Server/Types/Database/Permissions/UpdatePermission.ts +20 -6
  83. package/Server/Utils/AI/Chat/AgentContextCompactor.ts +200 -0
  84. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +307 -48
  85. package/Server/Utils/AI/Chat/ChatRunCancellation.ts +156 -0
  86. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +78 -18
  87. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +1 -1
  88. package/Server/Utils/AI/Chat/ToolOutputPager.ts +322 -0
  89. package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +88 -30
  90. package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +13 -1
  91. package/Server/Utils/AI/CodeFix/CodeFixReadiness.ts +0 -32
  92. package/Server/Utils/AI/CodeFix/FixRunBudget.ts +27 -25
  93. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +7 -1
  94. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +34 -14
  95. package/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.ts +69 -24
  96. package/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.ts +9 -0
  97. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +53 -10
  98. package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +27 -10
  99. package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +27 -10
  100. package/Server/Utils/AI/SRE/InvestigationQueue.ts +16 -19
  101. package/Server/Utils/AI/SRE/InvestigationThread.ts +646 -0
  102. package/Server/Utils/AI/SRE/InvestigationThreadService.ts +715 -0
  103. package/Server/Utils/AI/Toolbox/Serializer.ts +10 -3
  104. package/Server/Utils/EditionEnforcement.ts +19 -202
  105. package/Server/Utils/LLM/LLMService.ts +259 -63
  106. package/Server/Utils/RelaxedScimTeamLocksReport.ts +245 -0
  107. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +25 -0
  108. package/Server/Utils/SameOriginRequest.ts +203 -0
  109. package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +2 -0
  110. package/Server/Utils/Telemetry/EntityRegistry.ts +4 -1
  111. package/Server/Utils/Telemetry/MessageQueueDiscovery.ts +1402 -0
  112. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +4 -1
  113. package/Server/Utils/VendorAssets.ts +3 -3
  114. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +3 -1
  115. package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +3 -1
  116. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +10 -1
  117. package/Tests/App/Accounts/IncidentFormPage.test.tsx +2036 -0
  118. package/Tests/App/Accounts/IncidentFormServerMessages.test.ts +119 -0
  119. package/Tests/App/Accounts/IncidentFormUtils.test.ts +1157 -0
  120. package/Tests/App/Accounts/LoginPasskey.test.tsx +6 -4
  121. package/Tests/App/Accounts/LoginSso.test.tsx +724 -0
  122. package/Tests/App/AdminDashboard/AdminAuthenticationRequireSso.test.tsx +338 -0
  123. package/Tests/App/AdminDashboard/AdminAuthenticationSsoCardLocales.test.tsx +331 -0
  124. package/Tests/App/AdminDashboard/AdminEnterprisePluginPage.test.tsx +47 -33
  125. package/Tests/App/AdminDashboard/GlobalSsoOidcPages.test.tsx +1021 -0
  126. package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +876 -78
  127. package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +20 -3
  128. package/Tests/App/Dashboard/CephProxmoxVMwareDocumentationCards.test.tsx +359 -0
  129. package/Tests/App/Dashboard/CephSetupGuide.test.ts +961 -0
  130. package/Tests/App/Dashboard/CloudDocumentationCard.test.tsx +381 -95
  131. package/Tests/App/Dashboard/CloudSetupGuide.test.ts +1463 -0
  132. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +174 -12
  133. package/Tests/App/Dashboard/ContainerSetupGuideCards.test.tsx +345 -0
  134. package/Tests/App/Dashboard/CoreListEmptyStates.test.tsx +501 -0
  135. package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +0 -1
  136. package/Tests/App/Dashboard/DatabaseDocumentationCard.test.tsx +400 -0
  137. package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +5 -0
  138. package/Tests/App/Dashboard/DatabaseSetupGuide.test.ts +1869 -0
  139. package/Tests/App/Dashboard/DatabaseTelemetryScope.test.ts +44 -0
  140. package/Tests/App/Dashboard/DatabaseTelemetryTabs.test.tsx +50 -9
  141. package/Tests/App/Dashboard/DockerSetupGuide.test.ts +899 -0
  142. package/Tests/App/Dashboard/DockerSwarmSetupGuide.test.ts +964 -0
  143. package/Tests/App/Dashboard/EnterpriseFeatureUpgrade.test.tsx +141 -7
  144. package/Tests/App/Dashboard/EnterprisePluginPage.test.tsx +52 -39
  145. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +7 -1
  146. package/Tests/App/Dashboard/GettingStartedTaskRoutes.test.tsx +331 -0
  147. package/Tests/App/Dashboard/HomeOverviewStatsFirstRun.test.tsx +925 -0
  148. package/Tests/App/Dashboard/HostDocumentationCard.test.tsx +225 -0
  149. package/Tests/App/Dashboard/HostSetupGuide.test.ts +1183 -0
  150. package/Tests/App/Dashboard/IdentityEnterpriseShells.test.tsx +69 -45
  151. package/Tests/App/Dashboard/IncidentCreateTemplateCustomFieldSettings.test.tsx +745 -0
  152. package/Tests/App/Dashboard/IncidentCreateTemplateOwners.test.tsx +414 -0
  153. package/Tests/App/Dashboard/IncidentCustomFieldCreateSettingsForm.test.ts +1042 -0
  154. package/Tests/App/Dashboard/IncidentCustomFieldSettingsCard.test.tsx +1816 -0
  155. package/Tests/App/Dashboard/IncidentFormPlan.test.ts +89 -0
  156. package/Tests/App/Dashboard/IncidentFormShareLinkCard.test.tsx +846 -0
  157. package/Tests/App/Dashboard/IncidentFormsPages.test.tsx +1324 -0
  158. package/Tests/App/Dashboard/IncidentTemplateCustomFieldSettings.test.tsx +630 -0
  159. package/Tests/App/Dashboard/IncidentsSideMenu.test.tsx +13 -0
  160. package/Tests/App/Dashboard/IncomingEmailMonitorLinkVerifyHint.test.tsx +104 -0
  161. package/Tests/App/Dashboard/InvestigationConversation.test.tsx +1107 -0
  162. package/Tests/App/Dashboard/InvestigationConversationData.test.ts +546 -0
  163. package/Tests/App/Dashboard/InvestigationEvidenceList.test.tsx +170 -5
  164. package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +207 -2
  165. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +729 -34
  166. package/Tests/App/Dashboard/InvestigationPanelStatus.test.tsx +9 -21
  167. package/Tests/App/Dashboard/InvestigationReportView.test.tsx +274 -37
  168. package/Tests/App/Dashboard/InvestigationStatusBadge.test.tsx +130 -0
  169. package/Tests/App/Dashboard/IoTDocumentationCard.test.tsx +218 -0
  170. package/Tests/App/Dashboard/IoTSetupGuide.test.ts +1124 -0
  171. package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +20 -6
  172. package/Tests/App/Dashboard/KubernetesClusterAiDocumentationMarkdown.test.ts +107 -83
  173. package/Tests/App/Dashboard/KubernetesSetupGuide.test.ts +787 -0
  174. package/Tests/App/Dashboard/MessageQueueBreadcrumbs.test.tsx +228 -0
  175. package/Tests/App/Dashboard/MessageQueueBrokerHealth.test.tsx +1538 -0
  176. package/Tests/App/Dashboard/MessageQueueBrokerHealthMetricChart.test.tsx +969 -0
  177. package/Tests/App/Dashboard/MessageQueueBrokerHealthReads.test.ts +889 -0
  178. package/Tests/App/Dashboard/MessageQueueBrokerMetricsReach.test.ts +236 -0
  179. package/Tests/App/Dashboard/MessageQueueDocumentationCard.test.tsx +479 -0
  180. package/Tests/App/Dashboard/MessageQueueListPages.test.tsx +1232 -0
  181. package/Tests/App/Dashboard/MessageQueueMetricDescriptions.test.ts +244 -0
  182. package/Tests/App/Dashboard/MessageQueueMonitorLink.test.ts +952 -0
  183. package/Tests/App/Dashboard/MessageQueueMonitorLinkMonitorCreate.test.tsx +859 -0
  184. package/Tests/App/Dashboard/MessageQueueOverview.test.tsx +1773 -0
  185. package/Tests/App/Dashboard/MessageQueueOverviewPresentation.test.ts +196 -0
  186. package/Tests/App/Dashboard/MessageQueuePresentation.test.ts +799 -0
  187. package/Tests/App/Dashboard/MessageQueueProducersConsumers.test.tsx +680 -0
  188. package/Tests/App/Dashboard/MessageQueueRoutes.test.tsx +383 -0
  189. package/Tests/App/Dashboard/MessageQueueRulePages.test.tsx +278 -0
  190. package/Tests/App/Dashboard/MessageQueueSetupGuide.test.ts +2778 -0
  191. package/Tests/App/Dashboard/MessageQueueSideMenus.test.tsx +273 -0
  192. package/Tests/App/Dashboard/MessageQueueSpanPopulations.test.ts +930 -0
  193. package/Tests/App/Dashboard/MessageQueueSpanStore.ts +351 -0
  194. package/Tests/App/Dashboard/MessageQueueTelemetryScope.test.ts +375 -0
  195. package/Tests/App/Dashboard/MessageQueueTelemetryScopeTabs.test.tsx +668 -0
  196. package/Tests/App/Dashboard/MessageQueueViewPages.test.tsx +685 -0
  197. package/Tests/App/Dashboard/MonitorAndHomeNotOperationalMenus.test.tsx +342 -0
  198. package/Tests/App/Dashboard/MonitorCreateFirstRunDefaults.test.tsx +623 -0
  199. package/Tests/App/Dashboard/MonitorCreateFromMonitorBackedDevice.test.tsx +442 -0
  200. package/Tests/App/Dashboard/MonitorLogsPageViewSummary.test.tsx +814 -0
  201. package/Tests/App/Dashboard/MonitorSetupCardBodies.test.tsx +246 -0
  202. package/Tests/App/Dashboard/NavigationSearchKeywords.test.tsx +8 -0
  203. package/Tests/App/Dashboard/OnboardingSsoEmptyState.test.tsx +27 -12
  204. package/Tests/App/Dashboard/PlanGatedPage.test.tsx +292 -0
  205. package/Tests/App/Dashboard/PodmanSetupGuide.test.ts +948 -0
  206. package/Tests/App/Dashboard/ProductOrientation.test.tsx +5 -4
  207. package/Tests/App/Dashboard/ProductSettingsSectionCollapsed.test.tsx +266 -0
  208. package/Tests/App/Dashboard/ProxmoxSetupGuide.test.ts +1318 -0
  209. package/Tests/App/Dashboard/ResourceAiAgentAttentionServerParity.test.ts +555 -0
  210. package/Tests/App/Dashboard/ResourceAiAgentPage.test.tsx +298 -9
  211. package/Tests/App/Dashboard/ResourceAiAgentStatus.test.ts +453 -0
  212. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +16 -0
  213. package/Tests/App/Dashboard/ResourceConnectionGuideCard.test.tsx +444 -0
  214. package/Tests/App/Dashboard/ResourceConnectionGuideOverviewsInfra.test.tsx +798 -0
  215. package/Tests/App/Dashboard/ResourceConnectionGuideOverviewsPlatforms.test.tsx +717 -0
  216. package/Tests/App/Dashboard/ResourceConnectionGuides.test.ts +981 -0
  217. package/Tests/App/Dashboard/ResourceConnectionState.test.ts +132 -0
  218. package/Tests/App/Dashboard/RumDocumentationCard.test.tsx +293 -0
  219. package/Tests/App/Dashboard/RumSessionReplayRetentionSettings.test.tsx +467 -0
  220. package/Tests/App/Dashboard/RumSetupGuide.test.ts +758 -0
  221. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +25 -0
  222. package/Tests/App/Dashboard/ServerlessDocumentationCard.test.tsx +254 -0
  223. package/Tests/App/Dashboard/ServerlessSetupGuide.test.ts +1074 -0
  224. package/Tests/App/Dashboard/SetupGuide.test.ts +246 -0
  225. package/Tests/App/Dashboard/SetupGuideCard.test.tsx +756 -0
  226. package/Tests/App/Dashboard/SloListSideMenu.test.tsx +120 -0
  227. package/Tests/App/Dashboard/SsoPages.test.tsx +833 -0
  228. package/Tests/App/Dashboard/TelemetryDocumentation.test.tsx +253 -0
  229. package/Tests/App/Dashboard/TelemetryRetentionShells.test.tsx +730 -0
  230. package/Tests/App/Dashboard/VMwareSetupGuide.test.ts +1033 -0
  231. package/Tests/App/StatusPage/StatusPageSsoEmptyState.test.tsx +87 -7
  232. package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +28 -9
  233. package/Tests/Models/DatabaseModels/IncidentFormModel.test.ts +1250 -0
  234. package/Tests/Models/DatabaseModels/MessageQueueModels.test.ts +1983 -0
  235. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  236. package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +190 -14
  237. package/Tests/Models/IncidentTemplateCustomFieldSettingsColumn.test.ts +291 -0
  238. package/Tests/Server/API/AIInvestigationConversationAPI.test.ts +412 -0
  239. package/Tests/Server/API/GlobalConfigLicense.test.ts +38 -9
  240. package/Tests/Server/API/IncidentFormAPI.test.ts +942 -0
  241. package/Tests/Server/API/IncidentFormPublicRoutes.test.ts +1653 -0
  242. package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +2 -1
  243. package/Tests/Server/API/{SsoProviderListingsEdition.test.ts → SsoProviderListings.test.ts} +164 -110
  244. package/Tests/Server/Enterprise/EnterpriseEdition.test.ts +76 -54
  245. package/Tests/Server/Enterprise/EnterpriseFeatureActive.test.ts +125 -42
  246. package/Tests/Server/Enterprise/EnterpriseLicenseSnapshot.test.ts +36 -10
  247. package/Tests/Server/Enterprise/EnterpriseModelFeatureMap.test.ts +51 -9
  248. package/Tests/Server/Enterprise/FakeEnterpriseModule.ts +13 -8
  249. package/Tests/Server/Enterprise/SsoInCommunityEdition.test.ts +540 -0
  250. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  251. package/Tests/Server/Infrastructure/Postgres/AddIncidentFormsMigration.test.ts +438 -0
  252. package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleNotificationChannelsMigration.test.ts +255 -0
  253. package/Tests/Server/Infrastructure/Postgres/MessageQueueTablesMigration.test.ts +637 -0
  254. package/Tests/Server/Middleware/IncidentFormRateLimit.test.ts +1630 -0
  255. package/Tests/Server/Middleware/UserAuthorization.test.ts +16 -17
  256. package/Tests/Server/Middleware/UserAuthorizationSsoEnforcement.test.ts +544 -0
  257. package/Tests/Server/Services/AIServiceDailyBudget.test.ts +37 -8
  258. package/Tests/Server/Services/IncidentCreatedFeedTitle.test.ts +212 -0
  259. package/Tests/Server/Services/IncidentCustomFieldDeleteFormQuestions.test.ts +297 -0
  260. package/Tests/Server/Services/IncidentFormOwnersCreatedNotification.test.ts +497 -0
  261. package/Tests/Server/Services/IncidentFormPostgres.test.ts +958 -0
  262. package/Tests/Server/Services/IncidentFormPublicForm.test.ts +1260 -0
  263. package/Tests/Server/Services/IncidentFormPublicSubmission.test.ts +2520 -0
  264. package/Tests/Server/Services/IncidentFormRegistration.test.ts +159 -0
  265. package/Tests/Server/Services/IncidentFormService.test.ts +663 -0
  266. package/Tests/Server/Services/IncidentFormSubmissionService.test.ts +430 -0
  267. package/Tests/Server/Services/IncidentFormSubmissionTemplateBranch.test.ts +473 -0
  268. package/Tests/Server/Services/IncidentGroupingTemplateSubstitution.test.ts +81 -0
  269. package/Tests/Server/Services/IncidentTemplateCustomFieldSettings.test.ts +262 -0
  270. package/Tests/Server/Services/IncidentTemplateOwnersOnDeclare.test.ts +566 -0
  271. package/Tests/Server/Services/IncidentTitleMarkdownSinks.test.ts +640 -0
  272. package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +3 -3
  273. package/Tests/Server/Services/MessageQueueOwnerServices.test.ts +440 -0
  274. package/Tests/Server/Services/MessageQueueRegistries.test.ts +577 -0
  275. package/Tests/Server/Services/MessageQueueRuleEngines.test.ts +900 -0
  276. package/Tests/Server/Services/MessageQueueService.test.ts +2210 -0
  277. package/Tests/Server/Services/MessageQueueSqlPostgres.test.ts +1271 -0
  278. package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +28 -1
  279. package/Tests/Server/Services/OpenTelemetryRetentionOverrideEdition.test.ts +474 -0
  280. package/Tests/Server/Services/OpenTelemetryServiceResolutionCache.test.ts +21 -0
  281. package/Tests/Server/Services/OwnerRowUniqueness.test.ts +3 -0
  282. package/Tests/Server/Services/ProjectServiceAuditLogLicense.test.ts +0 -1
  283. package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +56 -6
  284. package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -1
  285. package/Tests/Server/Services/SsoRequirementReads.test.ts +454 -0
  286. package/Tests/Server/Services/SsoRequirementWrites.test.ts +529 -0
  287. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +3 -3
  288. package/Tests/Server/Services/StatusPagePrivateUserInvitationPolicy.test.ts +107 -89
  289. package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +1068 -176
  290. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +1962 -200
  291. package/Tests/Server/Types/Database/Permissions/EditionPermission.test.ts +654 -297
  292. package/Tests/Server/Types/Database/Permissions/RetentionOverrideColumns.test.ts +784 -0
  293. package/Tests/Server/Types/Database/Permissions/SsoScalePlanGate.test.ts +418 -0
  294. package/Tests/Server/Utils/AI/AIAlertGating.test.ts +0 -1
  295. package/Tests/Server/Utils/AI/AIInvestigationEngineRunLimits.test.ts +136 -0
  296. package/Tests/Server/Utils/AI/AIInvestigationQueue.test.ts +15 -14
  297. package/Tests/Server/Utils/AI/AgentContextCompactor.test.ts +351 -0
  298. package/Tests/Server/Utils/AI/ChatAgentRunnerSharedThread.test.ts +842 -0
  299. package/Tests/Server/Utils/AI/ChatRunCancellation.test.ts +195 -0
  300. package/Tests/Server/Utils/AI/CodeFixReadiness.test.ts +10 -57
  301. package/Tests/Server/Utils/AI/FixRunBudget.test.ts +20 -24
  302. package/Tests/Server/Utils/AI/Insights/InvestigationQueueInsightSubject.test.ts +7 -30
  303. package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +225 -29
  304. package/Tests/Server/Utils/AI/InvestigationInfrastructureAccessWiring.test.ts +266 -25
  305. package/Tests/Server/Utils/AI/InvestigationThread.test.ts +580 -0
  306. package/Tests/Server/Utils/AI/InvestigationThreadService.test.ts +801 -0
  307. package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +525 -43
  308. package/Tests/Server/Utils/AI/KubectlJobRunner.test.ts +4 -2
  309. package/Tests/Server/Utils/AI/LLMServiceUnreachableProvider.test.ts +435 -0
  310. package/Tests/Server/Utils/AI/ObservabilityAssistantLimits.test.ts +462 -0
  311. package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +14 -25
  312. package/Tests/Server/Utils/AI/RemediationExecutionRunnerResourceMode.test.ts +2 -0
  313. package/Tests/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.test.ts +297 -15
  314. package/Tests/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.test.ts +10 -6
  315. package/Tests/Server/Utils/AI/ToolOutputPager.test.ts +382 -0
  316. package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +23 -4
  317. package/Tests/Server/Utils/AI/Toolbox/LogTools.test.ts +25 -6
  318. package/Tests/Server/Utils/AI/Toolbox/Serializer.test.ts +14 -3
  319. package/Tests/Server/Utils/EditionEnforcement.test.ts +107 -147
  320. package/Tests/Server/Utils/{CommunityEditionSsoReport.test.ts → RelaxedScimTeamLocksReport.test.ts} +212 -208
  321. package/Tests/Server/Utils/SameOriginRequest.test.ts +372 -0
  322. package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +20 -0
  323. package/Tests/Server/Utils/Telemetry/MessageQueueDiscovery.test.ts +2590 -0
  324. package/Tests/Server/Utils/Telemetry/MessageQueueSpanQueryTwin.ts +246 -0
  325. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +23 -0
  326. package/Tests/Types/AI/AIAgentRunLimits.test.ts +255 -0
  327. package/Tests/Types/Code/JsonSyntax.test.ts +1300 -0
  328. package/Tests/Types/CustomField/CustomFieldCreateSettings.test.ts +1113 -0
  329. package/Tests/Types/CustomField/CustomFieldMappingCatalog.test.ts +91 -0
  330. package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +30 -0
  331. package/Tests/Types/Incident/IncidentFormIpAllowlist.test.ts +227 -0
  332. package/Tests/Types/Incident/IncidentFormPublic.test.ts +1916 -0
  333. package/Tests/Types/MessageQueue/MessageQueueEntityKey.test.ts +916 -0
  334. package/Tests/Types/MessageQueue/MessageQueueIdentity.test.ts +567 -0
  335. package/Tests/Types/MessageQueue/MessageQueueManualIdentity.test.ts +686 -0
  336. package/Tests/Types/MessageQueue/MessageQueueMetricCatalog.test.ts +1726 -0
  337. package/Tests/Types/MessageQueue/MessageQueueMetricResolution.test.ts +1758 -0
  338. package/Tests/Types/MessageQueue/MessagingInstrumentationFixtures.test.ts +4686 -0
  339. package/Tests/Types/MessageQueue/MessagingSystem.test.ts +840 -0
  340. package/Tests/Types/MessageQueue/MessagingTelemetryFixtures.ts +3287 -0
  341. package/Tests/Types/MessageQueue/MessagingTelemetryResolver.test.ts +2966 -0
  342. package/Tests/Types/Monitor/MessageQueueAlertTemplates.test.ts +5401 -0
  343. package/Tests/Types/Monitor/MetricEngineFold.ts +154 -0
  344. package/Tests/Types/ResourceAiAgent/ResourceAiAccess.test.ts +5 -3
  345. package/Tests/Types/Team/ComplianceRule.test.ts +588 -37
  346. package/Tests/Types/Team/TeamComplianceStatus.test.ts +64 -11
  347. package/Tests/Types/XML.test.ts +51 -0
  348. package/Tests/UI/Components/CodeEditor/CodeEditor.test.tsx +2362 -0
  349. package/Tests/UI/Components/CodeEditor/CodeEditorCommands.test.ts +810 -0
  350. package/Tests/UI/Components/CodeEditor/CodeEditorHighlight.test.ts +338 -0
  351. package/Tests/UI/Components/CodeEditor/CodeEditorLanguages.test.ts +222 -0
  352. package/Tests/UI/Components/CodeEditor/CodeEditorStyles.test.ts +807 -0
  353. package/Tests/UI/Components/CodeEditor/NoMonaco.test.ts +245 -0
  354. package/Tests/UI/Components/CodeEditor/YamlEditor.test.tsx +729 -0
  355. package/Tests/UI/Components/CopyTextButton.test.tsx +76 -0
  356. package/Tests/UI/Components/EditionLabelLapseCopy.ts +51 -14
  357. package/Tests/UI/Components/EditionLabelLicenseStatus.test.tsx +131 -23
  358. package/Tests/UI/Components/ErrorMessageAction.test.tsx +142 -0
  359. package/Tests/UI/Components/FacetSidebarInScope.test.tsx +459 -0
  360. package/Tests/UI/Components/FacetValuesInScope.test.ts +270 -0
  361. package/Tests/UI/Components/Forms/FormFieldCodeEditors.test.tsx +294 -0
  362. package/Tests/UI/Components/Forms/FormFieldMarkdown.test.tsx +126 -0
  363. package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +22 -75
  364. package/Tests/UI/Components/Forms/ModelFormOnBeforeCreateMiscData.test.tsx +254 -0
  365. package/Tests/UI/Components/Icon.test.tsx +32 -0
  366. package/Tests/UI/Components/Input.test.tsx +31 -0
  367. package/Tests/UI/Components/LockedFilterChipScope.test.tsx +501 -0
  368. package/Tests/UI/Components/MarkdownConverters.test.ts +253 -1
  369. package/Tests/UI/Components/MarkdownEditor.test.tsx +2785 -2
  370. package/Tests/UI/Components/MarkdownEditorHistory.test.ts +243 -0
  371. package/Tests/UI/Components/MarkdownListEditing.test.ts +1118 -0
  372. package/Tests/UI/Components/MarkdownPaste.test.ts +1180 -0
  373. package/Tests/UI/Components/MarkdownViewerCopy.test.tsx +466 -0
  374. package/Tests/UI/Components/MarkdownVisualEditing.test.ts +1089 -0
  375. package/Tests/UI/Components/ModelTable/BaseModelTableEmptyStateAction.test.tsx +623 -0
  376. package/Tests/UI/Components/ResetObjectID.test.tsx +282 -0
  377. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  378. package/Tests/UI/Components/Table/EmptyTableMessage.test.ts +439 -0
  379. package/Tests/UI/Components/Table/TableEmptyState.test.tsx +352 -0
  380. package/Tests/UI/Components/TableRowMobileTitles.test.tsx +78 -0
  381. package/Tests/UI/Components/TextArea.test.tsx +19 -0
  382. package/Tests/UI/Components/Workflow/ModelColumnEditor.render.test.tsx +12 -29
  383. package/Tests/UI/Components/fixtures/MarkdownPasteFixtures.ts +460 -0
  384. package/Tests/UI/EsbuildConfig.test.ts +115 -39
  385. package/Tests/UI/EsbuildEnterpriseAlias.test.ts +4 -4
  386. package/Tests/UI/Monitor/MonitorLogSummaryRender.test.tsx +377 -0
  387. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +81 -1
  388. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +17 -0
  389. package/Tests/Utils/LabelRuleImportExport.test.ts +5 -0
  390. package/Tests/Utils/Markdown/DashboardMarkdownRenderer.ts +81 -0
  391. package/Tests/Utils/Markdown/UntrustedMarkdown.test.ts +1339 -0
  392. package/Tests/Utils/Monitor/MonitorLogSummaryUtil.test.ts +862 -0
  393. package/Types/AI/AIAgentRunLimits.ts +146 -0
  394. package/Types/Code/CodeType.ts +2 -0
  395. package/Types/Code/JsonSyntax.ts +923 -0
  396. package/Types/CustomField/CustomFieldCreateSettings.ts +549 -0
  397. package/Types/CustomField/CustomFieldMappingCatalog.ts +77 -0
  398. package/Types/CustomField/CustomFieldValueValidator.ts +10 -7
  399. package/Types/EnterpriseLicense/EnterpriseLicensePeriods.ts +1 -1
  400. package/Types/Icon/IconProp.ts +2 -0
  401. package/Types/Incident/IncidentFormIpAllowlist.ts +206 -0
  402. package/Types/Incident/IncidentFormPublic.ts +1145 -0
  403. package/Types/Kubernetes/KubernetesClusterAiAccess.ts +10 -3
  404. package/Types/MessageQueue/MessageQueueIdentity.ts +256 -0
  405. package/Types/MessageQueue/MessageQueueManualIdentity.ts +325 -0
  406. package/Types/MessageQueue/MessageQueueMetricCatalog.ts +1357 -0
  407. package/Types/MessageQueue/MessagingSystem.ts +518 -0
  408. package/Types/MessageQueue/MessagingTelemetryResolver.ts +2311 -0
  409. package/Types/Monitor/MessageQueueAlertTemplates.ts +2128 -0
  410. package/Types/Permission.ts +310 -0
  411. package/Types/ResourceAiAgent/ResourceAiAccess.ts +10 -4
  412. package/Types/Rules/RuleCriteriaFieldRegistry.ts +12 -0
  413. package/Types/Rules/RuleRun.ts +4 -0
  414. package/Types/Team/ComplianceNotificationChannel.ts +4 -3
  415. package/Types/Team/ComplianceRule.ts +71 -18
  416. package/Types/Team/ComplianceRuleType.ts +2 -2
  417. package/Types/Team/TeamComplianceStatus.ts +6 -2
  418. package/Types/Telemetry/EntityType.ts +16 -0
  419. package/Types/Telemetry/LockedFilterDetail.ts +22 -0
  420. package/Types/XML.ts +26 -0
  421. package/UI/Components/CodeEditor/CodeEditor.css +379 -0
  422. package/UI/Components/CodeEditor/CodeEditor.tsx +1128 -294
  423. package/UI/Components/CodeEditor/CodeEditorCommands.ts +774 -0
  424. package/UI/Components/CodeEditor/CodeEditorHighlight.ts +106 -0
  425. package/UI/Components/CodeEditor/CodeEditorLanguages.ts +164 -0
  426. package/UI/Components/CodeEditor/YamlEditor.tsx +28 -326
  427. package/UI/Components/CopyTextButton/CopyTextButton.tsx +5 -1
  428. package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +6 -25
  429. package/UI/Components/EditionLabel/EditionLabel.tsx +14 -13
  430. package/UI/Components/ErrorMessage/ErrorMessage.tsx +11 -1
  431. package/UI/Components/Forms/Fields/FormField.tsx +8 -0
  432. package/UI/Components/Forms/Types/Field.ts +8 -0
  433. package/UI/Components/Icon/Icon.tsx +18 -0
  434. package/UI/Components/Input/Input.tsx +11 -4
  435. package/UI/Components/LogsViewer/LogsViewer.tsx +4 -0
  436. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +21 -3
  437. package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
  438. package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +877 -79
  439. package/UI/Components/Markdown.tsx/MarkdownEditorHistory.ts +284 -0
  440. package/UI/Components/Markdown.tsx/MarkdownListEditing.ts +1362 -0
  441. package/UI/Components/Markdown.tsx/MarkdownPaste.ts +1326 -0
  442. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +106 -10
  443. package/UI/Components/Markdown.tsx/MarkdownVisualEditing.ts +1032 -0
  444. package/UI/Components/ModelTable/BaseModelTable.tsx +85 -7
  445. package/UI/Components/ResetObjectID/ResetObjectID.tsx +35 -9
  446. package/UI/Components/Table/EmptyTableMessage.ts +81 -0
  447. package/UI/Components/Table/Table.tsx +50 -21
  448. package/UI/Components/Table/TableRow.tsx +9 -1
  449. package/UI/Components/TelemetryViewer/FacetVisibility.ts +97 -2
  450. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +13 -0
  451. package/UI/Components/TelemetryViewer/components/LockedFilterChip.tsx +214 -7
  452. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +21 -3
  453. package/UI/Components/TextArea/TextArea.tsx +9 -2
  454. package/UI/Components/Workflow/ModelFieldPicker.tsx +10 -12
  455. package/UI/Styles/Theme.css +8 -0
  456. package/UI/esbuild-config.js +2 -32
  457. package/Utils/LabelRuleImportExport.ts +2 -0
  458. package/Utils/Markdown/UntrustedMarkdown.ts +1513 -0
  459. package/Utils/Monitor/IncomingEmailMonitorRequestUtil.ts +41 -0
  460. package/Utils/Monitor/MonitorLogSummaryUtil.ts +212 -0
  461. package/Utils/Telemetry/EntityKey.ts +48 -0
  462. package/build/dist/Models/DatabaseModels/AIConversation.js +125 -1
  463. package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -1
  464. package/build/dist/Models/DatabaseModels/CephCluster.js +12 -0
  465. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  466. package/build/dist/Models/DatabaseModels/CloudResource.js +12 -0
  467. package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
  468. package/build/dist/Models/DatabaseModels/DatabaseServer.js +12 -0
  469. package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -1
  470. package/build/dist/Models/DatabaseModels/DockerHost.js +12 -0
  471. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  472. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js +12 -0
  473. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js.map +1 -1
  474. package/build/dist/Models/DatabaseModels/GlobalOidc.js +0 -4
  475. package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -1
  476. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js +0 -4
  477. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js.map +1 -1
  478. package/build/dist/Models/DatabaseModels/GlobalSso.js +0 -4
  479. package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -1
  480. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js +0 -4
  481. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js.map +1 -1
  482. package/build/dist/Models/DatabaseModels/Host.js +12 -0
  483. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  484. package/build/dist/Models/DatabaseModels/IncidentForm.js +605 -0
  485. package/build/dist/Models/DatabaseModels/IncidentForm.js.map +1 -0
  486. package/build/dist/Models/DatabaseModels/IncidentFormSubmission.js +299 -0
  487. package/build/dist/Models/DatabaseModels/IncidentFormSubmission.js.map +1 -0
  488. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +60 -0
  489. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  490. package/build/dist/Models/DatabaseModels/Index.js +14 -0
  491. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  492. package/build/dist/Models/DatabaseModels/IoTFleet.js +12 -0
  493. package/build/dist/Models/DatabaseModels/IoTFleet.js.map +1 -1
  494. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +12 -0
  495. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  496. package/build/dist/Models/DatabaseModels/MessageQueue.js +1106 -0
  497. package/build/dist/Models/DatabaseModels/MessageQueue.js.map +1 -0
  498. package/build/dist/Models/DatabaseModels/MessageQueueLabelRule.js +562 -0
  499. package/build/dist/Models/DatabaseModels/MessageQueueLabelRule.js.map +1 -0
  500. package/build/dist/Models/DatabaseModels/MessageQueueOwnerRule.js +643 -0
  501. package/build/dist/Models/DatabaseModels/MessageQueueOwnerRule.js.map +1 -0
  502. package/build/dist/Models/DatabaseModels/MessageQueueOwnerTeam.js +506 -0
  503. package/build/dist/Models/DatabaseModels/MessageQueueOwnerTeam.js.map +1 -0
  504. package/build/dist/Models/DatabaseModels/MessageQueueOwnerUser.js +505 -0
  505. package/build/dist/Models/DatabaseModels/MessageQueueOwnerUser.js.map +1 -0
  506. package/build/dist/Models/DatabaseModels/PodmanHost.js +12 -0
  507. package/build/dist/Models/DatabaseModels/PodmanHost.js.map +1 -1
  508. package/build/dist/Models/DatabaseModels/Project.js +61 -0
  509. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  510. package/build/dist/Models/DatabaseModels/ProjectOidc.js +0 -4
  511. package/build/dist/Models/DatabaseModels/ProjectOidc.js.map +1 -1
  512. package/build/dist/Models/DatabaseModels/ProjectSso.js +0 -4
  513. package/build/dist/Models/DatabaseModels/ProjectSso.js.map +1 -1
  514. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +12 -0
  515. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  516. package/build/dist/Models/DatabaseModels/RumApplication.js +12 -0
  517. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  518. package/build/dist/Models/DatabaseModels/ServerlessFunction.js +12 -0
  519. package/build/dist/Models/DatabaseModels/ServerlessFunction.js.map +1 -1
  520. package/build/dist/Models/DatabaseModels/Service.js +12 -0
  521. package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
  522. package/build/dist/Models/DatabaseModels/StatusPageOidc.js +0 -4
  523. package/build/dist/Models/DatabaseModels/StatusPageOidc.js.map +1 -1
  524. package/build/dist/Models/DatabaseModels/StatusPageSso.js +0 -4
  525. package/build/dist/Models/DatabaseModels/StatusPageSso.js.map +1 -1
  526. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js +49 -3
  527. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js.map +1 -1
  528. package/build/dist/Models/DatabaseModels/VMwareVCenter.js +12 -0
  529. package/build/dist/Models/DatabaseModels/VMwareVCenter.js.map +1 -1
  530. package/build/dist/Server/API/AIChatAPI.js +9 -103
  531. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  532. package/build/dist/Server/API/AIInvestigationAPI.js +3 -3
  533. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  534. package/build/dist/Server/API/AIInvestigationConversationAPI.js +162 -0
  535. package/build/dist/Server/API/AIInvestigationConversationAPI.js.map +1 -0
  536. package/build/dist/Server/API/IncidentFormAPI.js +245 -0
  537. package/build/dist/Server/API/IncidentFormAPI.js.map +1 -0
  538. package/build/dist/Server/API/ProjectOIDC.js +16 -24
  539. package/build/dist/Server/API/ProjectOIDC.js.map +1 -1
  540. package/build/dist/Server/API/ProjectSSO.js +16 -24
  541. package/build/dist/Server/API/ProjectSSO.js.map +1 -1
  542. package/build/dist/Server/API/StatusPageAPI.js +42 -75
  543. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  544. package/build/dist/Server/Enterprise/EnterpriseEdition.js +79 -25
  545. package/build/dist/Server/Enterprise/EnterpriseEdition.js.map +1 -1
  546. package/build/dist/Server/Enterprise/EnterpriseFeature.js +24 -3
  547. package/build/dist/Server/Enterprise/EnterpriseFeature.js.map +1 -1
  548. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js +1 -1
  549. package/build/dist/Server/Enterprise/EnterpriseServerModule.js.map +1 -1
  550. package/build/dist/Server/EnvironmentConfig.js +1 -1
  551. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796400000000-AddIncidentForms.js +63 -0
  552. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796400000000-AddIncidentForms.js.map +1 -0
  553. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796500000000-AddTeamComplianceRuleNotificationChannels.js +41 -0
  554. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796500000000-AddTeamComplianceRuleNotificationChannels.js.map +1 -0
  555. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796600000000-AddAIInvestigationConversationAndTimeLimits.js +40 -0
  556. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796600000000-AddAIInvestigationConversationAndTimeLimits.js.map +1 -0
  557. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796700000000-AddMessageQueueTables.js +183 -0
  558. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796700000000-AddMessageQueueTables.js.map +1 -0
  559. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  560. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  561. package/build/dist/Server/Middleware/IncidentFormRateLimit.js +559 -0
  562. package/build/dist/Server/Middleware/IncidentFormRateLimit.js.map +1 -0
  563. package/build/dist/Server/Middleware/UserAuthorization.js +11 -30
  564. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  565. package/build/dist/Server/Services/AIService.js +10 -13
  566. package/build/dist/Server/Services/AIService.js.map +1 -1
  567. package/build/dist/Server/Services/IncidentCustomFieldService.js +83 -0
  568. package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
  569. package/build/dist/Server/Services/IncidentEpisodeMemberService.js +8 -4
  570. package/build/dist/Server/Services/IncidentEpisodeMemberService.js.map +1 -1
  571. package/build/dist/Server/Services/IncidentEpisodeService.js +10 -1
  572. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  573. package/build/dist/Server/Services/IncidentFormService.js +1202 -0
  574. package/build/dist/Server/Services/IncidentFormService.js.map +1 -0
  575. package/build/dist/Server/Services/IncidentFormSubmissionService.js +91 -0
  576. package/build/dist/Server/Services/IncidentFormSubmissionService.js.map +1 -0
  577. package/build/dist/Server/Services/IncidentGroupingEngineService.js +20 -8
  578. package/build/dist/Server/Services/IncidentGroupingEngineService.js.map +1 -1
  579. package/build/dist/Server/Services/IncidentService.js +106 -4
  580. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  581. package/build/dist/Server/Services/IncidentTemplateService.js +18 -0
  582. package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
  583. package/build/dist/Server/Services/Index.js +14 -0
  584. package/build/dist/Server/Services/Index.js.map +1 -1
  585. package/build/dist/Server/Services/MessageQueueLabelRuleEngineService.js +262 -0
  586. package/build/dist/Server/Services/MessageQueueLabelRuleEngineService.js.map +1 -0
  587. package/build/dist/Server/Services/MessageQueueLabelRuleService.js +13 -0
  588. package/build/dist/Server/Services/MessageQueueLabelRuleService.js.map +1 -0
  589. package/build/dist/Server/Services/MessageQueueOwnerRuleEngineService.js +334 -0
  590. package/build/dist/Server/Services/MessageQueueOwnerRuleEngineService.js.map +1 -0
  591. package/build/dist/Server/Services/MessageQueueOwnerRuleService.js +13 -0
  592. package/build/dist/Server/Services/MessageQueueOwnerRuleService.js.map +1 -0
  593. package/build/dist/Server/Services/MessageQueueOwnerTeamService.js +62 -0
  594. package/build/dist/Server/Services/MessageQueueOwnerTeamService.js.map +1 -0
  595. package/build/dist/Server/Services/MessageQueueOwnerUserService.js +62 -0
  596. package/build/dist/Server/Services/MessageQueueOwnerUserService.js.map +1 -0
  597. package/build/dist/Server/Services/MessageQueueService.js +1140 -0
  598. package/build/dist/Server/Services/MessageQueueService.js.map +1 -0
  599. package/build/dist/Server/Services/OpenTelemetryIngestService.js +25 -4
  600. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  601. package/build/dist/Server/Services/ProjectService.js +0 -54
  602. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  603. package/build/dist/Server/Services/StatusPagePrivateUserService.js +1 -4
  604. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  605. package/build/dist/Server/Services/StatusPageService.js +0 -46
  606. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  607. package/build/dist/Server/Services/TeamComplianceSettingService.js +120 -31
  608. package/build/dist/Server/Services/TeamComplianceSettingService.js.map +1 -1
  609. package/build/dist/Server/Services/UserNotificationRuleService.js +8 -1
  610. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  611. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +12 -3
  612. package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
  613. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +6 -3
  614. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
  615. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js +109 -37
  616. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js.map +1 -1
  617. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +16 -0
  618. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
  619. package/build/dist/Server/Types/Database/Permissions/TablePermission.js +3 -3
  620. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js +9 -6
  621. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js.map +1 -1
  622. package/build/dist/Server/Utils/AI/Chat/AgentContextCompactor.js +128 -0
  623. package/build/dist/Server/Utils/AI/Chat/AgentContextCompactor.js.map +1 -0
  624. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +192 -50
  625. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
  626. package/build/dist/Server/Utils/AI/Chat/ChatRunCancellation.js +133 -0
  627. package/build/dist/Server/Utils/AI/Chat/ChatRunCancellation.js.map +1 -0
  628. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +53 -13
  629. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
  630. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +1 -1
  631. package/build/dist/Server/Utils/AI/Chat/ToolOutputPager.js +206 -0
  632. package/build/dist/Server/Utils/AI/Chat/ToolOutputPager.js.map +1 -0
  633. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +53 -26
  634. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -1
  635. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +4 -2
  636. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -1
  637. package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js +1 -29
  638. package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js.map +1 -1
  639. package/build/dist/Server/Utils/AI/CodeFix/FixRunBudget.js +24 -24
  640. package/build/dist/Server/Utils/AI/CodeFix/FixRunBudget.js.map +1 -1
  641. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +7 -2
  642. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  643. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +29 -14
  644. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  645. package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js +38 -14
  646. package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js.map +1 -1
  647. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js +2 -0
  648. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js.map +1 -1
  649. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +40 -10
  650. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  651. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +19 -7
  652. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
  653. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +19 -7
  654. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
  655. package/build/dist/Server/Utils/AI/SRE/InvestigationQueue.js +15 -18
  656. package/build/dist/Server/Utils/AI/SRE/InvestigationQueue.js.map +1 -1
  657. package/build/dist/Server/Utils/AI/SRE/InvestigationThread.js +412 -0
  658. package/build/dist/Server/Utils/AI/SRE/InvestigationThread.js.map +1 -0
  659. package/build/dist/Server/Utils/AI/SRE/InvestigationThreadService.js +495 -0
  660. package/build/dist/Server/Utils/AI/SRE/InvestigationThreadService.js.map +1 -0
  661. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +10 -3
  662. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -1
  663. package/build/dist/Server/Utils/EditionEnforcement.js +20 -158
  664. package/build/dist/Server/Utils/EditionEnforcement.js.map +1 -1
  665. package/build/dist/Server/Utils/LLM/LLMService.js +193 -54
  666. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  667. package/build/dist/Server/Utils/RelaxedScimTeamLocksReport.js +160 -0
  668. package/build/dist/Server/Utils/RelaxedScimTeamLocksReport.js.map +1 -0
  669. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +25 -0
  670. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  671. package/build/dist/Server/Utils/SameOriginRequest.js +151 -0
  672. package/build/dist/Server/Utils/SameOriginRequest.js.map +1 -0
  673. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +2 -0
  674. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -1
  675. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +4 -1
  676. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  677. package/build/dist/Server/Utils/Telemetry/MessageQueueDiscovery.js +1015 -0
  678. package/build/dist/Server/Utils/Telemetry/MessageQueueDiscovery.js.map +1 -0
  679. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +4 -1
  680. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
  681. package/build/dist/Server/Utils/VendorAssets.js +3 -3
  682. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +3 -1
  683. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  684. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +3 -1
  685. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
  686. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +10 -1
  687. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  688. package/build/dist/Types/AI/AIAgentRunLimits.js +91 -0
  689. package/build/dist/Types/AI/AIAgentRunLimits.js.map +1 -0
  690. package/build/dist/Types/Code/CodeType.js +2 -0
  691. package/build/dist/Types/Code/CodeType.js.map +1 -1
  692. package/build/dist/Types/Code/JsonSyntax.js +604 -0
  693. package/build/dist/Types/Code/JsonSyntax.js.map +1 -0
  694. package/build/dist/Types/CustomField/CustomFieldCreateSettings.js +310 -0
  695. package/build/dist/Types/CustomField/CustomFieldCreateSettings.js.map +1 -0
  696. package/build/dist/Types/CustomField/CustomFieldMappingCatalog.js +38 -0
  697. package/build/dist/Types/CustomField/CustomFieldMappingCatalog.js.map +1 -1
  698. package/build/dist/Types/CustomField/CustomFieldValueValidator.js +10 -7
  699. package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -1
  700. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js +1 -1
  701. package/build/dist/Types/Icon/IconProp.js +2 -0
  702. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  703. package/build/dist/Types/Incident/IncidentFormIpAllowlist.js +119 -0
  704. package/build/dist/Types/Incident/IncidentFormIpAllowlist.js.map +1 -0
  705. package/build/dist/Types/Incident/IncidentFormPublic.js +678 -0
  706. package/build/dist/Types/Incident/IncidentFormPublic.js.map +1 -0
  707. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +10 -3
  708. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -1
  709. package/build/dist/Types/MessageQueue/MessageQueueIdentity.js +184 -0
  710. package/build/dist/Types/MessageQueue/MessageQueueIdentity.js.map +1 -0
  711. package/build/dist/Types/MessageQueue/MessageQueueManualIdentity.js +190 -0
  712. package/build/dist/Types/MessageQueue/MessageQueueManualIdentity.js.map +1 -0
  713. package/build/dist/Types/MessageQueue/MessageQueueMetricCatalog.js +1057 -0
  714. package/build/dist/Types/MessageQueue/MessageQueueMetricCatalog.js.map +1 -0
  715. package/build/dist/Types/MessageQueue/MessagingSystem.js +394 -0
  716. package/build/dist/Types/MessageQueue/MessagingSystem.js.map +1 -0
  717. package/build/dist/Types/MessageQueue/MessagingTelemetryResolver.js +1768 -0
  718. package/build/dist/Types/MessageQueue/MessagingTelemetryResolver.js.map +1 -0
  719. package/build/dist/Types/Monitor/MessageQueueAlertTemplates.js +1207 -0
  720. package/build/dist/Types/Monitor/MessageQueueAlertTemplates.js.map +1 -0
  721. package/build/dist/Types/Permission.js +274 -0
  722. package/build/dist/Types/Permission.js.map +1 -1
  723. package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js +10 -4
  724. package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js.map +1 -1
  725. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +12 -0
  726. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  727. package/build/dist/Types/Rules/RuleRun.js +4 -0
  728. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  729. package/build/dist/Types/Team/ComplianceNotificationChannel.js +4 -3
  730. package/build/dist/Types/Team/ComplianceNotificationChannel.js.map +1 -1
  731. package/build/dist/Types/Team/ComplianceRule.js +48 -14
  732. package/build/dist/Types/Team/ComplianceRule.js.map +1 -1
  733. package/build/dist/Types/Team/ComplianceRuleType.js +2 -2
  734. package/build/dist/Types/Telemetry/EntityType.js +16 -0
  735. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  736. package/build/dist/Types/XML.js +23 -0
  737. package/build/dist/Types/XML.js.map +1 -1
  738. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +641 -266
  739. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  740. package/build/dist/UI/Components/CodeEditor/CodeEditorCommands.js +464 -0
  741. package/build/dist/UI/Components/CodeEditor/CodeEditorCommands.js.map +1 -0
  742. package/build/dist/UI/Components/CodeEditor/CodeEditorHighlight.js +69 -0
  743. package/build/dist/UI/Components/CodeEditor/CodeEditorHighlight.js.map +1 -0
  744. package/build/dist/UI/Components/CodeEditor/CodeEditorLanguages.js +108 -0
  745. package/build/dist/UI/Components/CodeEditor/CodeEditorLanguages.js.map +1 -0
  746. package/build/dist/UI/Components/CodeEditor/YamlEditor.js +14 -186
  747. package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -1
  748. package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js +5 -1
  749. package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js.map +1 -1
  750. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +5 -14
  751. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -1
  752. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +14 -13
  753. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  754. package/build/dist/UI/Components/ErrorMessage/ErrorMessage.js +1 -1
  755. package/build/dist/UI/Components/ErrorMessage/ErrorMessage.js.map +1 -1
  756. package/build/dist/UI/Components/Forms/Fields/FormField.js +8 -3
  757. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  758. package/build/dist/UI/Components/Icon/Icon.js +8 -0
  759. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  760. package/build/dist/UI/Components/Input/Input.js +11 -3
  761. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  762. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +2 -1
  763. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  764. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +14 -4
  765. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  766. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
  767. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
  768. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +710 -65
  769. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
  770. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditorHistory.js +238 -0
  771. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditorHistory.js.map +1 -0
  772. package/build/dist/UI/Components/Markdown.tsx/MarkdownListEditing.js +1028 -0
  773. package/build/dist/UI/Components/Markdown.tsx/MarkdownListEditing.js.map +1 -0
  774. package/build/dist/UI/Components/Markdown.tsx/MarkdownPaste.js +1088 -0
  775. package/build/dist/UI/Components/Markdown.tsx/MarkdownPaste.js.map +1 -0
  776. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +70 -10
  777. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  778. package/build/dist/UI/Components/Markdown.tsx/MarkdownVisualEditing.js +887 -0
  779. package/build/dist/UI/Components/Markdown.tsx/MarkdownVisualEditing.js.map +1 -0
  780. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +57 -6
  781. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  782. package/build/dist/UI/Components/ResetObjectID/ResetObjectID.js +7 -5
  783. package/build/dist/UI/Components/ResetObjectID/ResetObjectID.js.map +1 -1
  784. package/build/dist/UI/Components/Table/EmptyTableMessage.js +51 -0
  785. package/build/dist/UI/Components/Table/EmptyTableMessage.js.map +1 -0
  786. package/build/dist/UI/Components/Table/Table.js +30 -11
  787. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  788. package/build/dist/UI/Components/Table/TableRow.js +9 -2
  789. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  790. package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js +63 -1
  791. package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js.map +1 -1
  792. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +4 -2
  793. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  794. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js +91 -10
  795. package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js.map +1 -1
  796. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +14 -4
  797. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  798. package/build/dist/UI/Components/TextArea/TextArea.js +9 -3
  799. package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
  800. package/build/dist/UI/Components/Workflow/ModelFieldPicker.js +1 -2
  801. package/build/dist/UI/Components/Workflow/ModelFieldPicker.js.map +1 -1
  802. package/build/dist/Utils/LabelRuleImportExport.js +2 -0
  803. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  804. package/build/dist/Utils/Markdown/UntrustedMarkdown.js +988 -0
  805. package/build/dist/Utils/Markdown/UntrustedMarkdown.js.map +1 -0
  806. package/build/dist/Utils/Monitor/IncomingEmailMonitorRequestUtil.js +34 -0
  807. package/build/dist/Utils/Monitor/IncomingEmailMonitorRequestUtil.js.map +1 -0
  808. package/build/dist/Utils/Monitor/MonitorLogSummaryUtil.js +148 -0
  809. package/build/dist/Utils/Monitor/MonitorLogSummaryUtil.js.map +1 -0
  810. package/build/dist/Utils/Telemetry/EntityKey.js +36 -0
  811. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  812. package/package.json +1 -3
  813. package/Server/Utils/CommunityEditionSsoReport.ts +0 -361
  814. package/Tests/App/Accounts/LoginSsoEdition.test.tsx +0 -616
  815. package/Tests/App/AdminDashboard/AdminAuthenticationSsoEdition.test.tsx +0 -217
  816. package/Tests/App/AdminDashboard/GlobalSSOEnterpriseShells.test.tsx +0 -271
  817. package/Tests/App/Dashboard/CloudDocumentationMarkdown.test.ts +0 -215
  818. package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +0 -784
  819. package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +0 -518
  820. package/Tests/Server/Services/SsoRequirementReadMasking.test.ts +0 -419
  821. package/Tests/Server/Services/SsoRequirementWriteGuard.test.ts +0 -769
  822. package/Tests/UI/Components/CodeEditor.test.tsx +0 -638
  823. package/Tests/UI/Components/MonacoLoader.test.ts +0 -67
  824. package/Tests/UI/Components/MonacoRuntime.test.ts +0 -121
  825. package/Tests/UI/Components/Telemetry/TelemetryRetentionConfigForm.test.tsx +0 -329
  826. package/Tests/UI/Components/YamlEditor.test.tsx +0 -847
  827. package/UI/Components/CodeEditor/MonacoLoader.ts +0 -36
  828. package/UI/Components/Telemetry/TelemetryRetentionConfigForm.tsx +0 -319
  829. package/UI/Components/Telemetry/TelemetryRetentionConfigSummary.tsx +0 -223
  830. package/build/dist/Server/Utils/CommunityEditionSsoReport.js +0 -223
  831. package/build/dist/Server/Utils/CommunityEditionSsoReport.js.map +0 -1
  832. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js +0 -34
  833. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js.map +0 -1
  834. package/build/dist/UI/Components/Telemetry/TelemetryRetentionConfigForm.js +0 -171
  835. package/build/dist/UI/Components/Telemetry/TelemetryRetentionConfigForm.js.map +0 -1
  836. package/build/dist/UI/Components/Telemetry/TelemetryRetentionConfigSummary.js +0 -119
  837. package/build/dist/UI/Components/Telemetry/TelemetryRetentionConfigSummary.js.map +0 -1
@@ -0,0 +1,2210 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
3
+
4
+ /*
5
+ * PasswordHash carries a pre-existing TS5.9 diagnostic that fails any suite
6
+ * whose runtime require graph reaches it, and DatabaseService imports it.
7
+ */
8
+ jest.mock("../../../Server/Utils/PasswordHash", () => {
9
+ return {
10
+ __esModule: true,
11
+ default: {
12
+ hash: jest.fn(),
13
+ verify: jest.fn(),
14
+ generateSalt: jest.fn(),
15
+ needsUpgrade: jest.fn(),
16
+ applyPepper: jest.fn(),
17
+ },
18
+ };
19
+ });
20
+
21
+ import MessageQueueService, {
22
+ MessageQueueFindOrCreateResult,
23
+ getMessagingSystemRuleMatchValues,
24
+ resolveManualMessageQueue,
25
+ } from "../../../Server/Services/MessageQueueService";
26
+ import MessageQueueLabelRuleEngineService from "../../../Server/Services/MessageQueueLabelRuleEngineService";
27
+ import MessageQueueOwnerRuleEngineService from "../../../Server/Services/MessageQueueOwnerRuleEngineService";
28
+ import DatabaseConfig from "../../../Server/DatabaseConfig";
29
+ import GlobalCache from "../../../Server/Infrastructure/GlobalCache";
30
+ import ResourceHeartbeat from "../../../Server/Utils/Telemetry/ResourceHeartbeat";
31
+ import SingleFlight from "../../../Server/Utils/SingleFlight";
32
+ import logger from "../../../Server/Utils/Logger";
33
+ import MessageQueue from "../../../Models/DatabaseModels/MessageQueue";
34
+ import URL from "../../../Types/API/URL";
35
+ import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
36
+ import BadDataException from "../../../Types/Exception/BadDataException";
37
+ import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
38
+ import {
39
+ MessageQueueIdentity,
40
+ buildMessageQueueDisplayName,
41
+ toMessageQueueIdentity,
42
+ } from "../../../Types/MessageQueue/MessageQueueIdentity";
43
+ import { resolveManualMessageQueue as sharedResolveManualMessageQueue } from "../../../Types/MessageQueue/MessageQueueManualIdentity";
44
+ import ObjectID from "../../../Types/ObjectID";
45
+ import Permission, { UserPermission } from "../../../Types/Permission";
46
+ import PositiveNumber from "../../../Types/PositiveNumber";
47
+ import { getJestSpyOn } from "../../Spy";
48
+ import crypto from "crypto";
49
+
50
+ /*
51
+ * MessageQueueService - the Queues product's root service.
52
+ *
53
+ * Pinned here, everything external mocked at its seam (no Postgres, no Redis):
54
+ *
55
+ * - a MANUAL create through the real create pipeline, column permission
56
+ * check included: system normalized, destination normalized as
57
+ * discovery normalizes it (a RabbitMQ queue's name taken whole, as its
58
+ * broker reports it), identity computed on the system's family,
59
+ * discoverySource forced to manual, and a friendly refusal for every
60
+ * unusable value and for a duplicate;
61
+ * - findOrCreateByIdentity: lookup first, the system refined within its
62
+ * family (never back, never across), discovery's archive undone and a
63
+ * person's never, allowCreate=false and the budget never create, the
64
+ * create as root without hooks, and the unique-index race (the winner's
65
+ * row returned, the rule engines run only by the winner);
66
+ * - recordSighting: two heartbeats in two namespaces, the broker address
67
+ * only when non-empty and changed, lastSeenAt as the time discovery saw
68
+ * the queue (never moving backwards, whichever source writes last or
69
+ * however late its data) and the broker's own datapoint time as
70
+ * brokerMetricsLastSeenAt;
71
+ * - the auto-archive and budget SQL, each scoping project + deletedAt
72
+ * itself, and a person's archive decisions and labels.
73
+ */
74
+
75
+ const PROJECT_ID: ObjectID = new ObjectID(
76
+ "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
77
+ );
78
+ const USER_ID: ObjectID = new ObjectID("dddddddd-dddd-4ddd-8ddd-dddddddddddd");
79
+
80
+ const service: any = MessageQueueService;
81
+
82
+ function userPermission(permission: Permission): UserPermission {
83
+ return {
84
+ permission,
85
+ labelIds: [],
86
+ isBlockPermission: false,
87
+ _type: "UserPermission",
88
+ };
89
+ }
90
+
91
+ function memberProps(
92
+ permissions: Array<Permission> = [Permission.ProjectMember],
93
+ ): DatabaseCommonInteractionProps {
94
+ return {
95
+ userId: USER_ID,
96
+ tenantId: PROJECT_ID,
97
+ userGlobalAccessPermission: {
98
+ projectIds: [PROJECT_ID],
99
+ globalPermissions: [Permission.Public, Permission.User],
100
+ _type: "UserGlobalAccessPermission",
101
+ },
102
+ userTenantAccessPermission: {
103
+ [PROJECT_ID.toString()]: {
104
+ projectId: PROJECT_ID,
105
+ permissions: permissions.map(userPermission),
106
+ _type: "UserTenantAccessPermission",
107
+ },
108
+ },
109
+ };
110
+ }
111
+
112
+ function queueRow(overrides: Partial<MessageQueue> = {}): MessageQueue {
113
+ const row: MessageQueue = new MessageQueue(ObjectID.generate());
114
+ row.projectId = PROJECT_ID;
115
+ row.name = "orders";
116
+ row.queueIdentifier = "jms||orders";
117
+ row.messagingSystem = "jms";
118
+ row.destinationName = "orders";
119
+ row.discoverySource = "traces";
120
+ row.isArchived = false;
121
+ Object.assign(row, overrides);
122
+ return row;
123
+ }
124
+
125
+ function identityOf(value: {
126
+ system: string;
127
+ brokerScope?: string;
128
+ destination: string;
129
+ }): MessageQueueIdentity {
130
+ const identity: MessageQueueIdentity | null = toMessageQueueIdentity(value);
131
+
132
+ if (!identity) {
133
+ throw new Error(`not an identity: ${JSON.stringify(value)}`);
134
+ }
135
+
136
+ return identity;
137
+ }
138
+
139
+ /** Let the fire-and-forget rule chains drain before asserting on them. */
140
+ function flushPromises(): Promise<void> {
141
+ return new Promise((resolve: () => void) => {
142
+ setTimeout(resolve, 0);
143
+ });
144
+ }
145
+
146
+ interface LoggerSpies {
147
+ error: jest.SpyInstance;
148
+ warn: jest.SpyInstance;
149
+ info: jest.SpyInstance;
150
+ debug: jest.SpyInstance;
151
+ }
152
+
153
+ function silenceLogs(): LoggerSpies {
154
+ return {
155
+ error: jest.spyOn(logger, "error").mockImplementation(() => {
156
+ return undefined as never;
157
+ }),
158
+ warn: jest.spyOn(logger, "warn").mockImplementation(() => {
159
+ return undefined as never;
160
+ }),
161
+ info: jest.spyOn(logger, "info").mockImplementation(() => {
162
+ return undefined as never;
163
+ }),
164
+ debug: jest.spyOn(logger, "debug").mockImplementation(() => {
165
+ return undefined as never;
166
+ }),
167
+ };
168
+ }
169
+
170
+ interface RuleEngineSpies {
171
+ labelRules: jest.SpyInstance;
172
+ ownerRules: jest.SpyInstance;
173
+ }
174
+
175
+ function mockRuleEngines(): RuleEngineSpies {
176
+ return {
177
+ labelRules: getJestSpyOn(
178
+ MessageQueueLabelRuleEngineService,
179
+ "applyRulesToMessageQueue",
180
+ ).mockResolvedValue(undefined),
181
+ ownerRules: getJestSpyOn(
182
+ MessageQueueOwnerRuleEngineService,
183
+ "applyRulesToMessageQueue",
184
+ ).mockResolvedValue(undefined),
185
+ };
186
+ }
187
+
188
+ /*
189
+ * Raw SQL issued through getRepository().manager.query, answered by what
190
+ * each statement is: the budget count, the system refinement, the restore.
191
+ * An Error answer is thrown.
192
+ */
193
+ interface RawQueryAnswers {
194
+ count?: unknown;
195
+ refined?: unknown;
196
+ restored?: unknown;
197
+ other?: unknown;
198
+ }
199
+
200
+ function mockRawQueries(answers: RawQueryAnswers = {}): jest.Mock {
201
+ const answer: (value: unknown, fallback: unknown) => unknown = (
202
+ value: unknown,
203
+ fallback: unknown,
204
+ ): unknown => {
205
+ if (value instanceof Error) {
206
+ throw value;
207
+ }
208
+ return value === undefined ? fallback : value;
209
+ };
210
+
211
+ const query: jest.Mock = jest.fn(async (sql: string) => {
212
+ if (sql.includes(`COUNT(*)::int AS "count"`)) {
213
+ return answer(answers.count, [{ count: 0 }]);
214
+ }
215
+ if (sql.includes(`WITH "refined"`)) {
216
+ return answer(answers.refined, []);
217
+ }
218
+ if (sql.includes(`WITH "restored"`)) {
219
+ return answer(answers.restored, []);
220
+ }
221
+ return answer(answers.other, []);
222
+ });
223
+
224
+ getJestSpyOn(service, "getRepository").mockReturnValue({
225
+ manager: { query },
226
+ } as never);
227
+
228
+ return query;
229
+ }
230
+
231
+ function callsMatching(query: jest.Mock, fragment: string): Array<any> {
232
+ return query.mock.calls.filter((call: Array<any>): boolean => {
233
+ return String(call[0]).includes(fragment);
234
+ });
235
+ }
236
+
237
+ function withEnv(name: string, value: string | undefined): () => void {
238
+ const previous: string | undefined = process.env[name];
239
+ if (value === undefined) {
240
+ delete process.env[name];
241
+ } else {
242
+ process.env[name] = value;
243
+ }
244
+ return () => {
245
+ if (previous === undefined) {
246
+ delete process.env[name];
247
+ } else {
248
+ process.env[name] = previous;
249
+ }
250
+ };
251
+ }
252
+
253
+ afterEach(() => {
254
+ jest.restoreAllMocks();
255
+ MessageQueueService.clearAutoCreateBudgetMemo();
256
+ });
257
+
258
+ /*
259
+ * ---------------------------------------------------------------------------
260
+ * The identity of a queue a person types
261
+ * ---------------------------------------------------------------------------
262
+ * Every system's normalization and refusal is pinned where the resolution
263
+ * lives, in the pure module the create form shares
264
+ * (Tests/Types/MessageQueue/MessageQueueManualIdentity.test.ts). Here: that
265
+ * the service hands its callers that same function, and applies it in
266
+ * onBeforeCreate (the manual create suite below).
267
+ */
268
+ describe("resolveManualMessageQueue", () => {
269
+ test("is the shared module's, re-exported", () => {
270
+ expect(resolveManualMessageQueue).toBe(sharedResolveManualMessageQueue);
271
+ });
272
+ });
273
+
274
+ describe("getMessagingSystemRuleMatchValues", () => {
275
+ test.each([
276
+ ["kafka", ["kafka", "Apache Kafka"]],
277
+ ["aws_sqs", ["aws_sqs", "Amazon SQS"]],
278
+ ["activemq", ["activemq", "Apache ActiveMQ"]],
279
+ ["jms", ["jms", "JMS"]],
280
+ // An alias is matched as the system it names.
281
+ ["AmazonSQS", ["aws_sqs", "Amazon SQS"]],
282
+ // A long-tail system has no display name of its own.
283
+ ["ibmmq", ["ibmmq"]],
284
+ ["", []],
285
+ ])("%p is matched as %p", (system: string, values: Array<string>) => {
286
+ expect(getMessagingSystemRuleMatchValues(system)).toEqual(values);
287
+ });
288
+
289
+ test("anything that is not a string matches nothing", () => {
290
+ expect(getMessagingSystemRuleMatchValues(undefined)).toEqual([]);
291
+ expect(getMessagingSystemRuleMatchValues(null)).toEqual([]);
292
+ expect(getMessagingSystemRuleMatchValues(7)).toEqual([]);
293
+ });
294
+ });
295
+
296
+ /*
297
+ * ---------------------------------------------------------------------------
298
+ * A person adding a queue - the real create pipeline
299
+ * ---------------------------------------------------------------------------
300
+ */
301
+ describe("MessageQueueService - manual create (real create pipeline)", () => {
302
+ let save: jest.Mock;
303
+ let findSameIdentity: jest.SpyInstance;
304
+ let ruleEngines: RuleEngineSpies;
305
+
306
+ function manualRequest(overrides: Partial<MessageQueue> = {}): {
307
+ data: MessageQueue;
308
+ props: DatabaseCommonInteractionProps;
309
+ } {
310
+ const data: MessageQueue = new MessageQueue();
311
+ data.messagingSystem = "AmazonSQS";
312
+ data.destinationName =
313
+ "https://sqs.eu-west-1.amazonaws.com/123456789012/Orders";
314
+ Object.assign(data, overrides);
315
+ return { data: data, props: memberProps() };
316
+ }
317
+
318
+ beforeEach(() => {
319
+ silenceLogs();
320
+ save = jest.fn(async (entity: any) => {
321
+ entity._id = ObjectID.generate().toString();
322
+ return entity;
323
+ });
324
+ getJestSpyOn(service, "getRepository").mockReturnValue({ save } as never);
325
+ getJestSpyOn(service, "countBy").mockResolvedValue(
326
+ new PositiveNumber(0) as never,
327
+ );
328
+ getJestSpyOn(service, "onTriggerWorkflow").mockResolvedValue(
329
+ undefined as never,
330
+ );
331
+ getJestSpyOn(service, "onTriggerRealtime").mockResolvedValue(
332
+ undefined as never,
333
+ );
334
+ findSameIdentity = getJestSpyOn(service, "findOneBy").mockResolvedValue(
335
+ null,
336
+ );
337
+ ruleEngines = mockRuleEngines();
338
+ });
339
+
340
+ test("computes identity from the normalized system and destination, and stamps 'manual'", async () => {
341
+ const created: MessageQueue = await MessageQueueService.create(
342
+ manualRequest({
343
+ // A caller cannot pretend discovery found it...
344
+ discoverySource: "broker-metrics",
345
+ // ...or choose its identity.
346
+ queueIdentifier: "kafka||somewhere-else",
347
+ }),
348
+ );
349
+
350
+ expect(save).toHaveBeenCalledTimes(1);
351
+ expect(created.messagingSystem).toBe("aws_sqs");
352
+ expect(created.destinationName).toBe("Orders");
353
+ expect(created.queueIdentifier).toBe("aws_sqs||orders");
354
+ expect(created.discoverySource).toBe("manual");
355
+ expect(created.name).toBe("Orders");
356
+ expect(created.brokerScope).toBeUndefined();
357
+ expect(created.projectId!.toString()).toBe(PROJECT_ID.toString());
358
+ expect(created.createdByUserId!.toString()).toBe(USER_ID.toString());
359
+ expect(created.slug).toBeTruthy();
360
+ });
361
+
362
+ test("looks for the same identity in this project, as root, before inserting", async () => {
363
+ await MessageQueueService.create(manualRequest());
364
+
365
+ expect(findSameIdentity).toHaveBeenCalledTimes(1);
366
+ const lookup: any = findSameIdentity.mock.calls[0]![0];
367
+ expect(lookup.query.projectId.toString()).toBe(PROJECT_ID.toString());
368
+ expect(lookup.query.queueIdentifier).toBe("aws_sqs||orders");
369
+ expect(lookup.props).toEqual({ isRoot: true });
370
+ });
371
+
372
+ test("an ActiveMQ queue keeps its broker and keys on the JMS family", async () => {
373
+ const created: MessageQueue = await MessageQueueService.create(
374
+ manualRequest({
375
+ messagingSystem: "activemq",
376
+ destinationName: "queue://orders",
377
+ }),
378
+ );
379
+
380
+ expect(created.messagingSystem).toBe("activemq");
381
+ expect(created.destinationName).toBe("orders");
382
+ expect(created.queueIdentifier).toBe("jms||orders");
383
+ });
384
+
385
+ /*
386
+ * The rabbitmq receiver keys a queue's metrics on its exact name, and
387
+ * Hutch (`namespace:consumer`) and EasyNetQ (`Type, Assembly_sub`) put
388
+ * the separators of a span's joined RabbitMQ names in real queue names.
389
+ */
390
+ test.each([
391
+ ["myapp:billing:invoice_consumer"],
392
+ ["MyApp.Messages.OrderCreated, MyApp.Messages_billing"],
393
+ ])(
394
+ "a RabbitMQ queue %p is stored and looked up under its whole name",
395
+ async (queue: string) => {
396
+ const created: MessageQueue = await MessageQueueService.create(
397
+ manualRequest({ messagingSystem: "rabbitmq", destinationName: queue }),
398
+ );
399
+
400
+ const identifier: string = `rabbitmq||${queue.toLowerCase()}`;
401
+ expect(created.messagingSystem).toBe("rabbitmq");
402
+ expect(created.destinationName).toBe(queue);
403
+ expect(created.name).toBe(queue);
404
+ expect(created.queueIdentifier).toBe(identifier);
405
+ expect(findSameIdentity.mock.calls[0]![0].query.queueIdentifier).toBe(
406
+ identifier,
407
+ );
408
+ },
409
+ );
410
+
411
+ /*
412
+ * Split like a span's joined name, a Hutch queue was cut to its last
413
+ * part, and an EasyNetQ queue to its message type - so every subscription
414
+ * to one type was refused as the first one's queue.
415
+ */
416
+ test.each([
417
+ ["app:payments:dlq", "dlq"],
418
+ [
419
+ "MyApp.Messages.OrderCreated, MyApp.Messages_shipping",
420
+ "MyApp.Messages.OrderCreated",
421
+ ],
422
+ ])(
423
+ "RabbitMQ queue %p is not refused as the queue %p",
424
+ async (queue: string, cutName: string) => {
425
+ // A row keyed by the identifier the name was once cut down to.
426
+ const cutIdentifier: string = `rabbitmq||${cutName.toLowerCase()}`;
427
+ findSameIdentity.mockImplementation(async (input: any) => {
428
+ return input.query?.queueIdentifier === cutIdentifier
429
+ ? queueRow({ name: cutName, queueIdentifier: cutIdentifier })
430
+ : null;
431
+ });
432
+
433
+ const created: MessageQueue = await MessageQueueService.create(
434
+ manualRequest({ messagingSystem: "rabbitmq", destinationName: queue }),
435
+ );
436
+
437
+ expect(created.queueIdentifier).toBe(`rabbitmq||${queue.toLowerCase()}`);
438
+ expect(save).toHaveBeenCalledTimes(1);
439
+ },
440
+ );
441
+
442
+ test("a Service Bus queue carries its namespace in its identity and its column", async () => {
443
+ const created: MessageQueue = await MessageQueueService.create(
444
+ manualRequest({
445
+ messagingSystem: "servicebus",
446
+ destinationName: "Orders",
447
+ brokerScope: "Orders-Prod.servicebus.windows.net",
448
+ }),
449
+ );
450
+
451
+ expect(created.brokerScope).toBe("orders-prod");
452
+ expect(created.queueIdentifier).toBe("servicebus|orders-prod|orders");
453
+ expect(created.name).toBe("Orders");
454
+ });
455
+
456
+ test("a namespace sent for a system without one is dropped, not stored", async () => {
457
+ const created: MessageQueue = await MessageQueueService.create(
458
+ manualRequest({
459
+ messagingSystem: "kafka",
460
+ destinationName: "orders",
461
+ brokerScope: "orders-prod",
462
+ }),
463
+ );
464
+
465
+ expect(created.queueIdentifier).toBe("kafka||orders");
466
+ expect(created.brokerScope).toBeUndefined();
467
+ expect(save.mock.calls[0]![0].brokerScope).toBeUndefined();
468
+ });
469
+
470
+ test("keeps a name the person chose, trimmed", async () => {
471
+ const created: MessageQueue = await MessageQueueService.create(
472
+ manualRequest({ name: " Checkout orders " }),
473
+ );
474
+
475
+ expect(created.name).toBe("Checkout orders");
476
+ expect(created.destinationName).toBe("Orders");
477
+ });
478
+
479
+ test("a long destination gets a clamped default name", async () => {
480
+ const destination: string = `orders-${"x".repeat(150)}`;
481
+ const created: MessageQueue = await MessageQueueService.create(
482
+ manualRequest({ messagingSystem: "kafka", destinationName: destination }),
483
+ );
484
+
485
+ expect(created.name).toBe(buildMessageQueueDisplayName({ destination }));
486
+ expect(created.name!.length).toBe(100);
487
+ expect(created.name!.endsWith("…")).toBe(true);
488
+ expect(created.destinationName).toBe(destination);
489
+ });
490
+
491
+ test("runs the label then owner rules on the created queue", async () => {
492
+ const created: MessageQueue =
493
+ await MessageQueueService.create(manualRequest());
494
+ await flushPromises();
495
+
496
+ expect(ruleEngines.labelRules).toHaveBeenCalledWith(created);
497
+ expect(ruleEngines.ownerRules).toHaveBeenCalledWith(created);
498
+ expect(ruleEngines.labelRules.mock.invocationCallOrder[0]!).toBeLessThan(
499
+ ruleEngines.ownerRules.mock.invocationCallOrder[0]!,
500
+ );
501
+ });
502
+
503
+ test("a failing rule engine never fails the create", async () => {
504
+ ruleEngines.labelRules.mockRejectedValue(new Error("rules exploded"));
505
+
506
+ await expect(
507
+ MessageQueueService.create(manualRequest()),
508
+ ).resolves.toBeDefined();
509
+ await flushPromises();
510
+ expect(ruleEngines.ownerRules).not.toHaveBeenCalled();
511
+ });
512
+
513
+ test("the same identity twice is refused before the unique index can, naming the queue", async () => {
514
+ findSameIdentity.mockImplementation(async (input: any) => {
515
+ if (input.props?.isRoot) {
516
+ return queueRow({ name: "Orders queue" });
517
+ }
518
+ // The caller may read it.
519
+ return queueRow({ name: "Orders queue" });
520
+ });
521
+
522
+ await expect(MessageQueueService.create(manualRequest())).rejects.toThrow(
523
+ 'The Amazon SQS queue "Orders" already exists: "Orders queue".',
524
+ );
525
+ expect(save).not.toHaveBeenCalled();
526
+ });
527
+
528
+ test("a duplicate in a namespace says which namespace", async () => {
529
+ findSameIdentity.mockResolvedValue(queueRow({ name: "Orders" }));
530
+
531
+ await expect(
532
+ MessageQueueService.create(
533
+ manualRequest({
534
+ messagingSystem: "servicebus",
535
+ destinationName: "orders",
536
+ brokerScope: "orders-prod",
537
+ }),
538
+ ),
539
+ ).rejects.toThrow(
540
+ 'The Azure Service Bus queue "orders" in the "orders-prod" namespace already exists',
541
+ );
542
+ });
543
+
544
+ test("a duplicate the caller cannot read is not named", async () => {
545
+ findSameIdentity.mockImplementation(async (input: any) => {
546
+ if (input.props?.isRoot) {
547
+ return queueRow({ name: "Secret queue" });
548
+ }
549
+ throw new NotAuthorizedException("no access");
550
+ });
551
+
552
+ let caught: unknown = null;
553
+
554
+ try {
555
+ await MessageQueueService.create(manualRequest());
556
+ } catch (error) {
557
+ caught = error;
558
+ }
559
+
560
+ expect(caught).toBeInstanceOf(BadDataException);
561
+ expect((caught as Error).message).toBe(
562
+ 'The Amazon SQS queue "Orders" already exists.',
563
+ );
564
+ expect((caught as Error).message).not.toContain("Secret");
565
+ });
566
+
567
+ test.each([
568
+ [{ messagingSystem: "" }, "Messaging system is required"],
569
+ [{ messagingSystem: "no such thing" }, "is not a messaging system"],
570
+ [{ destinationName: "" }, "Destination is required"],
571
+ [
572
+ { messagingSystem: "rabbitmq", destinationName: "amq.gen-abc" },
573
+ "cannot be a queue",
574
+ ],
575
+ [
576
+ { messagingSystem: "rabbitmq", destinationName: "amq.default" },
577
+ "is RabbitMQ's default exchange, not a queue",
578
+ ],
579
+ [
580
+ {
581
+ messagingSystem: "servicebus",
582
+ destinationName: "orders",
583
+ brokerScope: "not a namespace",
584
+ },
585
+ "is not an Azure namespace name",
586
+ ],
587
+ ] as Array<[Partial<MessageQueue>, string]>)(
588
+ "%p is refused before anything is looked up: %s",
589
+ async (overrides: Partial<MessageQueue>, message: string) => {
590
+ await expect(
591
+ MessageQueueService.create(manualRequest(overrides)),
592
+ ).rejects.toThrow(message);
593
+
594
+ expect(findSameIdentity).not.toHaveBeenCalled();
595
+ expect(save).not.toHaveBeenCalled();
596
+ },
597
+ );
598
+
599
+ test("a caller who may not add queues learns nothing: refused before any lookup", async () => {
600
+ await expect(
601
+ MessageQueueService.create({
602
+ data: manualRequest().data,
603
+ props: memberProps([Permission.ReadMessageQueue]),
604
+ }),
605
+ ).rejects.toThrow();
606
+
607
+ expect(findSameIdentity).not.toHaveBeenCalled();
608
+ expect(save).not.toHaveBeenCalled();
609
+ });
610
+
611
+ test("the granular create permission alone is enough", async () => {
612
+ await expect(
613
+ MessageQueueService.create({
614
+ data: manualRequest().data,
615
+ props: memberProps([Permission.CreateMessageQueue]),
616
+ }),
617
+ ).resolves.toBeDefined();
618
+ });
619
+
620
+ test.each([
621
+ ["lastSeenAt", new Date()],
622
+ ["brokerMetricsLastSeenAt", new Date()],
623
+ ["brokerAddress", "broker:9092"],
624
+ ["autoArchivedAt", new Date()],
625
+ ["automaticAssignments", { labelIds: [] }],
626
+ ])(
627
+ "a manual create carrying the root-only %s is refused",
628
+ async (column: string, value: unknown) => {
629
+ const request: {
630
+ data: MessageQueue;
631
+ props: DatabaseCommonInteractionProps;
632
+ } = manualRequest();
633
+ (request.data as unknown as Record<string, unknown>)[column] = value;
634
+
635
+ await expect(MessageQueueService.create(request)).rejects.toThrow(
636
+ `User is not allowed to create on ${column} column of Queue`,
637
+ );
638
+ expect(save).not.toHaveBeenCalled();
639
+ },
640
+ );
641
+
642
+ test("a root create passes through untouched - discovery computes its own identity", async () => {
643
+ const data: MessageQueue = new MessageQueue();
644
+ data.projectId = PROJECT_ID;
645
+ data.name = "orders";
646
+ data.queueIdentifier = "kafka||orders";
647
+ data.messagingSystem = "kafka";
648
+ data.destinationName = "orders";
649
+ data.discoverySource = "traces";
650
+
651
+ const created: MessageQueue = await MessageQueueService.create({
652
+ data: data,
653
+ props: { isRoot: true },
654
+ });
655
+
656
+ expect(findSameIdentity).not.toHaveBeenCalled();
657
+ expect(created.discoverySource).toBe("traces");
658
+ expect(created.queueIdentifier).toBe("kafka||orders");
659
+ });
660
+ });
661
+
662
+ /*
663
+ * ---------------------------------------------------------------------------
664
+ * Discovery: find or create by identity
665
+ * ---------------------------------------------------------------------------
666
+ */
667
+ describe("MessageQueueService.findOrCreateByIdentity", () => {
668
+ let findOneBy: jest.SpyInstance;
669
+ let create: jest.SpyInstance;
670
+ let query: jest.Mock;
671
+ let ruleEngines: RuleEngineSpies;
672
+ let logs: LoggerSpies;
673
+
674
+ const JMS_ORDERS: MessageQueueIdentity = identityOf({
675
+ system: "jms",
676
+ destination: "orders",
677
+ });
678
+
679
+ function sighting(overrides: Record<string, unknown> = {}): any {
680
+ return {
681
+ projectId: PROJECT_ID,
682
+ identity: JMS_ORDERS,
683
+ system: "activemq",
684
+ destination: "Orders",
685
+ brokerAddress: "broker-1.internal:61616",
686
+ source: "broker-metrics",
687
+ allowCreate: true,
688
+ ...overrides,
689
+ };
690
+ }
691
+
692
+ beforeEach(() => {
693
+ logs = silenceLogs();
694
+ findOneBy = getJestSpyOn(service, "findOneBy").mockResolvedValue(null);
695
+ create = getJestSpyOn(service, "create").mockImplementation(
696
+ async (input: any) => {
697
+ input.data._id = ObjectID.generate().toString();
698
+ return input.data;
699
+ },
700
+ );
701
+ query = mockRawQueries({ count: [{ count: 0 }] });
702
+ ruleEngines = mockRuleEngines();
703
+ });
704
+
705
+ test("looks the queue up by project and identifier first, as root", async () => {
706
+ const existing: MessageQueue = queueRow({ messagingSystem: "activemq" });
707
+ findOneBy.mockResolvedValue(existing);
708
+
709
+ const result: MessageQueueFindOrCreateResult =
710
+ await MessageQueueService.findOrCreateByIdentity(sighting());
711
+
712
+ expect(result).toEqual({ queue: existing, created: false });
713
+ expect(findOneBy).toHaveBeenCalledTimes(1);
714
+ const lookup: any = findOneBy.mock.calls[0]![0];
715
+ expect(lookup.query).toEqual({
716
+ projectId: PROJECT_ID,
717
+ queueIdentifier: "jms||orders",
718
+ });
719
+ expect(lookup.props).toEqual({ isRoot: true });
720
+ // Everything discovery reads back.
721
+ expect(lookup.select).toMatchObject({
722
+ _id: true,
723
+ projectId: true,
724
+ name: true,
725
+ queueIdentifier: true,
726
+ messagingSystem: true,
727
+ destinationName: true,
728
+ brokerScope: true,
729
+ isArchived: true,
730
+ autoArchivedAt: true,
731
+ });
732
+ expect(create).not.toHaveBeenCalled();
733
+ await flushPromises();
734
+ expect(ruleEngines.labelRules).not.toHaveBeenCalled();
735
+ });
736
+
737
+ test("the identity is canonicalized again - any spelling finds the same row", async () => {
738
+ findOneBy.mockResolvedValue(queueRow());
739
+
740
+ await MessageQueueService.findOrCreateByIdentity(
741
+ sighting({
742
+ identity: {
743
+ system: "ActiveMQ",
744
+ brokerScope: "",
745
+ destination: " ORDERS ",
746
+ },
747
+ }),
748
+ );
749
+
750
+ expect(findOneBy.mock.calls[0]![0].query.queueIdentifier).toBe(
751
+ "jms||orders",
752
+ );
753
+ });
754
+
755
+ test("an invalid identity answers nothing without a lookup", async () => {
756
+ for (const identity of [
757
+ { system: "", brokerScope: "", destination: "orders" },
758
+ { system: "kafka", brokerScope: "", destination: "" },
759
+ { system: "not a system", brokerScope: "", destination: "orders" },
760
+ { system: "kafka", brokerScope: "", destination: "x".repeat(600) },
761
+ ]) {
762
+ await expect(
763
+ MessageQueueService.findOrCreateByIdentity(sighting({ identity })),
764
+ ).resolves.toEqual({ queue: null, created: false });
765
+ }
766
+
767
+ await expect(
768
+ MessageQueueService.findOrCreateByIdentity(undefined as any),
769
+ ).resolves.toEqual({ queue: null, created: false });
770
+ expect(findOneBy).not.toHaveBeenCalled();
771
+ });
772
+
773
+ describe("an existing row is told about the sighting", () => {
774
+ test("a JMS row seen by ActiveMQ metrics is refined to activemq - one compare-and-set", async () => {
775
+ const existing: MessageQueue = queueRow({ messagingSystem: "jms" });
776
+ findOneBy.mockResolvedValue(existing);
777
+ query = mockRawQueries({ refined: [{ _id: existing.id!.toString() }] });
778
+
779
+ await MessageQueueService.findOrCreateByIdentity(sighting());
780
+
781
+ const refines: Array<any> = callsMatching(query, `WITH "refined"`);
782
+ expect(refines).toHaveLength(1);
783
+ const [sql, params] = refines[0]! as [string, Array<unknown>];
784
+ expect(sql).toContain(`SET "messagingSystem" = $1`);
785
+ expect(sql).toContain(`"messagingSystem" IS NOT DISTINCT FROM $4`);
786
+ expect(sql).toContain(`"projectId" = $3`);
787
+ expect(sql).toContain(`"deletedAt" IS NULL`);
788
+ expect(params).toEqual([
789
+ "activemq",
790
+ existing.id!.toString(),
791
+ PROJECT_ID.toString(),
792
+ "jms",
793
+ ]);
794
+ expect(existing.messagingSystem).toBe("activemq");
795
+ });
796
+
797
+ test("a refinement another writer beat keeps the row as read", async () => {
798
+ const existing: MessageQueue = queueRow({ messagingSystem: "jms" });
799
+ findOneBy.mockResolvedValue(existing);
800
+ mockRawQueries({ refined: [] });
801
+
802
+ await MessageQueueService.findOrCreateByIdentity(sighting());
803
+
804
+ expect(existing.messagingSystem).toBe("jms");
805
+ });
806
+
807
+ test("never back: an ActiveMQ row seen through JMS spans stays ActiveMQ", async () => {
808
+ findOneBy.mockResolvedValue(queueRow({ messagingSystem: "activemq" }));
809
+
810
+ await MessageQueueService.findOrCreateByIdentity(
811
+ sighting({ system: "jms", source: "traces" }),
812
+ );
813
+
814
+ expect(callsMatching(query, `WITH "refined"`)).toHaveLength(0);
815
+ });
816
+
817
+ test("never across families: a sighting's foreign system writes nothing", async () => {
818
+ const existing: MessageQueue = queueRow({ messagingSystem: "jms" });
819
+ findOneBy.mockResolvedValue(existing);
820
+
821
+ await MessageQueueService.findOrCreateByIdentity(
822
+ sighting({ system: "kafka" }),
823
+ );
824
+
825
+ expect(callsMatching(query, `WITH "refined"`)).toHaveLength(0);
826
+ expect(existing.messagingSystem).toBe("jms");
827
+ });
828
+
829
+ test("the same system writes nothing", async () => {
830
+ findOneBy.mockResolvedValue(queueRow({ messagingSystem: "jms" }));
831
+
832
+ await MessageQueueService.findOrCreateByIdentity(
833
+ sighting({ system: "JMS" }),
834
+ );
835
+
836
+ expect(callsMatching(query, `WITH "refined"`)).toHaveLength(0);
837
+ });
838
+
839
+ test("a failing refinement is logged, never thrown", async () => {
840
+ const existing: MessageQueue = queueRow({ messagingSystem: "jms" });
841
+ findOneBy.mockResolvedValue(existing);
842
+ mockRawQueries({ refined: new Error("deadlock detected") });
843
+
844
+ await expect(
845
+ MessageQueueService.findOrCreateByIdentity(sighting()),
846
+ ).resolves.toEqual({ queue: existing, created: false });
847
+ expect(logs.warn).toHaveBeenCalledWith(
848
+ expect.stringContaining("deadlock detected"),
849
+ expect.anything(),
850
+ );
851
+ expect(existing.messagingSystem).toBe("jms");
852
+ });
853
+
854
+ test("a row discovery auto-archived is restored when seen again", async () => {
855
+ const archived: MessageQueue = queueRow({
856
+ messagingSystem: "activemq",
857
+ isArchived: true,
858
+ autoArchivedAt: new Date(),
859
+ });
860
+ findOneBy.mockResolvedValue(archived);
861
+ query = mockRawQueries({ restored: [{ _id: archived.id!.toString() }] });
862
+
863
+ await MessageQueueService.findOrCreateByIdentity(sighting());
864
+
865
+ const restores: Array<any> = callsMatching(query, `WITH "restored"`);
866
+ expect(restores).toHaveLength(1);
867
+ const [sql, params] = restores[0]! as [string, Array<unknown>];
868
+ // The conditions live in the statement: only discovery's own archive.
869
+ expect(sql).toContain(`"isArchived" = true`);
870
+ expect(sql).toContain(`"autoArchivedAt" IS NOT NULL`);
871
+ expect(sql).toContain(`"deletedAt" IS NULL`);
872
+ expect(sql).toContain(`"archivedByUserId" = NULL`);
873
+ expect(params).toEqual([archived.id!.toString(), PROJECT_ID.toString()]);
874
+ expect(archived.isArchived).toBe(false);
875
+ expect(archived.autoArchivedAt).toBeUndefined();
876
+ });
877
+
878
+ test("a restore happens even when creating is not allowed - it is still a sighting", async () => {
879
+ const archived: MessageQueue = queueRow({
880
+ isArchived: true,
881
+ autoArchivedAt: new Date(),
882
+ });
883
+ findOneBy.mockResolvedValue(archived);
884
+ query = mockRawQueries({ restored: [{ _id: archived.id!.toString() }] });
885
+
886
+ const result: MessageQueueFindOrCreateResult =
887
+ await MessageQueueService.findOrCreateByIdentity(
888
+ sighting({ allowCreate: false }),
889
+ );
890
+
891
+ expect(result.queue).toBe(archived);
892
+ expect(archived.isArchived).toBe(false);
893
+ });
894
+
895
+ test("a row a PERSON archived is never un-archived by discovery", async () => {
896
+ const archivedByPerson: MessageQueue = queueRow({
897
+ messagingSystem: "activemq",
898
+ isArchived: true,
899
+ });
900
+ findOneBy.mockResolvedValue(archivedByPerson);
901
+
902
+ await MessageQueueService.findOrCreateByIdentity(sighting());
903
+
904
+ expect(callsMatching(query, `WITH "restored"`)).toHaveLength(0);
905
+ expect(archivedByPerson.isArchived).toBe(true);
906
+ });
907
+
908
+ test("a restore that lost to a person keeps the row as read", async () => {
909
+ const archived: MessageQueue = queueRow({
910
+ messagingSystem: "activemq",
911
+ isArchived: true,
912
+ autoArchivedAt: new Date(),
913
+ });
914
+ findOneBy.mockResolvedValue(archived);
915
+ mockRawQueries({ restored: [] });
916
+
917
+ await MessageQueueService.findOrCreateByIdentity(sighting());
918
+
919
+ expect(archived.isArchived).toBe(true);
920
+ expect(archived.autoArchivedAt).toBeDefined();
921
+ });
922
+
923
+ test("a failing restore never fails the lookup", async () => {
924
+ const archived: MessageQueue = queueRow({
925
+ messagingSystem: "activemq",
926
+ isArchived: true,
927
+ autoArchivedAt: new Date(),
928
+ });
929
+ findOneBy.mockResolvedValue(archived);
930
+ mockRawQueries({ restored: new Error("connection terminated") });
931
+
932
+ await expect(
933
+ MessageQueueService.findOrCreateByIdentity(sighting()),
934
+ ).resolves.toEqual({ queue: archived, created: false });
935
+ });
936
+
937
+ test("a live row costs no restore statement", async () => {
938
+ findOneBy.mockResolvedValue(queueRow({ messagingSystem: "activemq" }));
939
+
940
+ await MessageQueueService.findOrCreateByIdentity(sighting());
941
+
942
+ expect(query).not.toHaveBeenCalled();
943
+ });
944
+ });
945
+
946
+ test("nothing is created when creation is not allowed - not even a budget query", async () => {
947
+ await expect(
948
+ MessageQueueService.findOrCreateByIdentity(
949
+ sighting({ allowCreate: false }),
950
+ ),
951
+ ).resolves.toEqual({ queue: null, created: false });
952
+
953
+ expect(create).not.toHaveBeenCalled();
954
+ expect(query).not.toHaveBeenCalled();
955
+ });
956
+
957
+ test("creates the row as root, without hooks, with the sighting's SPECIFIC system", async () => {
958
+ const before: number = Date.now();
959
+ const result: MessageQueueFindOrCreateResult =
960
+ await MessageQueueService.findOrCreateByIdentity(sighting());
961
+
962
+ expect(result.created).toBe(true);
963
+ expect(create).toHaveBeenCalledTimes(1);
964
+ const call: any = create.mock.calls[0]![0];
965
+ expect(call.props).toEqual({ isRoot: true, ignoreHooks: true });
966
+
967
+ const row: MessageQueue = call.data;
968
+ expect(result.queue).toBe(row);
969
+ expect(row.projectId).toBe(PROJECT_ID);
970
+ expect(row.queueIdentifier).toBe("jms||orders");
971
+ expect(row.messagingSystem).toBe("activemq");
972
+ // The display destination keeps the sighting's casing.
973
+ expect(row.destinationName).toBe("Orders");
974
+ expect(row.name).toBe("Orders");
975
+ expect(row.brokerScope).toBeUndefined();
976
+ expect(row.brokerAddress).toBe("broker-1.internal:61616");
977
+ expect(row.discoverySource).toBe("broker-metrics");
978
+ expect(row.lastSeenAt!.getTime()).toBeGreaterThanOrEqual(before);
979
+ /*
980
+ * Only the broker-metrics sighting that follows knows when the newest
981
+ * datapoint was taken: it is that column's only writer.
982
+ */
983
+ expect(row.brokerMetricsLastSeenAt).toBeUndefined();
984
+ });
985
+
986
+ test("a trace-created row has no broker-metrics timestamp", async () => {
987
+ await MessageQueueService.findOrCreateByIdentity(
988
+ sighting({ source: "traces", system: "jms" }),
989
+ );
990
+
991
+ const row: MessageQueue = create.mock.calls[0]![0].data;
992
+ expect(row.discoverySource).toBe("traces");
993
+ expect(row.messagingSystem).toBe("jms");
994
+ expect(row.lastSeenAt).toBeDefined();
995
+ expect(row.brokerMetricsLastSeenAt).toBeUndefined();
996
+ });
997
+
998
+ test("an Azure namespace from the identity lands on the row", async () => {
999
+ await MessageQueueService.findOrCreateByIdentity(
1000
+ sighting({
1001
+ identity: identityOf({
1002
+ system: "servicebus",
1003
+ brokerScope: "orders-prod",
1004
+ destination: "orders",
1005
+ }),
1006
+ system: "servicebus",
1007
+ destination: "orders",
1008
+ }),
1009
+ );
1010
+
1011
+ const row: MessageQueue = create.mock.calls[0]![0].data;
1012
+ expect(row.brokerScope).toBe("orders-prod");
1013
+ expect(row.queueIdentifier).toBe("servicebus|orders-prod|orders");
1014
+ });
1015
+
1016
+ test("a system outside the identity's family falls back to the family itself", async () => {
1017
+ await MessageQueueService.findOrCreateByIdentity(
1018
+ sighting({ system: "kafka" }),
1019
+ );
1020
+
1021
+ expect(create.mock.calls[0]![0].data.messagingSystem).toBe("jms");
1022
+ });
1023
+
1024
+ test("a destination that is not the identity's is not stored - the canonical one is", async () => {
1025
+ await MessageQueueService.findOrCreateByIdentity(
1026
+ sighting({ destination: "payments" }),
1027
+ );
1028
+
1029
+ const row: MessageQueue = create.mock.calls[0]![0].data;
1030
+ expect(row.destinationName).toBe("orders");
1031
+ expect(row.name).toBe("orders");
1032
+ });
1033
+
1034
+ test("a long destination gets the clamped display name", async () => {
1035
+ const destination: string = `Orders-${"x".repeat(200)}`;
1036
+
1037
+ await MessageQueueService.findOrCreateByIdentity(
1038
+ sighting({
1039
+ identity: identityOf({ system: "kafka", destination }),
1040
+ system: "kafka",
1041
+ destination,
1042
+ }),
1043
+ );
1044
+
1045
+ const row: MessageQueue = create.mock.calls[0]![0].data;
1046
+ expect(row.destinationName).toBe(destination);
1047
+ expect(row.name).toBe(buildMessageQueueDisplayName({ destination }));
1048
+ expect(row.name!.length).toBe(100);
1049
+ });
1050
+
1051
+ test.each([[null], [""], [" "], ["broker\u0000:9092"]])(
1052
+ "broker address %p is not stored",
1053
+ async (brokerAddress: unknown) => {
1054
+ await MessageQueueService.findOrCreateByIdentity(
1055
+ sighting({ brokerAddress }),
1056
+ );
1057
+
1058
+ expect(create.mock.calls[0]![0].data.brokerAddress).toBeUndefined();
1059
+ },
1060
+ );
1061
+
1062
+ test("an oversized broker address is clamped to its column", async () => {
1063
+ await MessageQueueService.findOrCreateByIdentity(
1064
+ sighting({ brokerAddress: ` ${"b".repeat(900)} ` }),
1065
+ );
1066
+
1067
+ expect(create.mock.calls[0]![0].data.brokerAddress).toBe("b".repeat(500));
1068
+ });
1069
+
1070
+ test("the winner runs the label then owner rules - fire and forget", async () => {
1071
+ const result: MessageQueueFindOrCreateResult =
1072
+ await MessageQueueService.findOrCreateByIdentity(sighting());
1073
+ await flushPromises();
1074
+
1075
+ expect(ruleEngines.labelRules).toHaveBeenCalledWith(result.queue);
1076
+ expect(ruleEngines.ownerRules).toHaveBeenCalledWith(result.queue);
1077
+ expect(ruleEngines.labelRules.mock.invocationCallOrder[0]!).toBeLessThan(
1078
+ ruleEngines.ownerRules.mock.invocationCallOrder[0]!,
1079
+ );
1080
+ });
1081
+
1082
+ test("a failing rule run never fails the discovery that created the row", async () => {
1083
+ ruleEngines.labelRules.mockRejectedValue(new Error("rules exploded"));
1084
+
1085
+ await expect(
1086
+ MessageQueueService.findOrCreateByIdentity(sighting()),
1087
+ ).resolves.toMatchObject({ created: true });
1088
+ await flushPromises();
1089
+ expect(logs.error).toHaveBeenCalledWith(
1090
+ expect.stringContaining("rules exploded"),
1091
+ expect.anything(),
1092
+ );
1093
+ });
1094
+
1095
+ test("the unique-index race: the winner's row is re-read, sighted, and no rules run", async () => {
1096
+ const winner: MessageQueue = queueRow({ messagingSystem: "jms" });
1097
+ findOneBy.mockResolvedValueOnce(null).mockResolvedValueOnce(winner);
1098
+ create.mockRejectedValue(
1099
+ new BadDataException(
1100
+ 'duplicate key value violates unique constraint "IDX_message_queue_identifier"',
1101
+ ),
1102
+ );
1103
+ query = mockRawQueries({
1104
+ count: [{ count: 0 }],
1105
+ refined: [{ _id: winner.id!.toString() }],
1106
+ });
1107
+
1108
+ const result: MessageQueueFindOrCreateResult =
1109
+ await MessageQueueService.findOrCreateByIdentity(sighting());
1110
+
1111
+ expect(result).toEqual({ queue: winner, created: false });
1112
+ expect(findOneBy).toHaveBeenCalledTimes(2);
1113
+ expect(findOneBy.mock.calls[1]![0].query).toEqual({
1114
+ projectId: PROJECT_ID,
1115
+ queueIdentifier: "jms||orders",
1116
+ });
1117
+ // The loser's sighting still refines the winner's row.
1118
+ expect(winner.messagingSystem).toBe("activemq");
1119
+ await flushPromises();
1120
+ expect(ruleEngines.labelRules).not.toHaveBeenCalled();
1121
+ expect(ruleEngines.ownerRules).not.toHaveBeenCalled();
1122
+ });
1123
+
1124
+ test("a create failure that is not a race is surfaced", async () => {
1125
+ create.mockRejectedValue(new Error("connection terminated"));
1126
+
1127
+ await expect(
1128
+ MessageQueueService.findOrCreateByIdentity(sighting()),
1129
+ ).rejects.toThrow("connection terminated");
1130
+ await flushPromises();
1131
+ expect(ruleEngines.labelRules).not.toHaveBeenCalled();
1132
+ });
1133
+
1134
+ describe("the auto-create budget", () => {
1135
+ test("under the budget, the queue is created", async () => {
1136
+ query = mockRawQueries({ count: [{ count: 499 }] });
1137
+
1138
+ await expect(
1139
+ MessageQueueService.findOrCreateByIdentity(sighting()),
1140
+ ).resolves.toMatchObject({ created: true });
1141
+
1142
+ const counts: Array<any> = callsMatching(query, "COUNT(*)");
1143
+ expect(counts).toHaveLength(1);
1144
+ expect(counts[0]![1]).toEqual([PROJECT_ID.toString(), "manual"]);
1145
+ });
1146
+
1147
+ test("at the budget, nothing is created, and it is warned about once per ten minutes", async () => {
1148
+ mockRawQueries({ count: [{ count: 500 }] });
1149
+
1150
+ for (let attempt: number = 0; attempt < 3; attempt++) {
1151
+ await expect(
1152
+ MessageQueueService.findOrCreateByIdentity(sighting()),
1153
+ ).resolves.toEqual({ queue: null, created: false });
1154
+ }
1155
+
1156
+ expect(create).not.toHaveBeenCalled();
1157
+ const warnings: Array<any> = logs.warn.mock.calls.filter(
1158
+ (call: Array<any>): boolean => {
1159
+ return String(call[0]).includes("auto-create budget");
1160
+ },
1161
+ );
1162
+ expect(warnings).toHaveLength(1);
1163
+ expect(warnings[0]![0]).toContain("MESSAGE_QUEUE_AUTO_CREATE_BUDGET=500");
1164
+ expect(warnings[0]![0]).toContain("jms||orders");
1165
+ });
1166
+
1167
+ test("an existing row is always found, whatever the budget says", async () => {
1168
+ mockRawQueries({ count: [{ count: 10_000 }] });
1169
+ const existing: MessageQueue = queueRow({ messagingSystem: "activemq" });
1170
+ findOneBy.mockResolvedValue(existing);
1171
+
1172
+ await expect(
1173
+ MessageQueueService.findOrCreateByIdentity(sighting()),
1174
+ ).resolves.toEqual({ queue: existing, created: false });
1175
+ });
1176
+
1177
+ test("a count that cannot be read creates nothing (fail closed) and says why", async () => {
1178
+ mockRawQueries({ count: new Error("too many connections") });
1179
+
1180
+ await expect(
1181
+ MessageQueueService.findOrCreateByIdentity(sighting()),
1182
+ ).resolves.toEqual({ queue: null, created: false });
1183
+
1184
+ expect(create).not.toHaveBeenCalled();
1185
+ expect(logs.error).toHaveBeenCalledWith(
1186
+ expect.stringContaining("too many connections"),
1187
+ expect.anything(),
1188
+ );
1189
+ });
1190
+
1191
+ test("a zero budget turns auto-creation off without a query", async () => {
1192
+ const restore: () => void = withEnv(
1193
+ "MESSAGE_QUEUE_AUTO_CREATE_BUDGET",
1194
+ "0",
1195
+ );
1196
+
1197
+ try {
1198
+ await expect(
1199
+ MessageQueueService.findOrCreateByIdentity(sighting()),
1200
+ ).resolves.toEqual({ queue: null, created: false });
1201
+ expect(query).not.toHaveBeenCalled();
1202
+ expect(create).not.toHaveBeenCalled();
1203
+ } finally {
1204
+ restore();
1205
+ }
1206
+ });
1207
+
1208
+ test("a burst stops exactly at the budget: the cached count is bumped by every create", async () => {
1209
+ const restore: () => void = withEnv(
1210
+ "MESSAGE_QUEUE_AUTO_CREATE_BUDGET",
1211
+ "3",
1212
+ );
1213
+ query = mockRawQueries({ count: [{ count: 1 }] });
1214
+
1215
+ try {
1216
+ const results: Array<MessageQueueFindOrCreateResult> = [];
1217
+
1218
+ for (const destination of ["a", "b", "c", "d"]) {
1219
+ results.push(
1220
+ await MessageQueueService.findOrCreateByIdentity(
1221
+ sighting({
1222
+ identity: identityOf({ system: "kafka", destination }),
1223
+ system: "kafka",
1224
+ destination,
1225
+ }),
1226
+ ),
1227
+ );
1228
+ }
1229
+
1230
+ expect(
1231
+ results.map((result: MessageQueueFindOrCreateResult): boolean => {
1232
+ return result.created;
1233
+ }),
1234
+ ).toEqual([true, true, false, false]);
1235
+ // One count read for the whole burst.
1236
+ expect(callsMatching(query, "COUNT(*)")).toHaveLength(1);
1237
+ } finally {
1238
+ restore();
1239
+ }
1240
+ });
1241
+ });
1242
+ });
1243
+
1244
+ /*
1245
+ * ---------------------------------------------------------------------------
1246
+ * Liveness: two sightings, two namespaces
1247
+ * ---------------------------------------------------------------------------
1248
+ */
1249
+ describe("MessageQueueService.recordSighting", () => {
1250
+ type WriteCall = { id: ObjectID; data: Record<string, unknown> };
1251
+
1252
+ const QUEUE_ID: ObjectID = ObjectID.generate();
1253
+ let writes: Array<WriteCall>;
1254
+ let cache: Map<string, string>;
1255
+
1256
+ /** A faithful in-memory Redis for the atomic gate primitives. */
1257
+ function mockCache(): void {
1258
+ getJestSpyOn(GlobalCache, "setStringIfNotExists").mockImplementation(
1259
+ async (ns: string, key: string, value: string) => {
1260
+ const full: string = `${ns}:${key}`;
1261
+ if (cache.has(full)) {
1262
+ return false;
1263
+ }
1264
+ cache.set(full, value);
1265
+ return true;
1266
+ },
1267
+ );
1268
+ getJestSpyOn(GlobalCache, "setStringIfChanged").mockImplementation(
1269
+ async (ns: string, key: string, value: string) => {
1270
+ const full: string = `${ns}:${key}`;
1271
+ if (cache.get(full) === value) {
1272
+ return false;
1273
+ }
1274
+ cache.set(full, value);
1275
+ return true;
1276
+ },
1277
+ );
1278
+ getJestSpyOn(GlobalCache, "deleteKey").mockImplementation(
1279
+ async (ns: string, key: string) => {
1280
+ cache.delete(`${ns}:${key}`);
1281
+ },
1282
+ );
1283
+ }
1284
+
1285
+ function resetWindows(): void {
1286
+ SingleFlight.clear();
1287
+ ResourceHeartbeat.clearRecentHeartbeatMemo();
1288
+ }
1289
+
1290
+ // A new window: the liveness keys expire, the address fingerprints do not.
1291
+ function expireLivenessWindows(): void {
1292
+ resetWindows();
1293
+ for (const key of Array.from(cache.keys())) {
1294
+ if (!key.includes("-fingerprint:")) {
1295
+ cache.delete(key);
1296
+ }
1297
+ }
1298
+ }
1299
+
1300
+ function lastWrite(): Record<string, unknown> {
1301
+ return writes[writes.length - 1]!.data;
1302
+ }
1303
+
1304
+ function sighting(overrides: Record<string, unknown> = {}): any {
1305
+ return {
1306
+ projectId: PROJECT_ID,
1307
+ queueId: QUEUE_ID,
1308
+ source: "traces",
1309
+ ...overrides,
1310
+ };
1311
+ }
1312
+
1313
+ beforeEach(() => {
1314
+ silenceLogs();
1315
+ writes = [];
1316
+ cache = new Map<string, string>();
1317
+ resetWindows();
1318
+ mockCache();
1319
+ getJestSpyOn(
1320
+ service,
1321
+ "updateColumnsByIdIfUnlockedWithoutHooks",
1322
+ ).mockImplementation(async (input: any) => {
1323
+ writes.push({ id: input.id, data: { ...input.data } });
1324
+ return true;
1325
+ });
1326
+ });
1327
+
1328
+ afterEach(() => {
1329
+ resetWindows();
1330
+ });
1331
+
1332
+ test("a trace sighting moves lastSeenAt only, under the trace namespace", async () => {
1333
+ const before: number = Date.now();
1334
+
1335
+ await MessageQueueService.recordSighting(sighting());
1336
+
1337
+ expect(writes).toHaveLength(1);
1338
+ expect(writes[0]!.id.toString()).toBe(QUEUE_ID.toString());
1339
+ expect(Object.keys(lastWrite())).toEqual(["lastSeenAt"]);
1340
+ expect(
1341
+ (lastWrite()["lastSeenAt"] as Date).getTime(),
1342
+ ).toBeGreaterThanOrEqual(before);
1343
+ expect(
1344
+ cache.has(`message-queue-trace-sighting:${QUEUE_ID.toString()}`),
1345
+ ).toBe(true);
1346
+ });
1347
+
1348
+ test("a broker-metrics sighting moves lastSeenAt and brokerMetricsLastSeenAt, under its own namespace", async () => {
1349
+ await MessageQueueService.recordSighting(
1350
+ sighting({ source: "broker-metrics" }),
1351
+ );
1352
+
1353
+ expect(Object.keys(lastWrite()).sort()).toEqual([
1354
+ "brokerMetricsLastSeenAt",
1355
+ "lastSeenAt",
1356
+ ]);
1357
+ expect(lastWrite()["brokerMetricsLastSeenAt"]).toEqual(
1358
+ lastWrite()["lastSeenAt"],
1359
+ );
1360
+ expect(
1361
+ cache.has(`message-queue-broker-metrics-sighting:${QUEUE_ID.toString()}`),
1362
+ ).toBe(true);
1363
+ });
1364
+
1365
+ /*
1366
+ * Separate namespaces: spans every second must not be able to suppress the
1367
+ * heartbeat that says the broker's metrics still arrive.
1368
+ */
1369
+ test("a trace sighting never suppresses a broker-metrics sighting, nor the reverse", async () => {
1370
+ await MessageQueueService.recordSighting(sighting());
1371
+ SingleFlight.clear();
1372
+ await MessageQueueService.recordSighting(
1373
+ sighting({ source: "broker-metrics" }),
1374
+ );
1375
+ SingleFlight.clear();
1376
+ await MessageQueueService.recordSighting(sighting());
1377
+
1378
+ expect(writes).toHaveLength(2);
1379
+ expect(lastWrite()["brokerMetricsLastSeenAt"]).toBeDefined();
1380
+ });
1381
+
1382
+ test("one sighting per source per window, however many passes report it", async () => {
1383
+ for (let pass: number = 0; pass < 5; pass++) {
1384
+ SingleFlight.clear();
1385
+ await MessageQueueService.recordSighting(sighting());
1386
+ SingleFlight.clear();
1387
+ await MessageQueueService.recordSighting(
1388
+ sighting({ source: "broker-metrics" }),
1389
+ );
1390
+ }
1391
+
1392
+ expect(writes).toHaveLength(2);
1393
+ });
1394
+
1395
+ test("the broker address rides along when it is non-empty, trimmed", async () => {
1396
+ await MessageQueueService.recordSighting(
1397
+ sighting({ brokerAddress: " kafka-1.internal:9092 " }),
1398
+ );
1399
+
1400
+ expect(lastWrite()).toMatchObject({
1401
+ brokerAddress: "kafka-1.internal:9092",
1402
+ });
1403
+ expect(
1404
+ cache.get(
1405
+ `message-queue-trace-sighting-fingerprint:${QUEUE_ID.toString()}`,
1406
+ ),
1407
+ ).toBe(
1408
+ crypto.createHash("sha256").update("kafka-1.internal:9092").digest("hex"),
1409
+ );
1410
+ });
1411
+
1412
+ test.each([[undefined], [null], [""], [" "], ["kafka\u0007:9092"]])(
1413
+ "broker address %p is never written",
1414
+ async (brokerAddress: unknown) => {
1415
+ await MessageQueueService.recordSighting(sighting({ brokerAddress }));
1416
+
1417
+ expect(writes).toHaveLength(1);
1418
+ expect("brokerAddress" in lastWrite()).toBe(false);
1419
+ },
1420
+ );
1421
+
1422
+ test("an unchanged address is not written again; a changed one is", async () => {
1423
+ await MessageQueueService.recordSighting(
1424
+ sighting({ brokerAddress: "kafka-1:9092" }),
1425
+ );
1426
+
1427
+ expireLivenessWindows();
1428
+ await MessageQueueService.recordSighting(
1429
+ sighting({ brokerAddress: "kafka-1:9092" }),
1430
+ );
1431
+ expect(writes).toHaveLength(2);
1432
+ expect("brokerAddress" in lastWrite()).toBe(false);
1433
+
1434
+ expireLivenessWindows();
1435
+ for (const key of Array.from(cache.keys())) {
1436
+ if (key.includes("-write-window:")) {
1437
+ cache.delete(key);
1438
+ }
1439
+ }
1440
+ await MessageQueueService.recordSighting(
1441
+ sighting({ brokerAddress: "kafka-2:9092" }),
1442
+ );
1443
+ expect(lastWrite()["brokerAddress"]).toBe("kafka-2:9092");
1444
+ });
1445
+
1446
+ test("an oversized address is clamped to its column", async () => {
1447
+ await MessageQueueService.recordSighting(
1448
+ sighting({ brokerAddress: "a".repeat(2000) }),
1449
+ );
1450
+
1451
+ expect((lastWrite()["brokerAddress"] as string).length).toBe(500);
1452
+ });
1453
+
1454
+ test("a broker-metrics sighting stores the broker's datapoint time; lastSeenAt is when discovery saw it", async () => {
1455
+ const at: Date = new Date(Date.now() - 5 * 60 * 1000);
1456
+ const before: number = Date.now();
1457
+
1458
+ await MessageQueueService.recordSighting(
1459
+ sighting({ source: "broker-metrics", at }),
1460
+ );
1461
+
1462
+ expect(lastWrite()["brokerMetricsLastSeenAt"]).toEqual(at);
1463
+ const lastSeenAt: number = (lastWrite()["lastSeenAt"] as Date).getTime();
1464
+ expect(lastSeenAt).toBeGreaterThanOrEqual(before);
1465
+ expect(lastSeenAt).toBeLessThanOrEqual(Date.now());
1466
+ });
1467
+
1468
+ test("a trace sighting's time never reaches a column: lastSeenAt is when discovery saw it", async () => {
1469
+ const before: number = Date.now();
1470
+
1471
+ await MessageQueueService.recordSighting(
1472
+ sighting({ at: new Date(Date.now() - 14 * 60 * 1000) }),
1473
+ );
1474
+
1475
+ expect(Object.keys(lastWrite())).toEqual(["lastSeenAt"]);
1476
+ expect(
1477
+ (lastWrite()["lastSeenAt"] as Date).getTime(),
1478
+ ).toBeGreaterThanOrEqual(before);
1479
+ });
1480
+
1481
+ test.each([
1482
+ ["a future time", new Date(Date.now() + 24 * 3600 * 1000)],
1483
+ ["an invalid date", new Date("not a date")],
1484
+ ["something that is not a date", "2026-01-01"],
1485
+ ])(
1486
+ "%s as the broker's datapoint time is read as now",
1487
+ async (_label: string, at: unknown) => {
1488
+ const before: number = Date.now();
1489
+
1490
+ await MessageQueueService.recordSighting(
1491
+ sighting({ source: "broker-metrics", at }),
1492
+ );
1493
+
1494
+ for (const column of ["brokerMetricsLastSeenAt", "lastSeenAt"]) {
1495
+ const written: number = (lastWrite()[column] as Date).getTime();
1496
+ expect(written).toBeGreaterThanOrEqual(before);
1497
+ expect(written).toBeLessThanOrEqual(Date.now());
1498
+ }
1499
+ },
1500
+ );
1501
+
1502
+ /*
1503
+ * Both sources write lastSeenAt, one after the other in every discovery
1504
+ * pass, through separate heartbeat namespaces - so neither suppresses the
1505
+ * other and both land. Their newest data can be far apart: Azure Monitor,
1506
+ * CloudWatch and Cloud Monitoring deliver datapoints minutes late. Were
1507
+ * lastSeenAt a datapoint's own time, whichever source wrote last would win
1508
+ * and the column would move back behind the other source's sighting.
1509
+ */
1510
+ test.each([
1511
+ ["traces, then broker metrics", ["traces", "broker-metrics"]],
1512
+ ["broker metrics, then traces", ["broker-metrics", "traces"]],
1513
+ ])(
1514
+ "lastSeenAt never moves backwards within a pass: %s",
1515
+ async (_label: string, order: Array<string>) => {
1516
+ const newestSpan: Date = new Date(Date.now() - 60 * 1000);
1517
+ const newestDatapoint: Date = new Date(Date.now() - 40 * 60 * 1000);
1518
+ const row: Record<string, unknown> = {};
1519
+
1520
+ // A faithful row: every heartbeat write is a plain SET of its columns.
1521
+ getJestSpyOn(
1522
+ service,
1523
+ "updateColumnsByIdIfUnlockedWithoutHooks",
1524
+ ).mockImplementation(async (input: any) => {
1525
+ Object.assign(row, input.data);
1526
+ return true;
1527
+ });
1528
+
1529
+ const before: number = Date.now();
1530
+ let previous: number = 0;
1531
+
1532
+ for (const source of order) {
1533
+ await MessageQueueService.recordSighting(
1534
+ sighting({
1535
+ source,
1536
+ at: source === "traces" ? newestSpan : newestDatapoint,
1537
+ }),
1538
+ );
1539
+
1540
+ const lastSeenAt: number = (row["lastSeenAt"] as Date).getTime();
1541
+ expect(lastSeenAt).toBeGreaterThanOrEqual(previous);
1542
+ expect(lastSeenAt).toBeGreaterThanOrEqual(before);
1543
+ previous = lastSeenAt;
1544
+ }
1545
+
1546
+ expect(row["brokerMetricsLastSeenAt"]).toEqual(newestDatapoint);
1547
+ expect(
1548
+ (row["brokerMetricsLastSeenAt"] as Date).getTime(),
1549
+ ).toBeLessThanOrEqual((row["lastSeenAt"] as Date).getTime());
1550
+ },
1551
+ );
1552
+
1553
+ test("lastSeenAt never moves backwards across passes, whatever the data times", async () => {
1554
+ const row: Record<string, unknown> = {};
1555
+
1556
+ getJestSpyOn(
1557
+ service,
1558
+ "updateColumnsByIdIfUnlockedWithoutHooks",
1559
+ ).mockImplementation(async (input: any) => {
1560
+ Object.assign(row, input.data);
1561
+ return true;
1562
+ });
1563
+
1564
+ /*
1565
+ * A queue whose spans stop: pass one saw a span a minute old, pass two
1566
+ * sees only late cloud-monitoring datapoints from before it.
1567
+ */
1568
+ await MessageQueueService.recordSighting(
1569
+ sighting({ at: new Date(Date.now() - 60 * 1000) }),
1570
+ );
1571
+ const afterPassOne: number = (row["lastSeenAt"] as Date).getTime();
1572
+
1573
+ expireLivenessWindows();
1574
+ await MessageQueueService.recordSighting(
1575
+ sighting({
1576
+ source: "broker-metrics",
1577
+ at: new Date(Date.now() - 12 * 60 * 1000),
1578
+ }),
1579
+ );
1580
+
1581
+ expect((row["lastSeenAt"] as Date).getTime()).toBeGreaterThanOrEqual(
1582
+ afterPassOne,
1583
+ );
1584
+ });
1585
+
1586
+ /*
1587
+ * A pass creates a row, then records its sighting. The create cannot know
1588
+ * when the broker's newest datapoint was taken - only the sighting says -
1589
+ * so the create leaves brokerMetricsLastSeenAt to it, and the sighting's
1590
+ * lastSeenAt can never land behind the create's.
1591
+ */
1592
+ test("a row the pass just created: lastSeenAt never drops below the create's, brokerMetricsLastSeenAt comes from the sighting", async () => {
1593
+ mockRuleEngines();
1594
+ mockRawQueries({ count: [{ count: 0 }] });
1595
+ getJestSpyOn(service, "findOneBy").mockResolvedValue(null);
1596
+
1597
+ const row: Record<string, unknown> = {};
1598
+
1599
+ getJestSpyOn(service, "create").mockImplementation(async (input: any) => {
1600
+ input.data._id = QUEUE_ID.toString();
1601
+ row["lastSeenAt"] = input.data.lastSeenAt;
1602
+ row["brokerMetricsLastSeenAt"] = input.data.brokerMetricsLastSeenAt;
1603
+ return input.data;
1604
+ });
1605
+ getJestSpyOn(
1606
+ service,
1607
+ "updateColumnsByIdIfUnlockedWithoutHooks",
1608
+ ).mockImplementation(async (input: any) => {
1609
+ Object.assign(row, input.data);
1610
+ return true;
1611
+ });
1612
+
1613
+ const result: MessageQueueFindOrCreateResult =
1614
+ await MessageQueueService.findOrCreateByIdentity({
1615
+ projectId: PROJECT_ID,
1616
+ identity: identityOf({
1617
+ system: "servicebus",
1618
+ brokerScope: "orders-prod",
1619
+ destination: "orders",
1620
+ }),
1621
+ system: "servicebus",
1622
+ destination: "orders",
1623
+ source: "broker-metrics",
1624
+ allowCreate: true,
1625
+ });
1626
+
1627
+ expect(result.created).toBe(true);
1628
+ const createdLastSeenAt: number = (row["lastSeenAt"] as Date).getTime();
1629
+ expect(row["brokerMetricsLastSeenAt"]).toBeUndefined();
1630
+
1631
+ // Azure Monitor delivers minutes late: the newest datapoint is older than the create.
1632
+ const datapointAt: Date = new Date(Date.now() - 7 * 60 * 1000);
1633
+
1634
+ await MessageQueueService.recordSighting({
1635
+ projectId: PROJECT_ID,
1636
+ queueId: result.queue!.id!,
1637
+ source: "broker-metrics",
1638
+ at: datapointAt,
1639
+ });
1640
+
1641
+ expect((row["lastSeenAt"] as Date).getTime()).toBeGreaterThanOrEqual(
1642
+ createdLastSeenAt,
1643
+ );
1644
+ expect(row["brokerMetricsLastSeenAt"]).toEqual(datapointAt);
1645
+ await flushPromises();
1646
+ });
1647
+
1648
+ test("liveness survives a failing enriched write", async () => {
1649
+ getJestSpyOn(
1650
+ service,
1651
+ "updateColumnsByIdIfUnlockedWithoutHooks",
1652
+ ).mockImplementation(async (input: any) => {
1653
+ const data: Record<string, unknown> = { ...input.data };
1654
+ writes.push({ id: input.id, data: data });
1655
+ if ("brokerAddress" in data) {
1656
+ throw new Error("value too long");
1657
+ }
1658
+ return true;
1659
+ });
1660
+
1661
+ await MessageQueueService.recordSighting(
1662
+ sighting({ brokerAddress: "kafka-1:9092" }),
1663
+ );
1664
+
1665
+ expect(writes).toHaveLength(2);
1666
+ expect(Object.keys(lastWrite())).toEqual(["lastSeenAt"]);
1667
+ });
1668
+
1669
+ test("a sighting without a queue id writes nothing", async () => {
1670
+ await MessageQueueService.recordSighting(sighting({ queueId: undefined }));
1671
+ await MessageQueueService.recordSighting(undefined as any);
1672
+
1673
+ expect(writes).toHaveLength(0);
1674
+ });
1675
+ });
1676
+
1677
+ /*
1678
+ * ---------------------------------------------------------------------------
1679
+ * Sweeps: auto-archive, budget
1680
+ * ---------------------------------------------------------------------------
1681
+ */
1682
+ describe("MessageQueueService.autoArchiveStaleMessageQueues", () => {
1683
+ beforeEach(() => {
1684
+ silenceLogs();
1685
+ });
1686
+
1687
+ test("archives in ONE statement that checks every 'untouched' condition itself", async () => {
1688
+ const query: jest.Mock = mockRawQueries({ other: [] });
1689
+ const restore: () => void = withEnv(
1690
+ "MESSAGE_QUEUE_AUTO_ARCHIVE_DAYS",
1691
+ undefined,
1692
+ );
1693
+
1694
+ try {
1695
+ const before: number = Date.now();
1696
+ await MessageQueueService.autoArchiveStaleMessageQueues();
1697
+
1698
+ expect(query).toHaveBeenCalledTimes(1);
1699
+ const [sql, params] = query.mock.calls[0] as [string, Array<unknown>];
1700
+
1701
+ // Discovered, live, not yet archived, unseen since the cutoff.
1702
+ expect(sql).toContain(`mq."deletedAt" IS NULL`);
1703
+ expect(sql).toContain(`mq."isArchived" = false`);
1704
+ expect(sql).toContain(`mq."discoverySource" IS NOT NULL`);
1705
+ expect(sql).toContain(`mq."discoverySource" <> $3`);
1706
+ expect(params[2]).toBe("manual");
1707
+ expect(sql).toContain(`COALESCE(mq."lastSeenAt", mq."createdAt") < $1`);
1708
+ const cutoff: Date = params[0] as Date;
1709
+ const sevenDays: number = 7 * 24 * 3600 * 1000;
1710
+ expect(before - cutoff.getTime()).toBeGreaterThanOrEqual(
1711
+ sevenDays - 1000,
1712
+ );
1713
+ expect(before - cutoff.getTime()).toBeLessThanOrEqual(sevenDays + 5000);
1714
+
1715
+ // Nobody described or renamed it.
1716
+ expect(sql).toContain(
1717
+ `(mq."description" IS NULL OR btrim(mq."description") = '')`,
1718
+ );
1719
+ expect(sql).toContain(`mq."name" = mq."destinationName"`);
1720
+ expect(sql).toContain(`char_length(mq."destinationName") > 100`);
1721
+ expect(sql).toContain(
1722
+ `mq."name" = rtrim(left(mq."destinationName", 99)) || '…'`,
1723
+ );
1724
+
1725
+ // Nobody invested in it: every join is scoped to the row's project and live rows.
1726
+ for (const table of [
1727
+ "MessageQueueLabel",
1728
+ "MessageQueueOwnerUser",
1729
+ "MessageQueueOwnerTeam",
1730
+ ]) {
1731
+ expect(sql).toMatch(
1732
+ new RegExp(`NOT EXISTS \\(\\s*SELECT 1 FROM "${table}"`),
1733
+ );
1734
+ }
1735
+ expect((sql.match(/"projectId" = mq\."projectId"/g) || []).length).toBe(
1736
+ 3,
1737
+ );
1738
+ // The row and its three investment joins.
1739
+ expect((sql.match(/"deletedAt" IS NULL/g) || []).length).toBe(4);
1740
+
1741
+ /*
1742
+ * Only a PERSON's labels and owners count: the ones rules attached
1743
+ * (automaticAssignments) are excluded inside each join.
1744
+ */
1745
+ expect(sql).toContain(
1746
+ `NOT (COALESCE(mq."automaticAssignments" -> 'labelIds', '[]'::jsonb) @> jsonb_build_array(l."labelId"::text))`,
1747
+ );
1748
+ expect(sql).toContain(
1749
+ `NOT (COALESCE(mq."automaticAssignments" -> 'ownerUserIds', '[]'::jsonb) @> jsonb_build_array(ou."userId"::text))`,
1750
+ );
1751
+ expect(sql).toContain(
1752
+ `NOT (COALESCE(mq."automaticAssignments" -> 'ownerTeamIds', '[]'::jsonb) @> jsonb_build_array(ot."teamId"::text))`,
1753
+ );
1754
+
1755
+ /*
1756
+ * A person's Restore holds until the row is seen again or the grace
1757
+ * period (30 days, or the archive window if longer) passes.
1758
+ */
1759
+ expect(sql).toContain(`mq."manuallyRestoredAt" IS NULL`);
1760
+ expect(sql).toContain(`mq."manuallyRestoredAt" < $5`);
1761
+ expect(sql).toContain(
1762
+ `COALESCE(mq."lastSeenAt", mq."createdAt") > mq."manuallyRestoredAt"`,
1763
+ );
1764
+ const graceCutoff: Date = params[4] as Date;
1765
+ const thirtyDays: number = 30 * 24 * 3600 * 1000;
1766
+ expect(before - graceCutoff.getTime()).toBeGreaterThanOrEqual(
1767
+ thirtyDays - 1000,
1768
+ );
1769
+ expect(before - graceCutoff.getTime()).toBeLessThanOrEqual(
1770
+ thirtyDays + 5000,
1771
+ );
1772
+
1773
+ // Marked as discovery's own archive, oldest first, bounded, never re-archiving.
1774
+ expect(sql).toContain(`"autoArchivedAt" = $2`);
1775
+ expect(sql).toContain(`"archivedAt" = $2`);
1776
+ expect(sql).toContain(`"archivedByUserId" = NULL`);
1777
+ expect(sql).toContain(`"isArchived" = true`);
1778
+ expect(sql).toContain(`stale."isArchived" = false`);
1779
+ expect(sql).toContain(
1780
+ `ORDER BY COALESCE(mq."lastSeenAt", mq."createdAt") ASC`,
1781
+ );
1782
+ expect(sql).toContain("LIMIT $4");
1783
+ expect(params[3]).toBe(500);
1784
+ expect(params).toHaveLength(5);
1785
+ } finally {
1786
+ restore();
1787
+ }
1788
+ });
1789
+
1790
+ test("returns how many rows were archived", async () => {
1791
+ mockRawQueries({
1792
+ other: [
1793
+ {
1794
+ _id: ObjectID.generate().toString(),
1795
+ projectId: PROJECT_ID.toString(),
1796
+ },
1797
+ {
1798
+ _id: ObjectID.generate().toString(),
1799
+ projectId: PROJECT_ID.toString(),
1800
+ },
1801
+ ],
1802
+ });
1803
+
1804
+ await expect(
1805
+ MessageQueueService.autoArchiveStaleMessageQueues(),
1806
+ ).resolves.toBe(2);
1807
+ });
1808
+
1809
+ test("an unexpected driver answer is 0", async () => {
1810
+ mockRawQueries({ other: { rowCount: 3 } });
1811
+
1812
+ await expect(
1813
+ MessageQueueService.autoArchiveStaleMessageQueues(),
1814
+ ).resolves.toBe(0);
1815
+ });
1816
+
1817
+ test("the window follows MESSAGE_QUEUE_AUTO_ARCHIVE_DAYS, and a long window stretches the restore hold", async () => {
1818
+ const query: jest.Mock = mockRawQueries({ other: [] });
1819
+ const restore: () => void = withEnv(
1820
+ "MESSAGE_QUEUE_AUTO_ARCHIVE_DAYS",
1821
+ "45",
1822
+ );
1823
+
1824
+ try {
1825
+ const before: number = Date.now();
1826
+ await MessageQueueService.autoArchiveStaleMessageQueues();
1827
+
1828
+ const params: Array<unknown> = query.mock.calls[0]![1] as Array<unknown>;
1829
+ const day: number = 24 * 3600 * 1000;
1830
+ expect(before - (params[0] as Date).getTime()).toBeGreaterThanOrEqual(
1831
+ 45 * day - 1000,
1832
+ );
1833
+ expect(before - (params[4] as Date).getTime()).toBeGreaterThanOrEqual(
1834
+ 45 * day - 1000,
1835
+ );
1836
+ } finally {
1837
+ restore();
1838
+ }
1839
+ });
1840
+
1841
+ test("a failing statement is thrown to the cron, which logs it", async () => {
1842
+ mockRawQueries({ other: new Error("statement timeout") });
1843
+
1844
+ await expect(
1845
+ MessageQueueService.autoArchiveStaleMessageQueues(),
1846
+ ).rejects.toThrow("statement timeout");
1847
+ });
1848
+ });
1849
+
1850
+ describe("MessageQueueService.getAutoArchiveDays", () => {
1851
+ test.each([
1852
+ [undefined, 7],
1853
+ ["", 7],
1854
+ ["14", 14],
1855
+ [" 3 ", 3],
1856
+ ["1", 1],
1857
+ ["0", 1],
1858
+ ["-5", 1],
1859
+ ["2.5", 7],
1860
+ ["nope", 7],
1861
+ ])("%p -> %p days", (value: string | undefined, expected: number) => {
1862
+ const restore: () => void = withEnv(
1863
+ "MESSAGE_QUEUE_AUTO_ARCHIVE_DAYS",
1864
+ value,
1865
+ );
1866
+ try {
1867
+ expect(MessageQueueService.getAutoArchiveDays()).toBe(expected);
1868
+ } finally {
1869
+ restore();
1870
+ }
1871
+ });
1872
+ });
1873
+
1874
+ describe("MessageQueueService auto-create budget", () => {
1875
+ test.each([
1876
+ [undefined, 500],
1877
+ ["50", 50],
1878
+ ["0", 0],
1879
+ ["-1", 0],
1880
+ ["x", 500],
1881
+ ])("budget %p -> %p", (value: string | undefined, expected: number) => {
1882
+ const restore: () => void = withEnv(
1883
+ "MESSAGE_QUEUE_AUTO_CREATE_BUDGET",
1884
+ value,
1885
+ );
1886
+ try {
1887
+ expect(MessageQueueService.getAutoCreateBudget()).toBe(expected);
1888
+ } finally {
1889
+ restore();
1890
+ }
1891
+ });
1892
+
1893
+ test("counts live, non-archived DISCOVERED rows of this project only", async () => {
1894
+ const query: jest.Mock = mockRawQueries({ count: [{ count: 499 }] });
1895
+
1896
+ await expect(
1897
+ MessageQueueService.isUnderAutoCreateBudget(PROJECT_ID),
1898
+ ).resolves.toBe(true);
1899
+
1900
+ const [sql, params] = query.mock.calls[0] as [string, Array<unknown>];
1901
+ expect(sql).toContain(`FROM "MessageQueue"`);
1902
+ expect(sql).toContain(`"projectId" = $1`);
1903
+ expect(sql).toContain(`"deletedAt" IS NULL`);
1904
+ expect(sql).toContain(`"isArchived" = false`);
1905
+ // A person's rows never count against discovery's budget.
1906
+ expect(sql).toContain(`COALESCE("discoverySource", '') <> $2`);
1907
+ expect(params).toEqual([PROJECT_ID.toString(), "manual"]);
1908
+ });
1909
+
1910
+ test("at the budget, discovery stops creating (a string count too)", async () => {
1911
+ mockRawQueries({ count: [{ count: "500" }] });
1912
+
1913
+ await expect(
1914
+ MessageQueueService.isUnderAutoCreateBudget(PROJECT_ID),
1915
+ ).resolves.toBe(false);
1916
+ });
1917
+
1918
+ test("an empty answer counts as none", async () => {
1919
+ mockRawQueries({ count: [] });
1920
+
1921
+ await expect(
1922
+ MessageQueueService.countAutoCreatedMessageQueues(PROJECT_ID),
1923
+ ).resolves.toBe(0);
1924
+ });
1925
+
1926
+ test("a zero budget turns auto-creation off without a query", async () => {
1927
+ const query: jest.Mock = mockRawQueries({ count: [{ count: 0 }] });
1928
+ const restore: () => void = withEnv(
1929
+ "MESSAGE_QUEUE_AUTO_CREATE_BUDGET",
1930
+ "0",
1931
+ );
1932
+
1933
+ try {
1934
+ await expect(
1935
+ MessageQueueService.isUnderAutoCreateBudget(PROJECT_ID),
1936
+ ).resolves.toBe(false);
1937
+ await expect(
1938
+ MessageQueueService.isUnderAutoCreateBudgetCached(PROJECT_ID),
1939
+ ).resolves.toBe(false);
1940
+ expect(query).not.toHaveBeenCalled();
1941
+ } finally {
1942
+ restore();
1943
+ }
1944
+ });
1945
+
1946
+ test("the cached check reads the count once a minute per project", async () => {
1947
+ const query: jest.Mock = mockRawQueries({ count: [{ count: 10 }] });
1948
+ const other: ObjectID = ObjectID.generate();
1949
+
1950
+ for (let i: number = 0; i < 3; i++) {
1951
+ await MessageQueueService.isUnderAutoCreateBudgetCached(PROJECT_ID);
1952
+ }
1953
+ await MessageQueueService.isUnderAutoCreateBudgetCached(other);
1954
+
1955
+ expect(query).toHaveBeenCalledTimes(2);
1956
+
1957
+ MessageQueueService.clearAutoCreateBudgetMemo();
1958
+ await MessageQueueService.isUnderAutoCreateBudgetCached(PROJECT_ID);
1959
+ expect(query).toHaveBeenCalledTimes(3);
1960
+ });
1961
+ });
1962
+
1963
+ /*
1964
+ * ---------------------------------------------------------------------------
1965
+ * A person's edits
1966
+ * ---------------------------------------------------------------------------
1967
+ */
1968
+ describe("MessageQueueService.onUpdateSuccess", () => {
1969
+ let clearWrites: jest.SpyInstance;
1970
+ let forget: jest.SpyInstance;
1971
+
1972
+ function onUpdate(data: Record<string, unknown>): any {
1973
+ return {
1974
+ updateBy: {
1975
+ query: {},
1976
+ data: data,
1977
+ props: { userId: USER_ID },
1978
+ },
1979
+ carryForward: null,
1980
+ };
1981
+ }
1982
+
1983
+ beforeEach(() => {
1984
+ silenceLogs();
1985
+ clearWrites = getJestSpyOn(
1986
+ service,
1987
+ "updateColumnsByIdWithoutHooks",
1988
+ ).mockResolvedValue(undefined);
1989
+ forget = getJestSpyOn(
1990
+ service,
1991
+ "forgetAutomaticAssignments",
1992
+ ).mockResolvedValue(undefined);
1993
+ });
1994
+
1995
+ /*
1996
+ * A person archiving (or restoring) a row takes it out of discovery's
1997
+ * hands: from then on only a person restores it.
1998
+ */
1999
+ test.each([[true], [false]])(
2000
+ "a person setting isArchived=%s clears autoArchivedAt on every row",
2001
+ async (isArchived: boolean) => {
2002
+ const ids: Array<ObjectID> = [ObjectID.generate(), ObjectID.generate()];
2003
+ const before: number = Date.now();
2004
+
2005
+ await service.onUpdateSuccess(onUpdate({ isArchived }), ids);
2006
+
2007
+ expect(clearWrites).toHaveBeenCalledTimes(2);
2008
+ for (const [index, id] of ids.entries()) {
2009
+ const write: any = clearWrites.mock.calls[index]![0];
2010
+ expect(write.id).toBe(id);
2011
+ expect(write.skipUpdateDateColumn).toBe(true);
2012
+ expect(write.data.autoArchivedAt).toBeNull();
2013
+ /*
2014
+ * A Restore is stamped so the sweep leaves the row alone; an archive
2015
+ * clears the stamp.
2016
+ */
2017
+ if (isArchived) {
2018
+ expect(write.data.manuallyRestoredAt).toBeNull();
2019
+ } else {
2020
+ expect(
2021
+ (write.data.manuallyRestoredAt as Date).getTime(),
2022
+ ).toBeGreaterThanOrEqual(before);
2023
+ }
2024
+ }
2025
+ },
2026
+ );
2027
+
2028
+ test("an archive write that carries autoArchivedAt is not a person's", async () => {
2029
+ await service.onUpdateSuccess(
2030
+ onUpdate({ isArchived: true, autoArchivedAt: new Date() }),
2031
+ [ObjectID.generate()],
2032
+ );
2033
+
2034
+ expect(clearWrites).not.toHaveBeenCalled();
2035
+ });
2036
+
2037
+ test("an edit that is not an archive change leaves the archive columns alone", async () => {
2038
+ await service.onUpdateSuccess(onUpdate({ name: "Orders" }), [
2039
+ ObjectID.generate(),
2040
+ ]);
2041
+
2042
+ expect(clearWrites).not.toHaveBeenCalled();
2043
+ expect(forget).not.toHaveBeenCalled();
2044
+ });
2045
+
2046
+ test("labels a person saves become theirs on every updated row", async () => {
2047
+ const a: ObjectID = ObjectID.generate();
2048
+ const b: ObjectID = ObjectID.generate();
2049
+ const label: ObjectID = ObjectID.generate();
2050
+
2051
+ await service.onUpdateSuccess(
2052
+ onUpdate({ labels: [{ _id: label.toString() }, label, "not-an-id"] }),
2053
+ [a, b],
2054
+ );
2055
+
2056
+ expect(forget).toHaveBeenCalledTimes(2);
2057
+ expect(forget.mock.calls[0]![0]).toEqual({
2058
+ messageQueueId: a,
2059
+ kind: "labelIds",
2060
+ ids: [label.toString(), label.toString(), "not-an-id"],
2061
+ });
2062
+ expect(forget.mock.calls[1]![0].messageQueueId).toBe(b);
2063
+ });
2064
+
2065
+ test("a failing clear is logged, not thrown", async () => {
2066
+ clearWrites.mockRejectedValue(new Error("connection terminated"));
2067
+
2068
+ await expect(
2069
+ service.onUpdateSuccess(onUpdate({ isArchived: true }), [
2070
+ ObjectID.generate(),
2071
+ ]),
2072
+ ).resolves.toBeDefined();
2073
+ });
2074
+ });
2075
+
2076
+ describe("MessageQueueService automatic assignments", () => {
2077
+ let logs: LoggerSpies;
2078
+
2079
+ beforeEach(() => {
2080
+ logs = silenceLogs();
2081
+ });
2082
+
2083
+ test("records rule-made labels in one atomic statement, ids normalized and deduped", async () => {
2084
+ const query: jest.Mock = mockRawQueries({ other: [] });
2085
+ const queueId: ObjectID = ObjectID.generate();
2086
+ const label: string = "AAAAAAAA-1111-4111-8111-111111111111";
2087
+
2088
+ await MessageQueueService.recordAutomaticAssignments({
2089
+ messageQueueId: queueId,
2090
+ kind: "labelIds",
2091
+ ids: [label, label.toLowerCase(), "not-a-uuid"],
2092
+ });
2093
+
2094
+ expect(query).toHaveBeenCalledTimes(1);
2095
+ const [sql, params] = query.mock.calls[0] as [string, Array<unknown>];
2096
+ expect(sql).toContain(`UPDATE "MessageQueue"`);
2097
+ expect(sql).toContain(`jsonb_agg(DISTINCT assigned.id)`);
2098
+ expect(sql).toContain(`WHERE "_id" = $1`);
2099
+ expect(params).toEqual([
2100
+ queueId.toString(),
2101
+ "labelIds",
2102
+ JSON.stringify([label.toLowerCase()]),
2103
+ ]);
2104
+ });
2105
+
2106
+ test("forgets what a person re-added, only where a list exists", async () => {
2107
+ const query: jest.Mock = mockRawQueries({ other: [] });
2108
+ const queueId: ObjectID = ObjectID.generate();
2109
+ const team: ObjectID = ObjectID.generate();
2110
+
2111
+ await MessageQueueService.forgetAutomaticAssignments({
2112
+ messageQueueId: queueId,
2113
+ kind: "ownerTeamIds",
2114
+ ids: [team],
2115
+ });
2116
+
2117
+ const [sql, params] = query.mock.calls[0] as [string, Array<unknown>];
2118
+ expect(sql).toContain(`NOT (assigned.id = ANY($3::text[]))`);
2119
+ expect(sql).toContain(
2120
+ `jsonb_typeof("automaticAssignments" -> $2::text) = 'array'`,
2121
+ );
2122
+ expect(params).toEqual([
2123
+ queueId.toString(),
2124
+ "ownerTeamIds",
2125
+ [team.toString()],
2126
+ ]);
2127
+ });
2128
+
2129
+ test("nothing valid to record, or an unknown kind, is a no-op", async () => {
2130
+ const query: jest.Mock = mockRawQueries({ other: [] });
2131
+
2132
+ await MessageQueueService.recordAutomaticAssignments({
2133
+ messageQueueId: ObjectID.generate(),
2134
+ kind: "labelIds",
2135
+ ids: ["nope"],
2136
+ });
2137
+ await MessageQueueService.forgetAutomaticAssignments({
2138
+ messageQueueId: ObjectID.generate(),
2139
+ kind: "somethingElse" as any,
2140
+ ids: [ObjectID.generate()],
2141
+ });
2142
+
2143
+ expect(query).not.toHaveBeenCalled();
2144
+ });
2145
+
2146
+ test("a failing write is logged, never thrown - it only annotates a write that happened", async () => {
2147
+ mockRawQueries({ other: new Error("deadlock detected") });
2148
+
2149
+ await expect(
2150
+ MessageQueueService.recordAutomaticAssignments({
2151
+ messageQueueId: ObjectID.generate(),
2152
+ kind: "ownerUserIds",
2153
+ ids: [ObjectID.generate()],
2154
+ }),
2155
+ ).resolves.toBeUndefined();
2156
+ await expect(
2157
+ MessageQueueService.forgetAutomaticAssignments({
2158
+ messageQueueId: ObjectID.generate(),
2159
+ kind: "ownerUserIds",
2160
+ ids: [ObjectID.generate()],
2161
+ }),
2162
+ ).resolves.toBeUndefined();
2163
+ expect(logs.warn).toHaveBeenCalledTimes(2);
2164
+ });
2165
+ });
2166
+
2167
+ describe("MessageQueueService names and links", () => {
2168
+ test("links to /queues/<id> and names the queue in markdown", async () => {
2169
+ const id: ObjectID = ObjectID.generate();
2170
+ getJestSpyOn(service, "findOneById").mockResolvedValue(
2171
+ queueRow({ name: "orders.created" }),
2172
+ );
2173
+ getJestSpyOn(DatabaseConfig, "getDashboardUrl").mockResolvedValue(
2174
+ URL.fromString("https://oneuptime.example.com/dashboard"),
2175
+ );
2176
+
2177
+ const markdown: string =
2178
+ await MessageQueueService.getMessageQueueMarkdownLink(PROJECT_ID, id);
2179
+
2180
+ expect(markdown).toBe(
2181
+ `[Queue orders.created](https://oneuptime.example.com/dashboard/${PROJECT_ID.toString()}/queues/${id.toString()})`,
2182
+ );
2183
+ });
2184
+
2185
+ test("a missing row names as an empty string instead of throwing", async () => {
2186
+ getJestSpyOn(service, "findOneById").mockResolvedValue(null);
2187
+
2188
+ await expect(
2189
+ MessageQueueService.getMessageQueueName({
2190
+ messageQueueId: ObjectID.generate(),
2191
+ }),
2192
+ ).resolves.toBe("");
2193
+ });
2194
+
2195
+ test("a name is read with the caller's own permissions, and '' when refused", async () => {
2196
+ const findOneBy: jest.SpyInstance = getJestSpyOn(
2197
+ service,
2198
+ "findOneBy",
2199
+ ).mockRejectedValue(new NotAuthorizedException("no"));
2200
+ const props: DatabaseCommonInteractionProps = memberProps();
2201
+
2202
+ await expect(
2203
+ MessageQueueService.getMessageQueueNameIfReadable({
2204
+ messageQueueId: ObjectID.generate(),
2205
+ props,
2206
+ }),
2207
+ ).resolves.toBe("");
2208
+ expect(findOneBy.mock.calls[0]![0].props).toBe(props);
2209
+ });
2210
+ });