@oneuptime/common 14.0.8 → 14.0.9

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 (639) hide show
  1. package/Models/DatabaseModels/Index.ts +2 -0
  2. package/Models/DatabaseModels/KubernetesAiAgent.ts +364 -0
  3. package/Models/DatabaseModels/KubernetesCluster.ts +3 -3
  4. package/Models/DatabaseModels/Project.ts +55 -5
  5. package/Models/DatabaseModels/ProxmoxResource.ts +49 -1
  6. package/Models/DatabaseModels/RunnerJob.ts +75 -0
  7. package/Models/DatabaseModels/TeamComplianceSetting.ts +143 -2
  8. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +17 -0
  9. package/Server/API/AIChatAPI.ts +6 -3
  10. package/Server/API/AIInvestigationAPI.ts +17 -10
  11. package/Server/API/AutoRemediationAPI.ts +207 -25
  12. package/Server/API/KubernetesClusterAiAccessAPI.ts +623 -21
  13. package/Server/API/MicrosoftTeamsAPI.ts +93 -14
  14. package/Server/API/ProxmoxResourceAPI.ts +244 -14
  15. package/Server/Infrastructure/GlobalCache.ts +32 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.ts +99 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.ts +137 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.ts +42 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  20. package/Server/Middleware/TelemetryIngest.ts +41 -18
  21. package/Server/Services/AIService.ts +70 -1
  22. package/Server/Services/AlertService.ts +352 -11
  23. package/Server/Services/AlertSeverityService.ts +65 -15
  24. package/Server/Services/AutoRemediationRuleService.ts +1 -1
  25. package/Server/Services/IncidentService.ts +280 -104
  26. package/Server/Services/IncidentSeverityService.ts +66 -15
  27. package/Server/Services/KubernetesAiAgentJobService.ts +185 -0
  28. package/Server/Services/KubernetesAiAgentService.ts +1634 -0
  29. package/Server/Services/KubernetesClusterAiAccessService.ts +1118 -451
  30. package/Server/Services/KubernetesClusterService.ts +129 -21
  31. package/Server/Services/ProjectService.ts +32 -0
  32. package/Server/Services/ProxmoxResourceService.ts +322 -5
  33. package/Server/Services/RunnerJobService.ts +241 -104
  34. package/Server/Services/RunnerService.ts +1 -1
  35. package/Server/Services/ScheduledMaintenanceService.ts +737 -1
  36. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +414 -56
  37. package/Server/Services/TeamComplianceSettingService.ts +1125 -9
  38. package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +67 -9
  39. package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +39 -10
  40. package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +14 -3
  41. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +37 -15
  42. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +67 -24
  43. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +48 -5
  44. package/Server/Utils/AI/SRE/IncidentPostmortemRunner.ts +40 -8
  45. package/Server/Utils/AI/SRE/InvestigationEligibility.ts +13 -2
  46. package/Server/Utils/AI/SRE/InvestigationGrader.ts +1 -1
  47. package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +6 -1
  48. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +133 -7
  49. package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +58 -4
  50. package/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.ts +447 -0
  51. package/Server/Utils/Telemetry/ProxmoxNativePush.ts +903 -0
  52. package/Server/Utils/Telemetry/TelemetryEntity.ts +87 -27
  53. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +684 -47
  54. package/Tests/App/AdminDashboard/RowActionPlacementOverrides.test.tsx +278 -0
  55. package/Tests/App/Dashboard/AIChatChartWidgetZoom.test.tsx +738 -0
  56. package/Tests/App/Dashboard/AIFeaturesSettingsPointers.test.tsx +147 -0
  57. package/Tests/App/Dashboard/AIInsightsKubernetesClusterPage.test.tsx +1523 -0
  58. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +463 -3
  59. package/Tests/App/Dashboard/AffectedResourcesPickerDropdownPlacement.test.tsx +958 -0
  60. package/Tests/App/Dashboard/AffectedResourcesPickerFocusLeave.test.tsx +595 -0
  61. package/Tests/App/Dashboard/AskAiAvailability.test.ts +20 -3
  62. package/Tests/App/Dashboard/AskAiDisabled.test.tsx +8 -3
  63. package/Tests/App/Dashboard/CephRateChartReload.test.tsx +1456 -0
  64. package/Tests/App/Dashboard/CephTimeRangeZoom.test.tsx +1229 -0
  65. package/Tests/App/Dashboard/ChartCardTimeRangeZoom.test.tsx +151 -0
  66. package/Tests/App/Dashboard/ChartCardZoomAffordances.test.tsx +257 -0
  67. package/Tests/App/Dashboard/ChartRefetchFrame.test.tsx +141 -0
  68. package/Tests/App/Dashboard/ChartZoomHarness.tsx +249 -0
  69. package/Tests/App/Dashboard/ChartZoomStandIn.tsx +131 -0
  70. package/Tests/App/Dashboard/CloudServerlessSlowLoads.test.tsx +628 -0
  71. package/Tests/App/Dashboard/CloudServerlessTimeRangeZoom.test.tsx +607 -0
  72. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +91 -19
  73. package/Tests/App/Dashboard/CompanionMetricsTabZoom.test.tsx +632 -0
  74. package/Tests/App/Dashboard/ContainerChartZoomStandIn.tsx +195 -0
  75. package/Tests/App/Dashboard/ContainerHostOverviewSlowLoads.test.tsx +542 -0
  76. package/Tests/App/Dashboard/ContainerHostOverviewTimeRangeZoom.test.tsx +848 -0
  77. package/Tests/App/Dashboard/ContainerMetricsTimeRangeZoom.test.tsx +561 -0
  78. package/Tests/App/Dashboard/DashboardBoardWideZoomShell.test.tsx +1147 -0
  79. package/Tests/App/Dashboard/DashboardEditPermissions.test.tsx +3 -2
  80. package/Tests/App/Dashboard/DashboardHistogramWidgetDragCursor.test.tsx +399 -0
  81. package/Tests/App/Dashboard/DashboardHistogramWidgetZoom.test.tsx +985 -0
  82. package/Tests/App/Dashboard/DashboardHistogramWidgetZoomHint.test.tsx +803 -0
  83. package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +1 -0
  84. package/Tests/App/Dashboard/DashboardSettingsPreviewZoom.test.tsx +679 -0
  85. package/Tests/App/Dashboard/DashboardSloAndDataSourceChartZoom.test.tsx +881 -0
  86. package/Tests/App/Dashboard/DashboardTraceStatusWidgets.test.tsx +878 -0
  87. package/Tests/App/Dashboard/DatabaseMetricChartModalTimeRangeZoom.test.tsx +442 -0
  88. package/Tests/App/Dashboard/DatabaseOverviewTimeRangeZoom.test.tsx +831 -0
  89. package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +43 -0
  90. package/Tests/App/Dashboard/DockerSwarmInsightsTimeRangeZoom.test.tsx +524 -0
  91. package/Tests/App/Dashboard/DockerSwarmRateChartRefresh.test.tsx +259 -0
  92. package/Tests/App/Dashboard/EdgeDetailPanelHistory.test.tsx +763 -0
  93. package/Tests/App/Dashboard/EdgeDetailPanelTimeRangeZoom.test.tsx +547 -0
  94. package/Tests/App/Dashboard/EdgeHistoryBucketsCases.tsx +565 -0
  95. package/Tests/App/Dashboard/EdgeHistoryBucketsKathmandu.test.tsx +73 -0
  96. package/Tests/App/Dashboard/EdgeHistoryBucketsKolkata.test.tsx +74 -0
  97. package/Tests/App/Dashboard/EdgeHistoryBucketsNewYork.test.tsx +72 -0
  98. package/Tests/App/Dashboard/EdgeHistoryBucketsUtc.test.tsx +72 -0
  99. package/Tests/App/Dashboard/EmbeddedMetricCardRefresh.test.tsx +310 -0
  100. package/Tests/App/Dashboard/EmbeddedMetricCardTimeRangeZoom.test.tsx +536 -0
  101. package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +3 -1
  102. package/Tests/App/Dashboard/EscalationRuleRowActions.test.tsx +660 -0
  103. package/Tests/App/Dashboard/EventNotes.test.tsx +2 -2
  104. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +587 -13
  105. package/Tests/App/Dashboard/EventOverviewSnapshotExplorerZoom.test.tsx +1139 -0
  106. package/Tests/App/Dashboard/EventOverviewSnapshotZoomAcrossTabs.test.tsx +1057 -0
  107. package/Tests/App/Dashboard/EventOverviewTelemetrySnapshotZoom.test.tsx +757 -0
  108. package/Tests/App/Dashboard/ExceptionOccurrenceTrendZoom.test.tsx +675 -0
  109. package/Tests/App/Dashboard/ExceptionOccurrencesZoomSurvivesViewSwitch.test.tsx +405 -0
  110. package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +67 -3
  111. package/Tests/App/Dashboard/ExceptionTablesSpanStatus.test.tsx +515 -0
  112. package/Tests/App/Dashboard/ExplorerOwnZoomRegression.test.tsx +739 -0
  113. package/Tests/App/Dashboard/FlowBandwidthAxisCases.tsx +461 -0
  114. package/Tests/App/Dashboard/FlowBandwidthAxisKathmandu.test.tsx +25 -0
  115. package/Tests/App/Dashboard/FlowBandwidthAxisKolkata.test.tsx +25 -0
  116. package/Tests/App/Dashboard/FlowBandwidthAxisNewYork.test.tsx +25 -0
  117. package/Tests/App/Dashboard/FlowBandwidthAxisUtc.test.tsx +24 -0
  118. package/Tests/App/Dashboard/FlowTopTalkersZoomRefetch.test.tsx +722 -0
  119. package/Tests/App/Dashboard/FlowViewportGuard.test.tsx +2526 -0
  120. package/Tests/App/Dashboard/HostPagesSlowLoads.test.tsx +872 -0
  121. package/Tests/App/Dashboard/HostPagesTimeRangeZoom.test.tsx +1208 -0
  122. package/Tests/App/Dashboard/IncidentRootCauseTimeRangeZoom.test.tsx +358 -0
  123. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +100 -9
  124. package/Tests/App/Dashboard/InfrastructureMetricsTabTimeRangeZoom.test.tsx +485 -0
  125. package/Tests/App/Dashboard/InvestigationDrawerCompanionZoom.test.tsx +655 -0
  126. package/Tests/App/Dashboard/InvestigationDrawerZoom.test.tsx +830 -0
  127. package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +338 -2
  128. package/Tests/App/Dashboard/InvoicesPayInvoice.test.tsx +454 -16
  129. package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +799 -819
  130. package/Tests/App/Dashboard/KubernetesClusterAiAgentOverviewCard.test.tsx +367 -0
  131. package/Tests/App/Dashboard/KubernetesClusterAiAgentStatus.test.ts +1202 -0
  132. package/Tests/App/Dashboard/KubernetesClusterAiDocumentationMarkdown.test.ts +106 -0
  133. package/Tests/App/Dashboard/KubernetesClusterAiNavigation.test.tsx +354 -0
  134. package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +1884 -2054
  135. package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +478 -179
  136. package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +50 -0
  137. package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +2 -1
  138. package/Tests/App/Dashboard/KubernetesClusterOverviewZoom.test.tsx +1217 -0
  139. package/Tests/App/Dashboard/KubernetesClusterPagesZoom.test.tsx +731 -0
  140. package/Tests/App/Dashboard/KubernetesCostsLoadRecovery.test.tsx +869 -0
  141. package/Tests/App/Dashboard/KubernetesCostsZoom.test.tsx +1214 -0
  142. package/Tests/App/Dashboard/KubernetesCostsZoomExactHours.test.tsx +692 -0
  143. package/Tests/App/Dashboard/KubernetesNetworkThroughputReload.test.tsx +902 -0
  144. package/Tests/App/Dashboard/KubernetesNetworkZoomBucketEdges.test.tsx +571 -0
  145. package/Tests/App/Dashboard/KubernetesOverviewZoomBucketEdges.test.tsx +697 -0
  146. package/Tests/App/Dashboard/KubernetesResourceMetricsTabZoom.test.tsx +633 -0
  147. package/Tests/App/Dashboard/LogsExplorerTimeRangeZoom.test.tsx +470 -0
  148. package/Tests/App/Dashboard/LogsInsightsTimeRangeZoom.test.tsx +948 -0
  149. package/Tests/App/Dashboard/LogsViewerPinnedWindowZoom.test.tsx +486 -0
  150. package/Tests/App/Dashboard/MetricChartsBucketInspectorDoubleClick.test.tsx +1001 -0
  151. package/Tests/App/Dashboard/MetricExplorerInvestigationAutoRefresh.test.tsx +514 -0
  152. package/Tests/App/Dashboard/MetricExplorerSavedViewZoomRoundTrip.test.tsx +581 -0
  153. package/Tests/App/Dashboard/MetricExplorerTimeRangeZoom.test.tsx +1162 -0
  154. package/Tests/App/Dashboard/MetricMonitorPreviewZoomLabel.test.ts +72 -0
  155. package/Tests/App/Dashboard/MetricRowSparklineTimeRangeZoom.test.tsx +204 -0
  156. package/Tests/App/Dashboard/MetricViewFirstResultLands.test.tsx +408 -0
  157. package/Tests/App/Dashboard/MetricViewOwnZoomResetPlacement.test.tsx +952 -0
  158. package/Tests/App/Dashboard/MetricViewTimeRange.test.ts +139 -0
  159. package/Tests/App/Dashboard/MetricViewTimeRangeZoom.test.tsx +556 -0
  160. package/Tests/App/Dashboard/MonitorMetricsChartWrappersZoom.test.tsx +552 -0
  161. package/Tests/App/Dashboard/MonitorMetricsTabsTimeRangeZoom.test.tsx +740 -0
  162. package/Tests/App/Dashboard/MonitorOverviewChartsTimeRangeZoom.test.tsx +735 -0
  163. package/Tests/App/Dashboard/MonitorStepPreviewsTimeRangeZoom.test.tsx +835 -0
  164. package/Tests/App/Dashboard/NetworkDeviceTimeRangeZoom.test.tsx +1115 -0
  165. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +186 -17
  166. package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +155 -46
  167. package/Tests/App/Dashboard/PasskeySettings.test.tsx +87 -2
  168. package/Tests/App/Dashboard/ProjectAISettingsPages.test.tsx +884 -0
  169. package/Tests/App/Dashboard/ProxmoxNodeRemove.test.tsx +621 -0
  170. package/Tests/App/Dashboard/ProxmoxOverviewSlowLoads.test.tsx +584 -0
  171. package/Tests/App/Dashboard/ProxmoxPagesTimeRangeZoom.test.tsx +823 -0
  172. package/Tests/App/Dashboard/ProxmoxRateChartReload.test.tsx +1253 -0
  173. package/Tests/App/Dashboard/PublicDashboardBoardWideZoom.test.tsx +605 -0
  174. package/Tests/App/Dashboard/RealRowsLineChart.tsx +250 -0
  175. package/Tests/App/Dashboard/RechartsHostsPlotCursor.test.tsx +513 -0
  176. package/Tests/App/Dashboard/RowActionPlacementOverrides.test.tsx +865 -0
  177. package/Tests/App/Dashboard/RumOverviewTimeRangeZoom.test.tsx +877 -0
  178. package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +20 -4
  179. package/Tests/App/Dashboard/RunnerSupersededByAiAgentNote.test.tsx +296 -0
  180. package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +146 -36
  181. package/Tests/App/Dashboard/ServiceDatabaseSlowLoads.test.tsx +1166 -0
  182. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +1243 -104
  183. package/Tests/App/Dashboard/ServiceOverviewTimeRangeZoom.test.tsx +792 -0
  184. package/Tests/App/Dashboard/SloBudgetBurnDownZoom.test.tsx +771 -0
  185. package/Tests/App/Dashboard/SloBudgetBurnDownZoomAffordances.test.tsx +380 -0
  186. package/Tests/App/Dashboard/SloEventMetricsBarChartZoom.test.tsx +369 -0
  187. package/Tests/App/Dashboard/SloHistoryChartsTimeRangeZoom.test.tsx +694 -0
  188. package/Tests/App/Dashboard/SloHistoryChartsZoomAffordances.test.tsx +497 -0
  189. package/Tests/App/Dashboard/SloMetricsTimeRangeZoom.test.tsx +541 -0
  190. package/Tests/App/Dashboard/SlowLoadHarness.ts +123 -0
  191. package/Tests/App/Dashboard/SnapshotExplorerPrimaryZoom.test.tsx +1194 -0
  192. package/Tests/App/Dashboard/SnapshotViewerPinnedWindow.test.tsx +620 -0
  193. package/Tests/App/Dashboard/TelemetrySnapshotPanelTimeRangeZoom.test.tsx +765 -0
  194. package/Tests/App/Dashboard/TelemetrySnapshotZoom.test.tsx +516 -0
  195. package/Tests/App/Dashboard/TelemetrySnapshotZoomAcrossTabs.test.tsx +827 -0
  196. package/Tests/App/Dashboard/TimeRangeZoomPageHarness.tsx +463 -0
  197. package/Tests/App/Dashboard/TraceChartEditorStatusSplit.test.tsx +543 -0
  198. package/Tests/App/Dashboard/TraceStatusPresentation.test.tsx +967 -0
  199. package/Tests/App/Dashboard/TraceTimelineMinimapZoom.test.tsx +315 -0
  200. package/Tests/App/Dashboard/TracesAnalyticsStatusSplit.test.tsx +778 -0
  201. package/Tests/App/Dashboard/TracesAnalyticsViewZoom.test.tsx +349 -0
  202. package/Tests/App/Dashboard/TracesExplorerTimeRangeZoom.test.tsx +551 -0
  203. package/Tests/App/Dashboard/TracesStatusChartAndFacet.test.tsx +687 -0
  204. package/Tests/App/Dashboard/TracesViewerStaleResponses.test.tsx +458 -0
  205. package/Tests/App/Dashboard/UptimeStripsExcludedFromZoom.test.tsx +406 -0
  206. package/Tests/App/Dashboard/VMwareOverviewSlowLoads.test.tsx +535 -0
  207. package/Tests/App/Dashboard/VMwarePagesTimeRangeZoom.test.tsx +606 -0
  208. package/Tests/App/Dashboard/ValueWidgetSparklineStaleHover.test.tsx +1292 -0
  209. package/Tests/App/Dashboard/ValueWidgetSparklineZoom.test.tsx +765 -0
  210. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +98 -0
  211. package/Tests/Models/DatabaseModels/KubernetesAiAgentModel.test.ts +295 -0
  212. package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +347 -0
  213. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +58 -0
  214. package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +1 -1
  215. package/Tests/Server/API/AIInvestigationAPI.test.ts +123 -45
  216. package/Tests/Server/API/AutoRemediationApproveClusterRound.test.ts +1329 -0
  217. package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +25 -2
  218. package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +2 -0
  219. package/Tests/Server/API/KubernetesClusterAiAccessAPIAgent.test.ts +903 -0
  220. package/Tests/Server/API/MicrosoftTeamsChatsRefreshRoute.test.ts +667 -0
  221. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +294 -6
  222. package/Tests/Server/API/ProxmoxResourceAPIRemoveNode.test.ts +1932 -0
  223. package/Tests/Server/Infrastructure/GlobalCache.test.ts +195 -0
  224. package/Tests/Server/Infrastructure/Postgres/AddKubernetesAiAgentAndAiDefaultsMigration.test.ts +623 -0
  225. package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +87 -10
  226. package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleScopeMigration.test.ts +622 -0
  227. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +61 -4
  228. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +98 -2
  229. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +105 -2
  230. package/Tests/Server/Services/AIServiceBalanceBlocker.test.ts +282 -0
  231. package/Tests/Server/Services/AlertMonitorClearWritesNull.test.ts +409 -0
  232. package/Tests/Server/Services/AlertMonitorEditFeed.test.ts +474 -0
  233. package/Tests/Server/Services/AlertMonitorEditGuard.test.ts +610 -0
  234. package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +2 -0
  235. package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +2 -1
  236. package/Tests/Server/Services/IncidentAffectedMonitorsEdit.test.ts +1360 -0
  237. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +3 -1
  238. package/Tests/Server/Services/IncidentSeverityDeletePausesComplianceRules.test.ts +488 -0
  239. package/Tests/Server/Services/KubernetesAiAgentJobService.test.ts +329 -0
  240. package/Tests/Server/Services/KubernetesAiAgentServiceHelpers.test.ts +618 -0
  241. package/Tests/Server/Services/KubernetesAiAgentServiceLifecycle.test.ts +1444 -0
  242. package/Tests/Server/Services/KubernetesAiAgentServiceRegister.test.ts +1666 -0
  243. package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +140 -111
  244. package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +267 -105
  245. package/Tests/Server/Services/KubernetesClusterAiAccessLegacySuperseded.test.ts +440 -0
  246. package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +567 -411
  247. package/Tests/Server/Services/KubernetesClusterAiAccessTarget.test.ts +1168 -0
  248. package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +410 -12
  249. package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +55 -0
  250. package/Tests/Server/Services/ProjectServiceNewProjectAiDefaults.test.ts +288 -0
  251. package/Tests/Server/Services/ProxmoxResourcePostgres.test.ts +5633 -0
  252. package/Tests/Server/Services/ProxmoxResourceService.test.ts +6458 -5
  253. package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +48 -15
  254. package/Tests/Server/Services/RunnerJobEnqueueKubectlAiAgent.test.ts +620 -0
  255. package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +170 -3
  256. package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +72 -0
  257. package/Tests/Server/Services/RunnerServiceAgentName.test.ts +6 -4
  258. package/Tests/Server/Services/ScheduledMaintenanceAttachmentEdit.test.ts +2113 -0
  259. package/Tests/Server/Services/ScheduledMaintenanceMonitorRelease.test.ts +972 -0
  260. package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +2237 -0
  261. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +3988 -138
  262. package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +107 -1
  263. package/Tests/Server/Utils/AI/IncidentPostmortemRunner.test.ts +351 -0
  264. package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +2 -0
  265. package/Tests/Server/Utils/AI/InvestigationEligibility.test.ts +310 -0
  266. package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +70 -0
  267. package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +58 -0
  268. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +2 -0
  269. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +2 -0
  270. package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +2 -0
  271. package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +125 -1
  272. package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +252 -3
  273. package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +2 -0
  274. package/Tests/Server/Utils/AI/RemediationExecutionRunnerProjectGates.test.ts +240 -0
  275. package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +2 -0
  276. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +2 -0
  277. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +2 -0
  278. package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +2 -0
  279. package/Tests/Server/Utils/Telemetry/HostAssetAttributesFromOtlp.test.ts +68 -0
  280. package/Tests/Server/Utils/Telemetry/InventoryMirrorPostgres.test.ts +370 -0
  281. package/Tests/Server/Utils/Telemetry/NetworkDeviceInventoryAttributes.test.ts +522 -0
  282. package/Tests/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.test.ts +155 -0
  283. package/Tests/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.test.ts +8631 -0
  284. package/Tests/Server/Utils/Telemetry/ProxmoxNativePush.test.ts +2852 -0
  285. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +367 -1
  286. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +45 -3
  287. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +25 -0
  288. package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNames.test.ts +3119 -0
  289. package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNamesPostgres.test.ts +1721 -0
  290. package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +581 -113
  291. package/Tests/Types/Kubernetes/KubernetesAiAgentAccessContract.test.ts +510 -0
  292. package/Tests/Types/Team/ComplianceRule.test.ts +1029 -0
  293. package/Tests/Types/Team/TeamComplianceStatus.test.ts +176 -0
  294. package/Tests/UI/Components/ActionButton/RowActions.test.tsx +641 -0
  295. package/Tests/UI/Components/ActionButton/RowActionsAccessibility.test.tsx +414 -0
  296. package/Tests/UI/Components/ActionButton/SplitActionButtons.test.ts +565 -0
  297. package/Tests/UI/Components/Charts/BarChartDragToSelect.test.tsx +386 -0
  298. package/Tests/UI/Components/Charts/ChartDoubleClickResetWithoutDblclick.test.tsx +1025 -0
  299. package/Tests/UI/Components/Charts/ChartGroupPageZoomHint.test.tsx +196 -0
  300. package/Tests/UI/Components/Charts/ChartPlaceholdersTakeDoubleClick.test.tsx +1861 -0
  301. package/Tests/UI/Components/Charts/ChartPlotClicksWhileResetArmed.test.tsx +382 -0
  302. package/Tests/UI/Components/Charts/ChartRangeSelection.test.tsx +704 -0
  303. package/Tests/UI/Components/Charts/ChartRangeSelectionDoubleClickReset.test.tsx +1481 -0
  304. package/Tests/UI/Components/Charts/ChartRangeSelectionRealRecharts.test.tsx +918 -0
  305. package/Tests/UI/Components/Charts/ChartRowBucketStart.test.ts +350 -0
  306. package/Tests/UI/Components/Charts/DoubleClickReset.test.tsx +996 -0
  307. package/Tests/UI/Components/Charts/HistogramDoubleClickResetRealRecharts.test.tsx +965 -0
  308. package/Tests/UI/Components/Charts/HistogramDriftThenRefetch.test.tsx +277 -0
  309. package/Tests/UI/Components/Charts/HistogramRangeSelectionDataChange.test.tsx +1757 -0
  310. package/Tests/UI/Components/Charts/HistogramRangeSelectionRealRecharts.test.tsx +560 -0
  311. package/Tests/UI/Components/Charts/TimeRangeZoom/ChartPageTimeRangeZoom.test.tsx +513 -0
  312. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.test.tsx +845 -0
  313. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.test.tsx +119 -0
  314. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHintTailwind.test.tsx +274 -0
  315. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.test.ts +433 -0
  316. package/Tests/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.test.tsx +863 -0
  317. package/Tests/UI/Components/Charts/TimeRangeZoom/ZoomDocsPromises.test.tsx +1147 -0
  318. package/Tests/UI/Components/DisabledButtonTooltip.test.tsx +14 -3
  319. package/Tests/UI/Components/EntityDropdownEscape.test.tsx +454 -0
  320. package/Tests/UI/Components/EntityDropdownFocusLeave.test.tsx +420 -0
  321. package/Tests/UI/Components/Forms/FormFieldDropdownDisabled.test.tsx +233 -0
  322. package/Tests/UI/Components/Input.test.tsx +80 -1
  323. package/Tests/UI/Components/LogsAnalyticsViewZoom.test.tsx +947 -0
  324. package/Tests/UI/Components/LogsHistogramDragTooltip.test.tsx +17 -5
  325. package/Tests/UI/Components/LogsViewerAnalyticsZoom.test.tsx +501 -0
  326. package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +1 -1
  327. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +6 -4
  328. package/Tests/UI/Components/ModelTable/BaseModelTablePermissionGating.test.tsx +60 -8
  329. package/Tests/UI/Components/ModelTable/BaseModelTableRowActions.test.tsx +476 -0
  330. package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +7 -5
  331. package/Tests/UI/Components/MoreMenuPortal.test.tsx +623 -0
  332. package/Tests/UI/Components/StatusPage/ResourceGridActions.test.tsx +538 -0
  333. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +14 -4
  334. package/Tests/UI/Components/TableCellWrapping.test.tsx +3 -3
  335. package/Tests/UI/Components/TableRowActions.test.tsx +472 -0
  336. package/Tests/UI/Components/TelemetryHistogramDragTooltip.test.tsx +1 -1
  337. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +1 -1
  338. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerZoomScope.test.tsx +470 -0
  339. package/Tests/UI/Components/TelemetryViewer/UseViewerTimeRangeZoom.test.tsx +571 -0
  340. package/Tests/UI/Components/TelemetryViewer/ViewerDoubleClickResetUnderDataChange.test.tsx +928 -0
  341. package/Tests/UI/Components/TelemetryViewer/ViewerFollowsEnclosingZoom.test.tsx +1569 -0
  342. package/Tests/UI/Components/Toggle.test.tsx +66 -0
  343. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardRefreshNames.test.tsx +1023 -0
  344. package/Tests/UI/Rum/ChunkLoader.test.ts +201 -0
  345. package/Tests/UI/Rum/FidelityNotices.test.ts +54 -0
  346. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +72 -0
  347. package/Tests/UI/Rum/ReplayFrameCapture.test.ts +1009 -9
  348. package/Tests/UI/Rum/ReplayRecordedAssets.test.ts +1069 -0
  349. package/Tests/UI/Rum/ReplayStage.test.tsx +913 -2
  350. package/Tests/UI/Rum/SessionReplayTable.test.tsx +443 -8
  351. package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +439 -9
  352. package/Tests/UI/Rum/UserFlowTablesRowActions.test.tsx +501 -0
  353. package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +47 -19
  354. package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +2 -0
  355. package/Tests/Utils/AiRemediation/KubernetesAiAgentPolicyCopyParity.test.ts +396 -0
  356. package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +104 -1
  357. package/Types/AI/AIRunType.ts +9 -4
  358. package/Types/AI/InvestigationNotStartedReason.ts +6 -0
  359. package/Types/AutoRemediation/AiRemediationCommandPlan.ts +14 -16
  360. package/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.ts +28 -0
  361. package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +4 -2
  362. package/Types/Kubernetes/KubernetesClusterAiAccess.ts +263 -32
  363. package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +20 -16
  364. package/Types/Kubernetes/KubernetesClusterAiInsights.ts +81 -0
  365. package/Types/Monitor/ProxmoxAlertTemplates.ts +4 -4
  366. package/Types/Team/ComplianceNotificationChannel.ts +22 -0
  367. package/Types/Team/ComplianceRule.ts +434 -0
  368. package/Types/Team/ComplianceRuleType.ts +21 -0
  369. package/Types/Team/TeamComplianceStatus.ts +81 -0
  370. package/Types/Telemetry/TelemetryIngestSurface.ts +28 -0
  371. package/UI/Components/ActionButton/ActionButtonSchema.ts +21 -0
  372. package/UI/Components/ActionButton/RowActions.tsx +238 -0
  373. package/UI/Components/ActionButton/SplitActionButtons.ts +173 -0
  374. package/UI/Components/Charts/Area/AreaChart.tsx +32 -2
  375. package/UI/Components/Charts/Bar/BarChart.tsx +40 -3
  376. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +37 -11
  377. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +80 -243
  378. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +122 -31
  379. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +95 -244
  380. package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +214 -0
  381. package/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.ts +32 -0
  382. package/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.ts +599 -0
  383. package/UI/Components/Charts/Line/LineChart.tsx +32 -2
  384. package/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.tsx +70 -0
  385. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.tsx +226 -0
  386. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.tsx +73 -0
  387. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.ts +163 -0
  388. package/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.ts +215 -0
  389. package/UI/Components/Charts/Utils/DataPoint.ts +20 -8
  390. package/UI/Components/Charts/Utils/XAxis.ts +75 -0
  391. package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +493 -59
  392. package/UI/Components/EntityDropdown/EntityDropdown.tsx +107 -11
  393. package/UI/Components/Forms/Fields/FormField.tsx +10 -0
  394. package/UI/Components/Input/Input.tsx +90 -57
  395. package/UI/Components/List/ListRow.tsx +8 -74
  396. package/UI/Components/LogsViewer/LogsViewer.tsx +40 -11
  397. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +22 -6
  398. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +209 -6
  399. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +127 -84
  400. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +3 -0
  401. package/UI/Components/ModelTable/BaseModelTable.tsx +12 -1
  402. package/UI/Components/MoreMenu/MoreMenu.tsx +365 -30
  403. package/UI/Components/MoreMenu/MoreMenuItem.tsx +70 -17
  404. package/UI/Components/OrderedStatesList/Item.tsx +7 -72
  405. package/UI/Components/StatusPage/ResourceGrid.tsx +126 -77
  406. package/UI/Components/Table/TableBody.tsx +8 -2
  407. package/UI/Components/Table/TableRow.tsx +20 -140
  408. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +41 -10
  409. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +47 -9
  410. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +25 -6
  411. package/UI/Components/TelemetryViewer/types.ts +2 -0
  412. package/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.ts +448 -0
  413. package/UI/Components/Toggle/Toggle.tsx +26 -0
  414. package/UI/Components/Tooltip/Tooltip.tsx +8 -1
  415. package/UI/Styles/Theme.css +14 -2
  416. package/Utils/AiRemediation/CommandPolicy.ts +1 -1
  417. package/Utils/AiRemediation/KubectlPolicy.ts +1 -1
  418. package/Utils/Dashboard/DashboardTimeRangeZoom.ts +46 -2
  419. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  420. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  421. package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js +385 -0
  422. package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js.map +1 -0
  423. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +3 -3
  424. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  425. package/build/dist/Models/DatabaseModels/Project.js +56 -5
  426. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  427. package/build/dist/Models/DatabaseModels/ProxmoxResource.js +51 -1
  428. package/build/dist/Models/DatabaseModels/ProxmoxResource.js.map +1 -1
  429. package/build/dist/Models/DatabaseModels/RunnerJob.js +74 -0
  430. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  431. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js +134 -3
  432. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js.map +1 -1
  433. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  434. package/build/dist/Server/API/AIChatAPI.js +6 -1
  435. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  436. package/build/dist/Server/API/AIInvestigationAPI.js +17 -10
  437. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  438. package/build/dist/Server/API/AutoRemediationAPI.js +144 -24
  439. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  440. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +436 -22
  441. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -1
  442. package/build/dist/Server/API/MicrosoftTeamsAPI.js +69 -13
  443. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  444. package/build/dist/Server/API/ProxmoxResourceAPI.js +187 -14
  445. package/build/dist/Server/API/ProxmoxResourceAPI.js.map +1 -1
  446. package/build/dist/Server/Infrastructure/GlobalCache.js +27 -0
  447. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  448. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js +50 -0
  449. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js.map +1 -0
  450. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js +84 -0
  451. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js.map +1 -0
  452. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js +31 -0
  453. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js.map +1 -0
  454. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  455. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  456. package/build/dist/Server/Middleware/TelemetryIngest.js +34 -19
  457. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  458. package/build/dist/Server/Services/AIService.js +59 -1
  459. package/build/dist/Server/Services/AIService.js.map +1 -1
  460. package/build/dist/Server/Services/AlertService.js +248 -13
  461. package/build/dist/Server/Services/AlertService.js.map +1 -1
  462. package/build/dist/Server/Services/AlertSeverityService.js +51 -15
  463. package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
  464. package/build/dist/Server/Services/AutoRemediationRuleService.js +1 -1
  465. package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
  466. package/build/dist/Server/Services/IncidentService.js +199 -72
  467. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  468. package/build/dist/Server/Services/IncidentSeverityService.js +51 -15
  469. package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
  470. package/build/dist/Server/Services/KubernetesAiAgentJobService.js +158 -0
  471. package/build/dist/Server/Services/KubernetesAiAgentJobService.js.map +1 -0
  472. package/build/dist/Server/Services/KubernetesAiAgentService.js +1145 -0
  473. package/build/dist/Server/Services/KubernetesAiAgentService.js.map +1 -0
  474. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +842 -371
  475. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -1
  476. package/build/dist/Server/Services/KubernetesClusterService.js +86 -17
  477. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  478. package/build/dist/Server/Services/ProjectService.js +22 -0
  479. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  480. package/build/dist/Server/Services/ProxmoxResourceService.js +258 -3
  481. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  482. package/build/dist/Server/Services/RunnerJobService.js +178 -92
  483. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  484. package/build/dist/Server/Services/RunnerService.js +1 -1
  485. package/build/dist/Server/Services/RunnerService.js.map +1 -1
  486. package/build/dist/Server/Services/ScheduledMaintenanceService.js +535 -3
  487. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  488. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +316 -46
  489. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  490. package/build/dist/Server/Services/TeamComplianceSettingService.js +833 -7
  491. package/build/dist/Server/Services/TeamComplianceSettingService.js.map +1 -1
  492. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +52 -9
  493. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -1
  494. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +13 -9
  495. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -1
  496. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +7 -3
  497. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -1
  498. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +29 -15
  499. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  500. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +56 -20
  501. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  502. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +39 -5
  503. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  504. package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js +30 -3
  505. package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js.map +1 -1
  506. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js +13 -2
  507. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js.map +1 -1
  508. package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js +1 -1
  509. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +4 -2
  510. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
  511. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +111 -6
  512. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  513. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +40 -4
  514. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
  515. package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js +296 -0
  516. package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js.map +1 -0
  517. package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js +699 -0
  518. package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js.map +1 -0
  519. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +77 -19
  520. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  521. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +505 -42
  522. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  523. package/build/dist/Types/AI/AIRunType.js +9 -4
  524. package/build/dist/Types/AI/AIRunType.js.map +1 -1
  525. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +10 -16
  526. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
  527. package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js +28 -0
  528. package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js.map +1 -0
  529. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +72 -16
  530. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -1
  531. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +20 -16
  532. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -1
  533. package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js +17 -0
  534. package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js.map +1 -0
  535. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +4 -4
  536. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
  537. package/build/dist/Types/Team/ComplianceNotificationChannel.js +23 -0
  538. package/build/dist/Types/Team/ComplianceNotificationChannel.js.map +1 -0
  539. package/build/dist/Types/Team/ComplianceRule.js +304 -0
  540. package/build/dist/Types/Team/ComplianceRule.js.map +1 -0
  541. package/build/dist/Types/Team/ComplianceRuleType.js +21 -0
  542. package/build/dist/Types/Team/ComplianceRuleType.js.map +1 -1
  543. package/build/dist/Types/Team/TeamComplianceStatus.js +2 -0
  544. package/build/dist/Types/Team/TeamComplianceStatus.js.map +1 -0
  545. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +25 -0
  546. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
  547. package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js +20 -1
  548. package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js.map +1 -1
  549. package/build/dist/UI/Components/ActionButton/RowActions.js +113 -0
  550. package/build/dist/UI/Components/ActionButton/RowActions.js.map +1 -0
  551. package/build/dist/UI/Components/ActionButton/SplitActionButtons.js +71 -0
  552. package/build/dist/UI/Components/ActionButton/SplitActionButtons.js.map +1 -0
  553. package/build/dist/UI/Components/Charts/Area/AreaChart.js +16 -1
  554. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  555. package/build/dist/UI/Components/Charts/Bar/BarChart.js +16 -1
  556. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  557. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +27 -5
  558. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  559. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +61 -194
  560. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  561. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +84 -26
  562. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  563. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +75 -196
  564. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  565. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +133 -0
  566. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -1
  567. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js +26 -0
  568. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js.map +1 -0
  569. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js +371 -0
  570. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js.map +1 -0
  571. package/build/dist/UI/Components/Charts/Line/LineChart.js +16 -1
  572. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  573. package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js +35 -0
  574. package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js.map +1 -0
  575. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js +107 -0
  576. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js.map +1 -0
  577. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js +41 -0
  578. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js.map +1 -0
  579. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js +117 -0
  580. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js.map +1 -0
  581. package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js +121 -0
  582. package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js.map +1 -0
  583. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -7
  584. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  585. package/build/dist/UI/Components/Charts/Utils/XAxis.js +70 -0
  586. package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
  587. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +305 -38
  588. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -1
  589. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +89 -14
  590. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  591. package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -2
  592. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  593. package/build/dist/UI/Components/Input/Input.js +69 -54
  594. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  595. package/build/dist/UI/Components/List/ListRow.js +4 -39
  596. package/build/dist/UI/Components/List/ListRow.js.map +1 -1
  597. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +31 -8
  598. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  599. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +10 -1
  600. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
  601. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +139 -21
  602. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  603. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +30 -8
  604. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  605. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
  606. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  607. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +10 -0
  608. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  609. package/build/dist/UI/Components/MoreMenu/MoreMenu.js +253 -14
  610. package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
  611. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js +43 -13
  612. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js.map +1 -1
  613. package/build/dist/UI/Components/OrderedStatesList/Item.js +2 -38
  614. package/build/dist/UI/Components/OrderedStatesList/Item.js.map +1 -1
  615. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +53 -15
  616. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  617. package/build/dist/UI/Components/Table/TableBody.js +6 -2
  618. package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
  619. package/build/dist/UI/Components/Table/TableRow.js +12 -75
  620. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  621. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +32 -7
  622. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  623. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +27 -9
  624. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  625. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +11 -1
  626. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
  627. package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js +272 -0
  628. package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js.map +1 -0
  629. package/build/dist/UI/Components/Toggle/Toggle.js +14 -1
  630. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  631. package/build/dist/UI/Components/Tooltip/Tooltip.js +1 -1
  632. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  633. package/build/dist/Utils/AiRemediation/CommandPolicy.js +1 -1
  634. package/build/dist/Utils/AiRemediation/CommandPolicy.js.map +1 -1
  635. package/build/dist/Utils/AiRemediation/KubectlPolicy.js +1 -1
  636. package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -1
  637. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +27 -1
  638. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -1
  639. package/package.json +1 -1
@@ -0,0 +1,1444 @@
1
+ import KubernetesAiAgent from "../../../Models/DatabaseModels/KubernetesAiAgent";
2
+ import KubernetesCluster from "../../../Models/DatabaseModels/KubernetesCluster";
3
+ import Runner, {
4
+ RunnerConnectionStatus,
5
+ } from "../../../Models/DatabaseModels/Runner";
6
+ import AutoRemediationRuleService from "../../../Server/Services/AutoRemediationRuleService";
7
+ import KubernetesAiAgentService, {
8
+ KUBERNETES_AI_AGENT_SELECT_WITHOUT_KEY,
9
+ LEGACY_RUNNER_RETIREMENT_AGENT_AGE_HOURS,
10
+ LEGACY_RUNNER_RETIREMENT_CHECK_INTERVAL_MS,
11
+ LEGACY_RUNNER_RETIREMENT_OFFLINE_DAYS,
12
+ LegacyRunnerRetirementOutcome,
13
+ Service as KubernetesAiAgentServiceClass,
14
+ } from "../../../Server/Services/KubernetesAiAgentService";
15
+ import KubernetesClusterAiAccessService from "../../../Server/Services/KubernetesClusterAiAccessService";
16
+ import KubernetesClusterFeedService from "../../../Server/Services/KubernetesClusterFeedService";
17
+ import KubernetesClusterService from "../../../Server/Services/KubernetesClusterService";
18
+ import RunnerService from "../../../Server/Services/RunnerService";
19
+ import UserService from "../../../Server/Services/UserService";
20
+ import logger from "../../../Server/Utils/Logger";
21
+ import BadDataException from "../../../Types/Exception/BadDataException";
22
+ import { JSONObject } from "../../../Types/JSON";
23
+ import { KubernetesAiRemediationMode } from "../../../Types/Kubernetes/KubernetesClusterAiAccess";
24
+ import ObjectID from "../../../Types/ObjectID";
25
+ import PositiveNumber from "../../../Types/PositiveNumber";
26
+ import {
27
+ afterEach,
28
+ beforeEach,
29
+ describe,
30
+ expect,
31
+ jest,
32
+ test,
33
+ } from "@jest/globals";
34
+
35
+ /*
36
+ * The Kubernetes AI agent after registration (DESIGN §5.1, §5.2):
37
+ *
38
+ * - authenticate: every later request carries the agent id and key; the
39
+ * key's sha256 must match the row (timing-safe). Wrong key, reset row
40
+ * (null hash), unknown id and malformed input are all null, and the row
41
+ * handed back never carries the hash;
42
+ * - heartbeat: alive + connected, the posture stored with inCluster and
43
+ * the cluster's identifier forced, and — only on a cluster nobody
44
+ * configured whose fixes are Off — "Ask for approval" once the agent's
45
+ * write access appears;
46
+ * - markDisconnected: the sign-off, which admits a replacement at once;
47
+ * - resetAgent: the admin's reset — no key works any more, the row reads
48
+ * disconnected, and the feed says so;
49
+ * - retireLegacyRunnerIfUnused: the previous in-cluster Runner is deleted
50
+ * only when it is certainly unused.
51
+ *
52
+ * No database: everything below the service boundary is stubbed.
53
+ */
54
+
55
+ interface SpyCalls {
56
+ mock: { calls: Array<Array<unknown>> };
57
+ }
58
+
59
+ const PROJECT_ID: ObjectID = new ObjectID(
60
+ "11111111-1111-4111-8111-111111111111",
61
+ );
62
+ const CLUSTER_ID: ObjectID = new ObjectID(
63
+ "22222222-2222-4222-8222-222222222222",
64
+ );
65
+ const AGENT_ID: ObjectID = new ObjectID("33333333-3333-4333-8333-333333333333");
66
+ const LEGACY_RUNNER_ID: ObjectID = new ObjectID(
67
+ "44444444-4444-4444-8444-444444444444",
68
+ );
69
+ const USER_ID: ObjectID = new ObjectID("66666666-6666-4666-8666-666666666666");
70
+
71
+ const AGENT_KEY: string = "ab".repeat(32);
72
+ const HOUR_MS: number = 60 * 60 * 1000;
73
+ const DAY_MS: number = 24 * HOUR_MS;
74
+
75
+ function msAgo(ms: number, from: Date = new Date()): Date {
76
+ return new Date(from.getTime() - ms);
77
+ }
78
+
79
+ function makeAgent(
80
+ overrides: Partial<Record<string, unknown>> = {},
81
+ ): KubernetesAiAgent {
82
+ const agent: KubernetesAiAgent = new KubernetesAiAgent();
83
+ agent.id = AGENT_ID;
84
+ agent.projectId = PROJECT_ID;
85
+ agent.kubernetesClusterId = CLUSTER_ID;
86
+ agent.keyHash = KubernetesAiAgentServiceClass.hashKey(AGENT_KEY);
87
+ agent.connectionStatus = "connected";
88
+ agent.lastAliveAt = msAgo(20 * 1000);
89
+ agent.createdAt = msAgo(3 * DAY_MS);
90
+ agent.posture = {
91
+ clusterIdentifier: "prod-us",
92
+ inCluster: true,
93
+ allowWrites: false,
94
+ };
95
+ Object.assign(agent, overrides);
96
+ return agent;
97
+ }
98
+
99
+ function makeCluster(
100
+ overrides: Partial<KubernetesCluster> = {},
101
+ ): KubernetesCluster {
102
+ const cluster: KubernetesCluster = new KubernetesCluster();
103
+ cluster.id = CLUSTER_ID;
104
+ cluster.projectId = PROJECT_ID;
105
+ cluster.clusterIdentifier = "prod-us";
106
+ cluster.isAiInvestigationEnabled = true;
107
+ cluster.aiRemediationMode = KubernetesAiRemediationMode.Disabled;
108
+ Object.assign(cluster, overrides);
109
+ return cluster;
110
+ }
111
+
112
+ function silenceLogger(): void {
113
+ for (const level of ["info", "warn", "error", "debug"] as const) {
114
+ jest.spyOn(logger, level).mockImplementation((): void => {
115
+ return undefined;
116
+ });
117
+ }
118
+ }
119
+
120
+ beforeEach(() => {
121
+ silenceLogger();
122
+ });
123
+
124
+ afterEach(() => {
125
+ jest.restoreAllMocks();
126
+ });
127
+
128
+ describe("authenticate", () => {
129
+ let findOneBy: SpyCalls;
130
+
131
+ function stubRow(row: KubernetesAiAgent | null): void {
132
+ findOneBy = jest
133
+ .spyOn(KubernetesAiAgentService, "findOneBy")
134
+ .mockResolvedValue(row) as unknown as SpyCalls;
135
+ }
136
+
137
+ test("the right key returns the row, read by id as root with the hash selected, and hands it back without the hash", async () => {
138
+ stubRow(makeAgent());
139
+
140
+ const agent: KubernetesAiAgent | null =
141
+ await KubernetesAiAgentService.authenticate({
142
+ agentId: AGENT_ID.toString(),
143
+ agentKey: AGENT_KEY,
144
+ });
145
+
146
+ expect(agent).not.toBeNull();
147
+ expect(agent!.id!.toString()).toBe(AGENT_ID.toString());
148
+ expect(agent!.keyHash).toBeUndefined();
149
+ expect(Object.keys(agent!)).not.toContain("keyHash");
150
+
151
+ const read: {
152
+ query: Record<string, unknown>;
153
+ select: Record<string, boolean>;
154
+ props: Record<string, unknown>;
155
+ } = findOneBy.mock.calls[0]![0] as {
156
+ query: Record<string, unknown>;
157
+ select: Record<string, boolean>;
158
+ props: Record<string, unknown>;
159
+ };
160
+
161
+ expect(read.query).toEqual({ _id: AGENT_ID.toString() });
162
+ expect(read.select).toEqual({
163
+ ...KUBERNETES_AI_AGENT_SELECT_WITHOUT_KEY,
164
+ keyHash: true,
165
+ });
166
+ expect(read.props["isRoot"]).toBe(true);
167
+ });
168
+
169
+ test("accepts the id as an ObjectID too", async () => {
170
+ stubRow(makeAgent());
171
+
172
+ await expect(
173
+ KubernetesAiAgentService.authenticate({
174
+ agentId: AGENT_ID,
175
+ agentKey: AGENT_KEY,
176
+ }),
177
+ ).resolves.not.toBeNull();
178
+ });
179
+
180
+ test("a wrong key is null", async () => {
181
+ stubRow(makeAgent());
182
+
183
+ await expect(
184
+ KubernetesAiAgentService.authenticate({
185
+ agentId: AGENT_ID.toString(),
186
+ agentKey: "cd".repeat(32),
187
+ }),
188
+ ).resolves.toBeNull();
189
+ });
190
+
191
+ test("the stored hash presented as the key is null", async () => {
192
+ stubRow(makeAgent());
193
+
194
+ await expect(
195
+ KubernetesAiAgentService.authenticate({
196
+ agentId: AGENT_ID.toString(),
197
+ agentKey: KubernetesAiAgentServiceClass.hashKey(AGENT_KEY),
198
+ }),
199
+ ).resolves.toBeNull();
200
+ });
201
+
202
+ // After an admin reset no key works — the pod has to register again.
203
+ test("a reset row (no hash) is null, even for the key it used to have", async () => {
204
+ stubRow(makeAgent({ keyHash: null }));
205
+
206
+ await expect(
207
+ KubernetesAiAgentService.authenticate({
208
+ agentId: AGENT_ID.toString(),
209
+ agentKey: AGENT_KEY,
210
+ }),
211
+ ).resolves.toBeNull();
212
+ });
213
+
214
+ test("an unknown id is null, and costs the same hash comparison as a wrong key", async () => {
215
+ stubRow(null);
216
+ const compare: SpyCalls = jest.spyOn(
217
+ KubernetesAiAgentServiceClass,
218
+ "doesKeyMatchHash",
219
+ ) as unknown as SpyCalls;
220
+
221
+ await expect(
222
+ KubernetesAiAgentService.authenticate({
223
+ agentId: AGENT_ID.toString(),
224
+ agentKey: AGENT_KEY,
225
+ }),
226
+ ).resolves.toBeNull();
227
+
228
+ expect(compare.mock.calls).toHaveLength(1);
229
+ expect(compare.mock.calls[0]![0]).toBe(AGENT_KEY);
230
+ expect(String(compare.mock.calls[0]![1])).toMatch(/^[0-9a-f]{64}$/);
231
+ });
232
+
233
+ test.each([
234
+ ["an empty id", "", AGENT_KEY],
235
+ ["an id that is not a UUID", "agent-1", AGENT_KEY],
236
+ ["a SQL-ish id", "' OR 1=1 --", AGENT_KEY],
237
+ ["an empty key", AGENT_ID.toString(), ""],
238
+ ["a non-string key", AGENT_ID.toString(), 42],
239
+ ])(
240
+ "%s is null without touching the database",
241
+ async (_label: string, agentId: string, agentKey: unknown) => {
242
+ stubRow(makeAgent());
243
+
244
+ await expect(
245
+ KubernetesAiAgentService.authenticate({
246
+ agentId,
247
+ agentKey: agentKey as string,
248
+ }),
249
+ ).resolves.toBeNull();
250
+
251
+ expect(findOneBy.mock.calls).toHaveLength(0);
252
+ },
253
+ );
254
+ });
255
+
256
+ describe("heartbeat", () => {
257
+ interface HeartbeatHarness {
258
+ clusterFindOneBy: SpyCalls;
259
+ agentUpdateColumns: SpyCalls;
260
+ agentUpdateOneById: SpyCalls;
261
+ clusterUpdateOneBy: SpyCalls;
262
+ feed: SpyCalls;
263
+ retire: SpyCalls;
264
+ }
265
+
266
+ function setUp(
267
+ options: { cluster?: KubernetesCluster | null } = {},
268
+ ): HeartbeatHarness {
269
+ return {
270
+ clusterFindOneBy: jest
271
+ .spyOn(KubernetesClusterService, "findOneBy")
272
+ .mockResolvedValue(
273
+ options.cluster === undefined ? makeCluster() : options.cluster,
274
+ ) as unknown as SpyCalls,
275
+ agentUpdateColumns: jest
276
+ .spyOn(KubernetesAiAgentService, "updateColumnsByIdWithoutHooks")
277
+ .mockResolvedValue(undefined) as unknown as SpyCalls,
278
+ agentUpdateOneById: jest
279
+ .spyOn(KubernetesAiAgentService, "updateOneById")
280
+ .mockResolvedValue(1) as unknown as SpyCalls,
281
+ clusterUpdateOneBy: jest
282
+ .spyOn(KubernetesClusterService, "updateOneBy")
283
+ .mockResolvedValue(1) as unknown as SpyCalls,
284
+ feed: jest
285
+ .spyOn(KubernetesClusterFeedService, "createKubernetesClusterFeedItem")
286
+ .mockResolvedValue(undefined) as unknown as SpyCalls,
287
+ retire: jest
288
+ .spyOn(KubernetesAiAgentService, "retireLegacyRunnerIfUnused")
289
+ .mockResolvedValue("not_due") as unknown as SpyCalls,
290
+ };
291
+ }
292
+
293
+ function written(harness: HeartbeatHarness): {
294
+ id: ObjectID;
295
+ data: Record<string, unknown>;
296
+ } {
297
+ expect(harness.agentUpdateColumns.mock.calls).toHaveLength(1);
298
+ return harness.agentUpdateColumns.mock.calls[0]![0] as {
299
+ id: ObjectID;
300
+ data: Record<string, unknown>;
301
+ };
302
+ }
303
+
304
+ test("marks the agent alive and connected in one hook-free write", async () => {
305
+ const harness: HeartbeatHarness = setUp();
306
+ const before: number = Date.now();
307
+
308
+ await KubernetesAiAgentService.heartbeat({ agent: makeAgent() });
309
+
310
+ const write: { id: ObjectID; data: Record<string, unknown> } =
311
+ written(harness);
312
+ expect(write.id.toString()).toBe(AGENT_ID.toString());
313
+ expect(write.data["connectionStatus"]).toBe("connected");
314
+ expect(
315
+ (write.data["lastAliveAt"] as Date).getTime(),
316
+ ).toBeGreaterThanOrEqual(before);
317
+ expect(harness.agentUpdateOneById.mock.calls).toHaveLength(0);
318
+ });
319
+
320
+ // Every 30 seconds per cluster: the common case must not read anything.
321
+ test.each([
322
+ ["without a posture", undefined],
323
+ ["with an unchanged read-only posture", { allowWrites: false }],
324
+ [
325
+ "with writes it already had",
326
+ { allowWrites: true, writeNamespaces: ["web"] },
327
+ ],
328
+ ])(
329
+ "a normal heartbeat (%s) reads no cluster row",
330
+ async (_label: string, posture: unknown) => {
331
+ const harness: HeartbeatHarness = setUp();
332
+ const agent: KubernetesAiAgent = makeAgent();
333
+ if (posture && (posture as JSONObject)["allowWrites"] === true) {
334
+ agent.posture = {
335
+ clusterIdentifier: "prod-us",
336
+ inCluster: true,
337
+ allowWrites: true,
338
+ };
339
+ }
340
+
341
+ await KubernetesAiAgentService.heartbeat({ agent, posture });
342
+
343
+ expect(harness.clusterFindOneBy.mock.calls).toHaveLength(0);
344
+ expect(harness.agentUpdateColumns.mock.calls).toHaveLength(1);
345
+ },
346
+ );
347
+
348
+ test("stores the reported posture with in-cluster and the identifier registration stored forced", async () => {
349
+ const harness: HeartbeatHarness = setUp();
350
+
351
+ await KubernetesAiAgentService.heartbeat({
352
+ agent: makeAgent({
353
+ posture: {
354
+ clusterIdentifier: "Prod-US",
355
+ inCluster: true,
356
+ allowWrites: false,
357
+ },
358
+ }),
359
+ agentVersion: "14.2.0",
360
+ posture: {
361
+ clusterIdentifier: "someone-else",
362
+ inCluster: false,
363
+ allowWrites: false,
364
+ writeNamespaces: ["web"],
365
+ podNamespace: "oneuptime-agent",
366
+ kubectlVersion: "v1.33.2",
367
+ },
368
+ });
369
+
370
+ const write: { data: Record<string, unknown> } = written(harness);
371
+ expect(write.data["posture"]).toEqual({
372
+ clusterIdentifier: "Prod-US",
373
+ inCluster: true,
374
+ allowWrites: false,
375
+ writeNamespaces: ["web"],
376
+ podNamespace: "oneuptime-agent",
377
+ kubectlVersion: "v1.33.2",
378
+ });
379
+ expect(write.data["agentVersion"]).toBe("14.2.0");
380
+ expect(harness.clusterFindOneBy.mock.calls).toHaveLength(0);
381
+ });
382
+
383
+ test("a stored posture without an identifier: reads the agent's own cluster (in its project, as root) and forces its identifier", async () => {
384
+ const harness: HeartbeatHarness = setUp({
385
+ cluster: makeCluster({ clusterIdentifier: "Prod-US" }),
386
+ });
387
+
388
+ await KubernetesAiAgentService.heartbeat({
389
+ agent: makeAgent({ posture: { inCluster: true, allowWrites: false } }),
390
+ posture: { clusterIdentifier: "someone-else", allowWrites: false },
391
+ });
392
+
393
+ expect(harness.clusterFindOneBy.mock.calls).toHaveLength(1);
394
+ const read: {
395
+ query: Record<string, unknown>;
396
+ props: Record<string, unknown>;
397
+ } = harness.clusterFindOneBy.mock.calls[0]![0] as {
398
+ query: Record<string, unknown>;
399
+ props: Record<string, unknown>;
400
+ };
401
+
402
+ expect(read.query["_id"]).toBe(CLUSTER_ID.toString());
403
+ expect(read.query["projectId"]).toBe(PROJECT_ID);
404
+ expect(read.props["isRoot"]).toBe(true);
405
+ expect(
406
+ (written(harness).data["posture"] as JSONObject)["clusterIdentifier"],
407
+ ).toBe("Prod-US");
408
+ });
409
+
410
+ test("a blank stored identifier counts as none", async () => {
411
+ const harness: HeartbeatHarness = setUp();
412
+
413
+ await KubernetesAiAgentService.heartbeat({
414
+ agent: makeAgent({
415
+ posture: { clusterIdentifier: " ", inCluster: true },
416
+ }),
417
+ posture: { allowWrites: false },
418
+ });
419
+
420
+ expect(harness.clusterFindOneBy.mock.calls).toHaveLength(1);
421
+ expect(
422
+ (written(harness).data["posture"] as JSONObject)["clusterIdentifier"],
423
+ ).toBe("prod-us");
424
+ });
425
+
426
+ test("with neither a stored identifier nor a cluster row, the reported identifier is still never trusted", async () => {
427
+ const harness: HeartbeatHarness = setUp({ cluster: null });
428
+
429
+ await KubernetesAiAgentService.heartbeat({
430
+ agent: makeAgent({ posture: null }),
431
+ posture: { clusterIdentifier: "other", allowWrites: false },
432
+ });
433
+
434
+ const posture: JSONObject = written(harness).data["posture"] as JSONObject;
435
+ expect(posture).not.toHaveProperty("clusterIdentifier");
436
+ expect(posture["inCluster"]).toBe(true);
437
+ });
438
+
439
+ test("a heartbeat without a posture never needs the cluster row, even when the stored posture has no identifier", async () => {
440
+ const harness: HeartbeatHarness = setUp();
441
+
442
+ await KubernetesAiAgentService.heartbeat({
443
+ agent: makeAgent({ posture: null }),
444
+ });
445
+
446
+ expect(harness.clusterFindOneBy.mock.calls).toHaveLength(0);
447
+ expect(written(harness).data).not.toHaveProperty("posture");
448
+ });
449
+
450
+ test.each([
451
+ ["no posture", undefined],
452
+ ["a string", "allowWrites=true"],
453
+ ["null", null],
454
+ ])(
455
+ "%s keeps the stored posture (only liveness is written)",
456
+ async (_label: string, posture: unknown) => {
457
+ const harness: HeartbeatHarness = setUp();
458
+
459
+ await KubernetesAiAgentService.heartbeat({ agent: makeAgent(), posture });
460
+
461
+ expect(written(harness).data).not.toHaveProperty("posture");
462
+ },
463
+ );
464
+
465
+ test("an empty or missing version is not written", async () => {
466
+ const harness: HeartbeatHarness = setUp();
467
+
468
+ await KubernetesAiAgentService.heartbeat({
469
+ agent: makeAgent(),
470
+ agentVersion: " ",
471
+ });
472
+
473
+ expect(written(harness).data).not.toHaveProperty("agentVersion");
474
+ });
475
+
476
+ test("an agent without an id is a programming error", async () => {
477
+ setUp();
478
+ const agent: KubernetesAiAgent = makeAgent();
479
+ agent._id = undefined as unknown as string;
480
+
481
+ await expect(
482
+ KubernetesAiAgentService.heartbeat({ agent }),
483
+ ).rejects.toBeInstanceOf(BadDataException);
484
+ });
485
+
486
+ describe("write access appearing", () => {
487
+ test("on a cluster nobody configured with fixes Off: Ask for approval, and a feed item", async () => {
488
+ const harness: HeartbeatHarness = setUp();
489
+
490
+ await KubernetesAiAgentService.heartbeat({
491
+ agent: makeAgent(),
492
+ posture: { allowWrites: true, writeNamespaces: ["web"] },
493
+ });
494
+
495
+ expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(1);
496
+ expect(
497
+ (
498
+ harness.clusterUpdateOneBy.mock.calls[0]![0] as {
499
+ data: Record<string, unknown>;
500
+ }
501
+ ).data,
502
+ ).toEqual({
503
+ aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
504
+ });
505
+
506
+ expect(harness.feed.mock.calls).toHaveLength(1);
507
+ const text: string = String(
508
+ (harness.feed.mock.calls[0]![0] as Record<string, unknown>)[
509
+ "feedInfoInMarkdown"
510
+ ],
511
+ );
512
+ expect(text).toContain("can now make changes in web");
513
+ expect(text).toContain('"Ask for approval"');
514
+ expect(text).toContain("AI agent page");
515
+ });
516
+
517
+ test("never on a cluster someone configured", async () => {
518
+ const harness: HeartbeatHarness = setUp({
519
+ cluster: makeCluster({ aiAccessConfiguredAt: msAgo(HOUR_MS) }),
520
+ });
521
+
522
+ await KubernetesAiAgentService.heartbeat({
523
+ agent: makeAgent(),
524
+ posture: { allowWrites: true },
525
+ });
526
+
527
+ expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(0);
528
+ expect(harness.feed.mock.calls).toHaveLength(0);
529
+ });
530
+
531
+ test.each([
532
+ KubernetesAiRemediationMode.RequireApproval,
533
+ KubernetesAiRemediationMode.Automatic,
534
+ KubernetesAiRemediationMode.BypassApproval,
535
+ ])(
536
+ "never changes a mode that is not Off (%s)",
537
+ async (mode: KubernetesAiRemediationMode) => {
538
+ const harness: HeartbeatHarness = setUp({
539
+ cluster: makeCluster({ aiRemediationMode: mode }),
540
+ });
541
+
542
+ await KubernetesAiAgentService.heartbeat({
543
+ agent: makeAgent(),
544
+ posture: { allowWrites: true },
545
+ });
546
+
547
+ expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(0);
548
+ },
549
+ );
550
+
551
+ test("only when writes APPEAR: an agent that already had them changes nothing", async () => {
552
+ const harness: HeartbeatHarness = setUp();
553
+
554
+ await KubernetesAiAgentService.heartbeat({
555
+ agent: makeAgent({
556
+ posture: {
557
+ clusterIdentifier: "prod-us",
558
+ inCluster: true,
559
+ allowWrites: true,
560
+ },
561
+ }),
562
+ posture: { allowWrites: true },
563
+ });
564
+
565
+ expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(0);
566
+ });
567
+
568
+ test("an agent that stays read-only changes nothing", async () => {
569
+ const harness: HeartbeatHarness = setUp();
570
+
571
+ await KubernetesAiAgentService.heartbeat({
572
+ agent: makeAgent(),
573
+ posture: { allowWrites: false },
574
+ });
575
+
576
+ expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(0);
577
+ });
578
+
579
+ test("a heartbeat without a posture is not writes appearing", async () => {
580
+ const harness: HeartbeatHarness = setUp();
581
+
582
+ await KubernetesAiAgentService.heartbeat({ agent: makeAgent() });
583
+
584
+ expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(0);
585
+ });
586
+
587
+ test("a stored posture that was never set counts as read-only before", async () => {
588
+ const harness: HeartbeatHarness = setUp();
589
+
590
+ await KubernetesAiAgentService.heartbeat({
591
+ agent: makeAgent({ posture: null }),
592
+ posture: { allowWrites: true },
593
+ });
594
+
595
+ expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(1);
596
+ });
597
+
598
+ test("the defaults are conditional on the cluster still being unconfigured; a lost race writes no feed item", async () => {
599
+ const harness: HeartbeatHarness = setUp();
600
+ (
601
+ harness.clusterUpdateOneBy as unknown as {
602
+ mockResolvedValue: (value: number) => void;
603
+ }
604
+ ).mockResolvedValue(0);
605
+
606
+ await KubernetesAiAgentService.heartbeat({
607
+ agent: makeAgent(),
608
+ posture: { allowWrites: true },
609
+ });
610
+
611
+ const query: Record<string, unknown> = (
612
+ harness.clusterUpdateOneBy.mock.calls[0]![0] as {
613
+ query: Record<string, unknown>;
614
+ }
615
+ ).query;
616
+ expect(query).toHaveProperty("aiAccessConfiguredAt");
617
+ expect(harness.feed.mock.calls).toHaveLength(0);
618
+ });
619
+
620
+ test("reads the agent's own cluster (in its project, as root) for its current AI settings, after the liveness write", async () => {
621
+ const harness: HeartbeatHarness = setUp();
622
+
623
+ await KubernetesAiAgentService.heartbeat({
624
+ agent: makeAgent(),
625
+ posture: { allowWrites: true },
626
+ });
627
+
628
+ expect(harness.clusterFindOneBy.mock.calls).toHaveLength(1);
629
+ const read: {
630
+ query: Record<string, unknown>;
631
+ select: Record<string, boolean>;
632
+ props: Record<string, unknown>;
633
+ } = harness.clusterFindOneBy.mock.calls[0]![0] as {
634
+ query: Record<string, unknown>;
635
+ select: Record<string, boolean>;
636
+ props: Record<string, unknown>;
637
+ };
638
+ expect(read.query["_id"]).toBe(CLUSTER_ID.toString());
639
+ expect(read.query["projectId"]).toBe(PROJECT_ID);
640
+ expect(read.select["aiAccessConfiguredAt"]).toBe(true);
641
+ expect(read.select["aiRemediationMode"]).toBe(true);
642
+ expect(read.props["isRoot"]).toBe(true);
643
+ expect(written(harness).data["posture"]).toBeDefined();
644
+ });
645
+
646
+ test("the cluster row is read once, even when the identifier needed it too", async () => {
647
+ const harness: HeartbeatHarness = setUp();
648
+
649
+ await KubernetesAiAgentService.heartbeat({
650
+ agent: makeAgent({ posture: null }),
651
+ posture: { allowWrites: true },
652
+ });
653
+
654
+ expect(harness.clusterFindOneBy.mock.calls).toHaveLength(1);
655
+ expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(1);
656
+ });
657
+
658
+ test("a cluster row that is gone changes nothing", async () => {
659
+ const harness: HeartbeatHarness = setUp({ cluster: null });
660
+
661
+ await expect(
662
+ KubernetesAiAgentService.heartbeat({
663
+ agent: makeAgent(),
664
+ posture: { allowWrites: true },
665
+ }),
666
+ ).resolves.toBeUndefined();
667
+
668
+ expect(harness.clusterUpdateOneBy.mock.calls).toHaveLength(0);
669
+ expect(harness.feed.mock.calls).toHaveLength(0);
670
+ });
671
+
672
+ /*
673
+ * The liveness is already written; a heartbeat that failed here would
674
+ * make the agent think its connection broke. The next registration
675
+ * applies the defaults again.
676
+ */
677
+ test("a failure applying the defaults never fails the heartbeat, and no feed item claims a change", async () => {
678
+ const harness: HeartbeatHarness = setUp();
679
+ (
680
+ harness.clusterUpdateOneBy as unknown as {
681
+ mockRejectedValue: (error: Error) => void;
682
+ }
683
+ ).mockRejectedValue(new Error("database is down"));
684
+
685
+ await expect(
686
+ KubernetesAiAgentService.heartbeat({
687
+ agent: makeAgent(),
688
+ posture: { allowWrites: true },
689
+ }),
690
+ ).resolves.toBeUndefined();
691
+
692
+ expect(
693
+ (written(harness).data["posture"] as JSONObject)["allowWrites"],
694
+ ).toBe(true);
695
+ expect(harness.feed.mock.calls).toHaveLength(0);
696
+ expect(
697
+ JSON.stringify((logger.error as unknown as SpyCalls).mock.calls),
698
+ ).toContain("first-connection defaults");
699
+ });
700
+ });
701
+
702
+ test("starts the previous-Runner check without waiting for it", async () => {
703
+ const harness: HeartbeatHarness = setUp();
704
+ let resolveRetirement: (
705
+ value: LegacyRunnerRetirementOutcome,
706
+ ) => void = () => {
707
+ return undefined;
708
+ };
709
+ (
710
+ harness.retire as unknown as {
711
+ mockImplementation: (
712
+ impl: () => Promise<LegacyRunnerRetirementOutcome>,
713
+ ) => void;
714
+ }
715
+ ).mockImplementation(() => {
716
+ return new Promise<LegacyRunnerRetirementOutcome>(
717
+ (resolve: (value: LegacyRunnerRetirementOutcome) => void) => {
718
+ resolveRetirement = resolve;
719
+ },
720
+ );
721
+ });
722
+
723
+ // Resolves although the check never finishes.
724
+ await KubernetesAiAgentService.heartbeat({ agent: makeAgent() });
725
+
726
+ expect(harness.retire.mock.calls).toHaveLength(1);
727
+ const call: { agent: KubernetesAiAgent; cluster?: KubernetesCluster } =
728
+ harness.retire.mock.calls[0]![0] as {
729
+ agent: KubernetesAiAgent;
730
+ cluster?: KubernetesCluster;
731
+ };
732
+ expect(call.agent.id!.toString()).toBe(AGENT_ID.toString());
733
+ // The hourly check reads the cluster itself, only when it is due.
734
+ expect(call).not.toHaveProperty("cluster");
735
+ expect(harness.clusterFindOneBy.mock.calls).toHaveLength(0);
736
+
737
+ resolveRetirement("not_due");
738
+ });
739
+
740
+ test("hands the previous-Runner check the cluster row when the heartbeat already read it", async () => {
741
+ const harness: HeartbeatHarness = setUp();
742
+
743
+ await KubernetesAiAgentService.heartbeat({
744
+ agent: makeAgent({ posture: null }),
745
+ posture: { allowWrites: false },
746
+ });
747
+
748
+ const call: { cluster?: KubernetesCluster } = harness.retire.mock
749
+ .calls[0]![0] as { cluster?: KubernetesCluster };
750
+ expect(call.cluster!.id!.toString()).toBe(CLUSTER_ID.toString());
751
+ expect(harness.clusterFindOneBy.mock.calls).toHaveLength(1);
752
+ });
753
+
754
+ test("a failing previous-Runner check never fails the heartbeat", async () => {
755
+ const harness: HeartbeatHarness = setUp();
756
+ (
757
+ harness.retire as unknown as {
758
+ mockRejectedValue: (error: Error) => void;
759
+ }
760
+ ).mockRejectedValue(new Error("boom"));
761
+
762
+ await expect(
763
+ KubernetesAiAgentService.heartbeat({ agent: makeAgent() }),
764
+ ).resolves.toBeUndefined();
765
+ });
766
+ });
767
+
768
+ describe("markDisconnected", () => {
769
+ test("writes only the disconnected status — lastAliveAt stays the truth about the last heartbeat", async () => {
770
+ const update: SpyCalls = jest
771
+ .spyOn(KubernetesAiAgentService, "updateColumnsByIdWithoutHooks")
772
+ .mockResolvedValue(undefined) as unknown as SpyCalls;
773
+
774
+ await KubernetesAiAgentService.markDisconnected({
775
+ kubernetesAiAgentId: AGENT_ID,
776
+ });
777
+
778
+ expect(update.mock.calls[0]![0]).toEqual({
779
+ id: AGENT_ID,
780
+ data: { connectionStatus: "disconnected" },
781
+ });
782
+ });
783
+
784
+ test("requires the agent id", async () => {
785
+ await expect(
786
+ KubernetesAiAgentService.markDisconnected({
787
+ kubernetesAiAgentId: undefined as unknown as ObjectID,
788
+ }),
789
+ ).rejects.toBeInstanceOf(BadDataException);
790
+ });
791
+
792
+ // The sign-off is what admits a replacement pod at once.
793
+ test("a signed-off agent is offline and admits the next registration", () => {
794
+ const signedOff: KubernetesAiAgent = makeAgent({
795
+ connectionStatus: "disconnected",
796
+ lastAliveAt: msAgo(1000),
797
+ });
798
+
799
+ expect(KubernetesAiAgentService.isOnline(signedOff)).toBe(false);
800
+ expect(
801
+ KubernetesAiAgentService.getReRegistrationAdmission({ agent: signedOff }),
802
+ ).toBe("signed_off");
803
+ });
804
+ });
805
+
806
+ describe("resetAgent", () => {
807
+ const USER_MARKDOWN: string = `[Jane Doe](https://oneuptime.example/dashboard/${PROJECT_ID.toString()}/settings/users/${USER_ID.toString()})`;
808
+
809
+ interface ResetHarness {
810
+ find: SpyCalls;
811
+ update: SpyCalls;
812
+ feed: SpyCalls;
813
+ userName: SpyCalls;
814
+ }
815
+
816
+ function setUp(row: KubernetesAiAgent | null = makeAgent()): ResetHarness {
817
+ return {
818
+ find: jest
819
+ .spyOn(KubernetesAiAgentService, "findForCluster")
820
+ .mockResolvedValue(row) as unknown as SpyCalls,
821
+ update: jest
822
+ .spyOn(KubernetesAiAgentService, "updateOneById")
823
+ .mockResolvedValue(1) as unknown as SpyCalls,
824
+ feed: jest
825
+ .spyOn(KubernetesClusterFeedService, "createKubernetesClusterFeedItem")
826
+ .mockResolvedValue(undefined) as unknown as SpyCalls,
827
+ userName: jest
828
+ .spyOn(UserService, "getUserMarkdownString")
829
+ .mockResolvedValue(USER_MARKDOWN) as unknown as SpyCalls,
830
+ };
831
+ }
832
+
833
+ function feedText(harness: ResetHarness): string {
834
+ expect(harness.feed.mock.calls).toHaveLength(1);
835
+ return String(
836
+ (harness.feed.mock.calls[0]![0] as Record<string, unknown>)[
837
+ "feedInfoInMarkdown"
838
+ ],
839
+ );
840
+ }
841
+
842
+ test("clears the key hash and marks the agent disconnected, as root", async () => {
843
+ const harness: ResetHarness = setUp();
844
+
845
+ await KubernetesAiAgentService.resetAgent({
846
+ projectId: PROJECT_ID,
847
+ kubernetesClusterId: CLUSTER_ID,
848
+ userId: USER_ID,
849
+ });
850
+
851
+ expect(harness.find.mock.calls[0]![0]).toEqual({
852
+ projectId: PROJECT_ID,
853
+ kubernetesClusterId: CLUSTER_ID,
854
+ });
855
+
856
+ const write: {
857
+ id: ObjectID;
858
+ data: Record<string, unknown>;
859
+ props: Record<string, unknown>;
860
+ } = harness.update.mock.calls[0]![0] as {
861
+ id: ObjectID;
862
+ data: Record<string, unknown>;
863
+ props: Record<string, unknown>;
864
+ };
865
+
866
+ expect(write.id.toString()).toBe(AGENT_ID.toString());
867
+ expect(write.data).toEqual({
868
+ keyHash: null,
869
+ connectionStatus: "disconnected",
870
+ });
871
+ expect(write.props["isRoot"]).toBe(true);
872
+ });
873
+
874
+ test("writes a feed item, attributed to the user, that says who reset it and that it comes back on its own", async () => {
875
+ const harness: ResetHarness = setUp();
876
+
877
+ await KubernetesAiAgentService.resetAgent({
878
+ projectId: PROJECT_ID,
879
+ kubernetesClusterId: CLUSTER_ID,
880
+ userId: USER_ID,
881
+ });
882
+
883
+ const item: Record<string, unknown> = harness.feed.mock
884
+ .calls[0]![0] as Record<string, unknown>;
885
+ expect(item["kubernetesClusterId"]).toBe(CLUSTER_ID);
886
+ expect(item["projectId"]).toBe(PROJECT_ID);
887
+ expect(item["userId"]).toBe(USER_ID);
888
+ expect(feedText(harness)).toBe(
889
+ `🔄 The Kubernetes AI agent was reset by **${USER_MARKDOWN}**. It reconnects on its own within a few minutes.`,
890
+ );
891
+ });
892
+
893
+ test("names the user the way other cluster feed items do: looked up in the cluster's project", async () => {
894
+ const harness: ResetHarness = setUp();
895
+
896
+ await KubernetesAiAgentService.resetAgent({
897
+ projectId: PROJECT_ID,
898
+ kubernetesClusterId: CLUSTER_ID,
899
+ userId: USER_ID,
900
+ });
901
+
902
+ expect(harness.userName.mock.calls).toEqual([
903
+ [{ userId: USER_ID, projectId: PROJECT_ID }],
904
+ ]);
905
+ });
906
+
907
+ test("without a user (a master admin), the feed item names nobody and carries no user", async () => {
908
+ const harness: ResetHarness = setUp();
909
+
910
+ await KubernetesAiAgentService.resetAgent({
911
+ projectId: PROJECT_ID,
912
+ kubernetesClusterId: CLUSTER_ID,
913
+ });
914
+
915
+ expect(harness.feed.mock.calls[0]![0]).not.toHaveProperty("userId");
916
+ expect(harness.userName.mock.calls).toHaveLength(0);
917
+ expect(feedText(harness)).toBe(
918
+ "🔄 The Kubernetes AI agent was reset. It reconnects on its own within a few minutes.",
919
+ );
920
+ });
921
+
922
+ test("a user who cannot be named (no such user) gets the plain wording, still attributed", async () => {
923
+ const harness: ResetHarness = setUp();
924
+ (
925
+ harness.userName as unknown as {
926
+ mockResolvedValue: (value: string) => void;
927
+ }
928
+ ).mockResolvedValue("");
929
+
930
+ await KubernetesAiAgentService.resetAgent({
931
+ projectId: PROJECT_ID,
932
+ kubernetesClusterId: CLUSTER_ID,
933
+ userId: USER_ID,
934
+ });
935
+
936
+ expect(feedText(harness)).toBe(
937
+ "🔄 The Kubernetes AI agent was reset. It reconnects on its own within a few minutes.",
938
+ );
939
+ expect(
940
+ (harness.feed.mock.calls[0]![0] as Record<string, unknown>)["userId"],
941
+ ).toBe(USER_ID);
942
+ });
943
+
944
+ // The reset is done by then; the wording must not be able to fail it.
945
+ test("a failing name lookup never fails the reset: the key is cleared and the feed item uses the plain wording", async () => {
946
+ const harness: ResetHarness = setUp();
947
+ (
948
+ harness.userName as unknown as {
949
+ mockRejectedValue: (error: Error) => void;
950
+ }
951
+ ).mockRejectedValue(new Error("database is down"));
952
+
953
+ await expect(
954
+ KubernetesAiAgentService.resetAgent({
955
+ projectId: PROJECT_ID,
956
+ kubernetesClusterId: CLUSTER_ID,
957
+ userId: USER_ID,
958
+ }),
959
+ ).resolves.toBeUndefined();
960
+
961
+ expect(harness.update.mock.calls).toHaveLength(1);
962
+ expect(feedText(harness)).toBe(
963
+ "🔄 The Kubernetes AI agent was reset. It reconnects on its own within a few minutes.",
964
+ );
965
+ expect(
966
+ JSON.stringify((logger.error as unknown as SpyCalls).mock.calls),
967
+ ).toContain(USER_ID.toString());
968
+ });
969
+
970
+ test("a cluster without an agent is a plain error, and nothing is written", async () => {
971
+ const harness: ResetHarness = setUp(null);
972
+
973
+ await expect(
974
+ KubernetesAiAgentService.resetAgent({
975
+ projectId: PROJECT_ID,
976
+ kubernetesClusterId: CLUSTER_ID,
977
+ }),
978
+ ).rejects.toThrow("This cluster has no Kubernetes AI agent to reset.");
979
+
980
+ expect(harness.update.mock.calls).toHaveLength(0);
981
+ expect(harness.feed.mock.calls).toHaveLength(0);
982
+ });
983
+
984
+ /*
985
+ * What the reset is for: the old key (whoever holds it) stops working at
986
+ * once, and the real pod's next registration is admitted.
987
+ */
988
+ test("after a reset the old key no longer authenticates, and the next registration is admitted", async () => {
989
+ const reset: KubernetesAiAgent = makeAgent({
990
+ keyHash: null,
991
+ connectionStatus: "disconnected",
992
+ });
993
+ jest.spyOn(KubernetesAiAgentService, "findOneBy").mockResolvedValue(reset);
994
+
995
+ await expect(
996
+ KubernetesAiAgentService.authenticate({
997
+ agentId: AGENT_ID.toString(),
998
+ agentKey: AGENT_KEY,
999
+ }),
1000
+ ).resolves.toBeNull();
1001
+
1002
+ expect(
1003
+ KubernetesAiAgentService.getReRegistrationAdmission({
1004
+ agent: reset,
1005
+ previousAgentKey: AGENT_KEY,
1006
+ }),
1007
+ ).toBe("reset");
1008
+ });
1009
+ });
1010
+
1011
+ describe("retireLegacyRunnerIfUnused", () => {
1012
+ const NOW: Date = new Date("2026-09-28T10:00:00.000Z");
1013
+
1014
+ interface RetireHarness {
1015
+ service: KubernetesAiAgentServiceClass;
1016
+ lookup: SpyCalls;
1017
+ online: SpyCalls;
1018
+ holdings: SpyCalls;
1019
+ rules: SpyCalls;
1020
+ remove: SpyCalls;
1021
+ feed: SpyCalls;
1022
+ clusterFindOneBy: SpyCalls;
1023
+ }
1024
+
1025
+ function legacyRunner(overrides: Partial<Runner> = {}): Runner {
1026
+ const runner: Runner = new Runner();
1027
+ runner.id = LEGACY_RUNNER_ID;
1028
+ runner.name = "kubernetes-agent/prod-us";
1029
+ runner.lastAlive = msAgo(
1030
+ (LEGACY_RUNNER_RETIREMENT_OFFLINE_DAYS + 1) * DAY_MS,
1031
+ NOW,
1032
+ );
1033
+ runner.connectionStatus = RunnerConnectionStatus.Disconnected;
1034
+ Object.assign(runner, overrides);
1035
+ return runner;
1036
+ }
1037
+
1038
+ function setUp(
1039
+ options: {
1040
+ runner?: Runner | null;
1041
+ online?: boolean;
1042
+ holdings?: Array<{ description: string; remedy: string }>;
1043
+ rules?: number;
1044
+ clusterRow?: KubernetesCluster | null;
1045
+ } = {},
1046
+ ): RetireHarness {
1047
+ return {
1048
+ // A fresh instance: the hourly throttle is per process (per instance).
1049
+ service: new KubernetesAiAgentServiceClass(),
1050
+ lookup: jest
1051
+ .spyOn(
1052
+ KubernetesClusterAiAccessService,
1053
+ "getLegacyAgentRunnerForCluster",
1054
+ )
1055
+ .mockResolvedValue(
1056
+ options.runner === undefined ? legacyRunner() : options.runner,
1057
+ ) as unknown as SpyCalls,
1058
+ online: jest
1059
+ .spyOn(KubernetesClusterAiAccessService, "isRunnerOnline")
1060
+ .mockReturnValue(options.online === true) as unknown as SpyCalls,
1061
+ holdings: jest
1062
+ .spyOn(
1063
+ KubernetesClusterAiAccessService,
1064
+ "getRunnerHoldingsBeyondDefaults",
1065
+ )
1066
+ .mockResolvedValue(options.holdings || []) as unknown as SpyCalls,
1067
+ rules: jest
1068
+ .spyOn(AutoRemediationRuleService, "countBy")
1069
+ .mockResolvedValue(
1070
+ new PositiveNumber(options.rules || 0),
1071
+ ) as unknown as SpyCalls,
1072
+ remove: jest
1073
+ .spyOn(RunnerService, "deleteOneBy")
1074
+ .mockResolvedValue(1) as unknown as SpyCalls,
1075
+ feed: jest
1076
+ .spyOn(KubernetesClusterFeedService, "createKubernetesClusterFeedItem")
1077
+ .mockResolvedValue(undefined) as unknown as SpyCalls,
1078
+ clusterFindOneBy: jest
1079
+ .spyOn(KubernetesClusterService, "findOneBy")
1080
+ .mockResolvedValue(
1081
+ options.clusterRow === undefined ? makeCluster() : options.clusterRow,
1082
+ ) as unknown as SpyCalls,
1083
+ };
1084
+ }
1085
+
1086
+ function retire(
1087
+ harness: RetireHarness,
1088
+ options: {
1089
+ agent?: KubernetesAiAgent;
1090
+ now?: Date;
1091
+ // false: the caller has no cluster row (the heartbeat's usual case).
1092
+ withCluster?: boolean;
1093
+ } = {},
1094
+ ): Promise<LegacyRunnerRetirementOutcome> {
1095
+ return harness.service.retireLegacyRunnerIfUnused({
1096
+ agent:
1097
+ options.agent ||
1098
+ makeAgent({
1099
+ createdAt: msAgo(
1100
+ (LEGACY_RUNNER_RETIREMENT_AGENT_AGE_HOURS + 1) * HOUR_MS,
1101
+ NOW,
1102
+ ),
1103
+ }),
1104
+ ...(options.withCluster === false ? {} : { cluster: makeCluster() }),
1105
+ now: options.now || NOW,
1106
+ });
1107
+ }
1108
+
1109
+ describe("without the cluster row", () => {
1110
+ test("reads the agent's own cluster (in its project, as root) once the check is due, and checks against it", async () => {
1111
+ const harness: RetireHarness = setUp();
1112
+
1113
+ await expect(retire(harness, { withCluster: false })).resolves.toBe(
1114
+ "retired",
1115
+ );
1116
+
1117
+ expect(harness.clusterFindOneBy.mock.calls).toHaveLength(1);
1118
+ const read: {
1119
+ query: Record<string, unknown>;
1120
+ props: Record<string, unknown>;
1121
+ } = harness.clusterFindOneBy.mock.calls[0]![0] as {
1122
+ query: Record<string, unknown>;
1123
+ props: Record<string, unknown>;
1124
+ };
1125
+ expect(read.query["_id"]).toBe(CLUSTER_ID.toString());
1126
+ expect(read.query["projectId"]).toBe(PROJECT_ID);
1127
+ expect(read.props["isRoot"]).toBe(true);
1128
+ expect(harness.lookup.mock.calls[0]![0]).toEqual({
1129
+ projectId: PROJECT_ID,
1130
+ kubernetesClusterId: CLUSTER_ID,
1131
+ clusterIdentifier: "prod-us",
1132
+ });
1133
+ });
1134
+
1135
+ test("a check that is not due reads nothing", async () => {
1136
+ const harness: RetireHarness = setUp({ online: true });
1137
+
1138
+ await retire(harness, { withCluster: false });
1139
+ await expect(
1140
+ retire(harness, {
1141
+ withCluster: false,
1142
+ now: new Date(NOW.getTime() + 60 * 1000),
1143
+ }),
1144
+ ).resolves.toBe("not_due");
1145
+
1146
+ expect(harness.clusterFindOneBy.mock.calls).toHaveLength(1);
1147
+ });
1148
+
1149
+ test("an agent too new to retire anything reads nothing", async () => {
1150
+ const harness: RetireHarness = setUp();
1151
+
1152
+ await expect(
1153
+ retire(harness, {
1154
+ withCluster: false,
1155
+ agent: makeAgent({ createdAt: msAgo(HOUR_MS, NOW) }),
1156
+ }),
1157
+ ).resolves.toBe("agent_too_new");
1158
+
1159
+ expect(harness.clusterFindOneBy.mock.calls).toHaveLength(0);
1160
+ });
1161
+
1162
+ test("a cluster row that is gone: nothing to do", async () => {
1163
+ const harness: RetireHarness = setUp({ clusterRow: null });
1164
+
1165
+ await expect(retire(harness, { withCluster: false })).resolves.toBe(
1166
+ "no_legacy_runner",
1167
+ );
1168
+ expect(harness.lookup.mock.calls).toHaveLength(0);
1169
+ expect(harness.remove.mock.calls).toHaveLength(0);
1170
+ });
1171
+
1172
+ test("an agent that names no cluster reads nothing and does nothing", async () => {
1173
+ const harness: RetireHarness = setUp();
1174
+
1175
+ await expect(
1176
+ retire(harness, {
1177
+ withCluster: false,
1178
+ agent: makeAgent({
1179
+ kubernetesClusterId: undefined,
1180
+ createdAt: msAgo(3 * DAY_MS, NOW),
1181
+ }),
1182
+ }),
1183
+ ).resolves.toBe("no_legacy_runner");
1184
+ expect(harness.clusterFindOneBy.mock.calls).toHaveLength(0);
1185
+ expect(harness.remove.mock.calls).toHaveLength(0);
1186
+ });
1187
+
1188
+ test("a failing cluster read is reported, never thrown", async () => {
1189
+ const harness: RetireHarness = setUp();
1190
+ (
1191
+ harness.clusterFindOneBy as unknown as {
1192
+ mockRejectedValue: (error: Error) => void;
1193
+ }
1194
+ ).mockRejectedValue(new Error("database is down"));
1195
+
1196
+ await expect(retire(harness, { withCluster: false })).resolves.toBe(
1197
+ "failed",
1198
+ );
1199
+ expect(harness.remove.mock.calls).toHaveLength(0);
1200
+ expect(
1201
+ JSON.stringify((logger.error as unknown as SpyCalls).mock.calls),
1202
+ ).toContain(CLUSTER_ID.toString());
1203
+ });
1204
+ });
1205
+
1206
+ test("a caller that has the cluster row is not made to read it again", async () => {
1207
+ const harness: RetireHarness = setUp();
1208
+
1209
+ await expect(retire(harness)).resolves.toBe("retired");
1210
+ expect(harness.clusterFindOneBy.mock.calls).toHaveLength(0);
1211
+ });
1212
+
1213
+ test("a Runner silent for over a week, holding nothing, named by no rule, is deleted (hard, as root) and the feed says so", async () => {
1214
+ const harness: RetireHarness = setUp();
1215
+
1216
+ await expect(retire(harness)).resolves.toBe("retired");
1217
+
1218
+ expect(harness.remove.mock.calls).toHaveLength(1);
1219
+ expect(harness.remove.mock.calls[0]![0]).toEqual({
1220
+ query: { _id: LEGACY_RUNNER_ID.toString(), projectId: PROJECT_ID },
1221
+ props: { isRoot: true },
1222
+ });
1223
+
1224
+ expect(harness.feed.mock.calls).toHaveLength(1);
1225
+ const text: string = String(
1226
+ (harness.feed.mock.calls[0]![0] as Record<string, unknown>)[
1227
+ "feedInfoInMarkdown"
1228
+ ],
1229
+ );
1230
+ expect(text).toContain("kubernetes-agent/prod-us");
1231
+ expect(text).toContain("was removed");
1232
+ expect(text).toContain("Kubernetes AI agent replaced it");
1233
+ });
1234
+
1235
+ test("asks for this cluster's previous Runner, and for its holdings against this cluster", async () => {
1236
+ const harness: RetireHarness = setUp();
1237
+
1238
+ await retire(harness);
1239
+
1240
+ expect(harness.lookup.mock.calls[0]![0]).toEqual({
1241
+ projectId: PROJECT_ID,
1242
+ kubernetesClusterId: CLUSTER_ID,
1243
+ clusterIdentifier: "prod-us",
1244
+ });
1245
+ const holdingsCall: Record<string, unknown> = harness.holdings.mock
1246
+ .calls[0]![0] as Record<string, unknown>;
1247
+ expect(String(holdingsCall["clusterId"])).toBe(CLUSTER_ID.toString());
1248
+ expect(String(holdingsCall["projectId"])).toBe(PROJECT_ID.toString());
1249
+ expect((holdingsCall["runner"] as Runner).id!.toString()).toBe(
1250
+ LEGACY_RUNNER_ID.toString(),
1251
+ );
1252
+ });
1253
+
1254
+ test("the rule check looks for rules in the project that name the Runner, as root", async () => {
1255
+ const harness: RetireHarness = setUp();
1256
+
1257
+ await retire(harness);
1258
+
1259
+ const count: {
1260
+ query: Record<string, unknown>;
1261
+ props: Record<string, unknown>;
1262
+ } = harness.rules.mock.calls[0]![0] as {
1263
+ query: Record<string, unknown>;
1264
+ props: Record<string, unknown>;
1265
+ };
1266
+ expect(count.query["projectId"]).toBe(PROJECT_ID);
1267
+ expect(count.query["commandRunners"]).toBeDefined();
1268
+ expect(count.props["isRoot"]).toBe(true);
1269
+ });
1270
+
1271
+ test("an agent younger than a day keeps the Runner — a rollback may still need it", async () => {
1272
+ const harness: RetireHarness = setUp();
1273
+
1274
+ await expect(
1275
+ retire(harness, {
1276
+ agent: makeAgent({
1277
+ createdAt: msAgo(
1278
+ (LEGACY_RUNNER_RETIREMENT_AGENT_AGE_HOURS - 1) * HOUR_MS,
1279
+ NOW,
1280
+ ),
1281
+ }),
1282
+ }),
1283
+ ).resolves.toBe("agent_too_new");
1284
+
1285
+ expect(harness.lookup.mock.calls).toHaveLength(0);
1286
+ expect(harness.remove.mock.calls).toHaveLength(0);
1287
+ });
1288
+
1289
+ test("an agent whose age is unknown keeps the Runner", async () => {
1290
+ const harness: RetireHarness = setUp();
1291
+ const agent: KubernetesAiAgent = makeAgent();
1292
+ delete (agent as unknown as Record<string, unknown>)["createdAt"];
1293
+
1294
+ await expect(retire(harness, { agent })).resolves.toBe("agent_too_new");
1295
+ expect(harness.remove.mock.calls).toHaveLength(0);
1296
+ });
1297
+
1298
+ test("no previous Runner: nothing to do", async () => {
1299
+ const harness: RetireHarness = setUp({ runner: null });
1300
+
1301
+ await expect(retire(harness)).resolves.toBe("no_legacy_runner");
1302
+ expect(harness.remove.mock.calls).toHaveLength(0);
1303
+ });
1304
+
1305
+ test("an online Runner is never removed", async () => {
1306
+ const harness: RetireHarness = setUp({ online: true });
1307
+
1308
+ await expect(retire(harness)).resolves.toBe(
1309
+ "legacy_runner_recently_online",
1310
+ );
1311
+ expect(harness.remove.mock.calls).toHaveLength(0);
1312
+ });
1313
+
1314
+ test("a Runner heard from within the week is kept", async () => {
1315
+ const harness: RetireHarness = setUp({
1316
+ runner: legacyRunner({
1317
+ lastAlive: msAgo(
1318
+ (LEGACY_RUNNER_RETIREMENT_OFFLINE_DAYS - 1) * DAY_MS,
1319
+ NOW,
1320
+ ),
1321
+ }),
1322
+ });
1323
+
1324
+ await expect(retire(harness)).resolves.toBe(
1325
+ "legacy_runner_recently_online",
1326
+ );
1327
+ expect(harness.remove.mock.calls).toHaveLength(0);
1328
+ });
1329
+
1330
+ test("a Runner that never heartbeated is kept: its age is unknown", async () => {
1331
+ const runner: Runner = legacyRunner();
1332
+ delete (runner as unknown as Record<string, unknown>)["lastAlive"];
1333
+ const harness: RetireHarness = setUp({ runner });
1334
+
1335
+ await expect(retire(harness)).resolves.toBe(
1336
+ "legacy_runner_recently_online",
1337
+ );
1338
+ expect(harness.remove.mock.calls).toHaveLength(0);
1339
+ });
1340
+
1341
+ test("a Runner an operator gave more than its defaults is kept", async () => {
1342
+ const harness: RetireHarness = setUp({
1343
+ holdings: [
1344
+ {
1345
+ description: "1 Runner credential(s) assigned",
1346
+ remedy: "unassign its 1 Runner credential(s)",
1347
+ },
1348
+ ],
1349
+ });
1350
+
1351
+ await expect(retire(harness)).resolves.toBe("legacy_runner_holds_more");
1352
+ expect(harness.rules.mock.calls).toHaveLength(0);
1353
+ expect(harness.remove.mock.calls).toHaveLength(0);
1354
+ });
1355
+
1356
+ // Deleting it would silently widen that rule to "any Runner".
1357
+ test("a Runner an auto-remediation rule names is kept", async () => {
1358
+ const harness: RetireHarness = setUp({ rules: 1 });
1359
+
1360
+ await expect(retire(harness)).resolves.toBe("legacy_runner_in_rule");
1361
+ expect(harness.remove.mock.calls).toHaveLength(0);
1362
+ expect(harness.feed.mock.calls).toHaveLength(0);
1363
+ });
1364
+
1365
+ test("checks at most once an hour per agent", async () => {
1366
+ const harness: RetireHarness = setUp({ online: true });
1367
+
1368
+ await expect(retire(harness)).resolves.toBe(
1369
+ "legacy_runner_recently_online",
1370
+ );
1371
+ await expect(
1372
+ retire(harness, { now: new Date(NOW.getTime() + HOUR_MS - 1000) }),
1373
+ ).resolves.toBe("not_due");
1374
+ expect(harness.lookup.mock.calls).toHaveLength(1);
1375
+
1376
+ await expect(
1377
+ retire(harness, {
1378
+ now: new Date(
1379
+ NOW.getTime() + LEGACY_RUNNER_RETIREMENT_CHECK_INTERVAL_MS,
1380
+ ),
1381
+ }),
1382
+ ).resolves.toBe("legacy_runner_recently_online");
1383
+ expect(harness.lookup.mock.calls).toHaveLength(2);
1384
+ });
1385
+
1386
+ test("the throttle is per agent", async () => {
1387
+ const harness: RetireHarness = setUp({ online: true });
1388
+
1389
+ await retire(harness);
1390
+ await expect(
1391
+ retire(harness, {
1392
+ agent: makeAgent({
1393
+ _id: "77777777-7777-4777-8777-777777777777",
1394
+ createdAt: msAgo(3 * DAY_MS, NOW),
1395
+ }),
1396
+ }),
1397
+ ).resolves.toBe("legacy_runner_recently_online");
1398
+
1399
+ expect(harness.lookup.mock.calls).toHaveLength(2);
1400
+ });
1401
+
1402
+ test("never throws: a failing lookup is logged and reported", async () => {
1403
+ const harness: RetireHarness = setUp();
1404
+ (
1405
+ harness.holdings as unknown as {
1406
+ mockRejectedValue: (error: Error) => void;
1407
+ }
1408
+ ).mockRejectedValue(new Error("database is down"));
1409
+
1410
+ await expect(retire(harness)).resolves.toBe("failed");
1411
+ expect(harness.remove.mock.calls).toHaveLength(0);
1412
+ });
1413
+
1414
+ test("the thresholds", () => {
1415
+ expect(LEGACY_RUNNER_RETIREMENT_AGENT_AGE_HOURS).toBe(24);
1416
+ expect(LEGACY_RUNNER_RETIREMENT_OFFLINE_DAYS).toBe(7);
1417
+ expect(LEGACY_RUNNER_RETIREMENT_CHECK_INTERVAL_MS).toBe(HOUR_MS);
1418
+
1419
+ expect(
1420
+ KubernetesAiAgentServiceClass.isOldEnoughToRetireLegacyRunner(
1421
+ { createdAt: msAgo(24 * HOUR_MS, NOW) },
1422
+ NOW,
1423
+ ),
1424
+ ).toBe(true);
1425
+ expect(
1426
+ KubernetesAiAgentServiceClass.isOldEnoughToRetireLegacyRunner(
1427
+ { createdAt: msAgo(24 * HOUR_MS - 1, NOW) },
1428
+ NOW,
1429
+ ),
1430
+ ).toBe(false);
1431
+ expect(
1432
+ KubernetesAiAgentServiceClass.hasBeenOfflineLongEnoughToRetire(
1433
+ { lastAlive: msAgo(7 * DAY_MS, NOW) },
1434
+ NOW,
1435
+ ),
1436
+ ).toBe(true);
1437
+ expect(
1438
+ KubernetesAiAgentServiceClass.hasBeenOfflineLongEnoughToRetire(
1439
+ { lastAlive: msAgo(7 * DAY_MS - 1, NOW) },
1440
+ NOW,
1441
+ ),
1442
+ ).toBe(false);
1443
+ });
1444
+ });