@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,1279 @@
1
+ import AutoRemediationRuleEngineService, {
2
+ DEFAULT_VERIFICATION_WINDOW_MINUTES,
3
+ MAX_AUTO_EXECUTIONS_PER_RULE_PER_HOUR,
4
+ MAX_RESOURCE_REMEDIATION_ROUNDS_PER_SUBJECT,
5
+ MAX_SUGGESTIONS_PER_SUBJECT,
6
+ RESOURCE_BREAKER_LOCK_NAMESPACE,
7
+ ResourceBreakerState,
8
+ ResourceRoundHold,
9
+ getResourceBreakerLockKey,
10
+ doesResourceModeRunRoundUnattended,
11
+ getResourceRoundNameSnapshot,
12
+ parseClusterRoundNameSnapshot,
13
+ parseResourceRoundNumber,
14
+ } from "../../../Server/Services/AutoRemediationRuleEngineService";
15
+ import AutoRemediationRuleService from "../../../Server/Services/AutoRemediationRuleService";
16
+ import AutoRemediationSuggestionService from "../../../Server/Services/AutoRemediationSuggestionService";
17
+ import AlertFeedService from "../../../Server/Services/AlertFeedService";
18
+ import IncidentFeedService from "../../../Server/Services/IncidentFeedService";
19
+ import KubernetesClusterAiAccessService from "../../../Server/Services/KubernetesClusterAiAccessService";
20
+ import ProjectService from "../../../Server/Services/ProjectService";
21
+ import ResourceAiAccessService from "../../../Server/Services/ResourceAiAccessService";
22
+ import RunnerJobService from "../../../Server/Services/RunnerJobService";
23
+ import AIInvestigationQueue from "../../../Server/Utils/AI/SRE/InvestigationQueue";
24
+ import logger from "../../../Server/Utils/Logger";
25
+ import AutoRemediationSuggestion from "../../../Models/DatabaseModels/AutoRemediationSuggestion";
26
+ import Alert from "../../../Models/DatabaseModels/Alert";
27
+ import Incident from "../../../Models/DatabaseModels/Incident";
28
+ import Project from "../../../Models/DatabaseModels/Project";
29
+ import RunnerJob from "../../../Models/DatabaseModels/RunnerJob";
30
+ import AIRunType from "../../../Types/AI/AIRunType";
31
+ import AutoRemediationExecutionMode from "../../../Types/AutoRemediation/AutoRemediationExecutionMode";
32
+ import AutoRemediationSuggestionStatus from "../../../Types/AutoRemediation/AutoRemediationSuggestionStatus";
33
+ import AutoRemediationSuggestionType from "../../../Types/AutoRemediation/AutoRemediationSuggestionType";
34
+ import AutoRemediationVerificationStatus from "../../../Types/AutoRemediation/AutoRemediationVerificationStatus";
35
+ import {
36
+ INLINE_COMMAND_STEP_ID_PREFIX,
37
+ RESOURCE_ALWAYS_ASKS_SUMMARY,
38
+ RESOURCE_SAFE_CHANGES_SUMMARY,
39
+ } from "../../../Types/AutoRemediation/AiRemediationCommandPlan";
40
+ import {
41
+ KubernetesAiRemediationMode,
42
+ KubernetesClusterAiAccessStatus,
43
+ } from "../../../Types/Kubernetes/KubernetesClusterAiAccess";
44
+ import AiResourceType from "../../../Types/ResourceAiAgent/AiResourceType";
45
+ import {
46
+ ResourceAiAccessStatus,
47
+ ResourceAiRemediationMode,
48
+ } from "../../../Types/ResourceAiAgent/ResourceAiAccess";
49
+ import RunnerJobOrigin from "../../../Types/Runbook/RunnerJobOrigin";
50
+ import ObjectID from "../../../Types/ObjectID";
51
+ import PositiveNumber from "../../../Types/PositiveNumber";
52
+ import OneUptimeDate from "../../../Types/Date";
53
+ import { afterEach, beforeEach, describe, expect, it } from "@jest/globals";
54
+
55
+ /*
56
+ * Contract under test — resource-level remediation in the rule engine, the
57
+ * lane an infrastructure resource's AI page (a Docker or Podman host, a
58
+ * Docker Swarm, Proxmox, VMware or Ceph cluster, a database server, a host)
59
+ * turns on without any AutoRemediationRule — built BESIDE the cluster lane:
60
+ *
61
+ * - it runs only when the signal has no cluster round (one started now, or
62
+ * one it already had) and no resource round yet: one AI fix lane per
63
+ * signal;
64
+ * - the FIRST eligible linked resource in ALL_AI_RESOURCE_TYPES order —
65
+ * fixes on (mode not Disabled) and remediation-ready — gets ONE Planning
66
+ * CommandPlan suggestion that carries its resourceType and resourceId,
67
+ * snapshots the mode exactly like a cluster round (Automatic → FullAuto on
68
+ * round 1 with auto-resolve, RequireApproval → Suggest, BypassApproval →
69
+ * FullAuto every round), and enqueues a RemediationExecution run;
70
+ * - another round holding the resource (or a failed check) makes an
71
+ * unattended round ask first, and the feed says why;
72
+ * - a failed access lookup skips the lane quietly and rules still run;
73
+ * - the follow-up round ("ask again") mirrors the cluster's: Suggest for
74
+ * Automatic and RequireApproval, FullAuto for BypassApproval, capped at
75
+ * MAX_RESOURCE_REMEDIATION_ROUNDS_PER_SUBJECT, asking first when forced;
76
+ * - the per-resource breaker and hold read the resource's own rows and jobs
77
+ * (resourceType + resourceId), never a cluster's, and the breaker lock's
78
+ * namespace and key are the resource's own.
79
+ */
80
+
81
+ const PROJECT_ID: ObjectID = new ObjectID(
82
+ "22222222-2222-4222-8222-222222222222",
83
+ );
84
+ const INCIDENT_ID: ObjectID = new ObjectID(
85
+ "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
86
+ );
87
+ const ALERT_ID: ObjectID = new ObjectID("dddddddd-dddd-4ddd-8ddd-dddddddddddd");
88
+ const RESOURCE_ID: string = "33333333-3333-4333-8333-333333333333";
89
+ const OTHER_RESOURCE_ID: string = "34343434-3434-4343-8343-343434343434";
90
+ const AGENT_ID: string = "44444444-4444-4444-8444-444444444444";
91
+ const CLUSTER_ID: string = "35353535-3535-4353-8353-353535353535";
92
+ const SUGGESTION_ID: ObjectID = new ObjectID(
93
+ "77777777-7777-4777-8777-777777777777",
94
+ );
95
+ const OTHER_SUGGESTION_ID: ObjectID = new ObjectID(
96
+ "79797979-7979-4979-8979-797979797979",
97
+ );
98
+ const AI_RUN_ID: ObjectID = new ObjectID(
99
+ "88888888-8888-4888-8888-888888888888",
100
+ );
101
+
102
+ function readyResource(
103
+ overrides: Partial<ResourceAiAccessStatus> = {},
104
+ ): ResourceAiAccessStatus {
105
+ const resourceType: AiResourceType =
106
+ overrides.resourceType || AiResourceType.DockerHost;
107
+
108
+ return {
109
+ resourceType,
110
+ resourceId: RESOURCE_ID,
111
+ resourceName: "web-1",
112
+ isAiInvestigationEnabled: true,
113
+ aiRemediationMode: ResourceAiRemediationMode.Automatic,
114
+ aiCommandAllowlist: [],
115
+ agent: {
116
+ agentId: AGENT_ID,
117
+ connectionStatus: "connected",
118
+ isOnline: true,
119
+ posture: {
120
+ resourceType,
121
+ resourceIdentifier: "web-1",
122
+ allowWrites: true,
123
+ writeTargets: [],
124
+ protectedTargets: [],
125
+ reachable: true,
126
+ },
127
+ },
128
+ gaps: [],
129
+ isInvestigationReady: true,
130
+ isRemediationReady: true,
131
+ ...overrides,
132
+ };
133
+ }
134
+
135
+ function readyCluster(): KubernetesClusterAiAccessStatus {
136
+ return {
137
+ clusterId: CLUSTER_ID,
138
+ clusterName: "prod-us",
139
+ runner: {
140
+ id: AGENT_ID,
141
+ name: "kubernetes-agent/prod-us",
142
+ isOnline: true,
143
+ canRunAiCommands: true,
144
+ },
145
+ accessMethod: "in_cluster",
146
+ aiAgent: null,
147
+ automaticInvestigation: { incidents: false, alerts: false },
148
+ kubectlAllowlist: [],
149
+ isInvestigationEnabled: true,
150
+ isInvestigationReady: true,
151
+ remediationMode: KubernetesAiRemediationMode.Automatic,
152
+ isRemediationReady: true,
153
+ gaps: [],
154
+ evaluatedAt: new Date().toISOString(),
155
+ };
156
+ }
157
+
158
+ function fakeIncident(): Incident {
159
+ return {
160
+ id: INCIDENT_ID,
161
+ _id: INCIDENT_ID.toString(),
162
+ projectId: PROJECT_ID,
163
+ title: "web-1 container keeps exiting",
164
+ monitors: [],
165
+ labels: [],
166
+ } as unknown as Incident;
167
+ }
168
+
169
+ let createdSuggestions: Array<AutoRemediationSuggestion>;
170
+ let suggestionUpdates: Array<Record<string, unknown>>;
171
+ let enqueue: jest.SpyInstance;
172
+ let resourceStatuses: jest.SpyInstance;
173
+ let suggestionFindBy: jest.SpyInstance;
174
+
175
+ function mockBaseline(data: {
176
+ existing?: Array<AutoRemediationSuggestion> | undefined;
177
+ clusters?: Array<KubernetesClusterAiAccessStatus> | undefined;
178
+ resources?: Array<ResourceAiAccessStatus> | undefined;
179
+ }): void {
180
+ createdSuggestions = [];
181
+ suggestionUpdates = [];
182
+
183
+ jest.spyOn(logger, "error").mockImplementation((): void => {
184
+ return undefined;
185
+ });
186
+ jest.spyOn(logger, "warn").mockImplementation((): void => {
187
+ return undefined;
188
+ });
189
+ jest.spyOn(logger, "debug").mockImplementation((): void => {
190
+ return undefined;
191
+ });
192
+ jest.spyOn(ProjectService, "findOneById").mockResolvedValue({
193
+ enableAi: true,
194
+ enableAutoRemediation: true,
195
+ enableAiCommandExecution: false,
196
+ } as unknown as Project);
197
+ /*
198
+ * The first read is the subject's existing suggestions; every later read
199
+ * is the in-flight check's (nothing holds the resource unless a test says
200
+ * otherwise).
201
+ */
202
+ suggestionFindBy = jest
203
+ .spyOn(AutoRemediationSuggestionService, "findBy")
204
+ .mockResolvedValueOnce(data.existing || [])
205
+ .mockResolvedValue([]);
206
+ jest
207
+ .spyOn(AutoRemediationSuggestionService, "create")
208
+ .mockImplementation(
209
+ async (args: unknown): Promise<AutoRemediationSuggestion> => {
210
+ const suggestion: AutoRemediationSuggestion = (
211
+ args as { data: AutoRemediationSuggestion }
212
+ ).data;
213
+ suggestion.id = SUGGESTION_ID;
214
+ createdSuggestions.push(suggestion);
215
+ return suggestion;
216
+ },
217
+ );
218
+ jest
219
+ .spyOn(AutoRemediationSuggestionService, "updateOneById")
220
+ .mockImplementation(async (args: unknown): Promise<never> => {
221
+ suggestionUpdates.push((args as { data: Record<string, unknown> }).data);
222
+ return undefined as never;
223
+ });
224
+ jest
225
+ .spyOn(KubernetesClusterAiAccessService, "getStatusesForSubject")
226
+ .mockResolvedValue(data.clusters || []);
227
+ resourceStatuses = jest
228
+ .spyOn(ResourceAiAccessService, "getStatusesForSubject")
229
+ .mockResolvedValue(data.resources || [readyResource()]);
230
+ enqueue = jest
231
+ .spyOn(AIInvestigationQueue, "enqueue")
232
+ .mockResolvedValue(AI_RUN_ID);
233
+ jest.spyOn(AutoRemediationRuleService, "findBy").mockResolvedValue([]);
234
+ jest
235
+ .spyOn(IncidentFeedService, "createIncidentFeedItem")
236
+ .mockResolvedValue(undefined as never);
237
+ jest
238
+ .spyOn(AlertFeedService, "createAlertFeedItem")
239
+ .mockResolvedValue(undefined as never);
240
+ // The breaker and the hold read no AI jobs on the resource unless a test says so.
241
+ jest.spyOn(RunnerJobService, "findBy").mockResolvedValue([]);
242
+ }
243
+
244
+ function feedMarkdowns(): Array<string> {
245
+ const feed: jest.SpyInstance =
246
+ IncidentFeedService.createIncidentFeedItem as unknown as jest.SpyInstance;
247
+ return feed.mock.calls.map((call: Array<unknown>): string => {
248
+ return (call[0] as { feedInfoInMarkdown: string }).feedInfoInMarkdown;
249
+ });
250
+ }
251
+
252
+ describe("AutoRemediationRuleEngineService resource-level remediation", () => {
253
+ afterEach(() => {
254
+ jest.restoreAllMocks();
255
+ });
256
+
257
+ it("starts a FullAuto command run for an Automatic resource with no rule and no cluster", async () => {
258
+ mockBaseline({});
259
+
260
+ await AutoRemediationRuleEngineService.applyRulesToIncident(fakeIncident());
261
+
262
+ expect(createdSuggestions).toHaveLength(1);
263
+ const suggestion: AutoRemediationSuggestion = createdSuggestions[0]!;
264
+ expect(suggestion.resourceType).toBe(AiResourceType.DockerHost);
265
+ expect(suggestion.resourceId?.toString()).toBe(RESOURCE_ID);
266
+ expect(suggestion.kubernetesClusterId).toBeUndefined();
267
+ expect(suggestion.autoRemediationRuleId).toBeUndefined();
268
+ expect(suggestion.status).toBe(AutoRemediationSuggestionStatus.Planning);
269
+ expect(suggestion.suggestionType).toBe(
270
+ AutoRemediationSuggestionType.CommandPlan,
271
+ );
272
+ expect(suggestion.executionMode).toBe(
273
+ AutoRemediationExecutionMode.FullAuto,
274
+ );
275
+ expect(suggestion.autoResolveOnRecovery).toBe(true);
276
+ expect(suggestion.verificationWindowMinutes).toBe(
277
+ DEFAULT_VERIFICATION_WINDOW_MINUTES,
278
+ );
279
+ expect(suggestion.ruleNameSnapshot).toBe(
280
+ 'AI remediation for Docker host "web-1"',
281
+ );
282
+ expect(suggestion.incidentId?.toString()).toBe(INCIDENT_ID.toString());
283
+
284
+ expect(resourceStatuses).toHaveBeenCalledWith({
285
+ projectId: PROJECT_ID,
286
+ incidentId: INCIDENT_ID,
287
+ alertId: undefined,
288
+ });
289
+ expect(enqueue).toHaveBeenCalledTimes(1);
290
+ expect(enqueue.mock.calls[0]![0]).toMatchObject({
291
+ projectId: PROJECT_ID,
292
+ subjectIncidentId: INCIDENT_ID,
293
+ subjectAutoRemediationSuggestionId: SUGGESTION_ID,
294
+ remediationRunType: AIRunType.RemediationExecution,
295
+ });
296
+ expect(suggestionUpdates).toContainEqual({ aiRunId: AI_RUN_ID });
297
+
298
+ const markdowns: Array<string> = feedMarkdowns();
299
+ expect(markdowns).toHaveLength(1);
300
+ expect(markdowns[0]).toContain(
301
+ 'OneUptime AI is fixing Docker host "web-1"',
302
+ );
303
+ expect(markdowns[0]).toContain("Automatic remediation is on");
304
+ expect(markdowns[0]).toContain(RESOURCE_SAFE_CHANGES_SUMMARY);
305
+ expect(markdowns[0]).toContain("Docker AI agent");
306
+ expect(markdowns[0]).toContain(
307
+ "If the hourly circuit breaker for this Docker host trips",
308
+ );
309
+ expect(markdowns[0]).not.toContain("kubectl");
310
+ });
311
+
312
+ it("does not need the project's AI command execution opt-in (the resource's own mode is the consent)", async () => {
313
+ mockBaseline({});
314
+
315
+ await AutoRemediationRuleEngineService.applyRulesToIncident(fakeIncident());
316
+
317
+ // The baseline project has enableAiCommandExecution: false.
318
+ expect(createdSuggestions).toHaveLength(1);
319
+ });
320
+
321
+ it("starts a Suggest run without auto-resolve for a RequireApproval resource, on an alert", async () => {
322
+ mockBaseline({
323
+ resources: [
324
+ readyResource({
325
+ aiRemediationMode: ResourceAiRemediationMode.RequireApproval,
326
+ resourceType: AiResourceType.Host,
327
+ }),
328
+ ],
329
+ });
330
+
331
+ await AutoRemediationRuleEngineService.applyRulesToAlert({
332
+ id: ALERT_ID,
333
+ _id: ALERT_ID.toString(),
334
+ projectId: PROJECT_ID,
335
+ title: "nginx is down",
336
+ } as unknown as Alert);
337
+
338
+ expect(createdSuggestions).toHaveLength(1);
339
+ expect(createdSuggestions[0]!.executionMode).toBe(
340
+ AutoRemediationExecutionMode.Suggest,
341
+ );
342
+ expect(createdSuggestions[0]!.autoResolveOnRecovery).toBe(false);
343
+ expect(createdSuggestions[0]!.alertId?.toString()).toBe(
344
+ ALERT_ID.toString(),
345
+ );
346
+ expect(createdSuggestions[0]!.ruleNameSnapshot).toBe(
347
+ 'AI remediation for host "web-1"',
348
+ );
349
+ expect(enqueue.mock.calls[0]![0]).toMatchObject({
350
+ subjectAlertId: ALERT_ID,
351
+ });
352
+
353
+ const alertFeed: jest.SpyInstance =
354
+ AlertFeedService.createAlertFeedItem as unknown as jest.SpyInstance;
355
+ expect(alertFeed).toHaveBeenCalledTimes(1);
356
+ expect(
357
+ (alertFeed.mock.calls[0]![0] as { feedInfoInMarkdown: string })
358
+ .feedInfoInMarkdown,
359
+ ).toContain("Nothing runs until you approve the plan");
360
+ });
361
+
362
+ it("starts a FullAuto run with auto-resolve for a BypassApproval resource and says approvals are bypassed — except what always asks", async () => {
363
+ mockBaseline({
364
+ resources: [
365
+ readyResource({
366
+ aiRemediationMode: ResourceAiRemediationMode.BypassApproval,
367
+ }),
368
+ ],
369
+ });
370
+
371
+ await AutoRemediationRuleEngineService.applyRulesToIncident(fakeIncident());
372
+
373
+ expect(createdSuggestions[0]!.executionMode).toBe(
374
+ AutoRemediationExecutionMode.FullAuto,
375
+ );
376
+ expect(createdSuggestions[0]!.autoResolveOnRecovery).toBe(true);
377
+
378
+ const markdown: string = feedMarkdowns()[0]!;
379
+ expect(markdown).toContain("Approvals are bypassed for this Docker host");
380
+ expect(markdown).toContain(RESOURCE_ALWAYS_ASKS_SUMMARY);
381
+ expect(markdown).toContain("destructive commands never run");
382
+ });
383
+
384
+ it("picks the FIRST eligible resource in ALL_AI_RESOURCE_TYPES order, and only one", async () => {
385
+ mockBaseline({
386
+ resources: [
387
+ readyResource({
388
+ resourceType: AiResourceType.Host,
389
+ resourceId: OTHER_RESOURCE_ID,
390
+ resourceName: "web-2",
391
+ }),
392
+ readyResource({
393
+ resourceType: AiResourceType.PodmanHost,
394
+ resourceName: "pod-1",
395
+ }),
396
+ ],
397
+ });
398
+
399
+ await AutoRemediationRuleEngineService.applyRulesToIncident(fakeIncident());
400
+
401
+ expect(createdSuggestions).toHaveLength(1);
402
+ expect(createdSuggestions[0]!.resourceType).toBe(AiResourceType.PodmanHost);
403
+ expect(createdSuggestions[0]!.ruleNameSnapshot).toBe(
404
+ 'AI remediation for Podman host "pod-1"',
405
+ );
406
+ expect(enqueue).toHaveBeenCalledTimes(1);
407
+ });
408
+
409
+ it.each([
410
+ [
411
+ "not remediation-ready",
412
+ { isRemediationReady: false } as Partial<ResourceAiAccessStatus>,
413
+ ],
414
+ [
415
+ "fixes off",
416
+ {
417
+ aiRemediationMode: ResourceAiRemediationMode.Disabled,
418
+ } as Partial<ResourceAiAccessStatus>,
419
+ ],
420
+ [
421
+ "an unknown type",
422
+ {
423
+ resourceType: "Printer" as AiResourceType,
424
+ } as Partial<ResourceAiAccessStatus>,
425
+ ],
426
+ ])(
427
+ "skips a resource that is %s and falls through to the next eligible one",
428
+ async (_label: string, overrides: Partial<ResourceAiAccessStatus>) => {
429
+ mockBaseline({
430
+ resources: [
431
+ readyResource({ ...overrides }),
432
+ readyResource({
433
+ resourceType: AiResourceType.Host,
434
+ resourceId: OTHER_RESOURCE_ID,
435
+ resourceName: "web-2",
436
+ }),
437
+ ],
438
+ });
439
+
440
+ await AutoRemediationRuleEngineService.applyRulesToIncident(
441
+ fakeIncident(),
442
+ );
443
+
444
+ expect(createdSuggestions).toHaveLength(1);
445
+ expect(createdSuggestions[0]!.resourceId?.toString()).toBe(
446
+ OTHER_RESOURCE_ID,
447
+ );
448
+ },
449
+ );
450
+
451
+ it("starts nothing when no linked resource is eligible", async () => {
452
+ mockBaseline({
453
+ resources: [readyResource({ isRemediationReady: false })],
454
+ });
455
+
456
+ await AutoRemediationRuleEngineService.applyRulesToIncident(fakeIncident());
457
+
458
+ expect(createdSuggestions).toHaveLength(0);
459
+ expect(enqueue).not.toHaveBeenCalled();
460
+ });
461
+
462
+ it("never runs when a cluster round started for the signal: the cluster lane is the signal's lane", async () => {
463
+ mockBaseline({ clusters: [readyCluster()] });
464
+
465
+ await AutoRemediationRuleEngineService.applyRulesToIncident(fakeIncident());
466
+
467
+ expect(createdSuggestions).toHaveLength(1);
468
+ expect(createdSuggestions[0]!.kubernetesClusterId?.toString()).toBe(
469
+ CLUSTER_ID,
470
+ );
471
+ expect(createdSuggestions[0]!.resourceId).toBeUndefined();
472
+ // The resource lane never even looked.
473
+ expect(resourceStatuses).not.toHaveBeenCalled();
474
+ });
475
+
476
+ it.each([
477
+ ["a cluster round", { kubernetesClusterId: new ObjectID(CLUSTER_ID) }],
478
+ [
479
+ "a resource round",
480
+ {
481
+ resourceType: AiResourceType.Host,
482
+ resourceId: new ObjectID(OTHER_RESOURCE_ID),
483
+ },
484
+ ],
485
+ ])(
486
+ "does not start a resource round when the signal already has %s",
487
+ async (_label: string, fields: Record<string, unknown>) => {
488
+ mockBaseline({
489
+ existing: [
490
+ {
491
+ id: OTHER_SUGGESTION_ID,
492
+ _id: OTHER_SUGGESTION_ID.toString(),
493
+ ...fields,
494
+ } as unknown as AutoRemediationSuggestion,
495
+ ],
496
+ });
497
+
498
+ await AutoRemediationRuleEngineService.applyRulesToIncident(
499
+ fakeIncident(),
500
+ );
501
+
502
+ expect(createdSuggestions).toHaveLength(0);
503
+ expect(resourceStatuses).not.toHaveBeenCalled();
504
+ },
505
+ );
506
+
507
+ it("selects the resource columns of the subject's existing suggestions", async () => {
508
+ mockBaseline({});
509
+
510
+ await AutoRemediationRuleEngineService.applyRulesToIncident(fakeIncident());
511
+
512
+ expect(suggestionFindBy.mock.calls[0]![0]).toMatchObject({
513
+ select: expect.objectContaining({
514
+ kubernetesClusterId: true,
515
+ resourceType: true,
516
+ resourceId: true,
517
+ }),
518
+ });
519
+ });
520
+
521
+ it("respects the per-subject suggestion cap", async () => {
522
+ const existing: Array<AutoRemediationSuggestion> = [];
523
+ for (let i: number = 0; i < MAX_SUGGESTIONS_PER_SUBJECT; i++) {
524
+ existing.push({
525
+ id: ObjectID.generate(),
526
+ _id: ObjectID.generate().toString(),
527
+ autoRemediationRuleId: ObjectID.generate(),
528
+ } as unknown as AutoRemediationSuggestion);
529
+ }
530
+ mockBaseline({ existing });
531
+
532
+ await AutoRemediationRuleEngineService.applyRulesToIncident(fakeIncident());
533
+
534
+ expect(createdSuggestions).toHaveLength(0);
535
+ expect(resourceStatuses).not.toHaveBeenCalled();
536
+ });
537
+
538
+ it("settles the suggestion as NoneApplicable when the run cannot be queued", async () => {
539
+ mockBaseline({});
540
+ enqueue.mockResolvedValue(null);
541
+
542
+ await AutoRemediationRuleEngineService.applyRulesToIncident(fakeIncident());
543
+
544
+ expect(createdSuggestions).toHaveLength(1);
545
+ expect(suggestionUpdates[0]).toMatchObject({
546
+ status: AutoRemediationSuggestionStatus.NoneApplicable,
547
+ });
548
+ expect(feedMarkdowns()).toHaveLength(0);
549
+ });
550
+
551
+ it("skips the lane quietly when the infrastructure access lookup fails, and still evaluates rules", async () => {
552
+ mockBaseline({});
553
+ resourceStatuses.mockRejectedValue(new Error("db down"));
554
+
555
+ await AutoRemediationRuleEngineService.applyRulesToIncident(fakeIncident());
556
+
557
+ expect(createdSuggestions).toHaveLength(0);
558
+ expect(AutoRemediationRuleService.findBy).toHaveBeenCalledTimes(1);
559
+ });
560
+
561
+ it("still evaluates rules when starting the resource round throws", async () => {
562
+ mockBaseline({});
563
+ jest
564
+ .spyOn(AutoRemediationSuggestionService, "create")
565
+ .mockRejectedValue(new Error("insert failed"));
566
+
567
+ await AutoRemediationRuleEngineService.applyRulesToIncident(fakeIncident());
568
+
569
+ expect(AutoRemediationRuleService.findBy).toHaveBeenCalledTimes(1);
570
+ });
571
+
572
+ it("asks first when another unattended round already holds the resource, and the feed says so", async () => {
573
+ mockBaseline({});
574
+ suggestionFindBy.mockReset();
575
+ suggestionFindBy
576
+ .mockResolvedValueOnce([])
577
+ .mockResolvedValueOnce([
578
+ {
579
+ id: OTHER_SUGGESTION_ID,
580
+ _id: OTHER_SUGGESTION_ID.toString(),
581
+ status: AutoRemediationSuggestionStatus.AutoExecuted,
582
+ verificationStatus: AutoRemediationVerificationStatus.Pending,
583
+ verificationDeadlineAt: OneUptimeDate.addRemoveMinutes(
584
+ OneUptimeDate.getCurrentDate(),
585
+ 10,
586
+ ),
587
+ createdAt: OneUptimeDate.getSomeMinutesAgo(5),
588
+ } as unknown as AutoRemediationSuggestion,
589
+ ])
590
+ .mockResolvedValue([]);
591
+
592
+ await AutoRemediationRuleEngineService.applyRulesToIncident(fakeIncident());
593
+
594
+ expect(createdSuggestions).toHaveLength(1);
595
+ expect(createdSuggestions[0]!.executionMode).toBe(
596
+ AutoRemediationExecutionMode.Suggest,
597
+ );
598
+ expect(createdSuggestions[0]!.autoResolveOnRecovery).toBe(false);
599
+ const markdown: string = feedMarkdowns()[0]!;
600
+ expect(markdown).toContain(
601
+ "This round asks first because another OneUptime AI round on this Docker host applied a fix that is still being verified",
602
+ );
603
+ });
604
+
605
+ it("asks first when the hold cannot be checked", async () => {
606
+ mockBaseline({});
607
+ suggestionFindBy.mockReset();
608
+ suggestionFindBy
609
+ .mockResolvedValueOnce([])
610
+ .mockRejectedValueOnce(new Error("db down"))
611
+ .mockResolvedValue([]);
612
+
613
+ await AutoRemediationRuleEngineService.applyRulesToIncident(fakeIncident());
614
+
615
+ expect(createdSuggestions[0]!.executionMode).toBe(
616
+ AutoRemediationExecutionMode.Suggest,
617
+ );
618
+ expect(feedMarkdowns()[0]).toContain(
619
+ "OneUptime AI could not confirm that no other AI round is changing this Docker host",
620
+ );
621
+ });
622
+ });
623
+
624
+ /*
625
+ * One AI fix lane per signal holds BOTH ways. A resource round started by
626
+ * an earlier pass (while no linked cluster was ready) keeps the signal in
627
+ * the resource lane: a later pass — a second investigation settling after
628
+ * the cluster came back — must not start a cluster round next to it, and
629
+ * the cluster lane's "ask again" must not either. A signal with no
630
+ * resource round is untouched (the Kubernetes lane behaves as before).
631
+ */
632
+ describe("AutoRemediationRuleEngineService one AI fix lane per signal — a resource round keeps the cluster lane out", () => {
633
+ afterEach(() => {
634
+ jest.restoreAllMocks();
635
+ });
636
+
637
+ function existingResourceRound(): AutoRemediationSuggestion {
638
+ return {
639
+ id: OTHER_SUGGESTION_ID,
640
+ _id: OTHER_SUGGESTION_ID.toString(),
641
+ resourceType: AiResourceType.DockerHost,
642
+ resourceId: new ObjectID(RESOURCE_ID),
643
+ incidentId: INCIDENT_ID,
644
+ } as unknown as AutoRemediationSuggestion;
645
+ }
646
+
647
+ it("a later rule pass starts no cluster round on a signal that already has a resource round, even when the cluster is ready now", async () => {
648
+ mockBaseline({
649
+ existing: [existingResourceRound()],
650
+ clusters: [readyCluster()],
651
+ });
652
+
653
+ await AutoRemediationRuleEngineService.applyRulesToIncident(fakeIncident());
654
+
655
+ expect(createdSuggestions).toHaveLength(0);
656
+ expect(enqueue).not.toHaveBeenCalled();
657
+ expect(
658
+ KubernetesClusterAiAccessService.getStatusesForSubject,
659
+ ).not.toHaveBeenCalled();
660
+ // Nor a second resource round.
661
+ expect(resourceStatuses).not.toHaveBeenCalled();
662
+ // Project rules are still evaluated.
663
+ expect(AutoRemediationRuleService.findBy).toHaveBeenCalledTimes(1);
664
+ });
665
+
666
+ it("negative control: a signal whose existing rounds are not resource rounds still gets its cluster round", async () => {
667
+ mockBaseline({
668
+ existing: [
669
+ {
670
+ id: OTHER_SUGGESTION_ID,
671
+ _id: OTHER_SUGGESTION_ID.toString(),
672
+ autoRemediationRuleId: ObjectID.generate(),
673
+ } as unknown as AutoRemediationSuggestion,
674
+ ],
675
+ clusters: [readyCluster()],
676
+ });
677
+
678
+ await AutoRemediationRuleEngineService.applyRulesToIncident(fakeIncident());
679
+
680
+ expect(createdSuggestions).toHaveLength(1);
681
+ expect(createdSuggestions[0]!.kubernetesClusterId?.toString()).toBe(
682
+ CLUSTER_ID,
683
+ );
684
+ });
685
+
686
+ describe("startFollowUpClusterRemediation", () => {
687
+ let getStatusForCluster: jest.SpyInstance;
688
+
689
+ beforeEach(() => {
690
+ mockBaseline({});
691
+ suggestionFindBy.mockReset();
692
+ suggestionFindBy.mockResolvedValue([]);
693
+ jest
694
+ .spyOn(AutoRemediationSuggestionService, "countBy")
695
+ .mockResolvedValue(new PositiveNumber(1));
696
+ getStatusForCluster = jest
697
+ .spyOn(KubernetesClusterAiAccessService, "getStatusForCluster")
698
+ .mockResolvedValue({
699
+ ...readyCluster(),
700
+ remediationMode: KubernetesAiRemediationMode.BypassApproval,
701
+ });
702
+ });
703
+
704
+ function followUpCluster(): Promise<boolean> {
705
+ return AutoRemediationRuleEngineService.startFollowUpClusterRemediation({
706
+ projectId: PROJECT_ID,
707
+ kubernetesClusterId: new ObjectID(CLUSTER_ID),
708
+ incidentId: INCIDENT_ID,
709
+ });
710
+ }
711
+
712
+ it("does not ask again on the cluster when the signal has a resource round", async () => {
713
+ suggestionFindBy.mockResolvedValueOnce([existingResourceRound()]);
714
+
715
+ expect(await followUpCluster()).toBe(false);
716
+
717
+ expect(createdSuggestions).toHaveLength(0);
718
+ expect(enqueue).not.toHaveBeenCalled();
719
+ expect(getStatusForCluster).not.toHaveBeenCalled();
720
+
721
+ // It looked for a resource round of THIS subject.
722
+ const query: Record<string, unknown> = (
723
+ suggestionFindBy.mock.calls[0]![0] as {
724
+ query: Record<string, unknown>;
725
+ }
726
+ ).query;
727
+ expect(query["incidentId"]).toEqual(INCIDENT_ID);
728
+ expect(
729
+ (query["resourceId"] as { getSql: (alias: string) => string }).getSql(
730
+ "resourceId",
731
+ ),
732
+ ).toBe("(resourceId IS NOT NULL)");
733
+ });
734
+
735
+ it("negative control: with no resource round the cluster follow-up starts as before", async () => {
736
+ expect(await followUpCluster()).toBe(true);
737
+
738
+ expect(createdSuggestions).toHaveLength(1);
739
+ expect(createdSuggestions[0]!.kubernetesClusterId?.toString()).toBe(
740
+ CLUSTER_ID,
741
+ );
742
+ expect(createdSuggestions[0]!.executionMode).toBe(
743
+ AutoRemediationExecutionMode.FullAuto,
744
+ );
745
+ });
746
+ });
747
+ });
748
+
749
+ describe("AutoRemediationRuleEngineService.startFollowUpResourceRemediation", () => {
750
+ let getStatusForResource: jest.SpyInstance;
751
+ let countBy: jest.SpyInstance;
752
+
753
+ beforeEach(() => {
754
+ mockBaseline({});
755
+ suggestionFindBy.mockReset();
756
+ suggestionFindBy.mockResolvedValue([]);
757
+ getStatusForResource = jest
758
+ .spyOn(ResourceAiAccessService, "getStatusForResource")
759
+ .mockResolvedValue(readyResource());
760
+ countBy = jest
761
+ .spyOn(AutoRemediationSuggestionService, "countBy")
762
+ .mockResolvedValue(new PositiveNumber(1));
763
+ });
764
+
765
+ afterEach(() => {
766
+ jest.restoreAllMocks();
767
+ });
768
+
769
+ function followUp(
770
+ overrides: Partial<{
771
+ forceSuggest: boolean;
772
+ forceSuggestReason: string;
773
+ incidentId: ObjectID | undefined;
774
+ alertId: ObjectID | undefined;
775
+ resourceType: AiResourceType;
776
+ }> = {},
777
+ ): Promise<boolean> {
778
+ return AutoRemediationRuleEngineService.startFollowUpResourceRemediation({
779
+ projectId: PROJECT_ID,
780
+ resourceType: AiResourceType.DockerHost,
781
+ resourceId: new ObjectID(RESOURCE_ID),
782
+ incidentId: INCIDENT_ID,
783
+ ...overrides,
784
+ });
785
+ }
786
+
787
+ it("starts a Suggest round even on an Automatic resource and labels it as round 2", async () => {
788
+ expect(await followUp()).toBe(true);
789
+
790
+ expect(createdSuggestions).toHaveLength(1);
791
+ expect(createdSuggestions[0]!.executionMode).toBe(
792
+ AutoRemediationExecutionMode.Suggest,
793
+ );
794
+ expect(createdSuggestions[0]!.autoResolveOnRecovery).toBe(false);
795
+ expect(createdSuggestions[0]!.ruleNameSnapshot).toBe(
796
+ 'AI remediation for Docker host "web-1" (round 2)',
797
+ );
798
+ expect(createdSuggestions[0]!.resourceId?.toString()).toBe(RESOURCE_ID);
799
+ expect(enqueue).toHaveBeenCalledTimes(1);
800
+ expect(feedMarkdowns()[0]).toContain(
801
+ 'OneUptime AI is composing another fix for Docker host "web-1"** (round 2)',
802
+ );
803
+
804
+ // The rounds are counted on this resource, for this subject.
805
+ expect(countBy.mock.calls[0]![0]).toMatchObject({
806
+ query: {
807
+ incidentId: INCIDENT_ID,
808
+ resourceType: AiResourceType.DockerHost,
809
+ resourceId: new ObjectID(RESOURCE_ID),
810
+ },
811
+ });
812
+ });
813
+
814
+ it("runs the follow-up round unattended again on a BypassApproval resource", async () => {
815
+ getStatusForResource.mockResolvedValue(
816
+ readyResource({
817
+ aiRemediationMode: ResourceAiRemediationMode.BypassApproval,
818
+ }),
819
+ );
820
+
821
+ expect(await followUp()).toBe(true);
822
+
823
+ expect(createdSuggestions[0]!.executionMode).toBe(
824
+ AutoRemediationExecutionMode.FullAuto,
825
+ );
826
+ expect(createdSuggestions[0]!.autoResolveOnRecovery).toBe(true);
827
+ expect(feedMarkdowns()[0]).toContain(
828
+ 'OneUptime AI is applying another fix on Docker host "web-1"** (round 2)',
829
+ );
830
+ });
831
+
832
+ it("asks first even on a BypassApproval resource when the previous rollback did not complete, and says why", async () => {
833
+ getStatusForResource.mockResolvedValue(
834
+ readyResource({
835
+ aiRemediationMode: ResourceAiRemediationMode.BypassApproval,
836
+ }),
837
+ );
838
+
839
+ expect(await followUp({ forceSuggest: true })).toBe(true);
840
+
841
+ expect(createdSuggestions[0]!.executionMode).toBe(
842
+ AutoRemediationExecutionMode.Suggest,
843
+ );
844
+ expect(feedMarkdowns()[0]).toContain(
845
+ "This round asks first because the previous fix's rollback did not complete, so its change may still be applied",
846
+ );
847
+ });
848
+
849
+ it(`stops at ${MAX_RESOURCE_REMEDIATION_ROUNDS_PER_SUBJECT} rounds per subject`, async () => {
850
+ countBy.mockResolvedValue(
851
+ new PositiveNumber(MAX_RESOURCE_REMEDIATION_ROUNDS_PER_SUBJECT),
852
+ );
853
+
854
+ expect(await followUp()).toBe(false);
855
+ expect(createdSuggestions).toHaveLength(0);
856
+ expect(getStatusForResource).not.toHaveBeenCalled();
857
+ });
858
+
859
+ it.each([
860
+ ["deleted", null],
861
+ [
862
+ "no longer remediation-ready",
863
+ readyResource({ isRemediationReady: false }),
864
+ ],
865
+ [
866
+ "fixes off",
867
+ readyResource({ aiRemediationMode: ResourceAiRemediationMode.Disabled }),
868
+ ],
869
+ ])(
870
+ "does not ask again when the resource is %s",
871
+ async (_label: string, status: ResourceAiAccessStatus | null) => {
872
+ getStatusForResource.mockResolvedValue(status);
873
+
874
+ expect(await followUp()).toBe(false);
875
+ expect(createdSuggestions).toHaveLength(0);
876
+ },
877
+ );
878
+
879
+ it("does not ask again when auto-remediation was turned off for the project", async () => {
880
+ jest.spyOn(ProjectService, "findOneById").mockResolvedValue({
881
+ enableAutoRemediation: false,
882
+ } as unknown as Project);
883
+
884
+ expect(await followUp()).toBe(false);
885
+ expect(createdSuggestions).toHaveLength(0);
886
+ });
887
+
888
+ it("needs a subject and a known resource type", async () => {
889
+ expect(await followUp({ incidentId: undefined })).toBe(false);
890
+ expect(await followUp({ resourceType: "Printer" as AiResourceType })).toBe(
891
+ false,
892
+ );
893
+ expect(createdSuggestions).toHaveLength(0);
894
+ });
895
+
896
+ it("never throws: a failed lookup is no follow-up", async () => {
897
+ getStatusForResource.mockRejectedValue(new Error("db down"));
898
+
899
+ expect(await followUp()).toBe(false);
900
+ });
901
+ });
902
+
903
+ describe("AutoRemediationRuleEngineService.getResourceBreakerState", () => {
904
+ let findBy: jest.SpyInstance;
905
+ let countBy: jest.SpyInstance;
906
+ let jobs: jest.SpyInstance;
907
+
908
+ beforeEach(() => {
909
+ findBy = jest
910
+ .spyOn(AutoRemediationSuggestionService, "findBy")
911
+ .mockResolvedValue([]);
912
+ countBy = jest
913
+ .spyOn(AutoRemediationSuggestionService, "countBy")
914
+ .mockResolvedValue(new PositiveNumber(0));
915
+ jobs = jest.spyOn(RunnerJobService, "findBy").mockResolvedValue([]);
916
+ });
917
+
918
+ afterEach(() => {
919
+ jest.restoreAllMocks();
920
+ });
921
+
922
+ function state(
923
+ forRound?: { suggestionId: ObjectID; createdAt?: Date } | undefined,
924
+ ): Promise<ResourceBreakerState> {
925
+ return AutoRemediationRuleEngineService.getResourceBreakerState({
926
+ resourceType: AiResourceType.Host,
927
+ resourceId: RESOURCE_ID,
928
+ projectId: PROJECT_ID,
929
+ forRound,
930
+ });
931
+ }
932
+
933
+ it("reads the resource's own rounds and inline jobs — never a cluster's", async () => {
934
+ const result: ResourceBreakerState = await state();
935
+
936
+ expect(result).toEqual({ autoExecutedInWindow: 0, hasHeadroom: true });
937
+
938
+ const inFlightQuery: Record<string, unknown> = (
939
+ findBy.mock.calls[0]![0] as { query: Record<string, unknown> }
940
+ ).query;
941
+ expect(inFlightQuery).toMatchObject({
942
+ projectId: PROJECT_ID,
943
+ resourceType: AiResourceType.Host,
944
+ resourceId: new ObjectID(RESOURCE_ID),
945
+ suggestionType: AutoRemediationSuggestionType.CommandPlan,
946
+ status: AutoRemediationSuggestionStatus.Planning,
947
+ executionMode: AutoRemediationExecutionMode.FullAuto,
948
+ });
949
+ expect(inFlightQuery["kubernetesClusterId"]).toBeUndefined();
950
+
951
+ expect(
952
+ (countBy.mock.calls[0]![0] as { query: Record<string, unknown> }).query,
953
+ ).toMatchObject({
954
+ resourceType: AiResourceType.Host,
955
+ resourceId: new ObjectID(RESOURCE_ID),
956
+ status: AutoRemediationSuggestionStatus.AutoExecuted,
957
+ });
958
+
959
+ const jobQuery: Record<string, unknown> = (
960
+ jobs.mock.calls[0]![0] as { query: Record<string, unknown> }
961
+ ).query;
962
+ expect(jobQuery).toMatchObject({
963
+ resourceType: AiResourceType.Host,
964
+ resourceId: new ObjectID(RESOURCE_ID),
965
+ origin: RunnerJobOrigin.AiRemediation,
966
+ });
967
+ expect(jobQuery["kubernetesClusterId"]).toBeUndefined();
968
+ expect(String(JSON.stringify(jobQuery["stepId"]))).toContain(
969
+ INLINE_COMMAND_STEP_ID_PREFIX,
970
+ );
971
+ });
972
+
973
+ it("trips at the limit on settled rounds", async () => {
974
+ countBy.mockResolvedValue(
975
+ new PositiveNumber(MAX_AUTO_EXECUTIONS_PER_RULE_PER_HOUR),
976
+ );
977
+
978
+ expect(await state()).toEqual({
979
+ autoExecutedInWindow: MAX_AUTO_EXECUTIONS_PER_RULE_PER_HOUR,
980
+ hasHeadroom: false,
981
+ });
982
+ });
983
+
984
+ it("counts distinct runs with inline jobs, and in-flight rounds created before the asking one — never itself", async () => {
985
+ const earlier: Date = OneUptimeDate.getSomeMinutesAgo(10);
986
+ const later: Date = OneUptimeDate.getSomeMinutesAgo(1);
987
+ const self: ObjectID = new ObjectID("abababab-abab-4bab-8bab-abababababab");
988
+ const runA: ObjectID = ObjectID.generate();
989
+ const runB: ObjectID = ObjectID.generate();
990
+
991
+ jobs.mockResolvedValue([
992
+ { id: ObjectID.generate(), autoRemediationSuggestionId: runA },
993
+ { id: ObjectID.generate(), autoRemediationSuggestionId: runA },
994
+ { id: ObjectID.generate(), autoRemediationSuggestionId: self },
995
+ ] as unknown as Array<RunnerJob>);
996
+ findBy.mockResolvedValue([
997
+ { id: runB, createdAt: earlier },
998
+ // Created after the asking round: not counted against it.
999
+ { id: ObjectID.generate(), createdAt: later },
1000
+ ] as unknown as Array<AutoRemediationSuggestion>);
1001
+
1002
+ const result: ResourceBreakerState = await state({
1003
+ suggestionId: self,
1004
+ createdAt: OneUptimeDate.getSomeMinutesAgo(5),
1005
+ });
1006
+
1007
+ expect(result.autoExecutedInWindow).toBe(2);
1008
+ expect(result.hasHeadroom).toBe(true);
1009
+ });
1010
+
1011
+ it("throws on a failed read, so callers fail safe", async () => {
1012
+ countBy.mockRejectedValue(new Error("db down"));
1013
+
1014
+ await expect(state()).rejects.toThrow("db down");
1015
+ });
1016
+ });
1017
+
1018
+ describe("AutoRemediationRuleEngineService.findRoundHoldingResource", () => {
1019
+ let findBy: jest.SpyInstance;
1020
+ let jobs: jest.SpyInstance;
1021
+
1022
+ beforeEach(() => {
1023
+ findBy = jest
1024
+ .spyOn(AutoRemediationSuggestionService, "findBy")
1025
+ .mockResolvedValue([]);
1026
+ jobs = jest.spyOn(RunnerJobService, "findBy").mockResolvedValue([]);
1027
+ });
1028
+
1029
+ afterEach(() => {
1030
+ jest.restoreAllMocks();
1031
+ });
1032
+
1033
+ function hold(
1034
+ overrides: Partial<{
1035
+ anyOrder: boolean;
1036
+ forRound: { suggestionId: ObjectID; createdAt?: Date };
1037
+ subject: { incidentId?: ObjectID; alertId?: ObjectID };
1038
+ }> = {},
1039
+ ): Promise<ResourceRoundHold | null> {
1040
+ return AutoRemediationRuleEngineService.findRoundHoldingResource({
1041
+ projectId: PROJECT_ID,
1042
+ resourceType: AiResourceType.DockerHost,
1043
+ resourceId: RESOURCE_ID,
1044
+ ...overrides,
1045
+ });
1046
+ }
1047
+
1048
+ it("nothing holds a resource with no other round and no jobs", async () => {
1049
+ expect(await hold()).toBeNull();
1050
+
1051
+ expect(
1052
+ (findBy.mock.calls[0]![0] as { query: Record<string, unknown> }).query,
1053
+ ).toMatchObject({
1054
+ projectId: PROJECT_ID,
1055
+ resourceType: AiResourceType.DockerHost,
1056
+ resourceId: new ObjectID(RESOURCE_ID),
1057
+ });
1058
+ expect(
1059
+ (jobs.mock.calls[0]![0] as { query: Record<string, unknown> }).query,
1060
+ ).toMatchObject({
1061
+ resourceType: AiResourceType.DockerHost,
1062
+ resourceId: new ObjectID(RESOURCE_ID),
1063
+ origin: RunnerJobOrigin.AiRemediation,
1064
+ });
1065
+ });
1066
+
1067
+ it("a fix still being verified holds it", async () => {
1068
+ findBy.mockResolvedValueOnce([
1069
+ {
1070
+ id: OTHER_SUGGESTION_ID,
1071
+ status: AutoRemediationSuggestionStatus.Approved,
1072
+ verificationStatus: AutoRemediationVerificationStatus.Pending,
1073
+ verificationDeadlineAt: OneUptimeDate.addRemoveMinutes(
1074
+ OneUptimeDate.getCurrentDate(),
1075
+ 5,
1076
+ ),
1077
+ ruleNameSnapshot: 'AI remediation for Docker host "web-1"',
1078
+ },
1079
+ ] as unknown as Array<AutoRemediationSuggestion>);
1080
+
1081
+ expect(await hold()).toMatchObject({
1082
+ suggestionId: OTHER_SUGGESTION_ID.toString(),
1083
+ description: "applied a fix that is still being verified",
1084
+ ruleNameSnapshot: 'AI remediation for Docker host "web-1"',
1085
+ });
1086
+ });
1087
+
1088
+ it("an unattended round still running holds it only when it was created first, unless anyOrder", async () => {
1089
+ const running: AutoRemediationSuggestion = {
1090
+ id: OTHER_SUGGESTION_ID,
1091
+ status: AutoRemediationSuggestionStatus.Planning,
1092
+ executionMode: AutoRemediationExecutionMode.FullAuto,
1093
+ createdAt: OneUptimeDate.getSomeMinutesAgo(1),
1094
+ } as unknown as AutoRemediationSuggestion;
1095
+
1096
+ findBy.mockResolvedValue([running]);
1097
+
1098
+ const olderRound: { suggestionId: ObjectID; createdAt: Date } = {
1099
+ suggestionId: SUGGESTION_ID,
1100
+ createdAt: OneUptimeDate.getSomeMinutesAgo(5),
1101
+ };
1102
+
1103
+ expect(await hold({ forRound: olderRound })).toBeNull();
1104
+ expect(await hold({ forRound: olderRound, anyOrder: true })).toMatchObject({
1105
+ description: "is still running unattended",
1106
+ });
1107
+ });
1108
+
1109
+ it("the asking round never holds itself", async () => {
1110
+ findBy.mockResolvedValue([
1111
+ {
1112
+ id: SUGGESTION_ID,
1113
+ status: AutoRemediationSuggestionStatus.Planning,
1114
+ executionMode: AutoRemediationExecutionMode.FullAuto,
1115
+ createdAt: OneUptimeDate.getSomeMinutesAgo(1),
1116
+ },
1117
+ ] as unknown as Array<AutoRemediationSuggestion>);
1118
+
1119
+ expect(
1120
+ await hold({ forRound: { suggestionId: SUGGESTION_ID }, anyOrder: true }),
1121
+ ).toBeNull();
1122
+ });
1123
+
1124
+ it("a run that already changed the resource (a forward job there) holds it while it runs", async () => {
1125
+ findBy.mockResolvedValueOnce([]).mockResolvedValueOnce([
1126
+ {
1127
+ id: OTHER_SUGGESTION_ID,
1128
+ status: AutoRemediationSuggestionStatus.Planning,
1129
+ executionMode: AutoRemediationExecutionMode.Suggest,
1130
+ createdAt: OneUptimeDate.getSomeMinutesAgo(1),
1131
+ },
1132
+ ] as unknown as Array<AutoRemediationSuggestion>);
1133
+ jobs.mockResolvedValue([
1134
+ {
1135
+ id: ObjectID.generate(),
1136
+ autoRemediationSuggestionId: OTHER_SUGGESTION_ID,
1137
+ stepId: `${INLINE_COMMAND_STEP_ID_PREFIX}1`,
1138
+ },
1139
+ ] as unknown as Array<RunnerJob>);
1140
+
1141
+ expect(await hold()).toMatchObject({
1142
+ suggestionId: OTHER_SUGGESTION_ID.toString(),
1143
+ description: "is still changing it",
1144
+ });
1145
+ });
1146
+
1147
+ it("a rollback job does not make its run hold the resource", async () => {
1148
+ jobs.mockResolvedValue([
1149
+ {
1150
+ id: ObjectID.generate(),
1151
+ autoRemediationSuggestionId: OTHER_SUGGESTION_ID,
1152
+ stepId: "ai-rollback-1",
1153
+ },
1154
+ ] as unknown as Array<RunnerJob>);
1155
+
1156
+ expect(await hold()).toBeNull();
1157
+ // No second suggestion read was needed.
1158
+ expect(findBy).toHaveBeenCalledTimes(1);
1159
+ });
1160
+
1161
+ it("a round of the same signal still composing holds it for a subject-aware caller", async () => {
1162
+ findBy.mockResolvedValue([
1163
+ {
1164
+ id: OTHER_SUGGESTION_ID,
1165
+ status: AutoRemediationSuggestionStatus.Planning,
1166
+ executionMode: AutoRemediationExecutionMode.Suggest,
1167
+ incidentId: INCIDENT_ID,
1168
+ createdAt: OneUptimeDate.getSomeMinutesAgo(1),
1169
+ },
1170
+ ] as unknown as Array<AutoRemediationSuggestion>);
1171
+
1172
+ expect(await hold({ subject: { incidentId: INCIDENT_ID } })).toMatchObject({
1173
+ description: "is still working on this same signal",
1174
+ isSameSubject: true,
1175
+ });
1176
+ });
1177
+ });
1178
+
1179
+ describe("resource round names and the breaker lock", () => {
1180
+ it("names a round after its resource, with the round number from round 2", () => {
1181
+ expect(
1182
+ getResourceRoundNameSnapshot(AiResourceType.DatabaseServer, "orders", 1),
1183
+ ).toBe('AI remediation for database server "orders"');
1184
+ expect(
1185
+ getResourceRoundNameSnapshot(AiResourceType.ProxmoxCluster, "pve", 2),
1186
+ ).toBe('AI remediation for Proxmox cluster "pve" (round 2)');
1187
+ });
1188
+
1189
+ it("reads a round's number back from its name — never from the resource's own name", () => {
1190
+ for (const round of [1, 2, 3]) {
1191
+ expect(
1192
+ parseResourceRoundNumber(
1193
+ getResourceRoundNameSnapshot(AiResourceType.DockerHost, "web", round),
1194
+ ),
1195
+ ).toBe(round);
1196
+ expect(
1197
+ parseResourceRoundNumber(
1198
+ getResourceRoundNameSnapshot(
1199
+ AiResourceType.Host,
1200
+ 'web" (round 7)',
1201
+ round,
1202
+ ),
1203
+ ),
1204
+ ).toBe(round);
1205
+ }
1206
+ expect(
1207
+ parseResourceRoundNumber(
1208
+ getResourceRoundNameSnapshot(AiResourceType.Host, "web (round 7)", 1),
1209
+ ),
1210
+ ).toBe(1);
1211
+ expect(parseResourceRoundNumber(undefined)).toBe(1);
1212
+ expect(parseResourceRoundNumber("")).toBe(1);
1213
+ });
1214
+
1215
+ it("Automatic runs only round 1 unattended; Bypass approval every round; the rest never", () => {
1216
+ expect(
1217
+ doesResourceModeRunRoundUnattended(
1218
+ ResourceAiRemediationMode.Automatic,
1219
+ 1,
1220
+ ),
1221
+ ).toBe(true);
1222
+ expect(
1223
+ doesResourceModeRunRoundUnattended(
1224
+ ResourceAiRemediationMode.Automatic,
1225
+ 2,
1226
+ ),
1227
+ ).toBe(false);
1228
+ for (const round of [1, 2]) {
1229
+ expect(
1230
+ doesResourceModeRunRoundUnattended(
1231
+ ResourceAiRemediationMode.BypassApproval,
1232
+ round,
1233
+ ),
1234
+ ).toBe(true);
1235
+ expect(
1236
+ doesResourceModeRunRoundUnattended(
1237
+ ResourceAiRemediationMode.RequireApproval,
1238
+ round,
1239
+ ),
1240
+ ).toBe(false);
1241
+ expect(
1242
+ doesResourceModeRunRoundUnattended(
1243
+ ResourceAiRemediationMode.Disabled,
1244
+ round,
1245
+ ),
1246
+ ).toBe(false);
1247
+ }
1248
+ });
1249
+
1250
+ it("a resource round's name is never read as a deleted cluster round's", () => {
1251
+ for (const type of [
1252
+ AiResourceType.DockerSwarmCluster,
1253
+ AiResourceType.ProxmoxCluster,
1254
+ AiResourceType.CephCluster,
1255
+ AiResourceType.Host,
1256
+ ]) {
1257
+ expect(
1258
+ parseClusterRoundNameSnapshot(
1259
+ getResourceRoundNameSnapshot(type, "x", 2),
1260
+ ),
1261
+ ).toBeNull();
1262
+ }
1263
+ });
1264
+
1265
+ it("the breaker lock has its own namespace, keyed by type and (lowercased) id", () => {
1266
+ expect(RESOURCE_BREAKER_LOCK_NAMESPACE).toBe(
1267
+ "AutoRemediationResourceBreaker",
1268
+ );
1269
+ expect(RESOURCE_BREAKER_LOCK_NAMESPACE).not.toBe(
1270
+ "AutoRemediationClusterBreaker",
1271
+ );
1272
+ expect(
1273
+ getResourceBreakerLockKey(
1274
+ AiResourceType.Host,
1275
+ "ABCDEFAB-3333-4333-8333-333333333333",
1276
+ ),
1277
+ ).toBe("Host:abcdefab-3333-4333-8333-333333333333");
1278
+ });
1279
+ });