@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,1050 @@
1
+ import DockerHost from "../../../Models/DatabaseModels/DockerHost";
2
+ import ResourceAiAgent from "../../../Models/DatabaseModels/ResourceAiAgent";
3
+ import CephClusterFeedService from "../../../Server/Services/CephClusterFeedService";
4
+ import DatabaseServerFeedService from "../../../Server/Services/DatabaseServerFeedService";
5
+ import DockerHostFeedService from "../../../Server/Services/DockerHostFeedService";
6
+ import DockerHostService from "../../../Server/Services/DockerHostService";
7
+ import DockerSwarmClusterFeedService from "../../../Server/Services/DockerSwarmClusterFeedService";
8
+ import HostFeedService from "../../../Server/Services/HostFeedService";
9
+ import PodmanHostFeedService from "../../../Server/Services/PodmanHostFeedService";
10
+ import ProxmoxClusterFeedService from "../../../Server/Services/ProxmoxClusterFeedService";
11
+ import ResourceAiAgentService, {
12
+ RESOURCE_AI_AGENT_RESOURCE_CHECK_INTERVAL_MS,
13
+ RESOURCE_AI_AGENT_SELECT_WITHOUT_KEY,
14
+ Service as ResourceAiAgentServiceClass,
15
+ } from "../../../Server/Services/ResourceAiAgentService";
16
+ import UserService from "../../../Server/Services/UserService";
17
+ import VMwareVCenterFeedService from "../../../Server/Services/VMwareVCenterFeedService";
18
+ import logger from "../../../Server/Utils/Logger";
19
+ import LIMIT_MAX from "../../../Types/Database/LimitMax";
20
+ import BadDataException from "../../../Types/Exception/BadDataException";
21
+ import NotAuthenticatedException from "../../../Types/Exception/NotAuthenticatedException";
22
+ import { JSONObject } from "../../../Types/JSON";
23
+ import ObjectID from "../../../Types/ObjectID";
24
+ import AiResourceType, {
25
+ AI_RESOURCE_TYPE_INFO,
26
+ } from "../../../Types/ResourceAiAgent/AiResourceType";
27
+ import { ResourceAiRemediationMode } from "../../../Types/ResourceAiAgent/ResourceAiAccess";
28
+ import {
29
+ afterEach,
30
+ beforeEach,
31
+ describe,
32
+ expect,
33
+ jest,
34
+ test,
35
+ } from "@jest/globals";
36
+ import { FindOperator } from "typeorm";
37
+
38
+ /*
39
+ * The resource AI agent after registration:
40
+ *
41
+ * - authenticate: every later request carries the agent id and key; the
42
+ * key's sha256 must match the row (timing-safe). Wrong key, reset row
43
+ * (null hash), unknown id and malformed input are all null, and the row
44
+ * handed back never carries the hash;
45
+ * - heartbeat: alive + connected, the posture stored with the resource
46
+ * type and the registered identity forced; "Ask for approval" once the
47
+ * agent's write access appears on a resource nobody configured; and, at
48
+ * most every few minutes, a check that the resource still exists — an
49
+ * agent whose resource is gone is retired and told to register again;
50
+ * - markDisconnected: the sign-off, which admits a replacement at once;
51
+ * - resetAgent: the admin's reset — no key works any more, the row reads
52
+ * disconnected, and the resource's feed says so;
53
+ * - deleteAgentsForResources: what a resource's delete path calls.
54
+ *
55
+ * No database: everything below the service boundary is stubbed.
56
+ */
57
+
58
+ interface SpyCalls {
59
+ mock: { calls: Array<Array<unknown>> };
60
+ }
61
+
62
+ type AnyObject = Record<string, any>;
63
+
64
+ const PROJECT_ID: ObjectID = new ObjectID(
65
+ "11111111-1111-4111-8111-111111111111",
66
+ );
67
+ const RESOURCE_ID: ObjectID = new ObjectID(
68
+ "22222222-2222-4222-8222-222222222222",
69
+ );
70
+ const AGENT_ID: ObjectID = new ObjectID("33333333-3333-4333-8333-333333333333");
71
+ const USER_ID: ObjectID = new ObjectID("66666666-6666-4666-8666-666666666666");
72
+
73
+ const AGENT_KEY: string = "ab".repeat(32);
74
+
75
+ function msAgo(ms: number, from: Date = new Date()): Date {
76
+ return new Date(from.getTime() - ms);
77
+ }
78
+
79
+ function makeAgent(overrides: AnyObject = {}): ResourceAiAgent {
80
+ const agent: ResourceAiAgent = new ResourceAiAgent();
81
+ agent.id = AGENT_ID;
82
+ agent.projectId = PROJECT_ID;
83
+ agent.resourceType = AiResourceType.DockerHost;
84
+ agent.resourceId = RESOURCE_ID;
85
+ agent.resourceIdentifier = "web-1";
86
+ agent.keyHash = ResourceAiAgentServiceClass.hashKey(AGENT_KEY);
87
+ agent.connectionStatus = "connected";
88
+ agent.lastAliveAt = msAgo(20 * 1000);
89
+ agent.posture = {
90
+ resourceType: AiResourceType.DockerHost,
91
+ resourceIdentifier: "web-1",
92
+ allowWrites: false,
93
+ writeTargets: [],
94
+ protectedTargets: [],
95
+ reachable: true,
96
+ };
97
+ Object.assign(agent, overrides);
98
+ return agent;
99
+ }
100
+
101
+ function makeResource(overrides: AnyObject = {}): DockerHost {
102
+ const host: DockerHost = new DockerHost();
103
+ host._id = RESOURCE_ID.toString();
104
+ host.projectId = PROJECT_ID;
105
+ host.name = "web-1";
106
+ host.isAiInvestigationEnabled = true;
107
+ host.aiRemediationMode = ResourceAiRemediationMode.Disabled;
108
+ Object.assign(host, overrides);
109
+ return host;
110
+ }
111
+
112
+ beforeEach(() => {
113
+ for (const level of ["info", "warn", "error", "debug"] as const) {
114
+ jest.spyOn(logger, level).mockImplementation((): void => {
115
+ return undefined;
116
+ });
117
+ }
118
+ });
119
+
120
+ afterEach(() => {
121
+ jest.restoreAllMocks();
122
+ });
123
+
124
+ describe("authenticate", () => {
125
+ let findOneBy: SpyCalls;
126
+
127
+ function stubRow(row: ResourceAiAgent | null): void {
128
+ findOneBy = jest
129
+ .spyOn(ResourceAiAgentService, "findOneBy")
130
+ .mockResolvedValue(row) as unknown as SpyCalls;
131
+ }
132
+
133
+ test("the right key returns the row, read by id as root with the hash selected, and hands it back without the hash", async () => {
134
+ stubRow(makeAgent());
135
+
136
+ const agent: ResourceAiAgent | null =
137
+ await ResourceAiAgentService.authenticate({
138
+ agentId: AGENT_ID.toString(),
139
+ agentKey: AGENT_KEY,
140
+ });
141
+
142
+ expect(agent).not.toBeNull();
143
+ expect(agent!.id!.toString()).toBe(AGENT_ID.toString());
144
+ expect(agent!.keyHash).toBeUndefined();
145
+ expect(Object.keys(agent!)).not.toContain("keyHash");
146
+
147
+ const read: AnyObject = findOneBy.mock.calls[0]![0] as AnyObject;
148
+
149
+ expect(read["query"]).toEqual({ _id: AGENT_ID.toString() });
150
+ expect(read["select"]).toEqual({
151
+ ...RESOURCE_AI_AGENT_SELECT_WITHOUT_KEY,
152
+ keyHash: true,
153
+ });
154
+ expect(read["props"]["isRoot"]).toBe(true);
155
+ });
156
+
157
+ test("accepts the id as an ObjectID too", async () => {
158
+ stubRow(makeAgent());
159
+
160
+ await expect(
161
+ ResourceAiAgentService.authenticate({
162
+ agentId: AGENT_ID,
163
+ agentKey: AGENT_KEY,
164
+ }),
165
+ ).resolves.not.toBeNull();
166
+ });
167
+
168
+ test("a wrong key is null", async () => {
169
+ stubRow(makeAgent());
170
+
171
+ await expect(
172
+ ResourceAiAgentService.authenticate({
173
+ agentId: AGENT_ID.toString(),
174
+ agentKey: "cd".repeat(32),
175
+ }),
176
+ ).resolves.toBeNull();
177
+ });
178
+
179
+ test("the stored hash presented as the key is null", async () => {
180
+ stubRow(makeAgent());
181
+
182
+ await expect(
183
+ ResourceAiAgentService.authenticate({
184
+ agentId: AGENT_ID.toString(),
185
+ agentKey: ResourceAiAgentServiceClass.hashKey(AGENT_KEY),
186
+ }),
187
+ ).resolves.toBeNull();
188
+ });
189
+
190
+ test("a reset row (no hash) is null, even for the key it used to have", async () => {
191
+ stubRow(makeAgent({ keyHash: null }));
192
+
193
+ await expect(
194
+ ResourceAiAgentService.authenticate({
195
+ agentId: AGENT_ID.toString(),
196
+ agentKey: AGENT_KEY,
197
+ }),
198
+ ).resolves.toBeNull();
199
+ });
200
+
201
+ test("an unknown id is null, and costs the same hash comparison as a wrong key", async () => {
202
+ stubRow(null);
203
+ const compare: SpyCalls = jest.spyOn(
204
+ ResourceAiAgentServiceClass,
205
+ "doesKeyMatchHash",
206
+ ) as unknown as SpyCalls;
207
+
208
+ await expect(
209
+ ResourceAiAgentService.authenticate({
210
+ agentId: AGENT_ID.toString(),
211
+ agentKey: AGENT_KEY,
212
+ }),
213
+ ).resolves.toBeNull();
214
+
215
+ expect(compare.mock.calls).toHaveLength(1);
216
+ expect(compare.mock.calls[0]![0]).toBe(AGENT_KEY);
217
+ expect(String(compare.mock.calls[0]![1])).toMatch(/^[0-9a-f]{64}$/);
218
+ });
219
+
220
+ test.each([
221
+ ["an empty id", "", AGENT_KEY],
222
+ ["an id that is not a UUID", "agent-1", AGENT_KEY],
223
+ ["a SQL-ish id", "' OR 1=1 --", AGENT_KEY],
224
+ ["an empty key", AGENT_ID.toString(), ""],
225
+ ["a non-string key", AGENT_ID.toString(), 42],
226
+ ])(
227
+ "%s is null without touching the database",
228
+ async (_label: string, agentId: string, agentKey: unknown) => {
229
+ stubRow(makeAgent());
230
+
231
+ await expect(
232
+ ResourceAiAgentService.authenticate({
233
+ agentId,
234
+ agentKey: agentKey as string,
235
+ }),
236
+ ).resolves.toBeNull();
237
+
238
+ expect(findOneBy.mock.calls).toHaveLength(0);
239
+ },
240
+ );
241
+ });
242
+
243
+ describe("heartbeat", () => {
244
+ /*
245
+ * A fresh service per test: the resource check is throttled per agent in
246
+ * the process, and each test starts with no check done.
247
+ */
248
+ let service: ResourceAiAgentServiceClass;
249
+
250
+ interface HeartbeatHarness {
251
+ resourceFindOneBy: SpyCalls;
252
+ resourceUpdateOneBy: SpyCalls;
253
+ agentUpdateColumns: SpyCalls;
254
+ agentDeleteOneBy: SpyCalls;
255
+ feed: SpyCalls;
256
+ }
257
+
258
+ function setUp(
259
+ options: { resource?: DockerHost | null; updateCount?: number } = {},
260
+ ): HeartbeatHarness {
261
+ service = new ResourceAiAgentServiceClass();
262
+
263
+ return {
264
+ resourceFindOneBy: jest
265
+ .spyOn(DockerHostService, "findOneBy")
266
+ .mockResolvedValue(
267
+ options.resource === undefined ? makeResource() : options.resource,
268
+ ) as unknown as SpyCalls,
269
+ resourceUpdateOneBy: jest
270
+ .spyOn(DockerHostService, "updateOneBy")
271
+ .mockResolvedValue(options.updateCount ?? 1) as unknown as SpyCalls,
272
+ agentUpdateColumns: jest
273
+ .spyOn(service, "updateColumnsByIdWithoutHooks")
274
+ .mockResolvedValue(undefined) as unknown as SpyCalls,
275
+ agentDeleteOneBy: jest
276
+ .spyOn(service, "deleteOneBy")
277
+ .mockResolvedValue(1) as unknown as SpyCalls,
278
+ feed: jest
279
+ .spyOn(DockerHostFeedService, "createDockerHostFeedItem")
280
+ .mockResolvedValue(undefined) as unknown as SpyCalls,
281
+ };
282
+ }
283
+
284
+ function written(harness: HeartbeatHarness): {
285
+ id: ObjectID;
286
+ data: AnyObject;
287
+ } {
288
+ expect(harness.agentUpdateColumns.mock.calls).toHaveLength(1);
289
+ return harness.agentUpdateColumns.mock.calls[0]![0] as {
290
+ id: ObjectID;
291
+ data: AnyObject;
292
+ };
293
+ }
294
+
295
+ // The first heartbeat a process sees for an agent checks its resource.
296
+ async function firstHeartbeat(now: Date = new Date()): Promise<void> {
297
+ await service.heartbeat({ agent: makeAgent(), now });
298
+ }
299
+
300
+ test("marks the agent alive and connected in one hook-free write", async () => {
301
+ const harness: HeartbeatHarness = setUp();
302
+ const before: number = Date.now();
303
+
304
+ await service.heartbeat({ agent: makeAgent() });
305
+
306
+ const write: { id: ObjectID; data: AnyObject } = written(harness);
307
+ expect(write.id.toString()).toBe(AGENT_ID.toString());
308
+ expect(write.data["connectionStatus"]).toBe("connected");
309
+ expect(
310
+ (write.data["lastAliveAt"] as Date).getTime(),
311
+ ).toBeGreaterThanOrEqual(before);
312
+ expect(harness.agentDeleteOneBy.mock.calls).toHaveLength(0);
313
+ });
314
+
315
+ test("the first heartbeat in a process checks that the resource exists: its own row, in its project, as root", async () => {
316
+ const harness: HeartbeatHarness = setUp();
317
+
318
+ await firstHeartbeat();
319
+
320
+ expect(harness.resourceFindOneBy.mock.calls).toHaveLength(1);
321
+ const read: AnyObject = harness.resourceFindOneBy.mock
322
+ .calls[0]![0] as AnyObject;
323
+ expect(read["query"]).toEqual({
324
+ _id: RESOURCE_ID.toString(),
325
+ projectId: PROJECT_ID,
326
+ });
327
+ expect(read["props"]).toEqual({ isRoot: true });
328
+ });
329
+
330
+ // Every 30 seconds per resource: the common case must not read anything.
331
+ test.each([
332
+ ["without a posture", undefined],
333
+ ["with an unchanged read-only posture", { allowWrites: false }],
334
+ ["with writes it already had", { allowWrites: true }],
335
+ ])(
336
+ "a normal heartbeat (%s) within the check interval reads no resource row",
337
+ async (_label: string, posture: unknown) => {
338
+ const harness: HeartbeatHarness = setUp();
339
+ const now: Date = new Date();
340
+ await firstHeartbeat(now);
341
+
342
+ const agent: ResourceAiAgent = makeAgent();
343
+ if (posture && (posture as JSONObject)["allowWrites"] === true) {
344
+ (agent.posture as JSONObject)["allowWrites"] = true;
345
+ }
346
+
347
+ await service.heartbeat({
348
+ agent,
349
+ posture,
350
+ now: new Date(now.getTime() + 30 * 1000),
351
+ });
352
+
353
+ expect(harness.resourceFindOneBy.mock.calls).toHaveLength(1);
354
+ expect(harness.agentUpdateColumns.mock.calls).toHaveLength(2);
355
+ },
356
+ );
357
+
358
+ test("the resource is checked again once the interval has passed", async () => {
359
+ const harness: HeartbeatHarness = setUp();
360
+ const now: Date = new Date();
361
+
362
+ await firstHeartbeat(now);
363
+ await firstHeartbeat(
364
+ new Date(
365
+ now.getTime() + RESOURCE_AI_AGENT_RESOURCE_CHECK_INTERVAL_MS - 1,
366
+ ),
367
+ );
368
+ expect(harness.resourceFindOneBy.mock.calls).toHaveLength(1);
369
+
370
+ await firstHeartbeat(
371
+ new Date(now.getTime() + RESOURCE_AI_AGENT_RESOURCE_CHECK_INTERVAL_MS),
372
+ );
373
+ expect(harness.resourceFindOneBy.mock.calls).toHaveLength(2);
374
+ });
375
+
376
+ test("the check is per agent", async () => {
377
+ const harness: HeartbeatHarness = setUp();
378
+ const now: Date = new Date();
379
+
380
+ await firstHeartbeat(now);
381
+ await service.heartbeat({
382
+ agent: makeAgent({
383
+ id: new ObjectID("44444444-4444-4444-8444-444444444444"),
384
+ }),
385
+ now,
386
+ });
387
+
388
+ expect(harness.resourceFindOneBy.mock.calls).toHaveLength(2);
389
+ });
390
+
391
+ describe("an agent whose resource is gone", () => {
392
+ test("is retired: its row is deleted (by id, in its project, as root), nothing is marked alive, and the heartbeat answers 401 so it registers again", async () => {
393
+ const harness: HeartbeatHarness = setUp({ resource: null });
394
+
395
+ const error: unknown = await service
396
+ .heartbeat({ agent: makeAgent(), posture: { allowWrites: true } })
397
+ .catch((e: unknown) => {
398
+ return e;
399
+ });
400
+
401
+ expect(error).toBeInstanceOf(NotAuthenticatedException);
402
+ expect((error as NotAuthenticatedException).message).toContain(
403
+ "Docker host no longer exists",
404
+ );
405
+ expect(harness.agentUpdateColumns.mock.calls).toHaveLength(0);
406
+ expect(harness.resourceUpdateOneBy.mock.calls).toHaveLength(0);
407
+
408
+ expect(harness.agentDeleteOneBy.mock.calls).toHaveLength(1);
409
+ const deleted: AnyObject = harness.agentDeleteOneBy.mock
410
+ .calls[0]![0] as AnyObject;
411
+ expect(deleted["query"]).toEqual({
412
+ _id: AGENT_ID.toString(),
413
+ projectId: PROJECT_ID,
414
+ });
415
+ expect(deleted["props"]).toEqual({ isRoot: true });
416
+ });
417
+
418
+ test("a row naming no resource type is treated the same way", async () => {
419
+ const harness: HeartbeatHarness = setUp();
420
+
421
+ await expect(
422
+ service.heartbeat({
423
+ agent: makeAgent({ resourceType: "KubernetesCluster" }),
424
+ }),
425
+ ).rejects.toBeInstanceOf(NotAuthenticatedException);
426
+
427
+ expect(harness.resourceFindOneBy.mock.calls).toHaveLength(0);
428
+ expect(harness.agentDeleteOneBy.mock.calls).toHaveLength(1);
429
+ });
430
+
431
+ test("a failed delete still answers 401 (the next check tries again)", async () => {
432
+ const harness: HeartbeatHarness = setUp({ resource: null });
433
+ (
434
+ harness.agentDeleteOneBy as unknown as {
435
+ mockRejectedValue: (error: Error) => void;
436
+ }
437
+ ).mockRejectedValue(new Error("database is down"));
438
+
439
+ await expect(
440
+ service.heartbeat({ agent: makeAgent() }),
441
+ ).rejects.toBeInstanceOf(NotAuthenticatedException);
442
+ expect(
443
+ JSON.stringify((logger.error as unknown as SpyCalls).mock.calls),
444
+ ).toContain("database is down");
445
+ });
446
+ });
447
+
448
+ test("a failing resource check is not a gone resource: the heartbeat is stored and nothing is deleted", async () => {
449
+ const harness: HeartbeatHarness = setUp();
450
+ (
451
+ harness.resourceFindOneBy as unknown as {
452
+ mockRejectedValue: (error: Error) => void;
453
+ }
454
+ ).mockRejectedValue(new Error("database is down"));
455
+
456
+ await service.heartbeat({ agent: makeAgent() });
457
+
458
+ expect(harness.agentUpdateColumns.mock.calls).toHaveLength(1);
459
+ expect(harness.agentDeleteOneBy.mock.calls).toHaveLength(0);
460
+ });
461
+
462
+ test("stores the reported posture with the resource type and the registered identity forced", async () => {
463
+ const harness: HeartbeatHarness = setUp();
464
+
465
+ await service.heartbeat({
466
+ agent: makeAgent(),
467
+ agentVersion: "14.2.0",
468
+ posture: {
469
+ resourceType: AiResourceType.Host,
470
+ resourceIdentifier: "someone-else",
471
+ allowWrites: false,
472
+ writeTargets: ["web-*"],
473
+ protectedTargets: ["oneuptime-ai-agent"],
474
+ reachable: true,
475
+ toolVersion: "27.3.1",
476
+ },
477
+ });
478
+
479
+ const write: { data: AnyObject } = written(harness);
480
+ const posture: JSONObject = write.data["posture"] as JSONObject;
481
+ expect(posture["resourceType"]).toBe(AiResourceType.DockerHost);
482
+ expect(posture["resourceIdentifier"]).toBe("web-1");
483
+ expect(posture["writeTargets"]).toEqual(["web-*"]);
484
+ expect(posture["toolVersion"]).toBe("27.3.1");
485
+ expect(Object.values(posture)).not.toContain(undefined);
486
+ expect(write.data["agentVersion"]).toBe("14.2.0");
487
+ });
488
+
489
+ test("a row without its identity column falls back to the stored posture's identity", async () => {
490
+ const harness: HeartbeatHarness = setUp();
491
+
492
+ await service.heartbeat({
493
+ agent: makeAgent({ resourceIdentifier: " " }),
494
+ posture: { allowWrites: false, resourceIdentifier: "evil" },
495
+ });
496
+
497
+ expect(
498
+ (written(harness).data["posture"] as JSONObject)["resourceIdentifier"],
499
+ ).toBe("web-1");
500
+ });
501
+
502
+ test("with neither, the reported identity is never trusted: the stored posture is kept", async () => {
503
+ const harness: HeartbeatHarness = setUp();
504
+
505
+ await service.heartbeat({
506
+ agent: makeAgent({ resourceIdentifier: undefined, posture: undefined }),
507
+ posture: { allowWrites: true, resourceIdentifier: "evil" },
508
+ });
509
+
510
+ expect(written(harness).data).not.toHaveProperty("posture");
511
+ });
512
+
513
+ test("an empty or missing version is not written, and a missing posture keeps the stored one", async () => {
514
+ const harness: HeartbeatHarness = setUp();
515
+
516
+ await service.heartbeat({ agent: makeAgent(), agentVersion: " " });
517
+
518
+ expect(written(harness).data).not.toHaveProperty("agentVersion");
519
+ expect(written(harness).data).not.toHaveProperty("posture");
520
+ });
521
+
522
+ test.each([
523
+ ["a string", "allowWrites=true"],
524
+ ["an array", [{ allowWrites: true }]],
525
+ ])(
526
+ "a posture that is %s is not a posture",
527
+ async (_label: string, posture: unknown) => {
528
+ const harness: HeartbeatHarness = setUp();
529
+
530
+ await service.heartbeat({ agent: makeAgent(), posture });
531
+
532
+ expect(written(harness).data).not.toHaveProperty("posture");
533
+ },
534
+ );
535
+
536
+ test("an agent without an id is a programming error", async () => {
537
+ setUp();
538
+ const agent: ResourceAiAgent = makeAgent();
539
+ delete (agent as unknown as AnyObject)["_id"];
540
+
541
+ await expect(service.heartbeat({ agent })).rejects.toBeInstanceOf(
542
+ BadDataException,
543
+ );
544
+ });
545
+
546
+ describe("write access appearing", () => {
547
+ function defaultsData(harness: HeartbeatHarness): AnyObject {
548
+ expect(harness.resourceUpdateOneBy.mock.calls).toHaveLength(1);
549
+ return (harness.resourceUpdateOneBy.mock.calls[0]![0] as AnyObject)[
550
+ "data"
551
+ ] as AnyObject;
552
+ }
553
+
554
+ test("on a resource nobody configured with fixes Off: Ask for approval, and a feed item", async () => {
555
+ const harness: HeartbeatHarness = setUp();
556
+
557
+ await service.heartbeat({
558
+ agent: makeAgent(),
559
+ posture: { allowWrites: true, writeTargets: ["web-*"] },
560
+ });
561
+
562
+ expect(defaultsData(harness)).toEqual({
563
+ aiRemediationMode: ResourceAiRemediationMode.RequireApproval,
564
+ });
565
+ expect(harness.feed.mock.calls).toHaveLength(1);
566
+ const text: string = String(
567
+ (harness.feed.mock.calls[0]![0] as AnyObject)["feedInfoInMarkdown"],
568
+ );
569
+ expect(text).toContain("can now make changes to web-*");
570
+ expect(text).toContain('"Ask for approval"');
571
+ });
572
+
573
+ test("never on a resource someone configured", async () => {
574
+ const harness: HeartbeatHarness = setUp({
575
+ resource: makeResource({ aiAccessConfiguredAt: new Date() }),
576
+ });
577
+
578
+ await service.heartbeat({
579
+ agent: makeAgent(),
580
+ posture: { allowWrites: true },
581
+ });
582
+
583
+ expect(harness.resourceUpdateOneBy.mock.calls).toHaveLength(0);
584
+ expect(harness.feed.mock.calls).toHaveLength(0);
585
+ });
586
+
587
+ test("only when writes APPEAR: an agent that already had them changes nothing", async () => {
588
+ const harness: HeartbeatHarness = setUp();
589
+ const agent: ResourceAiAgent = makeAgent();
590
+ (agent.posture as JSONObject)["allowWrites"] = true;
591
+
592
+ await service.heartbeat({ agent, posture: { allowWrites: true } });
593
+
594
+ expect(harness.resourceUpdateOneBy.mock.calls).toHaveLength(0);
595
+ });
596
+
597
+ test("an agent that stays read-only changes nothing", async () => {
598
+ const harness: HeartbeatHarness = setUp({
599
+ resource: makeResource({ isAiInvestigationEnabled: false }),
600
+ });
601
+
602
+ await service.heartbeat({
603
+ agent: makeAgent(),
604
+ posture: { allowWrites: false },
605
+ });
606
+
607
+ expect(harness.resourceUpdateOneBy.mock.calls).toHaveLength(0);
608
+ });
609
+
610
+ test("a heartbeat without a posture is not writes appearing", async () => {
611
+ const harness: HeartbeatHarness = setUp();
612
+
613
+ await service.heartbeat({ agent: makeAgent() });
614
+
615
+ expect(harness.resourceUpdateOneBy.mock.calls).toHaveLength(0);
616
+ });
617
+
618
+ test("a stored posture that was never set counts as read-only before", async () => {
619
+ const harness: HeartbeatHarness = setUp();
620
+
621
+ await service.heartbeat({
622
+ agent: makeAgent({ posture: undefined }),
623
+ posture: { allowWrites: true },
624
+ });
625
+
626
+ expect(defaultsData(harness)["aiRemediationMode"]).toBe(
627
+ ResourceAiRemediationMode.RequireApproval,
628
+ );
629
+ });
630
+
631
+ test("the defaults are conditional on the resource still being unconfigured; a lost race writes no feed item", async () => {
632
+ const harness: HeartbeatHarness = setUp({ updateCount: 0 });
633
+
634
+ await service.heartbeat({
635
+ agent: makeAgent(),
636
+ posture: { allowWrites: true },
637
+ });
638
+
639
+ const query: AnyObject = (
640
+ harness.resourceUpdateOneBy.mock.calls[0]![0] as AnyObject
641
+ )["query"] as AnyObject;
642
+ expect(query["aiAccessConfiguredAt"]).toBeInstanceOf(FindOperator);
643
+ expect(harness.feed.mock.calls).toHaveLength(0);
644
+ });
645
+
646
+ test("the resource row is read once, even when the periodic check read it too", async () => {
647
+ const harness: HeartbeatHarness = setUp();
648
+
649
+ await service.heartbeat({
650
+ agent: makeAgent(),
651
+ posture: { allowWrites: true },
652
+ });
653
+
654
+ expect(harness.resourceFindOneBy.mock.calls).toHaveLength(1);
655
+ });
656
+
657
+ test("between checks, the defaults read the resource after the liveness write", async () => {
658
+ const harness: HeartbeatHarness = setUp();
659
+ const now: Date = new Date();
660
+ await firstHeartbeat(now);
661
+
662
+ await service.heartbeat({
663
+ agent: makeAgent(),
664
+ posture: { allowWrites: true },
665
+ now: new Date(now.getTime() + 30 * 1000),
666
+ });
667
+
668
+ expect(harness.resourceFindOneBy.mock.calls).toHaveLength(2);
669
+ expect(harness.agentUpdateColumns.mock.calls).toHaveLength(2);
670
+ expect(harness.resourceUpdateOneBy.mock.calls).toHaveLength(1);
671
+ });
672
+
673
+ test("a resource row that vanished between checks changes nothing, and the heartbeat still succeeds", async () => {
674
+ const harness: HeartbeatHarness = setUp();
675
+ const now: Date = new Date();
676
+ await firstHeartbeat(now);
677
+ (
678
+ harness.resourceFindOneBy as unknown as {
679
+ mockResolvedValue: (value: unknown) => void;
680
+ }
681
+ ).mockResolvedValue(null);
682
+
683
+ await service.heartbeat({
684
+ agent: makeAgent(),
685
+ posture: { allowWrites: true },
686
+ now: new Date(now.getTime() + 30 * 1000),
687
+ });
688
+
689
+ expect(harness.resourceUpdateOneBy.mock.calls).toHaveLength(0);
690
+ expect(harness.feed.mock.calls).toHaveLength(0);
691
+ });
692
+
693
+ test("a failure applying the defaults never fails the heartbeat, and no feed item claims a change", async () => {
694
+ const harness: HeartbeatHarness = setUp();
695
+ (
696
+ harness.resourceUpdateOneBy as unknown as {
697
+ mockRejectedValue: (error: Error) => void;
698
+ }
699
+ ).mockRejectedValue(new Error("database is down"));
700
+
701
+ await expect(
702
+ service.heartbeat({
703
+ agent: makeAgent(),
704
+ posture: { allowWrites: true },
705
+ }),
706
+ ).resolves.toBeUndefined();
707
+
708
+ expect(harness.agentUpdateColumns.mock.calls).toHaveLength(1);
709
+ expect(harness.feed.mock.calls).toHaveLength(0);
710
+ });
711
+ });
712
+ });
713
+
714
+ describe("markDisconnected", () => {
715
+ test("writes only the disconnected status — lastAliveAt stays the truth about the last heartbeat", async () => {
716
+ const update: SpyCalls = jest
717
+ .spyOn(ResourceAiAgentService, "updateColumnsByIdWithoutHooks")
718
+ .mockResolvedValue(undefined) as unknown as SpyCalls;
719
+
720
+ await ResourceAiAgentService.markDisconnected({
721
+ resourceAiAgentId: AGENT_ID,
722
+ });
723
+
724
+ expect(update.mock.calls[0]![0]).toEqual({
725
+ id: AGENT_ID,
726
+ data: { connectionStatus: "disconnected" },
727
+ });
728
+ });
729
+
730
+ test("requires the agent id", async () => {
731
+ await expect(
732
+ ResourceAiAgentService.markDisconnected({
733
+ resourceAiAgentId: undefined as unknown as ObjectID,
734
+ }),
735
+ ).rejects.toBeInstanceOf(BadDataException);
736
+ });
737
+
738
+ test("a signed-off agent is offline and admits the next registration", () => {
739
+ const signedOff: ResourceAiAgent = makeAgent({
740
+ connectionStatus: "disconnected",
741
+ lastAliveAt: msAgo(1000),
742
+ });
743
+
744
+ expect(ResourceAiAgentService.isOnline(signedOff)).toBe(false);
745
+ expect(
746
+ ResourceAiAgentService.getReRegistrationAdmission({ agent: signedOff }),
747
+ ).toBe("signed_off");
748
+ });
749
+ });
750
+
751
+ describe("resetAgent", () => {
752
+ const USER_MARKDOWN: string = `[Jane Doe](https://oneuptime.example/dashboard/${PROJECT_ID.toString()}/settings/users/${USER_ID.toString()})`;
753
+
754
+ interface ResetHarness {
755
+ find: SpyCalls;
756
+ update: SpyCalls;
757
+ feed: SpyCalls;
758
+ userName: SpyCalls;
759
+ }
760
+
761
+ function setUp(row: ResourceAiAgent | null = makeAgent()): ResetHarness {
762
+ return {
763
+ find: jest
764
+ .spyOn(ResourceAiAgentService, "findAgentForResource")
765
+ .mockResolvedValue(row) as unknown as SpyCalls,
766
+ update: jest
767
+ .spyOn(ResourceAiAgentService, "updateOneById")
768
+ .mockResolvedValue(undefined as never) as unknown as SpyCalls,
769
+ feed: jest
770
+ .spyOn(DockerHostFeedService, "createDockerHostFeedItem")
771
+ .mockResolvedValue(undefined) as unknown as SpyCalls,
772
+ userName: jest
773
+ .spyOn(UserService, "getUserMarkdownString")
774
+ .mockResolvedValue(USER_MARKDOWN) as unknown as SpyCalls,
775
+ };
776
+ }
777
+
778
+ function reset(userId?: ObjectID): Promise<void> {
779
+ return ResourceAiAgentService.resetAgent({
780
+ projectId: PROJECT_ID,
781
+ resourceType: AiResourceType.DockerHost,
782
+ resourceId: RESOURCE_ID,
783
+ ...(userId ? { userId } : {}),
784
+ });
785
+ }
786
+
787
+ function feedText(harness: ResetHarness): string {
788
+ expect(harness.feed.mock.calls).toHaveLength(1);
789
+ return String(
790
+ (harness.feed.mock.calls[0]![0] as AnyObject)["feedInfoInMarkdown"],
791
+ );
792
+ }
793
+
794
+ test("clears the key hash and marks the agent disconnected, as root", async () => {
795
+ const harness: ResetHarness = setUp();
796
+
797
+ await reset(USER_ID);
798
+
799
+ expect(harness.find.mock.calls[0]![0]).toEqual({
800
+ projectId: PROJECT_ID,
801
+ resourceType: AiResourceType.DockerHost,
802
+ resourceId: RESOURCE_ID,
803
+ });
804
+
805
+ const write: { id: ObjectID; data: AnyObject; props: AnyObject } = harness
806
+ .update.mock.calls[0]![0] as {
807
+ id: ObjectID;
808
+ data: AnyObject;
809
+ props: AnyObject;
810
+ };
811
+
812
+ expect(write.id.toString()).toBe(AGENT_ID.toString());
813
+ expect(write.data).toEqual({
814
+ keyHash: null,
815
+ connectionStatus: "disconnected",
816
+ });
817
+ expect(write.props["isRoot"]).toBe(true);
818
+ });
819
+
820
+ test("writes a feed item on the resource's feed, attributed to the user, that says who reset it and that it comes back on its own", async () => {
821
+ const harness: ResetHarness = setUp();
822
+
823
+ await reset(USER_ID);
824
+
825
+ const item: AnyObject = harness.feed.mock.calls[0]![0] as AnyObject;
826
+ expect(item["dockerHostId"]).toBe(RESOURCE_ID);
827
+ expect(item["projectId"]).toBe(PROJECT_ID);
828
+ expect(item["userId"]).toBe(USER_ID);
829
+ expect(item["dockerHostFeedEventType"]).toBe("DockerHostUpdated");
830
+ expect(feedText(harness)).toBe(
831
+ `🔄 The Docker AI agent was reset by **${USER_MARKDOWN}**. It reconnects on its own within a few minutes.`,
832
+ );
833
+ expect(String(item["moreInformationInMarkdown"])).toContain(
834
+ AGENT_ID.toString(),
835
+ );
836
+ expect(harness.userName.mock.calls).toEqual([
837
+ [{ userId: USER_ID, projectId: PROJECT_ID }],
838
+ ]);
839
+ });
840
+
841
+ test("without a user (a master admin), the feed item names nobody and carries no user", async () => {
842
+ const harness: ResetHarness = setUp();
843
+
844
+ await reset();
845
+
846
+ expect(harness.userName.mock.calls).toHaveLength(0);
847
+ expect(
848
+ (harness.feed.mock.calls[0]![0] as AnyObject)["userId"],
849
+ ).toBeUndefined();
850
+ expect(feedText(harness)).toBe(
851
+ "🔄 The Docker AI agent was reset. It reconnects on its own within a few minutes.",
852
+ );
853
+ });
854
+
855
+ test("a user who cannot be named gets the plain wording, still attributed", async () => {
856
+ const harness: ResetHarness = setUp();
857
+ (
858
+ harness.userName as unknown as {
859
+ mockResolvedValue: (value: string) => void;
860
+ }
861
+ ).mockResolvedValue("");
862
+
863
+ await reset(USER_ID);
864
+
865
+ expect(feedText(harness)).toBe(
866
+ "🔄 The Docker AI agent was reset. It reconnects on its own within a few minutes.",
867
+ );
868
+ expect((harness.feed.mock.calls[0]![0] as AnyObject)["userId"]).toBe(
869
+ USER_ID,
870
+ );
871
+ });
872
+
873
+ test("a failing name lookup never fails the reset", async () => {
874
+ const harness: ResetHarness = setUp();
875
+ (
876
+ harness.userName as unknown as {
877
+ mockRejectedValue: (error: Error) => void;
878
+ }
879
+ ).mockRejectedValue(new Error("database is down"));
880
+
881
+ await expect(reset(USER_ID)).resolves.toBeUndefined();
882
+
883
+ expect(harness.update.mock.calls).toHaveLength(1);
884
+ expect(feedText(harness)).toBe(
885
+ "🔄 The Docker AI agent was reset. It reconnects on its own within a few minutes.",
886
+ );
887
+ });
888
+
889
+ test("a resource without an agent is a plain error, and nothing is written", async () => {
890
+ const harness: ResetHarness = setUp(null);
891
+
892
+ await expect(reset()).rejects.toThrow(
893
+ "This Docker host has no Docker AI agent to reset.",
894
+ );
895
+
896
+ expect(harness.update.mock.calls).toHaveLength(0);
897
+ expect(harness.feed.mock.calls).toHaveLength(0);
898
+ });
899
+
900
+ test("an unknown resource type is a plain error, and nothing is read", async () => {
901
+ const harness: ResetHarness = setUp();
902
+
903
+ await expect(
904
+ ResourceAiAgentService.resetAgent({
905
+ projectId: PROJECT_ID,
906
+ resourceType: "KubernetesCluster" as AiResourceType,
907
+ resourceId: RESOURCE_ID,
908
+ }),
909
+ ).rejects.toBeInstanceOf(BadDataException);
910
+
911
+ expect(harness.find.mock.calls).toHaveLength(0);
912
+ });
913
+
914
+ test.each([
915
+ [
916
+ AiResourceType.PodmanHost,
917
+ PodmanHostFeedService,
918
+ "createPodmanHostFeedItem",
919
+ ],
920
+ [
921
+ AiResourceType.DockerSwarmCluster,
922
+ DockerSwarmClusterFeedService,
923
+ "createDockerSwarmClusterFeedItem",
924
+ ],
925
+ [
926
+ AiResourceType.ProxmoxCluster,
927
+ ProxmoxClusterFeedService,
928
+ "createProxmoxClusterFeedItem",
929
+ ],
930
+ [
931
+ AiResourceType.VMwareVCenter,
932
+ VMwareVCenterFeedService,
933
+ "createVMwareVCenterFeedItem",
934
+ ],
935
+ [
936
+ AiResourceType.CephCluster,
937
+ CephClusterFeedService,
938
+ "createCephClusterFeedItem",
939
+ ],
940
+ [
941
+ AiResourceType.DatabaseServer,
942
+ DatabaseServerFeedService,
943
+ "createDatabaseServerFeedItem",
944
+ ],
945
+ [AiResourceType.Host, HostFeedService, "createHostFeedItem"],
946
+ ])(
947
+ "a %s's reset is told on its own feed, naming its agent",
948
+ async (
949
+ resourceType: AiResourceType,
950
+ feedService: unknown,
951
+ method: string,
952
+ ) => {
953
+ jest
954
+ .spyOn(ResourceAiAgentService, "findAgentForResource")
955
+ .mockResolvedValue(makeAgent({ resourceType }));
956
+ jest
957
+ .spyOn(ResourceAiAgentService, "updateOneById")
958
+ .mockResolvedValue(undefined as never);
959
+ const feed: SpyCalls = jest
960
+ .spyOn(feedService as AnyObject, method)
961
+ .mockResolvedValue(undefined as never) as unknown as SpyCalls;
962
+
963
+ await ResourceAiAgentService.resetAgent({
964
+ projectId: PROJECT_ID,
965
+ resourceType,
966
+ resourceId: RESOURCE_ID,
967
+ });
968
+
969
+ expect(feed.mock.calls).toHaveLength(1);
970
+ expect(
971
+ String((feed.mock.calls[0]![0] as AnyObject)["feedInfoInMarkdown"]),
972
+ ).toContain(
973
+ `The ${AI_RESOURCE_TYPE_INFO[resourceType].agentDisplayName} was reset`,
974
+ );
975
+ },
976
+ );
977
+
978
+ test("after a reset the old key no longer authenticates, and the next registration is admitted", async () => {
979
+ const reset: ResourceAiAgent = makeAgent({
980
+ keyHash: null,
981
+ connectionStatus: "disconnected",
982
+ });
983
+ jest.spyOn(ResourceAiAgentService, "findOneBy").mockResolvedValue(reset);
984
+
985
+ await expect(
986
+ ResourceAiAgentService.authenticate({
987
+ agentId: AGENT_ID.toString(),
988
+ agentKey: AGENT_KEY,
989
+ }),
990
+ ).resolves.toBeNull();
991
+
992
+ expect(
993
+ ResourceAiAgentService.getReRegistrationAdmission({
994
+ agent: reset,
995
+ previousAgentKey: AGENT_KEY,
996
+ }),
997
+ ).toBe("reset");
998
+ });
999
+ });
1000
+
1001
+ describe("deleteAgentsForResources", () => {
1002
+ test("removes the agents of the given resources of one type, as root", async () => {
1003
+ const deleteBy: SpyCalls = jest
1004
+ .spyOn(ResourceAiAgentService, "deleteBy")
1005
+ .mockResolvedValue(2) as unknown as SpyCalls;
1006
+
1007
+ await expect(
1008
+ ResourceAiAgentService.deleteAgentsForResources({
1009
+ resourceType: AiResourceType.CephCluster,
1010
+ resourceIds: [RESOURCE_ID],
1011
+ projectId: PROJECT_ID,
1012
+ }),
1013
+ ).resolves.toBe(2);
1014
+
1015
+ const call: AnyObject = deleteBy.mock.calls[0]![0] as AnyObject;
1016
+ expect(call["query"]["resourceType"]).toBe(AiResourceType.CephCluster);
1017
+ expect(call["query"]["projectId"]).toBe(PROJECT_ID);
1018
+ expect(call["query"]["resourceId"]).toBeInstanceOf(FindOperator);
1019
+ expect(
1020
+ Object.values(
1021
+ (call["query"]["resourceId"] as FindOperator<unknown>)
1022
+ .objectLiteralParameters || {},
1023
+ ),
1024
+ ).toEqual([[RESOURCE_ID.toString()]]);
1025
+ expect(call["limit"]).toBe(LIMIT_MAX);
1026
+ expect(call["props"]).toEqual({ isRoot: true });
1027
+ });
1028
+
1029
+ test("nothing to delete: no resources, or not a resource type", async () => {
1030
+ const deleteBy: SpyCalls = jest.spyOn(
1031
+ ResourceAiAgentService,
1032
+ "deleteBy",
1033
+ ) as unknown as SpyCalls;
1034
+
1035
+ await expect(
1036
+ ResourceAiAgentService.deleteAgentsForResources({
1037
+ resourceType: AiResourceType.Host,
1038
+ resourceIds: [],
1039
+ }),
1040
+ ).resolves.toBe(0);
1041
+ await expect(
1042
+ ResourceAiAgentService.deleteAgentsForResources({
1043
+ resourceType: "Nope" as AiResourceType,
1044
+ resourceIds: [RESOURCE_ID],
1045
+ }),
1046
+ ).resolves.toBe(0);
1047
+
1048
+ expect(deleteBy.mock.calls).toHaveLength(0);
1049
+ });
1050
+ });