@oneuptime/common 14.0.9 → 14.0.10

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 (383) hide show
  1. package/Models/DatabaseModels/AutoRemediationSuggestion.ts +60 -0
  2. package/Models/DatabaseModels/CephCluster.ts +213 -0
  3. package/Models/DatabaseModels/DatabaseServer.ts +213 -0
  4. package/Models/DatabaseModels/DockerHost.ts +213 -0
  5. package/Models/DatabaseModels/DockerSwarmCluster.ts +213 -0
  6. package/Models/DatabaseModels/GlobalConfig.ts +1 -1
  7. package/Models/DatabaseModels/Host.ts +213 -0
  8. package/Models/DatabaseModels/Index.ts +2 -0
  9. package/Models/DatabaseModels/PodmanHost.ts +213 -0
  10. package/Models/DatabaseModels/ProxmoxCluster.ts +213 -0
  11. package/Models/DatabaseModels/ResourceAiAgent.ts +419 -0
  12. package/Models/DatabaseModels/RunnerJob.ts +144 -0
  13. package/Models/DatabaseModels/VMwareVCenter.ts +213 -0
  14. package/Server/API/AutoRemediationAPI.ts +392 -1
  15. package/Server/API/ResourceAiAccessAPI.ts +1669 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.ts +423 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  18. package/Server/Infrastructure/Semaphore.ts +22 -0
  19. package/Server/Middleware/TelemetryIngest.ts +15 -5
  20. package/Server/Services/AnalyticsDatabaseService.ts +45 -1
  21. package/Server/Services/AutoRemediationRuleEngineService.ts +940 -112
  22. package/Server/Services/CephClusterService.ts +109 -1
  23. package/Server/Services/DatabaseServerService.ts +116 -1
  24. package/Server/Services/DockerHostService.ts +109 -1
  25. package/Server/Services/DockerSwarmClusterService.ts +113 -1
  26. package/Server/Services/HostService.ts +126 -4
  27. package/Server/Services/MetricRecordingRuleService.ts +47 -0
  28. package/Server/Services/PodmanHostService.ts +109 -1
  29. package/Server/Services/ProxmoxClusterService.ts +110 -1
  30. package/Server/Services/ResourceAiAccessService.ts +1439 -0
  31. package/Server/Services/ResourceAiAgentJobService.ts +202 -0
  32. package/Server/Services/ResourceAiAgentService.ts +2432 -0
  33. package/Server/Services/RunnerJobService.ts +611 -4
  34. package/Server/Services/TelemetryUsageBillingService.ts +28 -0
  35. package/Server/Services/TraceRecordingRuleService.ts +33 -0
  36. package/Server/Services/VMwareVCenterService.ts +110 -1
  37. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +1434 -32
  38. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +935 -21
  39. package/Server/Utils/AI/Remediation/RemediationPlanRunner.ts +25 -1
  40. package/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.ts +577 -0
  41. package/Server/Utils/AI/ResourceAccess/ResourceAccessContext.ts +271 -0
  42. package/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.ts +45 -0
  43. package/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.ts +964 -0
  44. package/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.ts +333 -0
  45. package/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.ts +873 -0
  46. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +72 -2
  47. package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +72 -5
  48. package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +72 -5
  49. package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +396 -13
  50. package/Server/Utils/AutoRemediation/RemediationVerifier.ts +35 -2
  51. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +9 -1
  52. package/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.ts +920 -0
  53. package/Server/Utils/SessionReplay/SessionReplayUsage.ts +84 -6
  54. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +35 -15
  55. package/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.ts +33 -15
  56. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +21 -1
  57. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +298 -224
  58. package/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.ts +35 -15
  59. package/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.ts +389 -175
  60. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +366 -143
  61. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.ts +163 -0
  62. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.ts +396 -0
  63. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.ts +418 -0
  64. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.ts +152 -0
  65. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.ts +251 -0
  66. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.ts +214 -0
  67. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +93 -0
  68. package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +18 -6
  69. package/Tests/App/Dashboard/InvestigationInfrastructureTools.test.tsx +506 -0
  70. package/Tests/App/Dashboard/RemediationSuggestionCardDescription.test.tsx +209 -0
  71. package/Tests/App/Dashboard/RemediationSuggestionCardResource.test.tsx +317 -0
  72. package/Tests/App/Dashboard/ResourceAiAccessSettingsUtil.test.ts +921 -0
  73. package/Tests/App/Dashboard/ResourceAiAgentInstall.test.ts +860 -0
  74. package/Tests/App/Dashboard/ResourceAiAgentPage.test.tsx +1721 -0
  75. package/Tests/App/Dashboard/ResourceAiAgentStatus.test.ts +1002 -0
  76. package/Tests/App/Dashboard/ResourceAiInsightsPage.test.tsx +931 -0
  77. package/Tests/App/Dashboard/ResourceAiNavigation.test.tsx +575 -0
  78. package/Tests/App/Dashboard/RunbookStepTypeMaps.test.ts +38 -7
  79. package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +33 -1
  80. package/Tests/Models/DatabaseModels/ResourceAiAccessColumns.test.ts +738 -0
  81. package/Tests/Models/DatabaseModels/ResourceAiAgentModel.test.ts +842 -0
  82. package/Tests/Server/API/AutoRemediationApproveResourceRound.test.ts +835 -0
  83. package/Tests/Server/API/ResourceAiAccessAPI.test.ts +2730 -0
  84. package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +60 -0
  85. package/Tests/Server/Infrastructure/Postgres/AddResourceAiAgentsMigration.test.ts +659 -0
  86. package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +31 -0
  87. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +63 -5
  88. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +103 -5
  89. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +19 -1
  90. package/Tests/Server/Services/AutoRemediationResourceRuleEngine.test.ts +1279 -0
  91. package/Tests/Server/Services/DatabaseServerService.test.ts +55 -0
  92. package/Tests/Server/Services/GroupTelemetryUsageExcludeNames.test.ts +299 -0
  93. package/Tests/Server/Services/HostServiceFindOrCreateMemo.test.ts +44 -0
  94. package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +45 -6
  95. package/Tests/Server/Services/RecordingRuleReservedMetricName.test.ts +171 -0
  96. package/Tests/Server/Services/ResourceAiAccessChangeAuthorization.test.ts +256 -0
  97. package/Tests/Server/Services/ResourceAiAccessService.test.ts +1373 -0
  98. package/Tests/Server/Services/ResourceAiAgentJobService.test.ts +375 -0
  99. package/Tests/Server/Services/ResourceAiAgentServiceHelpers.test.ts +1114 -0
  100. package/Tests/Server/Services/ResourceAiAgentServiceLifecycle.test.ts +1050 -0
  101. package/Tests/Server/Services/ResourceAiAgentServiceRegister.test.ts +2251 -0
  102. package/Tests/Server/Services/ResourceAiSettingsCreate.test.ts +373 -0
  103. package/Tests/Server/Services/ResourceAiSettingsPermission.test.ts +1042 -0
  104. package/Tests/Server/Services/ResourceServiceDeleteCleansUpAi.test.ts +319 -0
  105. package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +105 -0
  106. package/Tests/Server/Services/RunnerJobEnqueueResourceCommand.test.ts +986 -0
  107. package/Tests/Server/Services/RunnerJobResourceCommandLane.test.ts +211 -0
  108. package/Tests/Server/Services/RunnerJobResourceCommandTimeoutAndRedaction.test.ts +352 -0
  109. package/Tests/Server/Services/TelemetryUsageBillingSloExclusion.test.ts +218 -0
  110. package/Tests/Server/TestingUtils/Services/FakeRunnerJobCount.ts +145 -0
  111. package/Tests/Server/Utils/AI/InvestigationInfrastructureAccessWiring.test.ts +453 -0
  112. package/Tests/Server/Utils/AI/InvestigationInfrastructureReport.test.ts +721 -0
  113. package/Tests/Server/Utils/AI/RemediationCommandTools.test.ts +94 -1
  114. package/Tests/Server/Utils/AI/RemediationCommandToolsResource.test.ts +1530 -0
  115. package/Tests/Server/Utils/AI/RemediationExecutionRunnerResourceMode.test.ts +1249 -0
  116. package/Tests/Server/Utils/AI/RemediationPlanRunner.test.ts +117 -0
  117. package/Tests/Server/Utils/AI/RemediationResourceCopyParity.test.ts +463 -0
  118. package/Tests/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.test.ts +722 -0
  119. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAccessContext.test.ts +266 -0
  120. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.test.ts +1220 -0
  121. package/Tests/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.test.ts +569 -0
  122. package/Tests/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.test.ts +830 -0
  123. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorResource.test.ts +717 -0
  124. package/Tests/Server/Utils/AutoRemediation/RemediationVerifierResourceFollowUp.test.ts +338 -0
  125. package/Tests/Server/Utils/Monitor/Criteria/SessionReplayBudgetTemplateCriteria.test.ts +422 -0
  126. package/Tests/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.test.ts +1647 -0
  127. package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +143 -1
  128. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +33 -3
  129. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsAccountNotLinked.test.ts +1093 -0
  130. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.test.ts +1049 -0
  131. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.test.ts +1676 -0
  132. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCards.test.ts +2884 -0
  133. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.test.ts +2490 -0
  134. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmit.test.ts +2293 -0
  135. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateSubmitServerTimezone.test.ts +386 -0
  136. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.test.ts +1477 -0
  137. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.test.ts +1967 -0
  138. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsStripHtmlTags.test.ts +92 -0
  139. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsSubmittedFormRemoval.test.ts +1819 -0
  140. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.test.ts +1033 -0
  141. package/Tests/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezoneServerZone.test.ts +610 -0
  142. package/Tests/Server/Utils/Workspace/MicrosoftTeamsBotMessageHandling.test.ts +2610 -0
  143. package/Tests/Server/Utils/Workspace/MicrosoftTeamsCreateCommandsEndToEnd.test.ts +4672 -0
  144. package/Tests/Server/Utils/Workspace/WorkspaceCreateProjectReferences.test.ts +190 -65
  145. package/Tests/Types/AutoRemediation/AiRemediationCommandPlan.test.ts +10 -4
  146. package/Tests/Types/AutoRemediation/AiRemediationCommandPlanResourceCommand.test.ts +305 -0
  147. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +277 -13
  148. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +113 -0
  149. package/Tests/Types/Monitor/RumAlertTemplates.test.ts +773 -2
  150. package/Tests/Types/ResourceAiAgent/AiResourceType.test.ts +419 -0
  151. package/Tests/Types/ResourceAiAgent/ResourceAiAccess.test.ts +517 -0
  152. package/Tests/Types/Runbook/RunbookStepType.test.ts +147 -8
  153. package/Tests/UI/Rum/RecordingHealthDashboard.test.tsx +769 -1
  154. package/Tests/Utils/AiRemediation/Resource/CephCommandPolicy.test.ts +1653 -0
  155. package/Tests/Utils/AiRemediation/Resource/CephOutputRedaction.test.ts +204 -0
  156. package/Tests/Utils/AiRemediation/Resource/DatabaseCommandPolicy.test.ts +1077 -0
  157. package/Tests/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.test.ts +558 -0
  158. package/Tests/Utils/AiRemediation/Resource/DatabaseQueryRedactor.test.ts +834 -0
  159. package/Tests/Utils/AiRemediation/Resource/DockerCliGrammar.test.ts +747 -0
  160. package/Tests/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.test.ts +1259 -0
  161. package/Tests/Utils/AiRemediation/Resource/DockerOutputRedaction.test.ts +386 -0
  162. package/Tests/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.test.ts +782 -0
  163. package/Tests/Utils/AiRemediation/Resource/GovcCommandPolicy.test.ts +2259 -0
  164. package/Tests/Utils/AiRemediation/Resource/HostCommandPolicy.test.ts +2019 -0
  165. package/Tests/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.test.ts +2497 -0
  166. package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicy.test.ts +1305 -0
  167. package/Tests/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.test.ts +585 -0
  168. package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactor.test.ts +334 -0
  169. package/Tests/Utils/AiRemediation/Resource/ResourceOutputRedactorCopyParity.test.ts +98 -0
  170. package/Tests/Utils/AiRemediation/Resource/ResourcePolicyImportClosure.test.ts +391 -0
  171. package/Tests/Utils/AiRemediation/ResourceAiAgentPolicyCopyParity.test.ts +497 -0
  172. package/Tests/Utils/SessionReplay/SessionReplayBudgetMetricType.test.ts +383 -0
  173. package/Types/AI/ResourceAiAccessApi.ts +140 -0
  174. package/Types/AI/ResourceAiAccessPermissions.ts +126 -0
  175. package/Types/AutoRemediation/AiRemediationCommandPlan.ts +183 -2
  176. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +55 -22
  177. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +35 -3
  178. package/Types/Monitor/RumAlertTemplates.ts +351 -4
  179. package/Types/ResourceAiAgent/AiResourceType.ts +310 -0
  180. package/Types/ResourceAiAgent/ResourceAiAccess.ts +574 -0
  181. package/Types/Rum/SessionReplayBudgetMetricType.ts +47 -0
  182. package/Types/Runbook/RunbookStepType.ts +29 -0
  183. package/Types/Telemetry/TelemetryIngestSurface.ts +14 -2
  184. package/Utils/AI/InvestigationReport.ts +121 -15
  185. package/Utils/AiRemediation/Resource/CephCommandPolicy.ts +1936 -0
  186. package/Utils/AiRemediation/Resource/DatabaseCommandPolicy.ts +166 -0
  187. package/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.ts +1532 -0
  188. package/Utils/AiRemediation/Resource/DatabaseQueryRedactor.ts +1394 -0
  189. package/Utils/AiRemediation/Resource/DockerCliGrammar.ts +1839 -0
  190. package/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.ts +490 -0
  191. package/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.ts +687 -0
  192. package/Utils/AiRemediation/Resource/GovcCommandPolicy.ts +2096 -0
  193. package/Utils/AiRemediation/Resource/HostCommandPolicy.ts +2982 -0
  194. package/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.ts +1679 -0
  195. package/Utils/AiRemediation/Resource/ResourceCommandPolicy.ts +851 -0
  196. package/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.ts +593 -0
  197. package/Utils/AiRemediation/Resource/ResourceOutputRedactor.ts +2812 -0
  198. package/Utils/SessionReplay/SessionReplayBudgetMetricType.ts +184 -0
  199. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js +62 -0
  200. package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js.map +1 -1
  201. package/build/dist/Models/DatabaseModels/CephCluster.js +219 -0
  202. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  203. package/build/dist/Models/DatabaseModels/DatabaseServer.js +219 -0
  204. package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -1
  205. package/build/dist/Models/DatabaseModels/DockerHost.js +219 -0
  206. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  207. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js +219 -0
  208. package/build/dist/Models/DatabaseModels/DockerSwarmCluster.js.map +1 -1
  209. package/build/dist/Models/DatabaseModels/GlobalConfig.js +1 -1
  210. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  211. package/build/dist/Models/DatabaseModels/Host.js +219 -0
  212. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  213. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  214. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  215. package/build/dist/Models/DatabaseModels/PodmanHost.js +219 -0
  216. package/build/dist/Models/DatabaseModels/PodmanHost.js.map +1 -1
  217. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +219 -0
  218. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  219. package/build/dist/Models/DatabaseModels/ResourceAiAgent.js +439 -0
  220. package/build/dist/Models/DatabaseModels/ResourceAiAgent.js.map +1 -0
  221. package/build/dist/Models/DatabaseModels/RunnerJob.js +145 -0
  222. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  223. package/build/dist/Models/DatabaseModels/VMwareVCenter.js +219 -0
  224. package/build/dist/Models/DatabaseModels/VMwareVCenter.js.map +1 -1
  225. package/build/dist/Server/API/AutoRemediationAPI.js +231 -1
  226. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  227. package/build/dist/Server/API/ResourceAiAccessAPI.js +1119 -0
  228. package/build/dist/Server/API/ResourceAiAccessAPI.js.map +1 -0
  229. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js +168 -0
  230. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796300000000-AddResourceAiAgents.js.map +1 -0
  231. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  232. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  233. package/build/dist/Server/Infrastructure/Semaphore.js +6 -0
  234. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  235. package/build/dist/Server/Middleware/TelemetryIngest.js +12 -5
  236. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  237. package/build/dist/Server/Services/AnalyticsDatabaseService.js +26 -1
  238. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  239. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +601 -3
  240. package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -1
  241. package/build/dist/Server/Services/CephClusterService.js +104 -0
  242. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  243. package/build/dist/Server/Services/DatabaseServerService.js +99 -0
  244. package/build/dist/Server/Services/DatabaseServerService.js.map +1 -1
  245. package/build/dist/Server/Services/DockerHostService.js +104 -0
  246. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  247. package/build/dist/Server/Services/DockerSwarmClusterService.js +104 -0
  248. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  249. package/build/dist/Server/Services/HostService.js +114 -2
  250. package/build/dist/Server/Services/HostService.js.map +1 -1
  251. package/build/dist/Server/Services/MetricRecordingRuleService.js +46 -0
  252. package/build/dist/Server/Services/MetricRecordingRuleService.js.map +1 -1
  253. package/build/dist/Server/Services/PodmanHostService.js +104 -0
  254. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  255. package/build/dist/Server/Services/ProxmoxClusterService.js +104 -0
  256. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  257. package/build/dist/Server/Services/ResourceAiAccessService.js +1028 -0
  258. package/build/dist/Server/Services/ResourceAiAccessService.js.map +1 -0
  259. package/build/dist/Server/Services/ResourceAiAgentJobService.js +173 -0
  260. package/build/dist/Server/Services/ResourceAiAgentJobService.js.map +1 -0
  261. package/build/dist/Server/Services/ResourceAiAgentService.js +1687 -0
  262. package/build/dist/Server/Services/ResourceAiAgentService.js.map +1 -0
  263. package/build/dist/Server/Services/RunnerJobService.js +344 -5
  264. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  265. package/build/dist/Server/Services/TelemetryUsageBillingService.js +26 -0
  266. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  267. package/build/dist/Server/Services/TraceRecordingRuleService.js +37 -0
  268. package/build/dist/Server/Services/TraceRecordingRuleService.js.map +1 -1
  269. package/build/dist/Server/Services/VMwareVCenterService.js +104 -0
  270. package/build/dist/Server/Services/VMwareVCenterService.js.map +1 -1
  271. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +1029 -30
  272. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  273. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +633 -28
  274. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  275. package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js +21 -1
  276. package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js.map +1 -1
  277. package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js +330 -0
  278. package/build/dist/Server/Utils/AI/ResourceAccess/InfrastructureInvestigationToolkit.js.map +1 -0
  279. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js +160 -0
  280. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessContext.js.map +1 -0
  281. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js +33 -0
  282. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAccessToolNames.js.map +1 -0
  283. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js +640 -0
  284. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiAccessSettings.js.map +1 -0
  285. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js +226 -0
  286. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceAiDeleteCleanup.js.map +1 -0
  287. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js +600 -0
  288. package/build/dist/Server/Utils/AI/ResourceAccess/ResourceCommandJobRunner.js.map +1 -0
  289. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +50 -4
  290. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  291. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +55 -4
  292. package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
  293. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +55 -4
  294. package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
  295. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +290 -13
  296. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
  297. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js +29 -1
  298. package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js.map +1 -1
  299. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +9 -1
  300. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  301. package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js +627 -0
  302. package/build/dist/Server/Utils/SessionReplay/SessionReplayBudgetMetrics.js.map +1 -0
  303. package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js +60 -5
  304. package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js.map +1 -1
  305. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +19 -15
  306. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  307. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js +19 -15
  308. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/AlertEpisode.js.map +1 -1
  309. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +17 -1
  310. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  311. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +183 -209
  312. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  313. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js +19 -15
  314. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/IncidentEpisode.js.map +1 -1
  315. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js +233 -167
  316. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/ScheduledMaintenance.js.map +1 -1
  317. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +263 -110
  318. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  319. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js +129 -0
  320. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsActivityDeduplicator.js.map +1 -0
  321. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js +266 -0
  322. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCardChoices.js.map +1 -0
  323. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js +258 -0
  324. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsCreateCommands.js.map +1 -0
  325. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js +104 -0
  326. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsMessageSize.js.map +1 -0
  327. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js +183 -0
  328. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsReplies.js.map +1 -0
  329. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js +123 -0
  330. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeamsTimezone.js.map +1 -0
  331. package/build/dist/Types/AI/ResourceAiAccessApi.js +30 -0
  332. package/build/dist/Types/AI/ResourceAiAccessApi.js.map +1 -0
  333. package/build/dist/Types/AI/ResourceAiAccessPermissions.js +98 -0
  334. package/build/dist/Types/AI/ResourceAiAccessPermissions.js.map +1 -0
  335. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +116 -29
  336. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
  337. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +47 -21
  338. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  339. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +5 -0
  340. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  341. package/build/dist/Types/Monitor/RumAlertTemplates.js +199 -3
  342. package/build/dist/Types/Monitor/RumAlertTemplates.js.map +1 -1
  343. package/build/dist/Types/ResourceAiAgent/AiResourceType.js +242 -0
  344. package/build/dist/Types/ResourceAiAgent/AiResourceType.js.map +1 -0
  345. package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js +275 -0
  346. package/build/dist/Types/ResourceAiAgent/ResourceAiAccess.js.map +1 -0
  347. package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js +45 -0
  348. package/build/dist/Types/Rum/SessionReplayBudgetMetricType.js.map +1 -0
  349. package/build/dist/Types/Runbook/RunbookStepType.js +27 -0
  350. package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
  351. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +13 -2
  352. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
  353. package/build/dist/Utils/AI/InvestigationReport.js +71 -11
  354. package/build/dist/Utils/AI/InvestigationReport.js.map +1 -1
  355. package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js +1462 -0
  356. package/build/dist/Utils/AiRemediation/Resource/CephCommandPolicy.js.map +1 -0
  357. package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js +122 -0
  358. package/build/dist/Utils/AiRemediation/Resource/DatabaseCommandPolicy.js.map +1 -0
  359. package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js +1139 -0
  360. package/build/dist/Utils/AiRemediation/Resource/DatabaseDiagnosticCatalog.js.map +1 -0
  361. package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js +1060 -0
  362. package/build/dist/Utils/AiRemediation/Resource/DatabaseQueryRedactor.js.map +1 -0
  363. package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js +1301 -0
  364. package/build/dist/Utils/AiRemediation/Resource/DockerCliGrammar.js.map +1 -0
  365. package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js +370 -0
  366. package/build/dist/Utils/AiRemediation/Resource/DockerEngineCommandPolicy.js.map +1 -0
  367. package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js +498 -0
  368. package/build/dist/Utils/AiRemediation/Resource/DockerSwarmCommandPolicy.js.map +1 -0
  369. package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js +1565 -0
  370. package/build/dist/Utils/AiRemediation/Resource/GovcCommandPolicy.js.map +1 -0
  371. package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js +2206 -0
  372. package/build/dist/Utils/AiRemediation/Resource/HostCommandPolicy.js.map +1 -0
  373. package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js +1129 -0
  374. package/build/dist/Utils/AiRemediation/Resource/ProxmoxCommandPolicy.js.map +1 -0
  375. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js +565 -0
  376. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicy.js.map +1 -0
  377. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js +408 -0
  378. package/build/dist/Utils/AiRemediation/Resource/ResourceCommandPolicyCore.js.map +1 -0
  379. package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js +1910 -0
  380. package/build/dist/Utils/AiRemediation/Resource/ResourceOutputRedactor.js.map +1 -0
  381. package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js +160 -0
  382. package/build/dist/Utils/SessionReplay/SessionReplayBudgetMetricType.js.map +1 -0
  383. package/package.json +1 -1
@@ -0,0 +1,1114 @@
1
+ import ResourceAiAgent from "../../../Models/DatabaseModels/ResourceAiAgent";
2
+ import CephClusterFeedService from "../../../Server/Services/CephClusterFeedService";
3
+ import CephClusterService from "../../../Server/Services/CephClusterService";
4
+ import DatabaseServerFeedService from "../../../Server/Services/DatabaseServerFeedService";
5
+ import DatabaseServerService from "../../../Server/Services/DatabaseServerService";
6
+ import DockerHostFeedService from "../../../Server/Services/DockerHostFeedService";
7
+ import DockerHostService from "../../../Server/Services/DockerHostService";
8
+ import DockerSwarmClusterFeedService from "../../../Server/Services/DockerSwarmClusterFeedService";
9
+ import DockerSwarmClusterService from "../../../Server/Services/DockerSwarmClusterService";
10
+ import HostFeedService from "../../../Server/Services/HostFeedService";
11
+ import HostService from "../../../Server/Services/HostService";
12
+ import PodmanHostFeedService from "../../../Server/Services/PodmanHostFeedService";
13
+ import PodmanHostService from "../../../Server/Services/PodmanHostService";
14
+ import ProxmoxClusterFeedService from "../../../Server/Services/ProxmoxClusterFeedService";
15
+ import ProxmoxClusterService from "../../../Server/Services/ProxmoxClusterService";
16
+ import ResourceAiAgentService, {
17
+ MAX_DATABASE_AI_AGENT_IDENTIFIER_LENGTH,
18
+ MAX_RESOURCE_AI_AGENT_IDENTIFIER_LENGTH,
19
+ RESOURCE_AI_AGENT_RESOURCE_SELECT,
20
+ RESOURCE_AI_AGENT_SELECT_WITHOUT_KEY,
21
+ ResourceAiAgentResource,
22
+ ResourceAiAgentResourceBinding,
23
+ Service as ResourceAiAgentServiceClass,
24
+ describeResourceInSentence,
25
+ getResourceAiAgentResourceBinding,
26
+ } from "../../../Server/Services/ResourceAiAgentService";
27
+ import VMwareVCenterFeedService from "../../../Server/Services/VMwareVCenterFeedService";
28
+ import VMwareVCenterService from "../../../Server/Services/VMwareVCenterService";
29
+ import { Blue500 } from "../../../Types/BrandColors";
30
+ import ColumnLength from "../../../Types/Database/ColumnLength";
31
+ import LIMIT_MAX from "../../../Types/Database/LimitMax";
32
+ import ObjectID from "../../../Types/ObjectID";
33
+ import AiResourceType, {
34
+ ALL_AI_RESOURCE_TYPES,
35
+ } from "../../../Types/ResourceAiAgent/AiResourceType";
36
+ import {
37
+ MAX_POSTURE_STRING_LENGTH,
38
+ RESOURCE_AI_AGENT_ALIVE_WINDOW_IN_MINUTES,
39
+ ResourceAiAgentConnectionStatus,
40
+ ResourceAiAgentPosture,
41
+ ResourceAiAgentSummary,
42
+ } from "../../../Types/ResourceAiAgent/ResourceAiAccess";
43
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
44
+ import crypto from "crypto";
45
+ import { FindOperator } from "typeorm";
46
+
47
+ /*
48
+ * The shared helpers of the resource AI agent's identity:
49
+ *
50
+ * - an agent key is 256 random bits, and only its sha256 is stored;
51
+ * matching a presented key against the stored hash is timing-safe and
52
+ * never matches a reset row (null hash) or garbage;
53
+ * - ONE online rule: connected AND heard from within the alive window;
54
+ * - the summary the dashboard sees never carries the key hash, and its
55
+ * posture is re-validated on the way out;
56
+ * - the reads other units use (findAgentForResource,
57
+ * findOnlineAgentForResource, findAgentsForResources, findResource) are
58
+ * scoped to the project, the resource type and the resource(s), run as
59
+ * root, and never select the key hash;
60
+ * - every AiResourceType is bound to its own model's service, its
61
+ * collector identity lookup and its own feed.
62
+ *
63
+ * Nothing below the service boundary runs: no database.
64
+ */
65
+
66
+ // Spy handles are held through this rather than a SpiedFunction type.
67
+ interface SpyCalls {
68
+ mock: { calls: Array<Array<unknown>> };
69
+ }
70
+
71
+ type AnyObject = Record<string, any>;
72
+
73
+ const PROJECT_ID: ObjectID = new ObjectID(
74
+ "11111111-1111-4111-8111-111111111111",
75
+ );
76
+ const RESOURCE_A: ObjectID = new ObjectID(
77
+ "22222222-2222-4222-8222-222222222221",
78
+ );
79
+ const RESOURCE_B: ObjectID = new ObjectID(
80
+ "22222222-2222-4222-8222-222222222222",
81
+ );
82
+ const AGENT_ID: ObjectID = new ObjectID("33333333-3333-4333-8333-333333333333");
83
+ const USER_ID: ObjectID = new ObjectID("66666666-6666-4666-8666-666666666666");
84
+
85
+ const NOW: Date = new Date("2026-09-28T10:00:00.000Z");
86
+ const WINDOW_MS: number = RESOURCE_AI_AGENT_ALIVE_WINDOW_IN_MINUTES * 60 * 1000;
87
+
88
+ function msBeforeNow(ms: number): Date {
89
+ return new Date(NOW.getTime() - ms);
90
+ }
91
+
92
+ function agentRow(overrides: Partial<ResourceAiAgent> = {}): ResourceAiAgent {
93
+ const row: ResourceAiAgent = new ResourceAiAgent();
94
+ row.id = AGENT_ID;
95
+ row.projectId = PROJECT_ID;
96
+ row.resourceType = AiResourceType.DockerHost;
97
+ row.resourceId = RESOURCE_A;
98
+ row.resourceIdentifier = "web-1";
99
+ row.connectionStatus = "connected";
100
+ row.lastAliveAt = msBeforeNow(30 * 1000);
101
+ row.agentVersion = "14.1.0";
102
+ row.lastRegisteredAt = msBeforeNow(60 * 60 * 1000);
103
+ row.posture = {
104
+ resourceType: AiResourceType.DockerHost,
105
+ resourceIdentifier: "web-1",
106
+ allowWrites: false,
107
+ writeTargets: [],
108
+ protectedTargets: ["oneuptime-ai-agent"],
109
+ toolVersion: "27.3.1",
110
+ reachable: true,
111
+ };
112
+
113
+ Object.assign(row, overrides);
114
+
115
+ return row;
116
+ }
117
+
118
+ /*
119
+ * One row per resource type: the singletons its binding must reach, and
120
+ * how its feed item is spelled.
121
+ */
122
+ interface TypeFixture {
123
+ resourceType: AiResourceType;
124
+ service: AnyObject;
125
+ findOrCreateMethod: string | null;
126
+ findOrCreateIdentityKey: string | null;
127
+ feedService: AnyObject;
128
+ feedMethod: string;
129
+ feedIdKey: string;
130
+ feedEventKey: string;
131
+ feedEvent: string;
132
+ }
133
+
134
+ const TYPE_FIXTURES: Array<TypeFixture> = [
135
+ {
136
+ resourceType: AiResourceType.DockerHost,
137
+ service: DockerHostService as unknown as AnyObject,
138
+ findOrCreateMethod: "findOrCreateByHostIdentifier",
139
+ findOrCreateIdentityKey: "hostIdentifier",
140
+ feedService: DockerHostFeedService as unknown as AnyObject,
141
+ feedMethod: "createDockerHostFeedItem",
142
+ feedIdKey: "dockerHostId",
143
+ feedEventKey: "dockerHostFeedEventType",
144
+ feedEvent: "DockerHostUpdated",
145
+ },
146
+ {
147
+ resourceType: AiResourceType.PodmanHost,
148
+ service: PodmanHostService as unknown as AnyObject,
149
+ findOrCreateMethod: "findOrCreateByHostIdentifier",
150
+ findOrCreateIdentityKey: "hostIdentifier",
151
+ feedService: PodmanHostFeedService as unknown as AnyObject,
152
+ feedMethod: "createPodmanHostFeedItem",
153
+ feedIdKey: "podmanHostId",
154
+ feedEventKey: "podmanHostFeedEventType",
155
+ feedEvent: "PodmanHostUpdated",
156
+ },
157
+ {
158
+ resourceType: AiResourceType.DockerSwarmCluster,
159
+ service: DockerSwarmClusterService as unknown as AnyObject,
160
+ findOrCreateMethod: "findOrCreateByName",
161
+ findOrCreateIdentityKey: "name",
162
+ feedService: DockerSwarmClusterFeedService as unknown as AnyObject,
163
+ feedMethod: "createDockerSwarmClusterFeedItem",
164
+ feedIdKey: "dockerSwarmClusterId",
165
+ feedEventKey: "dockerSwarmClusterFeedEventType",
166
+ feedEvent: "DockerSwarmClusterUpdated",
167
+ },
168
+ {
169
+ resourceType: AiResourceType.ProxmoxCluster,
170
+ service: ProxmoxClusterService as unknown as AnyObject,
171
+ findOrCreateMethod: "findOrCreateByName",
172
+ findOrCreateIdentityKey: "name",
173
+ feedService: ProxmoxClusterFeedService as unknown as AnyObject,
174
+ feedMethod: "createProxmoxClusterFeedItem",
175
+ feedIdKey: "proxmoxClusterId",
176
+ feedEventKey: "proxmoxClusterFeedEventType",
177
+ feedEvent: "ProxmoxClusterUpdated",
178
+ },
179
+ {
180
+ resourceType: AiResourceType.VMwareVCenter,
181
+ service: VMwareVCenterService as unknown as AnyObject,
182
+ findOrCreateMethod: "findOrCreateByName",
183
+ findOrCreateIdentityKey: "name",
184
+ feedService: VMwareVCenterFeedService as unknown as AnyObject,
185
+ feedMethod: "createVMwareVCenterFeedItem",
186
+ feedIdKey: "vmwareVCenterId",
187
+ feedEventKey: "vmwareVCenterFeedEventType",
188
+ feedEvent: "VMwareVCenterUpdated",
189
+ },
190
+ {
191
+ resourceType: AiResourceType.CephCluster,
192
+ service: CephClusterService as unknown as AnyObject,
193
+ findOrCreateMethod: "findOrCreateByName",
194
+ findOrCreateIdentityKey: "name",
195
+ feedService: CephClusterFeedService as unknown as AnyObject,
196
+ feedMethod: "createCephClusterFeedItem",
197
+ feedIdKey: "cephClusterId",
198
+ feedEventKey: "cephClusterFeedEventType",
199
+ feedEvent: "CephClusterUpdated",
200
+ },
201
+ {
202
+ resourceType: AiResourceType.DatabaseServer,
203
+ service: DatabaseServerService as unknown as AnyObject,
204
+ findOrCreateMethod: null,
205
+ findOrCreateIdentityKey: null,
206
+ feedService: DatabaseServerFeedService as unknown as AnyObject,
207
+ feedMethod: "createDatabaseServerFeedItem",
208
+ feedIdKey: "databaseServerId",
209
+ feedEventKey: "databaseServerFeedEventType",
210
+ feedEvent: "DatabaseServerUpdated",
211
+ },
212
+ {
213
+ resourceType: AiResourceType.Host,
214
+ service: HostService as unknown as AnyObject,
215
+ findOrCreateMethod: "findOrCreateByHostIdentifier",
216
+ findOrCreateIdentityKey: "hostIdentifier",
217
+ feedService: HostFeedService as unknown as AnyObject,
218
+ feedMethod: "createHostFeedItem",
219
+ feedIdKey: "hostId",
220
+ feedEventKey: "hostFeedEventType",
221
+ feedEvent: "HostUpdated",
222
+ },
223
+ ];
224
+
225
+ afterEach(() => {
226
+ jest.restoreAllMocks();
227
+ });
228
+
229
+ describe("ResourceAiAgentService.hashKey", () => {
230
+ test("is the sha256 hex of the key", () => {
231
+ // FIPS 180-2 test vector.
232
+ expect(ResourceAiAgentServiceClass.hashKey("abc")).toBe(
233
+ "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
234
+ );
235
+ });
236
+
237
+ test("is deterministic, lowercase hex of 64 characters, and differs per key", () => {
238
+ const key: string = "a-key";
239
+
240
+ expect(ResourceAiAgentServiceClass.hashKey(key)).toBe(
241
+ ResourceAiAgentServiceClass.hashKey(key),
242
+ );
243
+ expect(ResourceAiAgentServiceClass.hashKey(key)).toMatch(/^[0-9a-f]{64}$/);
244
+ expect(ResourceAiAgentServiceClass.hashKey(key)).not.toBe(
245
+ ResourceAiAgentServiceClass.hashKey("b-key"),
246
+ );
247
+ });
248
+
249
+ test("the default export's instance form gives the same answer", () => {
250
+ expect(ResourceAiAgentService.hashKey("abc")).toBe(
251
+ ResourceAiAgentServiceClass.hashKey("abc"),
252
+ );
253
+ });
254
+ });
255
+
256
+ describe("ResourceAiAgentService.generateKey", () => {
257
+ test("is 32 random bytes, hex encoded", () => {
258
+ expect(ResourceAiAgentServiceClass.generateKey()).toMatch(/^[0-9a-f]{64}$/);
259
+ expect(ResourceAiAgentService.generateKey()).toMatch(/^[0-9a-f]{64}$/);
260
+ });
261
+
262
+ test("draws from the cryptographic random source", () => {
263
+ const randomBytes: SpyCalls = jest.spyOn(
264
+ crypto,
265
+ "randomBytes",
266
+ ) as unknown as SpyCalls;
267
+
268
+ ResourceAiAgentServiceClass.generateKey();
269
+
270
+ expect(randomBytes.mock.calls[0]?.[0]).toBe(32);
271
+ });
272
+
273
+ test("never repeats", () => {
274
+ const keys: Set<string> = new Set<string>();
275
+
276
+ for (let i: number = 0; i < 200; i++) {
277
+ keys.add(ResourceAiAgentServiceClass.generateKey());
278
+ }
279
+
280
+ expect(keys.size).toBe(200);
281
+ });
282
+ });
283
+
284
+ describe("ResourceAiAgentService.doesKeyMatchHash", () => {
285
+ const KEY: string = "0f".repeat(32);
286
+ const HASH: string = ResourceAiAgentServiceClass.hashKey(KEY);
287
+
288
+ test("matches the key the hash was made from, in either case", () => {
289
+ expect(ResourceAiAgentServiceClass.doesKeyMatchHash(KEY, HASH)).toBe(true);
290
+ expect(ResourceAiAgentService.doesKeyMatchHash(KEY, HASH)).toBe(true);
291
+ expect(
292
+ ResourceAiAgentServiceClass.doesKeyMatchHash(KEY, HASH.toUpperCase()),
293
+ ).toBe(true);
294
+ });
295
+
296
+ test("does not match another key, or the hash itself presented as the key", () => {
297
+ expect(
298
+ ResourceAiAgentServiceClass.doesKeyMatchHash("1f".repeat(32), HASH),
299
+ ).toBe(false);
300
+ expect(ResourceAiAgentServiceClass.doesKeyMatchHash(HASH, HASH)).toBe(
301
+ false,
302
+ );
303
+ });
304
+
305
+ test("never matches a reset row (no hash)", () => {
306
+ for (const hash of [null, undefined, ""]) {
307
+ expect(ResourceAiAgentServiceClass.doesKeyMatchHash(KEY, hash)).toBe(
308
+ false,
309
+ );
310
+ }
311
+ });
312
+
313
+ test("returns false, never throws, for a missing key or a malformed hash", () => {
314
+ for (const key of [undefined, null, "", 42, {}, ["k"]]) {
315
+ expect(ResourceAiAgentServiceClass.doesKeyMatchHash(key, HASH)).toBe(
316
+ false,
317
+ );
318
+ }
319
+
320
+ for (const hash of [
321
+ HASH.slice(0, 63),
322
+ `${HASH}0`,
323
+ `${HASH.slice(0, 63)}g`,
324
+ 42,
325
+ { hash: HASH },
326
+ ]) {
327
+ expect(() => {
328
+ return ResourceAiAgentServiceClass.doesKeyMatchHash(KEY, hash);
329
+ }).not.toThrow();
330
+ expect(ResourceAiAgentServiceClass.doesKeyMatchHash(KEY, hash)).toBe(
331
+ false,
332
+ );
333
+ }
334
+ });
335
+
336
+ test("compares in constant time", () => {
337
+ const timingSafeEqual: SpyCalls = jest.spyOn(
338
+ crypto,
339
+ "timingSafeEqual",
340
+ ) as unknown as SpyCalls;
341
+
342
+ ResourceAiAgentServiceClass.doesKeyMatchHash("1f".repeat(32), HASH);
343
+
344
+ expect(timingSafeEqual.mock.calls).toHaveLength(1);
345
+ });
346
+ });
347
+
348
+ describe("ResourceAiAgentService.isOnline", () => {
349
+ test("is online when connected and heard from within the alive window", () => {
350
+ expect(
351
+ ResourceAiAgentService.isOnline(
352
+ { connectionStatus: "connected", lastAliveAt: msBeforeNow(1000) },
353
+ NOW,
354
+ ),
355
+ ).toBe(true);
356
+ });
357
+
358
+ test("the window is inclusive at exactly five minutes and closed one millisecond later", () => {
359
+ expect(RESOURCE_AI_AGENT_ALIVE_WINDOW_IN_MINUTES).toBe(5);
360
+ expect(
361
+ ResourceAiAgentService.isOnline(
362
+ { connectionStatus: "connected", lastAliveAt: msBeforeNow(WINDOW_MS) },
363
+ NOW,
364
+ ),
365
+ ).toBe(true);
366
+ expect(
367
+ ResourceAiAgentService.isOnline(
368
+ {
369
+ connectionStatus: "connected",
370
+ lastAliveAt: msBeforeNow(WINDOW_MS + 1),
371
+ },
372
+ NOW,
373
+ ),
374
+ ).toBe(false);
375
+ });
376
+
377
+ test("is offline when disconnected, whatever lastAliveAt says", () => {
378
+ expect(
379
+ ResourceAiAgentService.isOnline(
380
+ { connectionStatus: "disconnected", lastAliveAt: NOW },
381
+ NOW,
382
+ ),
383
+ ).toBe(false);
384
+ });
385
+
386
+ test("is offline for an unknown or missing status", () => {
387
+ for (const status of [undefined, null, "", "Connected", "online"]) {
388
+ expect(
389
+ ResourceAiAgentService.isOnline(
390
+ { connectionStatus: status, lastAliveAt: NOW },
391
+ NOW,
392
+ ),
393
+ ).toBe(false);
394
+ }
395
+ });
396
+
397
+ test("is offline when it was never heard from, or the timestamp is not a date", () => {
398
+ for (const lastAliveAt of [undefined, null, "", "yesterday"]) {
399
+ expect(
400
+ ResourceAiAgentService.isOnline(
401
+ { connectionStatus: "connected", lastAliveAt },
402
+ NOW,
403
+ ),
404
+ ).toBe(false);
405
+ }
406
+ });
407
+
408
+ test("accepts an ISO string as well as a Date", () => {
409
+ expect(
410
+ ResourceAiAgentService.isOnline(
411
+ {
412
+ connectionStatus: "connected",
413
+ lastAliveAt: msBeforeNow(2000).toISOString(),
414
+ },
415
+ NOW,
416
+ ),
417
+ ).toBe(true);
418
+ });
419
+
420
+ test("a heartbeat stamped slightly in the future still counts as recent", () => {
421
+ expect(
422
+ ResourceAiAgentService.isOnline(
423
+ {
424
+ connectionStatus: "connected",
425
+ lastAliveAt: new Date(NOW.getTime() + 2000),
426
+ },
427
+ NOW,
428
+ ),
429
+ ).toBe(true);
430
+ });
431
+
432
+ test("defaults to the current time", () => {
433
+ expect(
434
+ ResourceAiAgentService.isOnline({
435
+ connectionStatus: "connected",
436
+ lastAliveAt: new Date(),
437
+ }),
438
+ ).toBe(true);
439
+ expect(
440
+ ResourceAiAgentService.isOnline({
441
+ connectionStatus: "connected",
442
+ lastAliveAt: new Date(Date.now() - WINDOW_MS - 60 * 1000),
443
+ }),
444
+ ).toBe(false);
445
+ });
446
+ });
447
+
448
+ describe("ResourceAiAgentService.getAgentSummary", () => {
449
+ test("summarises a connected agent", () => {
450
+ const summary: ResourceAiAgentSummary =
451
+ ResourceAiAgentService.getAgentSummary(agentRow(), NOW);
452
+
453
+ expect(summary).toEqual({
454
+ agentId: AGENT_ID.toString(),
455
+ connectionStatus: "connected",
456
+ isOnline: true,
457
+ agentVersion: "14.1.0",
458
+ lastAliveAt: msBeforeNow(30 * 1000).toISOString(),
459
+ lastRegisteredAt: msBeforeNow(60 * 60 * 1000).toISOString(),
460
+ posture: {
461
+ resourceType: AiResourceType.DockerHost,
462
+ resourceIdentifier: "web-1",
463
+ agentVersion: null,
464
+ allowWrites: false,
465
+ allowWritesSetting: null,
466
+ writeTargets: [],
467
+ protectedTargets: ["oneuptime-ai-agent"],
468
+ toolVersion: "27.3.1",
469
+ reachable: true,
470
+ reachError: null,
471
+ details: {},
472
+ },
473
+ lastRefusedRegistrationAt: null,
474
+ lastRefusedRegistrationReason: null,
475
+ });
476
+ });
477
+
478
+ test("never carries the key hash, even when the row was loaded with it", () => {
479
+ const row: ResourceAiAgent = agentRow({ keyHash: "ab".repeat(32) });
480
+
481
+ const summary: ResourceAiAgentSummary =
482
+ ResourceAiAgentService.getAgentSummary(row, NOW);
483
+
484
+ expect(JSON.stringify(summary)).not.toContain("ab".repeat(32));
485
+ expect(Object.keys(summary)).not.toContain("keyHash");
486
+ });
487
+
488
+ test("reports an agent whose heartbeat aged out as connected-but-offline", () => {
489
+ const summary: ResourceAiAgentSummary =
490
+ ResourceAiAgentService.getAgentSummary(
491
+ agentRow({ lastAliveAt: msBeforeNow(WINDOW_MS + 1000) }),
492
+ NOW,
493
+ );
494
+
495
+ expect(summary.connectionStatus).toBe("connected");
496
+ expect(summary.isOnline).toBe(false);
497
+ });
498
+
499
+ test("normalises any status other than connected to disconnected", () => {
500
+ for (const status of ["disconnected", "", "weird"]) {
501
+ const summary: ResourceAiAgentSummary =
502
+ ResourceAiAgentService.getAgentSummary(
503
+ agentRow({
504
+ connectionStatus: status as ResourceAiAgentConnectionStatus,
505
+ }),
506
+ NOW,
507
+ );
508
+
509
+ expect(summary.connectionStatus).toBe("disconnected");
510
+ expect(summary.isOnline).toBe(false);
511
+ }
512
+ });
513
+
514
+ test("re-validates the stored posture: a wrong-shaped one is dropped, and writes are never read from a non-boolean", () => {
515
+ expect(
516
+ ResourceAiAgentService.getAgentSummary(
517
+ agentRow({ posture: { allowWrites: true } }),
518
+ NOW,
519
+ ).posture,
520
+ ).toBeNull();
521
+
522
+ const posture: ResourceAiAgentPosture | null | undefined =
523
+ ResourceAiAgentService.getAgentSummary(
524
+ agentRow({
525
+ posture: {
526
+ resourceType: AiResourceType.DockerHost,
527
+ resourceIdentifier: "web-1",
528
+ allowWrites: "true",
529
+ },
530
+ }),
531
+ NOW,
532
+ ).posture;
533
+
534
+ expect(posture!.allowWrites).toBe(false);
535
+ });
536
+
537
+ test("has no posture when none was ever stored", () => {
538
+ const row: ResourceAiAgent = agentRow();
539
+ delete (row as unknown as AnyObject)["posture"];
540
+
541
+ expect(ResourceAiAgentService.getAgentSummary(row, NOW).posture).toBeNull();
542
+ });
543
+
544
+ test("carries the last refused registration, for the dashboard's warning", () => {
545
+ const summary: ResourceAiAgentSummary =
546
+ ResourceAiAgentService.getAgentSummary(
547
+ agentRow({
548
+ lastRefusedRegistrationAt: msBeforeNow(10 * 1000),
549
+ lastRefusedRegistrationReason: "previous_instance_online",
550
+ }),
551
+ NOW,
552
+ );
553
+
554
+ expect(summary.lastRefusedRegistrationAt).toBe(
555
+ msBeforeNow(10 * 1000).toISOString(),
556
+ );
557
+ expect(summary.lastRefusedRegistrationReason).toBe(
558
+ "previous_instance_online",
559
+ );
560
+ });
561
+
562
+ test("never-set values are null rather than invented", () => {
563
+ const row: ResourceAiAgent = new ResourceAiAgent();
564
+ row.id = AGENT_ID;
565
+
566
+ const summary: ResourceAiAgentSummary =
567
+ ResourceAiAgentService.getAgentSummary(row, NOW);
568
+
569
+ expect(summary).toEqual({
570
+ agentId: AGENT_ID.toString(),
571
+ connectionStatus: "disconnected",
572
+ isOnline: false,
573
+ agentVersion: null,
574
+ lastAliveAt: null,
575
+ lastRegisteredAt: null,
576
+ posture: null,
577
+ lastRefusedRegistrationAt: null,
578
+ lastRefusedRegistrationReason: null,
579
+ });
580
+ });
581
+ });
582
+
583
+ describe("RESOURCE_AI_AGENT_SELECT_WITHOUT_KEY", () => {
584
+ test("selects every column of the row except the key hash", () => {
585
+ const model: ResourceAiAgent = new ResourceAiAgent();
586
+ const columns: Array<string> = model
587
+ .getTableColumns()
588
+ .columns.filter((column: string): boolean => {
589
+ return !model.isEntityColumn(column);
590
+ });
591
+
592
+ const selected: Array<string> = Object.keys(
593
+ RESOURCE_AI_AGENT_SELECT_WITHOUT_KEY,
594
+ );
595
+
596
+ expect(selected).not.toContain("keyHash");
597
+
598
+ for (const column of columns) {
599
+ if (
600
+ column === "keyHash" ||
601
+ ["deletedAt", "version", "createdByUserId", "deletedByUserId"].includes(
602
+ column,
603
+ )
604
+ ) {
605
+ continue;
606
+ }
607
+
608
+ expect(selected).toContain(column);
609
+ }
610
+
611
+ for (const column of ["resourceType", "resourceId", "resourceIdentifier"]) {
612
+ expect(selected).toContain(column);
613
+ }
614
+ });
615
+ });
616
+
617
+ describe("ResourceAiAgentService.findAgentForResource", () => {
618
+ test("reads the resource's agent in its project by type and id, as root, without the key hash", async () => {
619
+ const row: ResourceAiAgent = agentRow();
620
+ const findOneBy: SpyCalls = jest
621
+ .spyOn(ResourceAiAgentService, "findOneBy")
622
+ .mockResolvedValue(row) as unknown as SpyCalls;
623
+
624
+ await expect(
625
+ ResourceAiAgentService.findAgentForResource({
626
+ projectId: PROJECT_ID,
627
+ resourceType: AiResourceType.CephCluster,
628
+ resourceId: RESOURCE_A,
629
+ }),
630
+ ).resolves.toBe(row);
631
+
632
+ const call: AnyObject = findOneBy.mock.calls[0]![0] as AnyObject;
633
+
634
+ expect(call["query"]["projectId"].toString()).toBe(PROJECT_ID.toString());
635
+ expect(call["query"]["resourceType"]).toBe(AiResourceType.CephCluster);
636
+ expect(call["query"]["resourceId"].toString()).toBe(RESOURCE_A.toString());
637
+ expect(call["select"]).toBe(RESOURCE_AI_AGENT_SELECT_WITHOUT_KEY);
638
+ expect(call["select"]["keyHash"]).toBeUndefined();
639
+ expect(call["props"]["isRoot"]).toBe(true);
640
+ });
641
+
642
+ test("is null when the resource has no agent", async () => {
643
+ jest.spyOn(ResourceAiAgentService, "findOneBy").mockResolvedValue(null);
644
+
645
+ await expect(
646
+ ResourceAiAgentService.findAgentForResource({
647
+ projectId: PROJECT_ID,
648
+ resourceType: AiResourceType.Host,
649
+ resourceId: RESOURCE_B,
650
+ }),
651
+ ).resolves.toBeNull();
652
+ });
653
+
654
+ test("an unknown resource type reads nothing", async () => {
655
+ const findOneBy: SpyCalls = jest.spyOn(
656
+ ResourceAiAgentService,
657
+ "findOneBy",
658
+ ) as unknown as SpyCalls;
659
+
660
+ await expect(
661
+ ResourceAiAgentService.findAgentForResource({
662
+ projectId: PROJECT_ID,
663
+ resourceType: "KubernetesCluster" as AiResourceType,
664
+ resourceId: RESOURCE_A,
665
+ }),
666
+ ).resolves.toBeNull();
667
+ expect(findOneBy.mock.calls).toHaveLength(0);
668
+ });
669
+ });
670
+
671
+ describe("ResourceAiAgentService.findOnlineAgentForResource", () => {
672
+ test("the agent when it is online", async () => {
673
+ const row: ResourceAiAgent = agentRow();
674
+ jest.spyOn(ResourceAiAgentService, "findOneBy").mockResolvedValue(row);
675
+
676
+ await expect(
677
+ ResourceAiAgentService.findOnlineAgentForResource({
678
+ projectId: PROJECT_ID,
679
+ resourceType: AiResourceType.DockerHost,
680
+ resourceId: RESOURCE_A,
681
+ now: NOW,
682
+ }),
683
+ ).resolves.toBe(row);
684
+ });
685
+
686
+ test.each([
687
+ ["signed off", { connectionStatus: "disconnected" }],
688
+ ["gone quiet", { lastAliveAt: msBeforeNow(WINDOW_MS + 1000) }],
689
+ ["never heard from", { lastAliveAt: undefined }],
690
+ ])(
691
+ "null when the agent is %s",
692
+ async (_label: string, overrides: AnyObject) => {
693
+ jest
694
+ .spyOn(ResourceAiAgentService, "findOneBy")
695
+ .mockResolvedValue(agentRow(overrides as Partial<ResourceAiAgent>));
696
+
697
+ await expect(
698
+ ResourceAiAgentService.findOnlineAgentForResource({
699
+ projectId: PROJECT_ID,
700
+ resourceType: AiResourceType.DockerHost,
701
+ resourceId: RESOURCE_A,
702
+ now: NOW,
703
+ }),
704
+ ).resolves.toBeNull();
705
+ },
706
+ );
707
+
708
+ test("null when there is no agent", async () => {
709
+ jest.spyOn(ResourceAiAgentService, "findOneBy").mockResolvedValue(null);
710
+
711
+ await expect(
712
+ ResourceAiAgentService.findOnlineAgentForResource({
713
+ projectId: PROJECT_ID,
714
+ resourceType: AiResourceType.DockerHost,
715
+ resourceId: RESOURCE_A,
716
+ }),
717
+ ).resolves.toBeNull();
718
+ });
719
+ });
720
+
721
+ describe("ResourceAiAgentService.findAgentsForResources", () => {
722
+ test("asks nothing of the database for no resources", async () => {
723
+ const findBy: SpyCalls = jest.spyOn(
724
+ ResourceAiAgentService,
725
+ "findBy",
726
+ ) as unknown as SpyCalls;
727
+
728
+ const result: Map<string, ResourceAiAgent> =
729
+ await ResourceAiAgentService.findAgentsForResources({
730
+ projectId: PROJECT_ID,
731
+ resourceType: AiResourceType.Host,
732
+ resourceIds: [],
733
+ });
734
+
735
+ expect(result.size).toBe(0);
736
+ expect(findBy.mock.calls).toHaveLength(0);
737
+ });
738
+
739
+ test("reads every resource's agent in one root query scoped to the project and type, keyed by resource id", async () => {
740
+ const agentA: ResourceAiAgent = agentRow();
741
+ const agentB: ResourceAiAgent = agentRow({ resourceId: RESOURCE_B });
742
+
743
+ const findBy: SpyCalls = jest
744
+ .spyOn(ResourceAiAgentService, "findBy")
745
+ .mockResolvedValue([agentA, agentB]) as unknown as SpyCalls;
746
+
747
+ const result: Map<string, ResourceAiAgent> =
748
+ await ResourceAiAgentService.findAgentsForResources({
749
+ projectId: PROJECT_ID,
750
+ resourceType: AiResourceType.DockerHost,
751
+ resourceIds: [RESOURCE_A, RESOURCE_B, RESOURCE_A],
752
+ });
753
+
754
+ expect(findBy.mock.calls).toHaveLength(1);
755
+
756
+ const call: AnyObject = findBy.mock.calls[0]![0] as AnyObject;
757
+
758
+ expect(call["query"]["projectId"].toString()).toBe(PROJECT_ID.toString());
759
+ expect(call["query"]["resourceType"]).toBe(AiResourceType.DockerHost);
760
+
761
+ const filter: FindOperator<unknown> = call["query"][
762
+ "resourceId"
763
+ ] as FindOperator<unknown>;
764
+
765
+ expect(filter).toBeInstanceOf(FindOperator);
766
+ expect(Object.values(filter.objectLiteralParameters || {})).toEqual([
767
+ [RESOURCE_A.toString(), RESOURCE_B.toString()],
768
+ ]);
769
+ expect(call["select"]).toBe(RESOURCE_AI_AGENT_SELECT_WITHOUT_KEY);
770
+ expect(call["limit"]).toBe(LIMIT_MAX);
771
+ expect(call["skip"]).toBe(0);
772
+ expect(call["props"]["isRoot"]).toBe(true);
773
+
774
+ expect(result.size).toBe(2);
775
+ expect(result.get(RESOURCE_A.toString())).toBe(agentA);
776
+ expect(result.get(RESOURCE_B.toString())).toBe(agentB);
777
+ });
778
+
779
+ test("ignores rows of other resources or types, and rows without a resource", async () => {
780
+ const stray: ResourceAiAgent = agentRow({
781
+ resourceId: new ObjectID("44444444-4444-4444-8444-444444444444"),
782
+ });
783
+ const otherType: ResourceAiAgent = agentRow({
784
+ resourceType: AiResourceType.PodmanHost,
785
+ });
786
+ const noResource: ResourceAiAgent = agentRow();
787
+ delete (noResource as unknown as AnyObject)["resourceId"];
788
+
789
+ jest
790
+ .spyOn(ResourceAiAgentService, "findBy")
791
+ .mockResolvedValue([stray, otherType, noResource]);
792
+
793
+ const result: Map<string, ResourceAiAgent> =
794
+ await ResourceAiAgentService.findAgentsForResources({
795
+ projectId: PROJECT_ID,
796
+ resourceType: AiResourceType.DockerHost,
797
+ resourceIds: [RESOURCE_A, RESOURCE_B],
798
+ });
799
+
800
+ expect(result.size).toBe(0);
801
+ });
802
+ });
803
+
804
+ describe("the per-type resource bindings", () => {
805
+ test("every resource type has one, bound to its own type", () => {
806
+ expect(
807
+ TYPE_FIXTURES.map((f: TypeFixture) => {
808
+ return f.resourceType;
809
+ }),
810
+ ).toEqual([...ALL_AI_RESOURCE_TYPES]);
811
+
812
+ for (const type of ALL_AI_RESOURCE_TYPES) {
813
+ const binding: ResourceAiAgentResourceBinding =
814
+ getResourceAiAgentResourceBinding(type);
815
+
816
+ expect(binding.resourceType).toBe(type);
817
+ expect(
818
+ ResourceAiAgentServiceClass.getResourceBinding(type).resourceType,
819
+ ).toBe(type);
820
+ }
821
+ });
822
+
823
+ test.each(TYPE_FIXTURES)(
824
+ "$resourceType reads through its own model's service",
825
+ (fixture: TypeFixture) => {
826
+ expect(
827
+ getResourceAiAgentResourceBinding(fixture.resourceType).getService(),
828
+ ).toBe(fixture.service);
829
+ },
830
+ );
831
+
832
+ test.each(
833
+ TYPE_FIXTURES.filter((f: TypeFixture) => {
834
+ return f.findOrCreateMethod !== null;
835
+ }),
836
+ )(
837
+ "$resourceType finds or creates its row by the collector's identity ($findOrCreateMethod)",
838
+ async (fixture: TypeFixture) => {
839
+ const found: AnyObject = { _id: RESOURCE_A.toString() };
840
+ const spy: SpyCalls = jest
841
+ .spyOn(fixture.service, fixture.findOrCreateMethod!)
842
+ .mockResolvedValue(found as never) as unknown as SpyCalls;
843
+
844
+ const binding: ResourceAiAgentResourceBinding =
845
+ getResourceAiAgentResourceBinding(fixture.resourceType);
846
+
847
+ await expect(
848
+ binding.findOrCreateByIdentity!({
849
+ projectId: PROJECT_ID,
850
+ identifier: "Prod-1",
851
+ }),
852
+ ).resolves.toBe(found);
853
+
854
+ expect(spy.mock.calls[0]![0]).toEqual({
855
+ projectId: PROJECT_ID,
856
+ [fixture.findOrCreateIdentityKey!]: "Prod-1",
857
+ });
858
+ },
859
+ );
860
+
861
+ test("a database server is never found by name: it has no identity lookup", () => {
862
+ expect(
863
+ getResourceAiAgentResourceBinding(AiResourceType.DatabaseServer)
864
+ .findOrCreateByIdentity,
865
+ ).toBeNull();
866
+ });
867
+
868
+ test.each(TYPE_FIXTURES)(
869
+ "$resourceType posts to its own feed as an update ($feedEvent)",
870
+ async (fixture: TypeFixture) => {
871
+ const spy: SpyCalls = jest
872
+ .spyOn(fixture.feedService, fixture.feedMethod)
873
+ .mockResolvedValue(undefined as never) as unknown as SpyCalls;
874
+
875
+ await getResourceAiAgentResourceBinding(
876
+ fixture.resourceType,
877
+ ).writeFeedItem({
878
+ resourceId: RESOURCE_A,
879
+ projectId: PROJECT_ID,
880
+ feedInfoInMarkdown: "hello",
881
+ moreInformationInMarkdown: "more",
882
+ displayColor: Blue500,
883
+ userId: USER_ID,
884
+ });
885
+
886
+ expect(spy.mock.calls).toHaveLength(1);
887
+ expect(spy.mock.calls[0]![0]).toEqual({
888
+ [fixture.feedIdKey]: RESOURCE_A,
889
+ projectId: PROJECT_ID,
890
+ [fixture.feedEventKey]: fixture.feedEvent,
891
+ feedInfoInMarkdown: "hello",
892
+ moreInformationInMarkdown: "more",
893
+ displayColor: Blue500,
894
+ userId: USER_ID,
895
+ });
896
+ },
897
+ );
898
+ });
899
+
900
+ describe("ResourceAiAgentService.findResource", () => {
901
+ test.each(TYPE_FIXTURES)(
902
+ "$resourceType: one row of its table, in the project, as root, with the AI columns",
903
+ async (fixture: TypeFixture) => {
904
+ const row: AnyObject = new (fixture.service[
905
+ "modelType"
906
+ ] as new () => AnyObject)();
907
+ row["_id"] = RESOURCE_A.toString();
908
+ row["projectId"] = PROJECT_ID;
909
+ row["name"] = " prod-1 ";
910
+ row["isAiInvestigationEnabled"] = false;
911
+ row["aiRemediationMode"] = "Disabled";
912
+
913
+ const findOneBy: SpyCalls = jest
914
+ .spyOn(fixture.service, "findOneBy")
915
+ .mockResolvedValue(row as never) as unknown as SpyCalls;
916
+
917
+ const resource: ResourceAiAgentResource | null =
918
+ await ResourceAiAgentService.findResource({
919
+ projectId: PROJECT_ID,
920
+ resourceType: fixture.resourceType,
921
+ resourceId: RESOURCE_A,
922
+ });
923
+
924
+ expect(resource).toEqual({
925
+ resourceType: fixture.resourceType,
926
+ id: RESOURCE_A,
927
+ projectId: PROJECT_ID,
928
+ name: "prod-1",
929
+ isAiInvestigationEnabled: false,
930
+ aiRemediationMode: "Disabled",
931
+ aiAccessConfiguredAt: undefined,
932
+ });
933
+
934
+ const call: AnyObject = findOneBy.mock.calls[0]![0] as AnyObject;
935
+ expect(call["query"]).toEqual({
936
+ _id: RESOURCE_A.toString(),
937
+ projectId: PROJECT_ID,
938
+ });
939
+ expect(call["select"]).toBe(RESOURCE_AI_AGENT_RESOURCE_SELECT);
940
+ expect(call["props"]).toEqual({ isRoot: true });
941
+ },
942
+ );
943
+
944
+ test("the select reads the identity and exactly the AI settings the defaults need", () => {
945
+ expect(Object.keys(RESOURCE_AI_AGENT_RESOURCE_SELECT).sort()).toEqual(
946
+ [
947
+ "_id",
948
+ "projectId",
949
+ "name",
950
+ "isAiInvestigationEnabled",
951
+ "aiRemediationMode",
952
+ "aiAccessConfiguredAt",
953
+ ].sort(),
954
+ );
955
+ });
956
+
957
+ test("null when the row is not there", async () => {
958
+ jest.spyOn(DockerHostService, "findOneBy").mockResolvedValue(null);
959
+
960
+ await expect(
961
+ ResourceAiAgentService.findResource({
962
+ projectId: PROJECT_ID,
963
+ resourceType: AiResourceType.DockerHost,
964
+ resourceId: RESOURCE_A,
965
+ }),
966
+ ).resolves.toBeNull();
967
+ });
968
+
969
+ test("an unknown type reads nothing", async () => {
970
+ await expect(
971
+ ResourceAiAgentService.findResource({
972
+ projectId: PROJECT_ID,
973
+ resourceType: "KubernetesCluster" as AiResourceType,
974
+ resourceId: RESOURCE_A,
975
+ }),
976
+ ).resolves.toBeNull();
977
+ });
978
+ });
979
+
980
+ describe("identity helpers", () => {
981
+ test("identities of name-keyed resources are bounded by the ShortText columns, databases by the posture", () => {
982
+ expect(MAX_RESOURCE_AI_AGENT_IDENTIFIER_LENGTH).toBe(
983
+ ColumnLength.ShortText,
984
+ );
985
+ expect(MAX_DATABASE_AI_AGENT_IDENTIFIER_LENGTH).toBe(
986
+ MAX_POSTURE_STRING_LENGTH,
987
+ );
988
+
989
+ for (const type of ALL_AI_RESOURCE_TYPES) {
990
+ expect(ResourceAiAgentServiceClass.getMaxIdentifierLength(type)).toBe(
991
+ type === AiResourceType.DatabaseServer
992
+ ? MAX_POSTURE_STRING_LENGTH
993
+ : ColumnLength.ShortText,
994
+ );
995
+ }
996
+ });
997
+
998
+ test("describeResourceInSentence keeps product names and lowercases common nouns", () => {
999
+ expect(describeResourceInSentence(AiResourceType.DockerHost)).toBe(
1000
+ "Docker host",
1001
+ );
1002
+ expect(describeResourceInSentence(AiResourceType.VMwareVCenter)).toBe(
1003
+ "VMware vCenter",
1004
+ );
1005
+ expect(describeResourceInSentence(AiResourceType.Host)).toBe("host");
1006
+ expect(describeResourceInSentence(AiResourceType.DatabaseServer)).toBe(
1007
+ "database server",
1008
+ );
1009
+ });
1010
+
1011
+ test("parseDatabaseEndpointIdentity reads '<system>|<address>' and nothing else", () => {
1012
+ expect(
1013
+ ResourceAiAgentServiceClass.parseDatabaseEndpointIdentity(
1014
+ "PostgreSQL|db.prod.example.com:5432",
1015
+ ),
1016
+ ).toEqual({
1017
+ system: "postgresql",
1018
+ address: "db.prod.example.com:5432",
1019
+ port: null,
1020
+ });
1021
+ expect(
1022
+ ResourceAiAgentServiceClass.parseDatabaseEndpointIdentity(
1023
+ "postgresql|[2001:db8::1]:5432",
1024
+ ),
1025
+ ).toEqual({
1026
+ system: "postgresql",
1027
+ address: "[2001:db8::1]:5432",
1028
+ port: null,
1029
+ });
1030
+
1031
+ for (const value of [
1032
+ "db.prod.example.com",
1033
+ "|db.prod",
1034
+ "postgresql|",
1035
+ "postgresql| ",
1036
+ "a|b|c",
1037
+ "",
1038
+ ]) {
1039
+ expect(
1040
+ ResourceAiAgentServiceClass.parseDatabaseEndpointIdentity(value),
1041
+ ).toBeNull();
1042
+ }
1043
+ });
1044
+
1045
+ test("readDatabaseEndpointFromPosture reads the agent's details, with a valid port only", () => {
1046
+ const posture: ResourceAiAgentPosture = {
1047
+ resourceType: AiResourceType.DatabaseServer,
1048
+ resourceIdentifier: "x",
1049
+ allowWrites: false,
1050
+ writeTargets: [],
1051
+ protectedTargets: [],
1052
+ reachable: true,
1053
+ details: {
1054
+ databaseSystem: "MySQL",
1055
+ serverAddress: "db.prod.example.com",
1056
+ serverPort: 3306,
1057
+ },
1058
+ };
1059
+
1060
+ expect(
1061
+ ResourceAiAgentServiceClass.readDatabaseEndpointFromPosture(posture),
1062
+ ).toEqual({ system: "mysql", address: "db.prod.example.com", port: 3306 });
1063
+
1064
+ expect(
1065
+ ResourceAiAgentServiceClass.readDatabaseEndpointFromPosture({
1066
+ ...posture,
1067
+ details: { ...posture.details, serverPort: 70000 },
1068
+ })!.port,
1069
+ ).toBeNull();
1070
+
1071
+ expect(
1072
+ ResourceAiAgentServiceClass.readDatabaseEndpointFromPosture({
1073
+ ...posture,
1074
+ details: { databaseSystem: "mysql" },
1075
+ }),
1076
+ ).toBeNull();
1077
+ expect(
1078
+ ResourceAiAgentServiceClass.readDatabaseEndpointFromPosture(null),
1079
+ ).toBeNull();
1080
+ });
1081
+
1082
+ test("readPinnedDatabaseServerId: the resourceId, else an identity that is an id, else none", () => {
1083
+ expect(
1084
+ ResourceAiAgentServiceClass.readPinnedDatabaseServerId({
1085
+ resourceId: ` ${RESOURCE_A.toString().toUpperCase()} `,
1086
+ identifier: "postgresql|db:5432",
1087
+ }),
1088
+ ).toBe(RESOURCE_A.toString());
1089
+ expect(
1090
+ ResourceAiAgentServiceClass.readPinnedDatabaseServerId({
1091
+ resourceId: undefined,
1092
+ identifier: RESOURCE_B.toString().toUpperCase(),
1093
+ }),
1094
+ ).toBe(RESOURCE_B.toString());
1095
+ expect(
1096
+ ResourceAiAgentServiceClass.readPinnedDatabaseServerId({
1097
+ resourceId: "not-an-id",
1098
+ identifier: "postgresql|db:5432",
1099
+ }),
1100
+ ).toBe("not-an-id");
1101
+ expect(
1102
+ ResourceAiAgentServiceClass.readPinnedDatabaseServerId({
1103
+ resourceId: "",
1104
+ identifier: "postgresql|db:5432",
1105
+ }),
1106
+ ).toBeNull();
1107
+ expect(
1108
+ ResourceAiAgentServiceClass.readPinnedDatabaseServerId({
1109
+ resourceId: 42,
1110
+ identifier: "postgresql|db:5432",
1111
+ }),
1112
+ ).toBeNull();
1113
+ });
1114
+ });