@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,1373 @@
1
+ import ResourceAiAccessService, {
2
+ LoadedResourceAiAccessTarget,
3
+ MAX_RESOURCES_PER_SUBJECT,
4
+ ResourceAiAccessProjectGates,
5
+ ResourceAiAccessRef,
6
+ ResourceAiAccessRow,
7
+ describeResourceNoun,
8
+ getResourceAiAgentInstallNextStep,
9
+ getResourceAiAgentPage,
10
+ normalizeResourceAllowlist,
11
+ } from "../../../Server/Services/ResourceAiAccessService";
12
+ import AlertService from "../../../Server/Services/AlertService";
13
+ import CephClusterService from "../../../Server/Services/CephClusterService";
14
+ import DatabaseServerService from "../../../Server/Services/DatabaseServerService";
15
+ import DockerHostService from "../../../Server/Services/DockerHostService";
16
+ import HostService from "../../../Server/Services/HostService";
17
+ import IncidentService from "../../../Server/Services/IncidentService";
18
+ import KubernetesClusterAiAccessService from "../../../Server/Services/KubernetesClusterAiAccessService";
19
+ import MonitorService from "../../../Server/Services/MonitorService";
20
+ import PodmanHostService from "../../../Server/Services/PodmanHostService";
21
+ import ResourceAiAgentService from "../../../Server/Services/ResourceAiAgentService";
22
+ import MonitorResourceContextUtil from "../../../Server/Utils/Monitor/MonitorResourceContext";
23
+ import { SeriesResolvedResourceIds } from "../../../Server/Utils/Monitor/SeriesResourceLinker";
24
+ import logger from "../../../Server/Utils/Logger";
25
+ import ResourceAiAgent from "../../../Models/DatabaseModels/ResourceAiAgent";
26
+ import OneUptimeDate from "../../../Types/Date";
27
+ import ObjectID from "../../../Types/ObjectID";
28
+ import AiResourceType, {
29
+ ALL_AI_RESOURCE_TYPES,
30
+ } from "../../../Types/ResourceAiAgent/AiResourceType";
31
+ import {
32
+ ResourceAiAccessGap,
33
+ ResourceAiAccessGapCode,
34
+ ResourceAiAccessStatus,
35
+ ResourceAiRemediationMode,
36
+ } from "../../../Types/ResourceAiAgent/ResourceAiAccess";
37
+ import { afterEach, beforeEach, describe, expect, it } from "@jest/globals";
38
+
39
+ /*
40
+ * Contract under test — ResourceAiAccessService, the resource-agnostic
41
+ * sibling of KubernetesClusterAiAccessService:
42
+ *
43
+ * - the status of a resource is computed from CURRENT configuration: its
44
+ * agent (connected, online, reaching the resource, writes allowed), its
45
+ * own switches and the project-wide gates, each missing piece a gap with
46
+ * a next step, and readiness is exactly "enabled and no blocking gap";
47
+ * - the resources an incident or alert offers an investigation are its
48
+ * linked resources (all eight relations), in ALL_AI_RESOURCE_TYPES order,
49
+ * capped at 10, with the monitors' step configuration resolved again only
50
+ * when none is linked; archived resources are left out;
51
+ * - the enqueue chokepoint's target is the resource's ONLINE agent;
52
+ * - a command's outcome is recorded on the resource row without the
53
+ * model's hooks, best effort.
54
+ */
55
+
56
+ const PROJECT_ID: ObjectID = new ObjectID(
57
+ "11111111-1111-4111-8111-111111111111",
58
+ );
59
+ const INCIDENT_ID: ObjectID = new ObjectID(
60
+ "22222222-2222-4222-8222-222222222222",
61
+ );
62
+ const ALERT_ID: ObjectID = new ObjectID("33333333-3333-4333-8333-333333333333");
63
+ const DOCKER_ID: ObjectID = new ObjectID(
64
+ "44444444-4444-4444-8444-444444444444",
65
+ );
66
+ const HOST_ID: ObjectID = new ObjectID("55555555-5555-4555-8555-555555555555");
67
+ const DB_ID: ObjectID = new ObjectID("66666666-6666-4666-8666-666666666666");
68
+ const CEPH_ID: ObjectID = new ObjectID("77777777-7777-4777-8777-777777777777");
69
+ const AGENT_ID: ObjectID = new ObjectID("88888888-8888-4888-8888-888888888888");
70
+ const MONITOR_ID: ObjectID = new ObjectID(
71
+ "99999999-9999-4999-8999-999999999999",
72
+ );
73
+
74
+ const READY_GATES: ResourceAiAccessProjectGates = {
75
+ isAiEnabled: true,
76
+ isAutoRemediationEnabled: true,
77
+ isAiCommandExecutionEnabled: false,
78
+ hasLlmProvider: true,
79
+ aiBalanceBlocker: null,
80
+ };
81
+
82
+ function row(
83
+ overrides: Partial<ResourceAiAccessRow> = {},
84
+ ): ResourceAiAccessRow {
85
+ return {
86
+ resourceType: AiResourceType.DockerHost,
87
+ id: DOCKER_ID,
88
+ projectId: PROJECT_ID,
89
+ name: "web-1",
90
+ identifier: "web-1",
91
+ isAiInvestigationEnabled: true,
92
+ aiRemediationMode: ResourceAiRemediationMode.RequireApproval,
93
+ aiCommandAllowlist: [],
94
+ isArchived: false,
95
+ ...overrides,
96
+ };
97
+ }
98
+
99
+ function agentRow(
100
+ overrides: Record<string, unknown> = {},
101
+ postureOverrides: Record<string, unknown> = {},
102
+ ): ResourceAiAgent {
103
+ return {
104
+ id: AGENT_ID,
105
+ _id: AGENT_ID.toString(),
106
+ projectId: PROJECT_ID,
107
+ resourceType: AiResourceType.DockerHost,
108
+ resourceId: DOCKER_ID,
109
+ resourceIdentifier: "web-1",
110
+ agentVersion: "1.2.3",
111
+ connectionStatus: "connected",
112
+ lastAliveAt: OneUptimeDate.getCurrentDate(),
113
+ posture: {
114
+ resourceType: AiResourceType.DockerHost,
115
+ resourceIdentifier: "web-1",
116
+ allowWrites: true,
117
+ writeTargets: [],
118
+ protectedTargets: [],
119
+ reachable: true,
120
+ ...postureOverrides,
121
+ },
122
+ ...overrides,
123
+ } as unknown as ResourceAiAgent;
124
+ }
125
+
126
+ function codes(status: ResourceAiAccessStatus): Array<ResourceAiAccessGapCode> {
127
+ return status.gaps.map(
128
+ (gap: ResourceAiAccessGap): ResourceAiAccessGapCode => {
129
+ return gap.code;
130
+ },
131
+ );
132
+ }
133
+
134
+ function gap(
135
+ status: ResourceAiAccessStatus,
136
+ code: ResourceAiAccessGapCode,
137
+ ): ResourceAiAccessGap {
138
+ const found: ResourceAiAccessGap | undefined = status.gaps.find(
139
+ (candidate: ResourceAiAccessGap): boolean => {
140
+ return candidate.code === code;
141
+ },
142
+ );
143
+
144
+ if (!found) {
145
+ throw new Error(`no ${code} gap in ${JSON.stringify(codes(status))}`);
146
+ }
147
+
148
+ return found;
149
+ }
150
+
151
+ function build(
152
+ resource: ResourceAiAccessRow,
153
+ agent: ResourceAiAgent | null,
154
+ gates: ResourceAiAccessProjectGates = READY_GATES,
155
+ ): ResourceAiAccessStatus {
156
+ return ResourceAiAccessService.buildStatus({
157
+ resource,
158
+ agentRow: agent,
159
+ gates,
160
+ });
161
+ }
162
+
163
+ describe("ResourceAiAccessService.buildStatus", () => {
164
+ it("is ready for both with an online, reaching, writable agent and everything on", () => {
165
+ const status: ResourceAiAccessStatus = build(row(), agentRow());
166
+
167
+ expect(status.gaps).toEqual([]);
168
+ expect(status.isInvestigationReady).toBe(true);
169
+ expect(status.isRemediationReady).toBe(true);
170
+ expect(status.resourceType).toBe(AiResourceType.DockerHost);
171
+ expect(status.resourceId).toBe(DOCKER_ID.toString());
172
+ expect(status.resourceName).toBe("web-1");
173
+ expect(status.agent?.agentId).toBe(AGENT_ID.toString());
174
+ expect(status.agent?.isOnline).toBe(true);
175
+ expect(status.agent?.posture?.allowWrites).toBe(true);
176
+ });
177
+
178
+ it("never exposes the agent's key hash", () => {
179
+ const status: ResourceAiAccessStatus = build(
180
+ row(),
181
+ agentRow({ keyHash: "a".repeat(64) }),
182
+ );
183
+
184
+ expect(JSON.stringify(status)).not.toContain("a".repeat(64));
185
+ });
186
+
187
+ it("blocks both when no agent ever connected, and says what to install for THIS resource", () => {
188
+ const status: ResourceAiAccessStatus = build(row(), null);
189
+ const notConnected: ResourceAiAccessGap = gap(
190
+ status,
191
+ "ai_agent_not_connected",
192
+ );
193
+
194
+ expect(status.agent).toBeNull();
195
+ expect(status.isInvestigationReady).toBe(false);
196
+ expect(status.isRemediationReady).toBe(false);
197
+ expect(notConnected.title).toBe("No Docker AI agent is connected");
198
+ expect(notConnected.blocksInvestigation).toBe(true);
199
+ expect(notConnected.blocksRemediation).toBe(true);
200
+ expect(notConnected.nextStep).toContain("oneuptime/resource-ai-agent");
201
+ expect(notConnected.nextStep).toContain(
202
+ "ONEUPTIME_AI_AGENT_RESOURCE_TYPE=docker",
203
+ );
204
+ expect(notConnected.nextStep).toContain("DOCKER_HOST_NAME=web-1");
205
+ expect(notConnected.nextStep).toContain(
206
+ "the Docker host's AI agent page (AI → AI agent)",
207
+ );
208
+ // No agent, so no "read-only agent" gap on top.
209
+ expect(codes(status)).not.toContain("remediation_write_access_missing");
210
+ });
211
+
212
+ it("names a database server's install by its id", () => {
213
+ const status: ResourceAiAccessStatus = build(
214
+ row({
215
+ resourceType: AiResourceType.DatabaseServer,
216
+ id: DB_ID,
217
+ name: "orders",
218
+ identifier: "postgresql|db:5432",
219
+ }),
220
+ null,
221
+ );
222
+
223
+ expect(gap(status, "ai_agent_not_connected").nextStep).toContain(
224
+ `DATABASE_SERVER_ID=${DB_ID.toString()}`,
225
+ );
226
+ expect(gap(status, "ai_agent_not_connected").nextStep).toContain(
227
+ "ONEUPTIME_AI_AGENT_RESOURCE_TYPE=database",
228
+ );
229
+ });
230
+
231
+ it("blocks both when the agent is offline", () => {
232
+ const status: ResourceAiAccessStatus = build(
233
+ row(),
234
+ agentRow({
235
+ lastAliveAt: OneUptimeDate.addRemoveMinutes(
236
+ OneUptimeDate.getCurrentDate(),
237
+ -30,
238
+ ),
239
+ }),
240
+ );
241
+
242
+ expect(status.agent?.isOnline).toBe(false);
243
+ expect(gap(status, "ai_agent_offline").title).toBe(
244
+ "The Docker AI agent is offline",
245
+ );
246
+ expect(status.isInvestigationReady).toBe(false);
247
+ expect(status.isRemediationReady).toBe(false);
248
+ });
249
+
250
+ it("says when an offline agent signed off", () => {
251
+ const status: ResourceAiAccessStatus = build(
252
+ row(),
253
+ agentRow({ connectionStatus: "disconnected" }),
254
+ );
255
+
256
+ expect(gap(status, "ai_agent_offline").nextStep).toContain(
257
+ "and then signed off",
258
+ );
259
+ });
260
+
261
+ it("blocks both when the agent cannot reach its resource, with its error and the hint", () => {
262
+ const status: ResourceAiAccessStatus = build(
263
+ row(),
264
+ agentRow({}, { reachable: false, reachError: "socket not found" }),
265
+ );
266
+ const unreachable: ResourceAiAccessGap = gap(
267
+ status,
268
+ "ai_agent_unreachable_resource",
269
+ );
270
+
271
+ expect(unreachable.title).toBe(
272
+ "The Docker AI agent cannot reach this Docker host",
273
+ );
274
+ expect(unreachable.nextStep).toContain("socket not found");
275
+ expect(unreachable.nextStep).toContain("/var/run/docker.sock");
276
+ expect(status.isInvestigationReady).toBe(false);
277
+ });
278
+
279
+ it.each<[string, ResourceAiAgent]>([
280
+ ["no posture", agentRow({ posture: null })],
281
+ ["an unreadable posture", agentRow({ posture: "nope" })],
282
+ ["another type's posture", agentRow({}, { resourceType: "Host" })],
283
+ ])(
284
+ "does not trust an agent with %s",
285
+ (_label: string, agent: ResourceAiAgent) => {
286
+ const status: ResourceAiAccessStatus = build(row(), agent);
287
+
288
+ expect(codes(status)).toContain("ai_agent_unreachable_resource");
289
+ expect(status.isInvestigationReady).toBe(false);
290
+ },
291
+ );
292
+
293
+ it("blocks only investigation when investigation is off", () => {
294
+ const status: ResourceAiAccessStatus = build(
295
+ row({ isAiInvestigationEnabled: false }),
296
+ agentRow(),
297
+ );
298
+ const disabled: ResourceAiAccessGap = gap(status, "investigation_disabled");
299
+
300
+ expect(disabled.blocksInvestigation).toBe(true);
301
+ expect(disabled.blocksRemediation).toBe(false);
302
+ expect(status.isInvestigationReady).toBe(false);
303
+ expect(status.isRemediationReady).toBe(true);
304
+ });
305
+
306
+ it("blocks only remediation when fixes are off, and never asks for writes then", () => {
307
+ const status: ResourceAiAccessStatus = build(
308
+ row({ aiRemediationMode: ResourceAiRemediationMode.Disabled }),
309
+ agentRow({}, { allowWrites: false }),
310
+ );
311
+
312
+ expect(codes(status)).toEqual(["remediation_disabled"]);
313
+ expect(status.isInvestigationReady).toBe(true);
314
+ expect(status.isRemediationReady).toBe(false);
315
+ });
316
+
317
+ it("blocks remediation on a read-only agent, naming the setting", () => {
318
+ const status: ResourceAiAccessStatus = build(
319
+ row({ aiRemediationMode: ResourceAiRemediationMode.Automatic }),
320
+ agentRow({}, { allowWrites: false }),
321
+ );
322
+ const readOnly: ResourceAiAccessGap = gap(
323
+ status,
324
+ "remediation_write_access_missing",
325
+ );
326
+
327
+ expect(readOnly.nextStep).toContain("ONEUPTIME_AI_ALLOW_WRITES=true");
328
+ expect(readOnly.nextStep).toContain("ONEUPTIME_AI_WRITE_TARGETS");
329
+ expect(readOnly.blocksInvestigation).toBe(false);
330
+ expect(status.isInvestigationReady).toBe(true);
331
+ expect(status.isRemediationReady).toBe(false);
332
+ });
333
+
334
+ it.each<
335
+ [
336
+ string,
337
+ Partial<ResourceAiAccessProjectGates>,
338
+ ResourceAiAccessGapCode,
339
+ boolean,
340
+ ]
341
+ >([
342
+ ["AI off", { isAiEnabled: false }, "ai_disabled_for_project", true],
343
+ ["no provider", { hasLlmProvider: false }, "llm_provider_missing", true],
344
+ [
345
+ "no credits",
346
+ { aiBalanceBlocker: "out of credits" },
347
+ "ai_balance_insufficient",
348
+ true,
349
+ ],
350
+ [
351
+ "auto-remediation off",
352
+ { isAutoRemediationEnabled: false },
353
+ "auto_remediation_disabled_for_project",
354
+ false,
355
+ ],
356
+ ])(
357
+ "folds in the project gate: %s",
358
+ (
359
+ _label: string,
360
+ gates: Partial<ResourceAiAccessProjectGates>,
361
+ code: ResourceAiAccessGapCode,
362
+ blocksInvestigation: boolean,
363
+ ) => {
364
+ const status: ResourceAiAccessStatus = build(row(), agentRow(), {
365
+ ...READY_GATES,
366
+ ...gates,
367
+ });
368
+
369
+ expect(gap(status, code).blocksInvestigation).toBe(blocksInvestigation);
370
+ expect(gap(status, code).blocksRemediation).toBe(true);
371
+ expect(status.isInvestigationReady).toBe(!blocksInvestigation);
372
+ expect(status.isRemediationReady).toBe(false);
373
+ },
374
+ );
375
+
376
+ it("never needs the project's Runner command-execution opt-in", () => {
377
+ const status: ResourceAiAccessStatus = build(row(), agentRow(), {
378
+ ...READY_GATES,
379
+ isAiCommandExecutionEnabled: false,
380
+ });
381
+
382
+ expect(status.isRemediationReady).toBe(true);
383
+ });
384
+
385
+ it("carries the resource's settings and bookkeeping", () => {
386
+ const verifiedAt: Date = new Date("2026-09-01T10:00:00.000Z");
387
+ const status: ResourceAiAccessStatus = build(
388
+ row({
389
+ aiCommandAllowlist: ["docker stop *"],
390
+ aiAccessLastVerifiedAt: verifiedAt,
391
+ aiAccessLastError: "boom",
392
+ }),
393
+ agentRow(),
394
+ );
395
+
396
+ expect(status.aiCommandAllowlist).toEqual(["docker stop *"]);
397
+ expect(status.aiAccessLastVerifiedAt).toBe(
398
+ OneUptimeDate.toString(verifiedAt),
399
+ );
400
+ expect(status.aiAccessLastError).toBe("boom");
401
+ expect(status.aiAccessConfiguredAt).toBeNull();
402
+ });
403
+ });
404
+
405
+ describe("ResourceAiAccessService helpers", () => {
406
+ it("names the resource the way sentences read", () => {
407
+ expect(describeResourceNoun(AiResourceType.Host)).toBe("host");
408
+ expect(describeResourceNoun(AiResourceType.DatabaseServer)).toBe(
409
+ "database server",
410
+ );
411
+ expect(describeResourceNoun(AiResourceType.VMwareVCenter)).toBe(
412
+ "VMware vCenter",
413
+ );
414
+ expect(getResourceAiAgentPage(AiResourceType.Host)).toBe(
415
+ "the host's AI agent page (AI → AI agent)",
416
+ );
417
+ });
418
+
419
+ it("writes an install step for every type", () => {
420
+ for (const type of ALL_AI_RESOURCE_TYPES) {
421
+ const step: string = getResourceAiAgentInstallNextStep({
422
+ resourceType: type,
423
+ });
424
+
425
+ expect(step).toContain("oneuptime/resource-ai-agent");
426
+ expect(step).toContain("ONEUPTIME_AI_AGENT_RESOURCE_TYPE=");
427
+ }
428
+ });
429
+
430
+ it("normalizes a stored allowlist and drops what it cannot read", () => {
431
+ expect(normalizeResourceAllowlist([" docker stop web ", "", 3])).toEqual([
432
+ "docker stop web",
433
+ ]);
434
+ expect(normalizeResourceAllowlist('["a b c"]')).toEqual(["a b c"]);
435
+ expect(normalizeResourceAllowlist("docker stop web")).toEqual([
436
+ "docker stop web",
437
+ ]);
438
+ expect(normalizeResourceAllowlist({ a: 1 })).toEqual([]);
439
+ expect(normalizeResourceAllowlist(null)).toEqual([]);
440
+ });
441
+ });
442
+
443
+ describe("ResourceAiAccessService.getStatusesForResources", () => {
444
+ let dockerFind: jest.SpyInstance;
445
+ let hostFind: jest.SpyInstance;
446
+ let agentsFind: jest.SpyInstance;
447
+ let gatesRead: jest.SpyInstance;
448
+
449
+ beforeEach(() => {
450
+ jest.spyOn(logger, "error").mockImplementation((): void => {
451
+ return undefined;
452
+ });
453
+ dockerFind = jest.spyOn(DockerHostService, "findBy").mockResolvedValue([
454
+ {
455
+ _id: DOCKER_ID.toString(),
456
+ projectId: PROJECT_ID,
457
+ name: "web-1",
458
+ hostIdentifier: "web-1",
459
+ isAiInvestigationEnabled: true,
460
+ aiRemediationMode: "Automatic",
461
+ aiCommandAllowlist: '["docker stop *"]',
462
+ },
463
+ ] as never);
464
+ hostFind = jest.spyOn(HostService, "findBy").mockResolvedValue([
465
+ {
466
+ _id: HOST_ID.toString(),
467
+ projectId: PROJECT_ID,
468
+ name: "",
469
+ hostIdentifier: "db-host",
470
+ isAiInvestigationEnabled: false,
471
+ },
472
+ ] as never);
473
+ agentsFind = jest
474
+ .spyOn(ResourceAiAgentService, "findAgentsForResources")
475
+ .mockImplementation(
476
+ async (data: {
477
+ resourceType: AiResourceType;
478
+ }): Promise<Map<string, ResourceAiAgent>> => {
479
+ return data.resourceType === AiResourceType.DockerHost
480
+ ? new Map<string, ResourceAiAgent>([
481
+ [DOCKER_ID.toString(), agentRow()],
482
+ ])
483
+ : new Map<string, ResourceAiAgent>();
484
+ },
485
+ );
486
+ gatesRead = jest
487
+ .spyOn(KubernetesClusterAiAccessService, "getProjectGates")
488
+ .mockResolvedValue(READY_GATES);
489
+ });
490
+
491
+ afterEach(() => {
492
+ jest.restoreAllMocks();
493
+ });
494
+
495
+ it("answers in the order asked, one row query and one agent query per type", async () => {
496
+ const statuses: Array<ResourceAiAccessStatus> =
497
+ await ResourceAiAccessService.getStatusesForResources({
498
+ projectId: PROJECT_ID,
499
+ resources: [
500
+ { resourceType: AiResourceType.Host, resourceId: HOST_ID.toString() },
501
+ {
502
+ resourceType: AiResourceType.DockerHost,
503
+ resourceId: DOCKER_ID.toString(),
504
+ },
505
+ ],
506
+ });
507
+
508
+ expect(
509
+ statuses.map((status: ResourceAiAccessStatus): string => {
510
+ return status.resourceId;
511
+ }),
512
+ ).toEqual([HOST_ID.toString(), DOCKER_ID.toString()]);
513
+ expect(dockerFind).toHaveBeenCalledTimes(1);
514
+ expect(hostFind).toHaveBeenCalledTimes(1);
515
+ expect(agentsFind).toHaveBeenCalledTimes(2);
516
+ expect(gatesRead).toHaveBeenCalledTimes(1);
517
+
518
+ const host: ResourceAiAccessStatus = statuses[0]!;
519
+ const docker: ResourceAiAccessStatus = statuses[1]!;
520
+
521
+ // A row without a name is named by its identity.
522
+ expect(host.resourceName).toBe("db-host");
523
+ expect(host.agent).toBeNull();
524
+ expect(host.isInvestigationReady).toBe(false);
525
+ expect(docker.isInvestigationReady).toBe(true);
526
+ expect(docker.aiRemediationMode).toBe(ResourceAiRemediationMode.Automatic);
527
+ expect(docker.aiCommandAllowlist).toEqual(["docker stop *"]);
528
+ });
529
+
530
+ it("reads every row in the caller's project, as root", async () => {
531
+ await ResourceAiAccessService.getStatusesForResources({
532
+ projectId: PROJECT_ID,
533
+ resources: [
534
+ {
535
+ resourceType: AiResourceType.DockerHost,
536
+ resourceId: DOCKER_ID.toString(),
537
+ },
538
+ ],
539
+ });
540
+
541
+ const call: {
542
+ query: Record<string, unknown>;
543
+ props: Record<string, unknown>;
544
+ select: Record<string, unknown>;
545
+ } = dockerFind.mock.calls[0]![0];
546
+
547
+ expect(call.query["projectId"]).toBe(PROJECT_ID);
548
+ expect(call.query["isArchived"]).toBeUndefined();
549
+ expect(call.props["isRoot"]).toBe(true);
550
+ expect(call.select["isAiInvestigationEnabled"]).toBe(true);
551
+ expect(call.select["aiRemediationMode"]).toBe(true);
552
+ expect(call.select["aiCommandAllowlist"]).toBe(true);
553
+ expect(call.select["hostIdentifier"]).toBe(true);
554
+ });
555
+
556
+ it("uses the gates it is handed instead of reading them", async () => {
557
+ await ResourceAiAccessService.getStatusesForResources({
558
+ projectId: PROJECT_ID,
559
+ resources: [
560
+ {
561
+ resourceType: AiResourceType.DockerHost,
562
+ resourceId: DOCKER_ID.toString(),
563
+ },
564
+ ],
565
+ gates: { ...READY_GATES, isAiEnabled: false },
566
+ });
567
+
568
+ expect(gatesRead).not.toHaveBeenCalled();
569
+ });
570
+
571
+ it("drops duplicates, unknown types, bad ids and resources that are not the project's", async () => {
572
+ // The project has no such Ceph cluster.
573
+ const cephFind: jest.SpyInstance = jest
574
+ .spyOn(CephClusterService, "findBy")
575
+ .mockResolvedValue([] as never);
576
+
577
+ const statuses: Array<ResourceAiAccessStatus> =
578
+ await ResourceAiAccessService.getStatusesForResources({
579
+ projectId: PROJECT_ID,
580
+ resources: [
581
+ {
582
+ resourceType: AiResourceType.DockerHost,
583
+ resourceId: DOCKER_ID.toString(),
584
+ },
585
+ {
586
+ resourceType: AiResourceType.DockerHost,
587
+ resourceId: DOCKER_ID.toString().toUpperCase(),
588
+ },
589
+ {
590
+ resourceType: "Kubernetes" as never,
591
+ resourceId: CEPH_ID.toString(),
592
+ },
593
+ { resourceType: AiResourceType.Host, resourceId: "not-a-uuid" },
594
+ {
595
+ resourceType: AiResourceType.CephCluster,
596
+ resourceId: CEPH_ID.toString(),
597
+ },
598
+ ],
599
+ });
600
+
601
+ expect(statuses).toHaveLength(1);
602
+ expect(hostFind).not.toHaveBeenCalled();
603
+ expect(cephFind).toHaveBeenCalledTimes(1);
604
+ // One docker id asked for twice (any case) is one row read.
605
+ expect((dockerFind.mock.calls[0]![0] as { limit: number }).limit).toBe(1);
606
+ });
607
+
608
+ it("returns nothing, reading no gates, when no resource exists", async () => {
609
+ dockerFind.mockResolvedValue([] as never);
610
+
611
+ const statuses: Array<ResourceAiAccessStatus> =
612
+ await ResourceAiAccessService.getStatusesForResources({
613
+ projectId: PROJECT_ID,
614
+ resources: [
615
+ {
616
+ resourceType: AiResourceType.DockerHost,
617
+ resourceId: DOCKER_ID.toString(),
618
+ },
619
+ ],
620
+ });
621
+
622
+ expect(statuses).toEqual([]);
623
+ expect(gatesRead).not.toHaveBeenCalled();
624
+ expect(agentsFind).not.toHaveBeenCalled();
625
+ });
626
+
627
+ it("reads every resource as agentless when the agent read fails — never more reachable", async () => {
628
+ agentsFind.mockRejectedValue(new Error("agent table unavailable"));
629
+
630
+ const statuses: Array<ResourceAiAccessStatus> =
631
+ await ResourceAiAccessService.getStatusesForResources({
632
+ projectId: PROJECT_ID,
633
+ resources: [
634
+ {
635
+ resourceType: AiResourceType.DockerHost,
636
+ resourceId: DOCKER_ID.toString(),
637
+ },
638
+ ],
639
+ });
640
+
641
+ expect(statuses[0]!.agent).toBeNull();
642
+ expect(statuses[0]!.isInvestigationReady).toBe(false);
643
+ });
644
+
645
+ it("leaves archived resources out when asked to", async () => {
646
+ dockerFind.mockResolvedValue([
647
+ {
648
+ _id: DOCKER_ID.toString(),
649
+ projectId: PROJECT_ID,
650
+ name: "web-1",
651
+ isArchived: true,
652
+ },
653
+ ] as never);
654
+
655
+ const statuses: Array<ResourceAiAccessStatus> =
656
+ await ResourceAiAccessService.getStatusesForResources({
657
+ projectId: PROJECT_ID,
658
+ resources: [
659
+ {
660
+ resourceType: AiResourceType.DockerHost,
661
+ resourceId: DOCKER_ID.toString(),
662
+ },
663
+ ],
664
+ excludeArchived: true,
665
+ });
666
+
667
+ expect(statuses).toEqual([]);
668
+ expect(
669
+ (dockerFind.mock.calls[0]![0] as { query: Record<string, unknown> })
670
+ .query["isArchived"],
671
+ ).toBe(false);
672
+ });
673
+
674
+ it("getStatusForResource answers for one resource, or null", async () => {
675
+ const status: ResourceAiAccessStatus | null =
676
+ await ResourceAiAccessService.getStatusForResource({
677
+ projectId: PROJECT_ID,
678
+ resourceType: AiResourceType.DockerHost,
679
+ resourceId: DOCKER_ID,
680
+ });
681
+
682
+ expect(status?.resourceName).toBe("web-1");
683
+
684
+ dockerFind.mockResolvedValue([] as never);
685
+
686
+ expect(
687
+ await ResourceAiAccessService.getStatusForResource({
688
+ projectId: PROJECT_ID,
689
+ resourceType: AiResourceType.DockerHost,
690
+ resourceId: DOCKER_ID,
691
+ }),
692
+ ).toBeNull();
693
+ });
694
+ });
695
+
696
+ describe("ResourceAiAccessService.toRow", () => {
697
+ it("builds a database server's identity from its endpoint", () => {
698
+ const built: ResourceAiAccessRow | null = ResourceAiAccessService.toRow(
699
+ AiResourceType.DatabaseServer,
700
+ {
701
+ _id: DB_ID.toString(),
702
+ projectId: PROJECT_ID,
703
+ name: "",
704
+ dbSystem: "postgresql",
705
+ serverAddress: "db.internal",
706
+ serverPort: 5432,
707
+ },
708
+ { projectId: PROJECT_ID },
709
+ );
710
+
711
+ expect(built?.identifier).toBe("postgresql|db.internal:5432");
712
+ expect(built?.name).toBe("postgresql|db.internal:5432");
713
+ });
714
+
715
+ it("names a cluster by its name, and falls back to the type", () => {
716
+ expect(
717
+ ResourceAiAccessService.toRow(
718
+ AiResourceType.CephCluster,
719
+ { _id: CEPH_ID.toString(), projectId: PROJECT_ID, name: "ceph-prod" },
720
+ { projectId: PROJECT_ID },
721
+ )?.identifier,
722
+ ).toBe("ceph-prod");
723
+ expect(
724
+ ResourceAiAccessService.toRow(
725
+ AiResourceType.CephCluster,
726
+ { _id: CEPH_ID.toString(), projectId: PROJECT_ID },
727
+ { projectId: PROJECT_ID },
728
+ )?.name,
729
+ ).toBe("Ceph cluster");
730
+ });
731
+
732
+ it("reads the switches fail-closed", () => {
733
+ const built: ResourceAiAccessRow | null = ResourceAiAccessService.toRow(
734
+ AiResourceType.Host,
735
+ {
736
+ _id: HOST_ID.toString(),
737
+ projectId: PROJECT_ID,
738
+ isAiInvestigationEnabled: "yes",
739
+ aiRemediationMode: "Everything",
740
+ },
741
+ { projectId: PROJECT_ID },
742
+ );
743
+
744
+ expect(built?.isAiInvestigationEnabled).toBe(false);
745
+ expect(built?.aiRemediationMode).toBe(ResourceAiRemediationMode.Disabled);
746
+ });
747
+
748
+ it("drops a row without an id or from another project", () => {
749
+ expect(
750
+ ResourceAiAccessService.toRow(
751
+ AiResourceType.Host,
752
+ { projectId: PROJECT_ID },
753
+ { projectId: PROJECT_ID },
754
+ ),
755
+ ).toBeNull();
756
+ expect(
757
+ ResourceAiAccessService.toRow(
758
+ AiResourceType.Host,
759
+ { _id: HOST_ID.toString(), projectId: ObjectID.generate() },
760
+ { projectId: PROJECT_ID },
761
+ ),
762
+ ).toBeNull();
763
+ });
764
+ });
765
+
766
+ describe("ResourceAiAccessService.getResourcesForSubject", () => {
767
+ let incidentFind: jest.SpyInstance;
768
+ let alertFind: jest.SpyInstance;
769
+ let monitorFind: jest.SpyInstance;
770
+ let resolveMonitor: jest.SpyInstance;
771
+ let loadResources: jest.SpyInstance;
772
+ let linked: Record<string, Array<ObjectID>>;
773
+ let monitors: Array<ObjectID>;
774
+ // Resource ids that read as archived.
775
+ let archived: Set<string>;
776
+
777
+ function relationsOf(
778
+ select: Record<string, unknown>,
779
+ ): Record<string, unknown> {
780
+ const subject: Record<string, unknown> = { _id: "x" };
781
+
782
+ for (const key of Object.keys(select)) {
783
+ if (key === "monitors") {
784
+ subject["monitors"] = monitors.map((id: ObjectID) => {
785
+ return { id };
786
+ });
787
+ } else if (key === "monitorId") {
788
+ subject["monitorId"] = monitors[0];
789
+ } else if (key !== "_id") {
790
+ subject[key] = (linked[key] || []).map((id: ObjectID) => {
791
+ return { id };
792
+ });
793
+ }
794
+ }
795
+
796
+ return subject;
797
+ }
798
+
799
+ beforeEach(() => {
800
+ linked = {};
801
+ monitors = [];
802
+ archived = new Set<string>();
803
+ // Every asked-for resource exists; the archived ones only without the filter.
804
+ loadResources = jest
805
+ .spyOn(ResourceAiAccessService, "loadResources")
806
+ .mockImplementation(
807
+ async (data: {
808
+ resources: Array<ResourceAiAccessRef>;
809
+ excludeArchived?: boolean | undefined;
810
+ }): Promise<Array<ResourceAiAccessRow>> => {
811
+ return data.resources
812
+ .filter((ref: ResourceAiAccessRef): boolean => {
813
+ return !(data.excludeArchived && archived.has(ref.resourceId));
814
+ })
815
+ .map((ref: ResourceAiAccessRef): ResourceAiAccessRow => {
816
+ return row({
817
+ resourceType: ref.resourceType,
818
+ id: new ObjectID(ref.resourceId),
819
+ });
820
+ });
821
+ },
822
+ );
823
+ incidentFind = jest
824
+ .spyOn(IncidentService, "findOneBy")
825
+ .mockImplementation(async (data: unknown): Promise<never> => {
826
+ return relationsOf(
827
+ (data as { select: Record<string, unknown> }).select,
828
+ ) as never;
829
+ });
830
+ alertFind = jest
831
+ .spyOn(AlertService, "findOneBy")
832
+ .mockImplementation(async (data: unknown): Promise<never> => {
833
+ return relationsOf(
834
+ (data as { select: Record<string, unknown> }).select,
835
+ ) as never;
836
+ });
837
+ monitorFind = jest
838
+ .spyOn(MonitorService, "findBy")
839
+ .mockResolvedValue([
840
+ { _id: MONITOR_ID.toString(), projectId: PROJECT_ID },
841
+ ] as never);
842
+ resolveMonitor = jest
843
+ .spyOn(MonitorResourceContextUtil, "resolveResourceContextForMonitor")
844
+ .mockResolvedValue({
845
+ ...MonitorResourceContextUtil.emptyContext(),
846
+ hostIds: [HOST_ID.toString()],
847
+ kubernetesClusterIds: [ObjectID.generate().toString()],
848
+ } as SeriesResolvedResourceIds);
849
+ });
850
+
851
+ afterEach(() => {
852
+ jest.restoreAllMocks();
853
+ });
854
+
855
+ it("returns the incident's linked resources in type priority order", async () => {
856
+ linked = {
857
+ hosts: [HOST_ID],
858
+ databaseServers: [DB_ID],
859
+ dockerHosts: [DOCKER_ID],
860
+ cephClusters: [CEPH_ID],
861
+ };
862
+ monitors = [MONITOR_ID];
863
+
864
+ const refs: Array<ResourceAiAccessRef> =
865
+ await ResourceAiAccessService.getResourcesForSubject({
866
+ projectId: PROJECT_ID,
867
+ incidentId: INCIDENT_ID,
868
+ });
869
+
870
+ expect(refs).toEqual([
871
+ {
872
+ resourceType: AiResourceType.DockerHost,
873
+ resourceId: DOCKER_ID.toString(),
874
+ },
875
+ {
876
+ resourceType: AiResourceType.CephCluster,
877
+ resourceId: CEPH_ID.toString(),
878
+ },
879
+ {
880
+ resourceType: AiResourceType.DatabaseServer,
881
+ resourceId: DB_ID.toString(),
882
+ },
883
+ { resourceType: AiResourceType.Host, resourceId: HOST_ID.toString() },
884
+ ]);
885
+ // Linked resources win: the monitors are not resolved.
886
+ expect(resolveMonitor).not.toHaveBeenCalled();
887
+ expect(monitorFind).not.toHaveBeenCalled();
888
+ });
889
+
890
+ it("reads each relation on its own, in the subject's project, as root", async () => {
891
+ linked = { dockerHosts: [DOCKER_ID] };
892
+
893
+ await ResourceAiAccessService.getResourcesForSubject({
894
+ projectId: PROJECT_ID,
895
+ incidentId: INCIDENT_ID,
896
+ });
897
+
898
+ const relations: Array<string> = [];
899
+
900
+ for (const call of incidentFind.mock.calls) {
901
+ const data: {
902
+ query: Record<string, unknown>;
903
+ select: Record<string, unknown>;
904
+ props: Record<string, unknown>;
905
+ } = call[0];
906
+
907
+ expect(data.query["projectId"]).toBe(PROJECT_ID);
908
+ expect(data.query["_id"]).toBe(INCIDENT_ID.toString());
909
+ expect(data.props["isRoot"]).toBe(true);
910
+
911
+ relations.push(
912
+ ...Object.keys(data.select).filter((key: string): boolean => {
913
+ return key !== "_id";
914
+ }),
915
+ );
916
+ }
917
+
918
+ expect(relations.sort()).toEqual(
919
+ [
920
+ "cephClusters",
921
+ "databaseServers",
922
+ "dockerHosts",
923
+ "dockerSwarmClusters",
924
+ "hosts",
925
+ "monitors",
926
+ "podmanHosts",
927
+ "proxmoxClusters",
928
+ "vmwareVCenters",
929
+ ].sort(),
930
+ );
931
+ expect(alertFind).not.toHaveBeenCalled();
932
+ });
933
+
934
+ it("reads an alert's relations and its monitor", async () => {
935
+ linked = { podmanHosts: [DOCKER_ID] };
936
+ monitors = [MONITOR_ID];
937
+
938
+ const refs: Array<ResourceAiAccessRef> =
939
+ await ResourceAiAccessService.getResourcesForSubject({
940
+ projectId: PROJECT_ID,
941
+ alertId: ALERT_ID,
942
+ });
943
+
944
+ expect(refs).toEqual([
945
+ {
946
+ resourceType: AiResourceType.PodmanHost,
947
+ resourceId: DOCKER_ID.toString(),
948
+ },
949
+ ]);
950
+ expect(incidentFind).not.toHaveBeenCalled();
951
+ expect(
952
+ alertFind.mock.calls.some((call: Array<unknown>): boolean => {
953
+ return (
954
+ (call[0] as { select: Record<string, unknown> }).select[
955
+ "monitorId"
956
+ ] === true
957
+ );
958
+ }),
959
+ ).toBe(true);
960
+ });
961
+
962
+ it("falls back to the monitors' step configuration when nothing is linked", async () => {
963
+ monitors = [MONITOR_ID];
964
+
965
+ const refs: Array<ResourceAiAccessRef> =
966
+ await ResourceAiAccessService.getResourcesForSubject({
967
+ projectId: PROJECT_ID,
968
+ incidentId: INCIDENT_ID,
969
+ });
970
+
971
+ // Only resource types an agent serves: the Kubernetes id is not one.
972
+ expect(refs).toEqual([
973
+ { resourceType: AiResourceType.Host, resourceId: HOST_ID.toString() },
974
+ ]);
975
+ const monitorQuery: {
976
+ query: Record<string, unknown>;
977
+ select: Record<string, unknown>;
978
+ } = monitorFind.mock.calls[0]![0];
979
+ expect(monitorQuery.query["projectId"]).toBe(PROJECT_ID);
980
+ expect(monitorQuery.select["monitorSteps"]).toBe(true);
981
+ expect(monitorQuery.select["monitorType"]).toBe(true);
982
+ expect(monitorQuery.select["projectId"]).toBe(true);
983
+ expect(resolveMonitor).toHaveBeenCalledTimes(1);
984
+ });
985
+
986
+ it("returns nothing for a subject with neither resources nor monitors", async () => {
987
+ const refs: Array<ResourceAiAccessRef> =
988
+ await ResourceAiAccessService.getResourcesForSubject({
989
+ projectId: PROJECT_ID,
990
+ incidentId: INCIDENT_ID,
991
+ });
992
+
993
+ expect(refs).toEqual([]);
994
+ expect(monitorFind).not.toHaveBeenCalled();
995
+ });
996
+
997
+ it("returns nothing without a subject", async () => {
998
+ expect(
999
+ await ResourceAiAccessService.getResourcesForSubject({
1000
+ projectId: PROJECT_ID,
1001
+ }),
1002
+ ).toEqual([]);
1003
+ expect(incidentFind).not.toHaveBeenCalled();
1004
+ expect(alertFind).not.toHaveBeenCalled();
1005
+ });
1006
+
1007
+ it("caps a subject at 10 resources, keeping the highest-priority types", async () => {
1008
+ linked = {
1009
+ hosts: Array.from({ length: 6 }, (): ObjectID => {
1010
+ return ObjectID.generate();
1011
+ }),
1012
+ dockerHosts: Array.from({ length: 6 }, (): ObjectID => {
1013
+ return ObjectID.generate();
1014
+ }),
1015
+ };
1016
+
1017
+ const refs: Array<ResourceAiAccessRef> =
1018
+ await ResourceAiAccessService.getResourcesForSubject({
1019
+ projectId: PROJECT_ID,
1020
+ incidentId: INCIDENT_ID,
1021
+ });
1022
+
1023
+ expect(MAX_RESOURCES_PER_SUBJECT).toBe(10);
1024
+ expect(refs).toHaveLength(10);
1025
+ expect(
1026
+ refs.filter((ref: ResourceAiAccessRef): boolean => {
1027
+ return ref.resourceType === AiResourceType.DockerHost;
1028
+ }),
1029
+ ).toHaveLength(6);
1030
+ });
1031
+
1032
+ /*
1033
+ * Regression: the cap used to be taken before archived resources were
1034
+ * dropped, so ten archived Docker hosts (a higher-priority type) hid the
1035
+ * incident's one active host and the investigation was offered nothing.
1036
+ */
1037
+ it("drops archived links before the cap, so an active resource behind ten archived ones is kept", async () => {
1038
+ const archivedDockerHosts: Array<ObjectID> = Array.from(
1039
+ { length: MAX_RESOURCES_PER_SUBJECT },
1040
+ (): ObjectID => {
1041
+ return ObjectID.generate();
1042
+ },
1043
+ );
1044
+ archivedDockerHosts.forEach((id: ObjectID): void => {
1045
+ archived.add(id.toString());
1046
+ });
1047
+ linked = { dockerHosts: archivedDockerHosts, hosts: [HOST_ID] };
1048
+ monitors = [MONITOR_ID];
1049
+
1050
+ const refs: Array<ResourceAiAccessRef> =
1051
+ await ResourceAiAccessService.getResourcesForSubject({
1052
+ projectId: PROJECT_ID,
1053
+ incidentId: INCIDENT_ID,
1054
+ });
1055
+
1056
+ expect(refs).toEqual([
1057
+ { resourceType: AiResourceType.Host, resourceId: HOST_ID.toString() },
1058
+ ]);
1059
+ for (const call of loadResources.mock.calls) {
1060
+ const data: { projectId: ObjectID; excludeArchived?: boolean } = call[0];
1061
+ expect(data.projectId).toBe(PROJECT_ID);
1062
+ expect(data.excludeArchived).toBe(true);
1063
+ }
1064
+ // A linked resource was active: the monitors are not resolved.
1065
+ expect(resolveMonitor).not.toHaveBeenCalled();
1066
+ });
1067
+
1068
+ it("falls back to the monitors' step configuration when every linked resource is archived", async () => {
1069
+ archived.add(DOCKER_ID.toString());
1070
+ linked = { dockerHosts: [DOCKER_ID] };
1071
+ monitors = [MONITOR_ID];
1072
+
1073
+ const refs: Array<ResourceAiAccessRef> =
1074
+ await ResourceAiAccessService.getResourcesForSubject({
1075
+ projectId: PROJECT_ID,
1076
+ incidentId: INCIDENT_ID,
1077
+ });
1078
+
1079
+ expect(refs).toEqual([
1080
+ { resourceType: AiResourceType.Host, resourceId: HOST_ID.toString() },
1081
+ ]);
1082
+ expect(resolveMonitor).toHaveBeenCalledTimes(1);
1083
+ });
1084
+
1085
+ it("leaves out an archived resource the monitors' step configuration names", async () => {
1086
+ archived.add(HOST_ID.toString());
1087
+ monitors = [MONITOR_ID];
1088
+
1089
+ expect(
1090
+ await ResourceAiAccessService.getResourcesForSubject({
1091
+ projectId: PROJECT_ID,
1092
+ incidentId: INCIDENT_ID,
1093
+ }),
1094
+ ).toEqual([]);
1095
+ });
1096
+
1097
+ it("reads a long list of archived links in batches, not one read per link", async () => {
1098
+ const archivedHosts: Array<ObjectID> = Array.from(
1099
+ { length: 60 },
1100
+ (): ObjectID => {
1101
+ return ObjectID.generate();
1102
+ },
1103
+ );
1104
+ archivedHosts.forEach((id: ObjectID): void => {
1105
+ archived.add(id.toString());
1106
+ });
1107
+ linked = { hosts: [...archivedHosts, HOST_ID] };
1108
+
1109
+ const refs: Array<ResourceAiAccessRef> =
1110
+ await ResourceAiAccessService.getResourcesForSubject({
1111
+ projectId: PROJECT_ID,
1112
+ incidentId: INCIDENT_ID,
1113
+ });
1114
+
1115
+ expect(refs).toEqual([
1116
+ { resourceType: AiResourceType.Host, resourceId: HOST_ID.toString() },
1117
+ ]);
1118
+ expect(loadResources).toHaveBeenCalledTimes(2);
1119
+ });
1120
+
1121
+ it("stops reading once the cap is reached", async () => {
1122
+ linked = {
1123
+ hosts: Array.from({ length: 120 }, (): ObjectID => {
1124
+ return ObjectID.generate();
1125
+ }),
1126
+ };
1127
+
1128
+ const refs: Array<ResourceAiAccessRef> =
1129
+ await ResourceAiAccessService.getResourcesForSubject({
1130
+ projectId: PROJECT_ID,
1131
+ incidentId: INCIDENT_ID,
1132
+ });
1133
+
1134
+ expect(refs).toHaveLength(MAX_RESOURCES_PER_SUBJECT);
1135
+ expect(loadResources).toHaveBeenCalledTimes(1);
1136
+ });
1137
+
1138
+ it("getStatusesForSubject reads statuses of the linked, unarchived resources only", async () => {
1139
+ linked = { dockerHosts: [DOCKER_ID] };
1140
+ const statusesSpy: jest.SpyInstance = jest
1141
+ .spyOn(ResourceAiAccessService, "getStatusesForResources")
1142
+ .mockResolvedValue([]);
1143
+
1144
+ await ResourceAiAccessService.getStatusesForSubject({
1145
+ projectId: PROJECT_ID,
1146
+ incidentId: INCIDENT_ID,
1147
+ });
1148
+
1149
+ expect(statusesSpy).toHaveBeenCalledWith({
1150
+ projectId: PROJECT_ID,
1151
+ resources: [
1152
+ {
1153
+ resourceType: AiResourceType.DockerHost,
1154
+ resourceId: DOCKER_ID.toString(),
1155
+ },
1156
+ ],
1157
+ excludeArchived: true,
1158
+ });
1159
+ });
1160
+
1161
+ it("getStatusesForSubject reads nothing more for a subject without resources", async () => {
1162
+ const statusesSpy: jest.SpyInstance = jest.spyOn(
1163
+ ResourceAiAccessService,
1164
+ "getStatusesForResources",
1165
+ );
1166
+
1167
+ expect(
1168
+ await ResourceAiAccessService.getStatusesForSubject({
1169
+ projectId: PROJECT_ID,
1170
+ alertId: ALERT_ID,
1171
+ }),
1172
+ ).toEqual([]);
1173
+ expect(statusesSpy).not.toHaveBeenCalled();
1174
+ });
1175
+ });
1176
+
1177
+ describe("ResourceAiAccessService.loadAccessTarget", () => {
1178
+ let cephFind: jest.SpyInstance;
1179
+ let onlineAgent: jest.SpyInstance;
1180
+
1181
+ beforeEach(() => {
1182
+ cephFind = jest
1183
+ .spyOn(CephClusterService, "findBy")
1184
+ .mockResolvedValue([
1185
+ { _id: CEPH_ID.toString(), projectId: PROJECT_ID, name: "ceph-prod" },
1186
+ ] as never);
1187
+ onlineAgent = jest
1188
+ .spyOn(ResourceAiAgentService, "findOnlineAgentForResource")
1189
+ .mockResolvedValue(
1190
+ agentRow({ resourceType: AiResourceType.CephCluster }),
1191
+ );
1192
+ });
1193
+
1194
+ afterEach(() => {
1195
+ jest.restoreAllMocks();
1196
+ });
1197
+
1198
+ it("returns the resource and its online agent", async () => {
1199
+ const loaded: LoadedResourceAiAccessTarget =
1200
+ await ResourceAiAccessService.loadAccessTarget({
1201
+ projectId: PROJECT_ID,
1202
+ resourceType: AiResourceType.CephCluster,
1203
+ resourceId: CEPH_ID,
1204
+ });
1205
+
1206
+ expect(loaded.resource?.name).toBe("ceph-prod");
1207
+ expect(loaded.agent?.id?.toString()).toBe(AGENT_ID.toString());
1208
+ expect(onlineAgent).toHaveBeenCalledWith({
1209
+ projectId: PROJECT_ID,
1210
+ resourceType: AiResourceType.CephCluster,
1211
+ resourceId: expect.anything(),
1212
+ });
1213
+ });
1214
+
1215
+ it("never looks for an agent of a resource the project does not have", async () => {
1216
+ cephFind.mockResolvedValue([] as never);
1217
+
1218
+ const loaded: LoadedResourceAiAccessTarget =
1219
+ await ResourceAiAccessService.loadAccessTarget({
1220
+ projectId: PROJECT_ID,
1221
+ resourceType: AiResourceType.CephCluster,
1222
+ resourceId: CEPH_ID,
1223
+ });
1224
+
1225
+ expect(loaded).toEqual({ resource: null, agent: null });
1226
+ expect(onlineAgent).not.toHaveBeenCalled();
1227
+ });
1228
+
1229
+ it("returns no agent when none is online", async () => {
1230
+ onlineAgent.mockResolvedValue(null);
1231
+
1232
+ const loaded: LoadedResourceAiAccessTarget =
1233
+ await ResourceAiAccessService.loadAccessTarget({
1234
+ projectId: PROJECT_ID,
1235
+ resourceType: AiResourceType.CephCluster,
1236
+ resourceId: CEPH_ID,
1237
+ });
1238
+
1239
+ expect(loaded.resource).not.toBeNull();
1240
+ expect(loaded.agent).toBeNull();
1241
+ });
1242
+
1243
+ it("propagates a failed read (the chokepoint fails closed)", async () => {
1244
+ cephFind.mockRejectedValue(new Error("db down"));
1245
+
1246
+ await expect(
1247
+ ResourceAiAccessService.loadAccessTarget({
1248
+ projectId: PROJECT_ID,
1249
+ resourceType: AiResourceType.CephCluster,
1250
+ resourceId: CEPH_ID,
1251
+ }),
1252
+ ).rejects.toThrow("db down");
1253
+ });
1254
+
1255
+ it("returns nothing for an unknown type", async () => {
1256
+ const loaded: LoadedResourceAiAccessTarget =
1257
+ await ResourceAiAccessService.loadAccessTarget({
1258
+ projectId: PROJECT_ID,
1259
+ resourceType: "Kubernetes" as never,
1260
+ resourceId: CEPH_ID,
1261
+ });
1262
+
1263
+ expect(loaded).toEqual({ resource: null, agent: null });
1264
+ });
1265
+ });
1266
+
1267
+ describe("ResourceAiAccessService.recordCommandOutcome", () => {
1268
+ let update: jest.SpyInstance;
1269
+
1270
+ beforeEach(() => {
1271
+ jest.spyOn(logger, "error").mockImplementation((): void => {
1272
+ return undefined;
1273
+ });
1274
+ update = jest
1275
+ .spyOn(DatabaseServerService, "updateOneById")
1276
+ .mockResolvedValue(undefined as never);
1277
+ });
1278
+
1279
+ afterEach(() => {
1280
+ jest.restoreAllMocks();
1281
+ });
1282
+
1283
+ it("records a success as verified and clears the last error, without hooks", async () => {
1284
+ await ResourceAiAccessService.recordCommandOutcome({
1285
+ resourceType: AiResourceType.DatabaseServer,
1286
+ resourceId: DB_ID,
1287
+ succeeded: true,
1288
+ });
1289
+
1290
+ const call: {
1291
+ id: ObjectID;
1292
+ data: Record<string, unknown>;
1293
+ props: Record<string, unknown>;
1294
+ } = update.mock.calls[0]![0];
1295
+
1296
+ expect(call.id).toBe(DB_ID);
1297
+ expect(call.data["aiAccessLastVerifiedAt"]).toBeInstanceOf(Date);
1298
+ expect(call.data["aiAccessLastError"]).toBeNull();
1299
+ expect(call.props).toEqual({ isRoot: true, ignoreHooks: true });
1300
+ });
1301
+
1302
+ it("records a failure's error, capped, and leaves last verified alone", async () => {
1303
+ await ResourceAiAccessService.recordCommandOutcome({
1304
+ resourceType: AiResourceType.DatabaseServer,
1305
+ resourceId: DB_ID,
1306
+ succeeded: false,
1307
+ errorMessage: "x".repeat(5000),
1308
+ });
1309
+
1310
+ const data: Record<string, unknown> = (
1311
+ update.mock.calls[0]![0] as { data: Record<string, unknown> }
1312
+ ).data;
1313
+
1314
+ expect((data["aiAccessLastError"] as string).length).toBe(2000);
1315
+ expect(data["aiAccessLastVerifiedAt"]).toBeUndefined();
1316
+ });
1317
+
1318
+ it("writes the right table for each type", async () => {
1319
+ const podmanUpdate: jest.SpyInstance = jest
1320
+ .spyOn(PodmanHostService, "updateOneById")
1321
+ .mockResolvedValue(undefined as never);
1322
+
1323
+ await ResourceAiAccessService.recordCommandOutcome({
1324
+ resourceType: AiResourceType.PodmanHost,
1325
+ resourceId: DOCKER_ID,
1326
+ succeeded: false,
1327
+ });
1328
+
1329
+ expect(podmanUpdate).toHaveBeenCalledTimes(1);
1330
+ expect(update).not.toHaveBeenCalled();
1331
+ expect(
1332
+ (podmanUpdate.mock.calls[0]![0] as { data: Record<string, unknown> })
1333
+ .data["aiAccessLastError"],
1334
+ ).toBe("Command failed.");
1335
+ });
1336
+
1337
+ it("never throws", async () => {
1338
+ update.mockRejectedValue(new Error("write failed"));
1339
+
1340
+ await expect(
1341
+ ResourceAiAccessService.recordCommandOutcome({
1342
+ resourceType: AiResourceType.DatabaseServer,
1343
+ resourceId: DB_ID,
1344
+ succeeded: true,
1345
+ }),
1346
+ ).resolves.toBeUndefined();
1347
+
1348
+ await expect(
1349
+ ResourceAiAccessService.recordCommandOutcome({
1350
+ resourceType: "Kubernetes" as never,
1351
+ resourceId: DB_ID,
1352
+ succeeded: true,
1353
+ }),
1354
+ ).resolves.toBeUndefined();
1355
+ });
1356
+ });
1357
+
1358
+ describe("ResourceAiAccessService.getProjectGates", () => {
1359
+ afterEach(() => {
1360
+ jest.restoreAllMocks();
1361
+ });
1362
+
1363
+ it("reads the project's gates exactly as the cluster service does", async () => {
1364
+ const gatesRead: jest.SpyInstance = jest
1365
+ .spyOn(KubernetesClusterAiAccessService, "getProjectGates")
1366
+ .mockResolvedValue(READY_GATES);
1367
+
1368
+ expect(await ResourceAiAccessService.getProjectGates(PROJECT_ID)).toBe(
1369
+ READY_GATES,
1370
+ );
1371
+ expect(gatesRead).toHaveBeenCalledWith(PROJECT_ID);
1372
+ });
1373
+ });