@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,1523 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ jest,
8
+ test,
9
+ } from "@jest/globals";
10
+ import {
11
+ act,
12
+ cleanup,
13
+ fireEvent,
14
+ render,
15
+ screen,
16
+ waitFor,
17
+ within,
18
+ } from "@testing-library/react";
19
+ import React from "react";
20
+ import {
21
+ MemoryRouter,
22
+ NavigateFunction,
23
+ Route as PageRoute,
24
+ Routes,
25
+ useNavigate,
26
+ } from "react-router-dom";
27
+ import KubernetesClusterAIInsights, {
28
+ AI_INSIGHTS_EMPTY_DESCRIPTION,
29
+ AI_INSIGHTS_EMPTY_TITLE,
30
+ AI_INSIGHTS_PAGE_SUBTITLE,
31
+ AI_INSIGHTS_PAGE_TITLE,
32
+ KUBECTL_COMMANDS_CARD_TITLE,
33
+ KUBECTL_COMMANDS_EMPTY_MESSAGE,
34
+ KUBECTL_JOB_ORIGIN_LABELS,
35
+ KUBECTL_JOBS_TABLE_PREFERENCES_KEY,
36
+ KubernetesAiInsights,
37
+ KubernetesAiInsightsInvestigation,
38
+ describeFixType,
39
+ describeInvestigationSubject,
40
+ describeKubectlJobOrigin,
41
+ getAgentPageHint,
42
+ getFixStatusLook,
43
+ getInvestigationStatusLook,
44
+ getInvestigationSummary,
45
+ parseKubernetesAiInsights,
46
+ } from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/AI/Insights";
47
+ import PageMap from "../../../../App/FeatureSet/Dashboard/src/Utils/PageMap";
48
+ import RouteMap from "../../../../App/FeatureSet/Dashboard/src/Utils/RouteMap";
49
+ import RunnerJob from "../../../Models/DatabaseModels/RunnerJob";
50
+ import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
51
+ import HTTPResponse from "../../../Types/API/HTTPResponse";
52
+ import Route from "../../../Types/API/Route";
53
+ import AIRunStatus from "../../../Types/AI/AIRunStatus";
54
+ import AutoRemediationSuggestionStatus from "../../../Types/AutoRemediation/AutoRemediationSuggestionStatus";
55
+ import AutoRemediationSuggestionType from "../../../Types/AutoRemediation/AutoRemediationSuggestionType";
56
+ import ListResult from "../../../Types/BaseDatabase/ListResult";
57
+ import {
58
+ Gray500,
59
+ Green500,
60
+ Red500,
61
+ Yellow500,
62
+ } from "../../../Types/BrandColors";
63
+ import { JSONObject } from "../../../Types/JSON";
64
+ import {
65
+ KubernetesAiRemediationMode,
66
+ KubernetesClusterAiAccessStatus,
67
+ } from "../../../Types/Kubernetes/KubernetesClusterAiAccess";
68
+ import ObjectID from "../../../Types/ObjectID";
69
+ import Permission from "../../../Types/Permission";
70
+ import RunbookStepType from "../../../Types/Runbook/RunbookStepType";
71
+ import RunnerJobOrigin from "../../../Types/Runbook/RunnerJobOrigin";
72
+ import RunnerJobStatus from "../../../Types/Runbook/RunnerJobStatus";
73
+ import API from "../../../UI/Utils/API/API";
74
+ import ModelAPI from "../../../UI/Utils/ModelAPI/ModelAPI";
75
+ import PermissionUtil from "../../../UI/Utils/Permission";
76
+ import User from "../../../UI/Utils/User";
77
+ import { goTo, PROJECT_ID } from "./SideMenuHarness";
78
+
79
+ jest.mock("react-i18next", () => {
80
+ return {
81
+ useTranslation: () => {
82
+ return {
83
+ t: (value: string): string => {
84
+ return value;
85
+ },
86
+ };
87
+ },
88
+ };
89
+ });
90
+
91
+ /*
92
+ * The cluster's AI Insights page (AI → Insights) renders for real on its
93
+ * real route, with the insights and status routes, the model API and the
94
+ * permission snapshot stubbed. It answers "what did OneUptime AI do on this
95
+ * cluster?": the investigations of incidents and alerts here, the fixes AI
96
+ * proposed or applied, and every kubectl command it ran — the table that
97
+ * used to sit at the bottom of the old AI page, moved here with the same
98
+ * columns, filters, preferences key and permission fallback.
99
+ */
100
+
101
+ // Real components fetch; give the waits room on a loaded CI box.
102
+ const WAIT_TIMEOUT: number = 20000;
103
+
104
+ const CLUSTER_ID: string = "44444444-0000-4000-8000-000000000004";
105
+ const INCIDENT_ID: string = "aaaaaaaa-0000-4000-8000-000000000001";
106
+ const ALERT_ID: string = "bbbbbbbb-0000-4000-8000-000000000002";
107
+ const RUN_ID: string = "66666666-0000-4000-8000-000000000006";
108
+ const SECOND_RUN_ID: string = "66666666-0000-4000-8000-000000000007";
109
+ const THIRD_RUN_ID: string = "66666666-0000-4000-8000-000000000008";
110
+ const FIX_ID: string = "77777777-0000-4000-8000-000000000001";
111
+ const SECOND_FIX_ID: string = "77777777-0000-4000-8000-000000000002";
112
+ const INSIGHTS_PATH: string = `/dashboard/${PROJECT_ID}/kubernetes/${CLUSTER_ID}/ai/insights`;
113
+ const AGENT_HREF: string = `/dashboard/${PROJECT_ID}/kubernetes/${CLUSTER_ID}/ai/agent`;
114
+
115
+ const INSIGHTS_ROUTE: string = "/kubernetes-cluster/ai-access/insights";
116
+ const STATUS_ROUTE: string = "/kubernetes-cluster/ai-access/status";
117
+
118
+ const NOT_READY_HINT: string =
119
+ "OneUptime AI can't run kubectl on this cluster right now.";
120
+ const AUTOMATIC_OFF_HINT: string =
121
+ "Automatic investigation is off for new incidents and alerts in this project.";
122
+
123
+ const BASE_PERMISSIONS: Array<Permission> = [
124
+ Permission.Public,
125
+ Permission.User,
126
+ Permission.CurrentUser,
127
+ ];
128
+
129
+ const MEMBER_PERMISSIONS: Array<Permission> = [
130
+ ...BASE_PERMISSIONS,
131
+ Permission.ProjectMember,
132
+ ];
133
+
134
+ function makeStatus(
135
+ overrides: Partial<KubernetesClusterAiAccessStatus> = {},
136
+ ): KubernetesClusterAiAccessStatus {
137
+ return {
138
+ clusterId: CLUSTER_ID,
139
+ clusterName: "prod-east",
140
+ clusterIdentifier: "prod-east",
141
+ runner: {
142
+ id: "88888888-0000-4000-8000-000000000008",
143
+ name: "Kubernetes AI agent",
144
+ kind: "ai_agent",
145
+ isOnline: true,
146
+ canRunAiCommands: true,
147
+ },
148
+ accessMethod: "in_cluster",
149
+ aiAgent: null,
150
+ automaticInvestigation: { incidents: true, alerts: false },
151
+ kubectlAllowlist: [],
152
+ isInvestigationEnabled: true,
153
+ isInvestigationReady: true,
154
+ remediationMode: KubernetesAiRemediationMode.Disabled,
155
+ isRemediationReady: false,
156
+ gaps: [],
157
+ evaluatedAt: "2026-09-22T10:00:30.000Z",
158
+ ...overrides,
159
+ };
160
+ }
161
+
162
+ function incidentInvestigation(): JSONObject {
163
+ return {
164
+ aiRunId: RUN_ID,
165
+ status: AIRunStatus.Completed,
166
+ analysisTldr:
167
+ "The web deployment ran out of memory after the 14:02 rollout.",
168
+ createdAt: "2026-09-22T09:00:00.000Z",
169
+ completedAt: "2026-09-22T09:04:00.000Z",
170
+ incident: { id: INCIDENT_ID, title: "Checkout is slow", number: 42 },
171
+ };
172
+ }
173
+
174
+ function alertInvestigation(): JSONObject {
175
+ return {
176
+ aiRunId: SECOND_RUN_ID,
177
+ status: AIRunStatus.Running,
178
+ analysisTldr: null,
179
+ createdAt: "2026-09-22T09:30:00.000Z",
180
+ completedAt: null,
181
+ alert: { id: ALERT_ID, title: "Pod restarts in api" },
182
+ };
183
+ }
184
+
185
+ function subjectlessInvestigation(): JSONObject {
186
+ return {
187
+ aiRunId: THIRD_RUN_ID,
188
+ status: AIRunStatus.Error,
189
+ createdAt: "2026-09-22T08:00:00.000Z",
190
+ };
191
+ }
192
+
193
+ function waitingFix(): JSONObject {
194
+ return {
195
+ id: FIX_ID,
196
+ status: AutoRemediationSuggestionStatus.Suggested,
197
+ executionMode: "Suggest",
198
+ suggestionType: AutoRemediationSuggestionType.CommandPlan,
199
+ rationale: "Restart the web deployment to clear the leaked connections.",
200
+ createdAt: "2026-09-22T09:05:00.000Z",
201
+ incidentId: INCIDENT_ID,
202
+ alertId: null,
203
+ approvedAt: null,
204
+ };
205
+ }
206
+
207
+ function appliedFix(): JSONObject {
208
+ return {
209
+ id: SECOND_FIX_ID,
210
+ status: AutoRemediationSuggestionStatus.AutoExecuted,
211
+ executionMode: "FullAuto",
212
+ suggestionType: AutoRemediationSuggestionType.Runbook,
213
+ rationale: "Scale api back to 3 replicas.",
214
+ createdAt: "2026-09-22T09:35:00.000Z",
215
+ incidentId: null,
216
+ alertId: ALERT_ID,
217
+ approvedAt: null,
218
+ };
219
+ }
220
+
221
+ function fullInsights(): JSONObject {
222
+ return {
223
+ investigations: [
224
+ incidentInvestigation(),
225
+ alertInvestigation(),
226
+ subjectlessInvestigation(),
227
+ ],
228
+ fixes: [waitingFix(), appliedFix()],
229
+ commandCounts: { investigation: 1717, remediation: 2929 },
230
+ };
231
+ }
232
+
233
+ function emptyInsights(): JSONObject {
234
+ return {
235
+ investigations: [],
236
+ fixes: [],
237
+ commandCounts: { investigation: 0, remediation: 0 },
238
+ };
239
+ }
240
+
241
+ type Answer = () => Promise<HTTPResponse<JSONObject> | HTTPErrorResponse>;
242
+
243
+ function ok(body: unknown): Answer {
244
+ return async (): Promise<HTTPResponse<JSONObject>> => {
245
+ return new HTTPResponse<JSONObject>(200, body as JSONObject, {});
246
+ };
247
+ }
248
+
249
+ function httpError(statusCode: number, message: string): Answer {
250
+ return async (): Promise<HTTPErrorResponse> => {
251
+ return new HTTPErrorResponse(statusCode, { message }, {});
252
+ };
253
+ }
254
+
255
+ /*
256
+ * A request that fails the way a dropped connection does. Thrown from inside
257
+ * the async body rather than returned as a pre-built rejected promise, so no
258
+ * unhandled rejection is reported between construction and the await.
259
+ */
260
+ function networkError(): Answer {
261
+ return async (): Promise<HTTPResponse<JSONObject>> => {
262
+ throw new Error("Network Error");
263
+ };
264
+ }
265
+
266
+ let postSpy: ReturnType<typeof jest.spyOn>;
267
+ let getListSpy: ReturnType<typeof jest.spyOn>;
268
+ let jobs: Array<RunnerJob> = [];
269
+ let insightsAnswers: Array<Answer> = [];
270
+ let statusAnswer: Answer = ok(makeStatus());
271
+
272
+ /*
273
+ * Answers the insights route with the queued answers in order (the last one
274
+ * repeats) and the status route with statusAnswer.
275
+ */
276
+ function serve(insights: Answer | Array<Answer>, status?: Answer): void {
277
+ insightsAnswers = Array.isArray(insights) ? [...insights] : [insights];
278
+ if (status) {
279
+ statusAnswer = status;
280
+ }
281
+ }
282
+
283
+ function postsTo(route: string): Array<JSONObject> {
284
+ return postSpy.mock.calls
285
+ .map((call: Array<unknown>): JSONObject => {
286
+ return (call[0] || {}) as JSONObject;
287
+ })
288
+ .filter((request: JSONObject): boolean => {
289
+ return String(request["url"]).endsWith(route);
290
+ });
291
+ }
292
+
293
+ interface ListRequest {
294
+ modelType?: unknown;
295
+ query?: Record<string, unknown> | undefined;
296
+ select?: Record<string, unknown> | undefined;
297
+ }
298
+
299
+ function jobListRequest(): ListRequest | undefined {
300
+ return getListSpy.mock.calls
301
+ .map((call: Array<unknown>): ListRequest => {
302
+ return (call[0] || {}) as ListRequest;
303
+ })
304
+ .find((request: ListRequest): boolean => {
305
+ return request.modelType === RunnerJob;
306
+ });
307
+ }
308
+
309
+ type JobOverrides = {
310
+ [Key in keyof RunnerJob]?: RunnerJob[Key] | undefined;
311
+ };
312
+
313
+ function makeJob(id: string, overrides: JobOverrides = {}): RunnerJob {
314
+ return Object.assign(
315
+ new RunnerJob(),
316
+ {
317
+ _id: id,
318
+ createdAt: new Date("2026-09-22T09:59:00Z"),
319
+ origin: RunnerJobOrigin.AiInvestigation,
320
+ stepType: RunbookStepType.Kubectl,
321
+ status: RunnerJobStatus.Succeeded,
322
+ payload: { displayCommand: `kubectl get pods -n web-${id}` },
323
+ exitCode: 0,
324
+ },
325
+ overrides,
326
+ );
327
+ }
328
+
329
+ function grant(permissions: Array<Permission>): void {
330
+ jest.spyOn(User, "isMasterAdmin").mockReturnValue(false);
331
+ jest.spyOn(PermissionUtil, "getAllPermissions").mockReturnValue(permissions);
332
+ jest.spyOn(PermissionUtil, "getGlobalPermissions").mockReturnValue(null);
333
+ jest.spyOn(PermissionUtil, "getProjectPermissions").mockReturnValue({
334
+ projectId: new ObjectID(PROJECT_ID),
335
+ userId: ObjectID.generate(),
336
+ permissions: permissions.map((permission: Permission) => {
337
+ return {
338
+ permission: permission,
339
+ labelIds: [],
340
+ _type: "UserPermission",
341
+ };
342
+ }),
343
+ _type: "UserTenantAccessPermission",
344
+ } as unknown as ReturnType<typeof PermissionUtil.getProjectPermissions>);
345
+ }
346
+
347
+ // Lets a test move the router to another cluster's page, as a link would.
348
+ let navigate: NavigateFunction | undefined;
349
+
350
+ function NavigationProbe(): React.ReactElement {
351
+ navigate = useNavigate();
352
+ return <></>;
353
+ }
354
+
355
+ function openInsightsPage(): void {
356
+ goTo(INSIGHTS_PATH);
357
+
358
+ render(
359
+ <MemoryRouter initialEntries={[INSIGHTS_PATH]}>
360
+ <NavigationProbe />
361
+ <Routes>
362
+ <PageRoute
363
+ path={String(RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_AI_INSIGHTS])}
364
+ element={
365
+ <KubernetesClusterAIInsights
366
+ pageRoute={
367
+ RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_AI_INSIGHTS] as Route
368
+ }
369
+ currentProject={null}
370
+ hasPaymentMethod={true}
371
+ />
372
+ }
373
+ />
374
+ </Routes>
375
+ </MemoryRouter>,
376
+ );
377
+ }
378
+
379
+ async function findText(text: string | RegExp): Promise<HTMLElement> {
380
+ return await screen.findByText(text, {}, { timeout: WAIT_TIMEOUT });
381
+ }
382
+
383
+ async function findTestId(testId: string): Promise<HTMLElement> {
384
+ return await screen.findByTestId(testId, {}, { timeout: WAIT_TIMEOUT });
385
+ }
386
+
387
+ function hrefOf(element: HTMLElement): string {
388
+ const anchor: HTMLAnchorElement | null = element.closest("a");
389
+ if (!anchor) {
390
+ throw new Error(`"${element.textContent}" is not inside a link.`);
391
+ }
392
+ return anchor.getAttribute("href") || "";
393
+ }
394
+
395
+ function investigationRows(): Array<HTMLElement> {
396
+ return screen.queryAllByTestId("ai-insights-investigation");
397
+ }
398
+
399
+ function fixRows(): Array<HTMLElement> {
400
+ return screen.queryAllByTestId("ai-insights-fix");
401
+ }
402
+
403
+ beforeEach(() => {
404
+ window.localStorage.clear();
405
+ window.sessionStorage.clear();
406
+ grant(MEMBER_PERMISSIONS);
407
+
408
+ statusAnswer = ok(makeStatus());
409
+ insightsAnswers = [ok(fullInsights())];
410
+
411
+ postSpy = jest.spyOn(API, "post");
412
+ postSpy.mockImplementation(
413
+ async (
414
+ request: unknown,
415
+ ): Promise<HTTPResponse<JSONObject> | HTTPErrorResponse> => {
416
+ const url: string = String((request as JSONObject)["url"]);
417
+ if (url.endsWith(INSIGHTS_ROUTE)) {
418
+ const answer: Answer =
419
+ insightsAnswers.length > 1
420
+ ? insightsAnswers.shift()!
421
+ : insightsAnswers[0]!;
422
+ return await answer();
423
+ }
424
+ if (url.endsWith(STATUS_ROUTE)) {
425
+ return await statusAnswer();
426
+ }
427
+ throw new Error(`Unexpected request to ${url}`);
428
+ },
429
+ );
430
+
431
+ jobs = [];
432
+ getListSpy = jest.spyOn(ModelAPI, "getList");
433
+ getListSpy.mockImplementation(
434
+ async (args: unknown): Promise<ListResult<RunnerJob>> => {
435
+ const request: ListRequest = args as ListRequest;
436
+ if (request.modelType === RunnerJob) {
437
+ return { data: jobs, count: jobs.length, skip: 0, limit: 10 };
438
+ }
439
+ return { data: [], count: 0, skip: 0, limit: 10 };
440
+ },
441
+ );
442
+ jest.spyOn(ModelAPI, "getCommonHeaders").mockReturnValue({});
443
+ });
444
+
445
+ afterEach(() => {
446
+ cleanup();
447
+ jest.restoreAllMocks();
448
+ });
449
+
450
+ describe("parseKubernetesAiInsights", () => {
451
+ test("reads the shape the insights route returns", () => {
452
+ const parsed: KubernetesAiInsights | null =
453
+ parseKubernetesAiInsights(fullInsights());
454
+
455
+ expect(parsed).not.toBeNull();
456
+ expect(parsed!.investigations).toEqual([
457
+ {
458
+ aiRunId: RUN_ID,
459
+ status: AIRunStatus.Completed,
460
+ analysisTldr:
461
+ "The web deployment ran out of memory after the 14:02 rollout.",
462
+ createdAt: "2026-09-22T09:00:00.000Z",
463
+ completedAt: "2026-09-22T09:04:00.000Z",
464
+ incident: { id: INCIDENT_ID, title: "Checkout is slow", number: 42 },
465
+ alert: null,
466
+ },
467
+ {
468
+ aiRunId: SECOND_RUN_ID,
469
+ status: AIRunStatus.Running,
470
+ analysisTldr: null,
471
+ createdAt: "2026-09-22T09:30:00.000Z",
472
+ completedAt: null,
473
+ incident: null,
474
+ alert: { id: ALERT_ID, title: "Pod restarts in api" },
475
+ },
476
+ {
477
+ aiRunId: THIRD_RUN_ID,
478
+ status: AIRunStatus.Error,
479
+ analysisTldr: null,
480
+ createdAt: "2026-09-22T08:00:00.000Z",
481
+ completedAt: null,
482
+ incident: null,
483
+ alert: null,
484
+ },
485
+ ]);
486
+ expect(parsed!.fixes).toEqual([
487
+ {
488
+ id: FIX_ID,
489
+ status: AutoRemediationSuggestionStatus.Suggested,
490
+ executionMode: "Suggest",
491
+ suggestionType: AutoRemediationSuggestionType.CommandPlan,
492
+ rationale:
493
+ "Restart the web deployment to clear the leaked connections.",
494
+ createdAt: "2026-09-22T09:05:00.000Z",
495
+ incidentId: INCIDENT_ID,
496
+ alertId: null,
497
+ approvedAt: null,
498
+ },
499
+ {
500
+ id: SECOND_FIX_ID,
501
+ status: AutoRemediationSuggestionStatus.AutoExecuted,
502
+ executionMode: "FullAuto",
503
+ suggestionType: AutoRemediationSuggestionType.Runbook,
504
+ rationale: "Scale api back to 3 replicas.",
505
+ createdAt: "2026-09-22T09:35:00.000Z",
506
+ incidentId: null,
507
+ alertId: ALERT_ID,
508
+ approvedAt: null,
509
+ },
510
+ ]);
511
+ });
512
+
513
+ test.each([
514
+ ["null", null],
515
+ ["a string", "insights"],
516
+ ["a number", 7],
517
+ ["an array", [incidentInvestigation()]],
518
+ ["an object with neither list", { commandCounts: {} }],
519
+ ["an object whose lists are not arrays", { investigations: {}, fixes: 3 }],
520
+ ])("refuses %s", (_label: string, value: unknown) => {
521
+ expect(parseKubernetesAiInsights(value)).toBeNull();
522
+ });
523
+
524
+ test("reads a missing list as empty when the other one is there", () => {
525
+ expect(
526
+ parseKubernetesAiInsights({ investigations: [incidentInvestigation()] }),
527
+ ).toEqual({
528
+ investigations: [expect.objectContaining({ aiRunId: RUN_ID })],
529
+ fixes: [],
530
+ });
531
+ expect(parseKubernetesAiInsights({ fixes: [waitingFix()] })).toEqual({
532
+ investigations: [],
533
+ fixes: [expect.objectContaining({ id: FIX_ID })],
534
+ });
535
+ });
536
+
537
+ test("drops rows without an id, which have nothing to key or link by", () => {
538
+ const parsed: KubernetesAiInsights | null = parseKubernetesAiInsights({
539
+ investigations: [
540
+ null,
541
+ "run",
542
+ { status: AIRunStatus.Completed },
543
+ { aiRunId: " ", status: AIRunStatus.Completed },
544
+ { aiRunId: 12, status: AIRunStatus.Completed },
545
+ incidentInvestigation(),
546
+ ],
547
+ fixes: [
548
+ [],
549
+ { status: AutoRemediationSuggestionStatus.Suggested },
550
+ { id: "", status: AutoRemediationSuggestionStatus.Suggested },
551
+ waitingFix(),
552
+ ],
553
+ });
554
+
555
+ expect(
556
+ parsed!.investigations.map(
557
+ (row: KubernetesAiInsightsInvestigation): string => {
558
+ return row.aiRunId;
559
+ },
560
+ ),
561
+ ).toEqual([RUN_ID]);
562
+ expect(parsed!.fixes.length).toBe(1);
563
+ expect(parsed!.fixes[0]!.id).toBe(FIX_ID);
564
+ });
565
+
566
+ // The contract (KubernetesClusterAiInsights.ts) leaves status optional.
567
+ test("keeps a row whose status is missing, with the status as null", () => {
568
+ const parsed: KubernetesAiInsights | null = parseKubernetesAiInsights({
569
+ investigations: [
570
+ { aiRunId: RUN_ID },
571
+ { aiRunId: SECOND_RUN_ID, status: "" },
572
+ ],
573
+ fixes: [{ id: FIX_ID, status: 5 }],
574
+ });
575
+
576
+ expect(
577
+ parsed!.investigations.map(
578
+ (row: KubernetesAiInsightsInvestigation): string | null => {
579
+ return row.status;
580
+ },
581
+ ),
582
+ ).toEqual([null, null]);
583
+ expect(parsed!.fixes[0]!.status).toBeNull();
584
+ });
585
+
586
+ test("accepts ids and dates in the server's serialized envelope", () => {
587
+ const parsed: KubernetesAiInsights | null = parseKubernetesAiInsights({
588
+ investigations: [
589
+ {
590
+ aiRunId: { _type: "ObjectID", value: RUN_ID },
591
+ status: AIRunStatus.Completed,
592
+ createdAt: { _type: "DateTime", value: "2026-09-22T09:00:00.000Z" },
593
+ incident: {
594
+ id: { _type: "ObjectID", value: INCIDENT_ID },
595
+ title: "Checkout is slow",
596
+ number: 42,
597
+ },
598
+ },
599
+ ],
600
+ fixes: [
601
+ {
602
+ id: { _type: "ObjectID", value: FIX_ID },
603
+ status: AutoRemediationSuggestionStatus.Dismissed,
604
+ alertId: { _type: "ObjectID", value: ALERT_ID },
605
+ },
606
+ ],
607
+ });
608
+
609
+ expect(parsed!.investigations[0]).toEqual(
610
+ expect.objectContaining({
611
+ aiRunId: RUN_ID,
612
+ createdAt: "2026-09-22T09:00:00.000Z",
613
+ incident: { id: INCIDENT_ID, title: "Checkout is slow", number: 42 },
614
+ }),
615
+ );
616
+ expect(parsed!.fixes[0]).toEqual(
617
+ expect.objectContaining({ id: FIX_ID, alertId: ALERT_ID }),
618
+ );
619
+ });
620
+
621
+ test("reads a subject without an id, a bad number or blank text as missing", () => {
622
+ const parsed: KubernetesAiInsights | null = parseKubernetesAiInsights({
623
+ investigations: [
624
+ {
625
+ aiRunId: RUN_ID,
626
+ status: AIRunStatus.Completed,
627
+ analysisTldr: " ",
628
+ incident: { title: "No id" },
629
+ alert: { id: "", title: "Blank id" },
630
+ },
631
+ {
632
+ aiRunId: SECOND_RUN_ID,
633
+ status: AIRunStatus.Completed,
634
+ incident: { id: INCIDENT_ID, title: 5, number: Number.NaN },
635
+ },
636
+ {
637
+ aiRunId: THIRD_RUN_ID,
638
+ status: AIRunStatus.Completed,
639
+ incident: { id: INCIDENT_ID, number: "42" },
640
+ },
641
+ ],
642
+ });
643
+
644
+ expect(parsed!.investigations[0]).toEqual(
645
+ expect.objectContaining({
646
+ analysisTldr: null,
647
+ incident: null,
648
+ alert: null,
649
+ }),
650
+ );
651
+ expect(parsed!.investigations[1]!.incident).toEqual({
652
+ id: INCIDENT_ID,
653
+ title: "",
654
+ number: null,
655
+ });
656
+ expect(parsed!.investigations[2]!.incident).toEqual({
657
+ id: INCIDENT_ID,
658
+ title: "",
659
+ number: null,
660
+ });
661
+ });
662
+ });
663
+
664
+ describe("status words", () => {
665
+ test("every investigation status has its own plain label and a colour", () => {
666
+ for (const status of Object.values(AIRunStatus)) {
667
+ const look: { label: string } = getInvestigationStatusLook(status);
668
+ expect(look.label.trim().length).toBeGreaterThan(0);
669
+ }
670
+
671
+ expect(getInvestigationStatusLook(AIRunStatus.Completed)).toEqual({
672
+ label: "Completed",
673
+ color: Green500,
674
+ });
675
+ expect(getInvestigationStatusLook(AIRunStatus.Running)).toEqual({
676
+ label: "Investigating",
677
+ color: Yellow500,
678
+ });
679
+ expect(getInvestigationStatusLook(AIRunStatus.Queued).color).toBe(
680
+ Yellow500,
681
+ );
682
+ expect(getInvestigationStatusLook(AIRunStatus.Error)).toEqual({
683
+ label: "Failed",
684
+ color: Red500,
685
+ });
686
+ expect(getInvestigationStatusLook(AIRunStatus.Stale)).toEqual({
687
+ label: "Timed out",
688
+ color: Gray500,
689
+ });
690
+ });
691
+
692
+ test("every fix status has its own plain label", () => {
693
+ const labels: Array<string> = Object.values(
694
+ AutoRemediationSuggestionStatus,
695
+ ).map((status: AutoRemediationSuggestionStatus): string => {
696
+ return getFixStatusLook(status).label;
697
+ });
698
+
699
+ expect(new Set(labels).size).toBe(labels.length);
700
+ expect(getFixStatusLook(AutoRemediationSuggestionStatus.Suggested)).toEqual(
701
+ { label: "Waiting for approval", color: Yellow500 },
702
+ );
703
+ expect(getFixStatusLook(AutoRemediationSuggestionStatus.Approved)).toEqual({
704
+ label: "Applied after approval",
705
+ color: Green500,
706
+ });
707
+ expect(
708
+ getFixStatusLook(AutoRemediationSuggestionStatus.AutoExecuted),
709
+ ).toEqual({ label: "Applied automatically", color: Green500 });
710
+ expect(getFixStatusLook(AutoRemediationSuggestionStatus.Dismissed)).toEqual(
711
+ { label: "Dismissed", color: Gray500 },
712
+ );
713
+ });
714
+
715
+ test("a status a newer server added shows as it is, in a neutral pill", () => {
716
+ expect(getInvestigationStatusLook("Paused")).toEqual({
717
+ label: "Paused",
718
+ color: Gray500,
719
+ });
720
+ expect(getFixStatusLook("Reverted")).toEqual({
721
+ label: "Reverted",
722
+ color: Gray500,
723
+ });
724
+ });
725
+
726
+ test("names the kind of fix", () => {
727
+ expect(describeFixType(AutoRemediationSuggestionType.CommandPlan)).toBe(
728
+ "Command plan",
729
+ );
730
+ expect(describeFixType(AutoRemediationSuggestionType.Runbook)).toBe(
731
+ "Runbook",
732
+ );
733
+ expect(describeFixType(null)).toBeNull();
734
+ expect(describeFixType("Something")).toBeNull();
735
+ });
736
+ });
737
+
738
+ describe("investigation rows", () => {
739
+ function investigation(
740
+ overrides: Partial<KubernetesAiInsightsInvestigation> = {},
741
+ ): KubernetesAiInsightsInvestigation {
742
+ return {
743
+ aiRunId: RUN_ID,
744
+ status: AIRunStatus.Completed,
745
+ analysisTldr: null,
746
+ createdAt: null,
747
+ completedAt: null,
748
+ incident: null,
749
+ alert: null,
750
+ ...overrides,
751
+ };
752
+ }
753
+
754
+ test("lead with the incident, its number and its title", () => {
755
+ expect(
756
+ describeInvestigationSubject(
757
+ investigation({
758
+ incident: { id: INCIDENT_ID, title: "Checkout is slow", number: 42 },
759
+ }),
760
+ ),
761
+ ).toEqual({
762
+ text: "Incident #42: Checkout is slow",
763
+ incidentId: INCIDENT_ID,
764
+ alertId: null,
765
+ });
766
+ expect(
767
+ describeInvestigationSubject(
768
+ investigation({
769
+ incident: { id: INCIDENT_ID, title: "", number: null },
770
+ }),
771
+ ).text,
772
+ ).toBe("Incident");
773
+ });
774
+
775
+ test("lead with the alert when there is no incident", () => {
776
+ expect(
777
+ describeInvestigationSubject(
778
+ investigation({ alert: { id: ALERT_ID, title: "Pod restarts" } }),
779
+ ),
780
+ ).toEqual({
781
+ text: "Alert: Pod restarts",
782
+ incidentId: null,
783
+ alertId: ALERT_ID,
784
+ });
785
+ expect(
786
+ describeInvestigationSubject(
787
+ investigation({ alert: { id: ALERT_ID, title: "" } }),
788
+ ).text,
789
+ ).toBe("Alert");
790
+ });
791
+
792
+ test("say only 'Investigation' when the run has no incident or alert", () => {
793
+ expect(describeInvestigationSubject(investigation())).toEqual({
794
+ text: "Investigation",
795
+ incidentId: null,
796
+ alertId: null,
797
+ });
798
+ });
799
+
800
+ test("show the finding, or why there is none yet", () => {
801
+ expect(
802
+ getInvestigationSummary(investigation({ analysisTldr: "OOM killed." })),
803
+ ).toBe("OOM killed.");
804
+ expect(
805
+ getInvestigationSummary(investigation({ status: AIRunStatus.Running })),
806
+ ).toBe("Still investigating.");
807
+ expect(
808
+ getInvestigationSummary(investigation({ status: AIRunStatus.Queued })),
809
+ ).toBe("Still investigating.");
810
+ expect(
811
+ getInvestigationSummary(investigation({ status: AIRunStatus.Error })),
812
+ ).toBe("No summary was recorded.");
813
+ });
814
+ });
815
+
816
+ describe("the pointer to the AI agent page", () => {
817
+ test("is there when AI cannot run kubectl on the cluster", () => {
818
+ expect(getAgentPageHint(makeStatus({ isInvestigationReady: false }))).toBe(
819
+ NOT_READY_HINT,
820
+ );
821
+ });
822
+
823
+ test("is there when the project investigates neither incidents nor alerts", () => {
824
+ expect(
825
+ getAgentPageHint(
826
+ makeStatus({
827
+ automaticInvestigation: { incidents: false, alerts: false },
828
+ }),
829
+ ),
830
+ ).toBe(AUTOMATIC_OFF_HINT);
831
+ });
832
+
833
+ test("names kubectl first when both are true", () => {
834
+ expect(
835
+ getAgentPageHint(
836
+ makeStatus({
837
+ isInvestigationReady: false,
838
+ automaticInvestigation: { incidents: false, alerts: false },
839
+ }),
840
+ ),
841
+ ).toBe(NOT_READY_HINT);
842
+ });
843
+
844
+ test("is not there when AI is ready and one kind is investigated", () => {
845
+ expect(getAgentPageHint(makeStatus())).toBeNull();
846
+ expect(
847
+ getAgentPageHint(
848
+ makeStatus({
849
+ automaticInvestigation: { incidents: false, alerts: true },
850
+ }),
851
+ ),
852
+ ).toBeNull();
853
+ });
854
+
855
+ test("is not there without a status, or from a server that does not report the opt-ins", () => {
856
+ expect(getAgentPageHint(null)).toBeNull();
857
+ expect(
858
+ getAgentPageHint(
859
+ makeStatus({
860
+ // An older server sends no opt-ins, which the current type requires.
861
+ automaticInvestigation:
862
+ undefined as unknown as KubernetesClusterAiAccessStatus["automaticInvestigation"],
863
+ }),
864
+ ),
865
+ ).toBeNull();
866
+ });
867
+ });
868
+
869
+ describe("kubectl command rows", () => {
870
+ test("tell an investigation from a connection test and a fix", () => {
871
+ expect(
872
+ describeKubectlJobOrigin({
873
+ origin: RunnerJobOrigin.AiInvestigation,
874
+ aiRunId: new ObjectID(RUN_ID),
875
+ }),
876
+ ).toBe("Investigation (read-only)");
877
+ expect(
878
+ describeKubectlJobOrigin({ origin: RunnerJobOrigin.AiInvestigation }),
879
+ ).toBe("Connection test (read-only)");
880
+ expect(
881
+ describeKubectlJobOrigin({ origin: RunnerJobOrigin.AiRemediation }),
882
+ ).toBe("Fix");
883
+ expect(describeKubectlJobOrigin({ origin: "Runbook" })).toBe("Runbook");
884
+ expect(describeKubectlJobOrigin({})).toBe("");
885
+ });
886
+
887
+ test("keep the preferences key the old AI page used", () => {
888
+ expect(KUBECTL_JOBS_TABLE_PREFERENCES_KEY).toBe(
889
+ "kubernetes-cluster-ai-kubectl-jobs",
890
+ );
891
+ expect(KUBECTL_JOB_ORIGIN_LABELS[RunnerJobOrigin.AiInvestigation]).toBe(
892
+ "Investigation or connection test",
893
+ );
894
+ expect(KUBECTL_JOB_ORIGIN_LABELS[RunnerJobOrigin.AiRemediation]).toBe(
895
+ "Fix",
896
+ );
897
+ });
898
+ });
899
+
900
+ describe("AI Insights page", () => {
901
+ test("shows its title and subtitle, and a loader until the insights arrive", async () => {
902
+ let release: (() => void) | undefined;
903
+ serve(async (): Promise<HTTPResponse<JSONObject>> => {
904
+ await new Promise<void>((resolve: () => void) => {
905
+ release = resolve;
906
+ });
907
+ return new HTTPResponse<JSONObject>(200, fullInsights(), {});
908
+ });
909
+ openInsightsPage();
910
+
911
+ expect(screen.getByText(AI_INSIGHTS_PAGE_TITLE)).toBeInTheDocument();
912
+ expect(screen.getByText(AI_INSIGHTS_PAGE_SUBTITLE)).toBeInTheDocument();
913
+ expect(await findTestId("ai-insights-loading")).toBeInTheDocument();
914
+ expect(screen.queryByText("Investigations")).not.toBeInTheDocument();
915
+
916
+ await waitFor(
917
+ () => {
918
+ expect(release).toBeDefined();
919
+ },
920
+ { timeout: WAIT_TIMEOUT },
921
+ );
922
+ release!();
923
+
924
+ expect(await findText("Investigations")).toBeInTheDocument();
925
+ expect(screen.queryByTestId("ai-insights-loading")).not.toBeInTheDocument();
926
+ });
927
+
928
+ test("asks the insights and status routes about this cluster", async () => {
929
+ openInsightsPage();
930
+ await findText("Investigations");
931
+
932
+ const insightsRequests: Array<JSONObject> = postsTo(INSIGHTS_ROUTE);
933
+ expect(insightsRequests.length).toBe(1);
934
+ expect(insightsRequests[0]!["data"]).toEqual({ clusterId: CLUSTER_ID });
935
+ expect(String(insightsRequests[0]!["url"])).toContain(
936
+ "/api/kubernetes-cluster/ai-access/insights",
937
+ );
938
+
939
+ await waitFor(
940
+ () => {
941
+ expect(postsTo(STATUS_ROUTE).length).toBe(1);
942
+ },
943
+ { timeout: WAIT_TIMEOUT },
944
+ );
945
+ expect(postsTo(STATUS_ROUTE)[0]!["data"]).toEqual({
946
+ clusterId: CLUSTER_ID,
947
+ });
948
+ });
949
+
950
+ test("lists investigations with their subject link, finding, status and time", async () => {
951
+ openInsightsPage();
952
+ await findText("Investigations");
953
+
954
+ const rows: Array<HTMLElement> = investigationRows();
955
+ expect(rows.length).toBe(3);
956
+
957
+ const incidentRow: HTMLElement = rows[0]!;
958
+ expect(
959
+ hrefOf(within(incidentRow).getByText("Incident #42: Checkout is slow")),
960
+ ).toBe(`/dashboard/${PROJECT_ID}/incidents/${INCIDENT_ID}`);
961
+ expect(
962
+ within(incidentRow).getByText(
963
+ "The web deployment ran out of memory after the 14:02 rollout.",
964
+ ),
965
+ ).toBeInTheDocument();
966
+ expect(within(incidentRow).getByText("Completed")).toBeInTheDocument();
967
+ const time: HTMLElement | null = incidentRow.querySelector("time");
968
+ expect(time).not.toBeNull();
969
+ expect(time!.getAttribute("dateTime")).toBe("2026-09-22T09:00:00.000Z");
970
+ expect(time!.textContent).toMatch(/ago|in /);
971
+
972
+ const alertRow: HTMLElement = rows[1]!;
973
+ expect(
974
+ hrefOf(within(alertRow).getByText("Alert: Pod restarts in api")),
975
+ ).toBe(`/dashboard/${PROJECT_ID}/alerts/${ALERT_ID}`);
976
+ expect(within(alertRow).getByText("Investigating")).toBeInTheDocument();
977
+ expect(
978
+ within(alertRow).getByText("Still investigating."),
979
+ ).toBeInTheDocument();
980
+
981
+ const subjectlessRow: HTMLElement = rows[2]!;
982
+ expect(
983
+ within(subjectlessRow).getByText("Investigation").closest("a"),
984
+ ).toBeNull();
985
+ expect(within(subjectlessRow).getByText("Failed")).toBeInTheDocument();
986
+ expect(
987
+ within(subjectlessRow).getByText("No summary was recorded."),
988
+ ).toBeInTheDocument();
989
+ });
990
+
991
+ test("lists fixes with their status, reason, kind and a link to where they were proposed", async () => {
992
+ openInsightsPage();
993
+ await findText("Fixes");
994
+
995
+ const rows: Array<HTMLElement> = fixRows();
996
+ expect(rows.length).toBe(2);
997
+
998
+ const waiting: HTMLElement = rows[0]!;
999
+ expect(
1000
+ within(waiting).getByText("Waiting for approval"),
1001
+ ).toBeInTheDocument();
1002
+ expect(within(waiting).getByText("Command plan")).toBeInTheDocument();
1003
+ expect(
1004
+ within(waiting).getByText(
1005
+ "Restart the web deployment to clear the leaked connections.",
1006
+ ),
1007
+ ).toBeInTheDocument();
1008
+ expect(hrefOf(within(waiting).getByText("Open incident"))).toBe(
1009
+ `/dashboard/${PROJECT_ID}/incidents/${INCIDENT_ID}`,
1010
+ );
1011
+
1012
+ const applied: HTMLElement = rows[1]!;
1013
+ expect(
1014
+ within(applied).getByText("Applied automatically"),
1015
+ ).toBeInTheDocument();
1016
+ expect(within(applied).getByText("Runbook")).toBeInTheDocument();
1017
+ expect(hrefOf(within(applied).getByText("Open alert"))).toBe(
1018
+ `/dashboard/${PROJECT_ID}/alerts/${ALERT_ID}`,
1019
+ );
1020
+ });
1021
+
1022
+ test("a fix with no incident or alert has no link and says when no reason was recorded", async () => {
1023
+ serve(
1024
+ ok({
1025
+ investigations: [incidentInvestigation()],
1026
+ fixes: [
1027
+ {
1028
+ id: FIX_ID,
1029
+ status: AutoRemediationSuggestionStatus.NoneApplicable,
1030
+ createdAt: "2026-09-22T09:05:00.000Z",
1031
+ },
1032
+ ],
1033
+ }),
1034
+ );
1035
+ openInsightsPage();
1036
+ await findText("Fixes");
1037
+
1038
+ const row: HTMLElement = fixRows()[0]!;
1039
+ expect(within(row).getByText("No fix found")).toBeInTheDocument();
1040
+ expect(
1041
+ within(row).getByText("No reason was recorded."),
1042
+ ).toBeInTheDocument();
1043
+ expect(within(row).queryByRole("link")).toBeNull();
1044
+ });
1045
+
1046
+ test("shows no KPI tiles: the command counts the route returns are not rendered", async () => {
1047
+ openInsightsPage();
1048
+ await findText("Investigations");
1049
+
1050
+ expect(screen.queryByText(/1717/)).not.toBeInTheDocument();
1051
+ expect(screen.queryByText(/2929/)).not.toBeInTheDocument();
1052
+ });
1053
+
1054
+ test("shows a row without a status, just without a status pill", async () => {
1055
+ serve(
1056
+ ok({
1057
+ investigations: [
1058
+ {
1059
+ aiRunId: RUN_ID,
1060
+ analysisTldr: "Found it.",
1061
+ incident: {
1062
+ id: INCIDENT_ID,
1063
+ title: "Checkout is slow",
1064
+ number: 42,
1065
+ },
1066
+ },
1067
+ ],
1068
+ fixes: [
1069
+ { id: FIX_ID, rationale: "Restart web.", incidentId: INCIDENT_ID },
1070
+ ],
1071
+ }),
1072
+ );
1073
+ openInsightsPage();
1074
+
1075
+ await findText("Found it.");
1076
+ const investigation: HTMLElement = investigationRows()[0]!;
1077
+ const fix: HTMLElement = fixRows()[0]!;
1078
+ expect(
1079
+ within(investigation).getByText("Incident #42: Checkout is slow"),
1080
+ ).toBeInTheDocument();
1081
+ expect(within(fix).getByText("Restart web.")).toBeInTheDocument();
1082
+ for (const row of [investigation, fix]) {
1083
+ for (const label of [
1084
+ "Completed",
1085
+ "Failed",
1086
+ "Waiting for approval",
1087
+ "Dismissed",
1088
+ ]) {
1089
+ expect(within(row).queryByText(label)).not.toBeInTheDocument();
1090
+ }
1091
+ }
1092
+ });
1093
+
1094
+ test("renders server text as text, never as markup", async () => {
1095
+ serve(
1096
+ ok({
1097
+ investigations: [
1098
+ {
1099
+ ...incidentInvestigation(),
1100
+ analysisTldr: '<img src=x onerror="window.pwned=1">',
1101
+ incident: {
1102
+ id: INCIDENT_ID,
1103
+ title: "<script>window.pwned=1</script>",
1104
+ number: 1,
1105
+ },
1106
+ },
1107
+ ],
1108
+ fixes: [
1109
+ { ...waitingFix(), rationale: "<b>bold</b> [link](javascript:1)" },
1110
+ ],
1111
+ }),
1112
+ );
1113
+ openInsightsPage();
1114
+
1115
+ expect(
1116
+ await findText('<img src=x onerror="window.pwned=1">'),
1117
+ ).toBeInTheDocument();
1118
+ expect(
1119
+ screen.getByText("Incident #1: <script>window.pwned=1</script>"),
1120
+ ).toBeInTheDocument();
1121
+ expect(
1122
+ screen.getByText("<b>bold</b> [link](javascript:1)"),
1123
+ ).toBeInTheDocument();
1124
+ expect(document.querySelector("img[src='x']")).toBeNull();
1125
+ });
1126
+
1127
+ describe("empty and partial states", () => {
1128
+ test("shows one 'Nothing yet' state instead of two empty lists", async () => {
1129
+ serve(ok(emptyInsights()));
1130
+ openInsightsPage();
1131
+
1132
+ expect(await findText(AI_INSIGHTS_EMPTY_TITLE)).toBeInTheDocument();
1133
+ expect(
1134
+ screen.getByText(AI_INSIGHTS_EMPTY_DESCRIPTION),
1135
+ ).toBeInTheDocument();
1136
+ expect(screen.queryByText("Investigations")).not.toBeInTheDocument();
1137
+ expect(screen.queryByText("Fixes")).not.toBeInTheDocument();
1138
+ // The command history stays: connection tests land there too.
1139
+ expect(
1140
+ await findText(KUBECTL_COMMANDS_EMPTY_MESSAGE),
1141
+ ).toBeInTheDocument();
1142
+ });
1143
+
1144
+ test("points at the AI agent page when AI cannot run kubectl here", async () => {
1145
+ serve(
1146
+ ok(emptyInsights()),
1147
+ ok(makeStatus({ isInvestigationReady: false })),
1148
+ );
1149
+ openInsightsPage();
1150
+
1151
+ const hint: HTMLElement = await findTestId("ai-insights-agent-hint");
1152
+ expect(hint).toHaveTextContent(NOT_READY_HINT);
1153
+ expect(hrefOf(within(hint).getByText("Open the AI agent page"))).toBe(
1154
+ AGENT_HREF,
1155
+ );
1156
+ expect(
1157
+ screen.getByTestId("kubernetes-ai-insights-empty-footer"),
1158
+ ).toContainElement(hint);
1159
+ });
1160
+
1161
+ test("points at the AI agent page when automatic investigation is off", async () => {
1162
+ serve(
1163
+ ok(emptyInsights()),
1164
+ ok(
1165
+ makeStatus({
1166
+ automaticInvestigation: { incidents: false, alerts: false },
1167
+ }),
1168
+ ),
1169
+ );
1170
+ openInsightsPage();
1171
+
1172
+ const hint: HTMLElement = await findTestId("ai-insights-agent-hint");
1173
+ expect(hint).toHaveTextContent(AUTOMATIC_OFF_HINT);
1174
+ expect(hrefOf(within(hint).getByText("Open the AI agent page"))).toBe(
1175
+ AGENT_HREF,
1176
+ );
1177
+ });
1178
+
1179
+ test("does not point anywhere when AI is ready", async () => {
1180
+ serve(ok(emptyInsights()));
1181
+ openInsightsPage();
1182
+
1183
+ await findText(AI_INSIGHTS_EMPTY_TITLE);
1184
+ await waitFor(
1185
+ () => {
1186
+ expect(postsTo(STATUS_ROUTE).length).toBe(1);
1187
+ },
1188
+ { timeout: WAIT_TIMEOUT },
1189
+ );
1190
+ expect(
1191
+ screen.queryByTestId("ai-insights-agent-hint"),
1192
+ ).not.toBeInTheDocument();
1193
+ });
1194
+
1195
+ for (const [label, answer] of [
1196
+ ["fails", networkError()],
1197
+ ["is refused", httpError(403, "You do not have permission.")],
1198
+ ["is not a status", ok({ something: "else" })],
1199
+ ] as Array<[string, Answer]>) {
1200
+ test(`still renders when the status request ${label}, just without the pointer`, async () => {
1201
+ serve(ok(emptyInsights()), answer);
1202
+ openInsightsPage();
1203
+
1204
+ expect(await findText(AI_INSIGHTS_EMPTY_TITLE)).toBeInTheDocument();
1205
+ await waitFor(
1206
+ () => {
1207
+ expect(postsTo(STATUS_ROUTE).length).toBe(1);
1208
+ },
1209
+ { timeout: WAIT_TIMEOUT },
1210
+ );
1211
+ expect(
1212
+ screen.queryByTestId("ai-insights-agent-hint"),
1213
+ ).not.toBeInTheDocument();
1214
+ });
1215
+ }
1216
+
1217
+ test("shows the pointer above the lists when there is history but AI is not ready", async () => {
1218
+ serve(
1219
+ ok(fullInsights()),
1220
+ ok(makeStatus({ isInvestigationReady: false })),
1221
+ );
1222
+ openInsightsPage();
1223
+
1224
+ const hint: HTMLElement = await findTestId("ai-insights-agent-hint");
1225
+ expect(hint).toHaveTextContent(NOT_READY_HINT);
1226
+ expect(screen.getByText("Investigations")).toBeInTheDocument();
1227
+ expect(
1228
+ screen.queryByText(AI_INSIGHTS_EMPTY_TITLE),
1229
+ ).not.toBeInTheDocument();
1230
+ // Before the lists, so it is read first.
1231
+ expect(
1232
+ hint.compareDocumentPosition(screen.getByText("Investigations")) &
1233
+ Node.DOCUMENT_POSITION_FOLLOWING,
1234
+ ).toBeTruthy();
1235
+ });
1236
+
1237
+ test("says so in the Fixes list when only investigations exist", async () => {
1238
+ serve(ok({ investigations: [incidentInvestigation()], fixes: [] }));
1239
+ openInsightsPage();
1240
+
1241
+ expect(await findTestId("ai-insights-no-fixes")).toHaveTextContent(
1242
+ "No fixes yet.",
1243
+ );
1244
+ expect(investigationRows().length).toBe(1);
1245
+ expect(
1246
+ screen.queryByTestId("ai-insights-no-investigations"),
1247
+ ).not.toBeInTheDocument();
1248
+ });
1249
+
1250
+ test("says so in the Investigations list when only fixes exist", async () => {
1251
+ serve(ok({ investigations: [], fixes: [waitingFix()] }));
1252
+ openInsightsPage();
1253
+
1254
+ expect(
1255
+ await findTestId("ai-insights-no-investigations"),
1256
+ ).toHaveTextContent("No investigations yet.");
1257
+ expect(fixRows().length).toBe(1);
1258
+ });
1259
+ });
1260
+
1261
+ /*
1262
+ * Moving from one cluster's AI Insights to another's keeps the page
1263
+ * mounted. An answer about the cluster the user just left must not paint
1264
+ * over the one they are looking at.
1265
+ */
1266
+ test("drops a late answer about the cluster the user navigated away from", async () => {
1267
+ const OTHER_CLUSTER_ID: string = "44444444-0000-4000-8000-0000000000ff";
1268
+ let answerFirstCluster: (() => void) | undefined;
1269
+
1270
+ postSpy.mockImplementation(
1271
+ async (
1272
+ request: unknown,
1273
+ ): Promise<HTTPResponse<JSONObject> | HTTPErrorResponse> => {
1274
+ const url: string = String((request as JSONObject)["url"]);
1275
+ const clusterId: string = String(
1276
+ ((request as JSONObject)["data"] as JSONObject)["clusterId"],
1277
+ );
1278
+ if (url.endsWith(STATUS_ROUTE)) {
1279
+ return new HTTPResponse<JSONObject>(
1280
+ 200,
1281
+ makeStatus({ clusterId }) as unknown as JSONObject,
1282
+ {},
1283
+ );
1284
+ }
1285
+ if (clusterId === CLUSTER_ID) {
1286
+ await new Promise<void>((resolve: () => void) => {
1287
+ answerFirstCluster = resolve;
1288
+ });
1289
+ return new HTTPResponse<JSONObject>(
1290
+ 200,
1291
+ { investigations: [incidentInvestigation()], fixes: [] },
1292
+ {},
1293
+ );
1294
+ }
1295
+ return new HTTPResponse<JSONObject>(
1296
+ 200,
1297
+ { investigations: [alertInvestigation()], fixes: [] },
1298
+ {},
1299
+ );
1300
+ },
1301
+ );
1302
+ openInsightsPage();
1303
+
1304
+ await waitFor(
1305
+ () => {
1306
+ expect(answerFirstCluster).toBeDefined();
1307
+ },
1308
+ { timeout: WAIT_TIMEOUT },
1309
+ );
1310
+
1311
+ act(() => {
1312
+ navigate!(
1313
+ `/dashboard/${PROJECT_ID}/kubernetes/${OTHER_CLUSTER_ID}/ai/insights`,
1314
+ );
1315
+ });
1316
+
1317
+ expect(await findText("Alert: Pod restarts in api")).toBeInTheDocument();
1318
+ expect(
1319
+ postsTo(INSIGHTS_ROUTE).map((request: JSONObject): unknown => {
1320
+ return (request["data"] as JSONObject)["clusterId"];
1321
+ }),
1322
+ ).toEqual([CLUSTER_ID, OTHER_CLUSTER_ID]);
1323
+
1324
+ await act(async () => {
1325
+ answerFirstCluster!();
1326
+ await Promise.resolve();
1327
+ });
1328
+
1329
+ expect(screen.getByText("Alert: Pod restarts in api")).toBeInTheDocument();
1330
+ expect(
1331
+ screen.queryByText("Incident #42: Checkout is slow"),
1332
+ ).not.toBeInTheDocument();
1333
+ });
1334
+
1335
+ describe("errors", () => {
1336
+ test("shows the server's message and retries on request", async () => {
1337
+ serve([
1338
+ httpError(500, "The insights service is unavailable."),
1339
+ ok(fullInsights()),
1340
+ ]);
1341
+ openInsightsPage();
1342
+
1343
+ expect(
1344
+ await findText("The insights service is unavailable."),
1345
+ ).toBeInTheDocument();
1346
+ expect(screen.queryByText("Investigations")).not.toBeInTheDocument();
1347
+
1348
+ fireEvent.click(
1349
+ within(screen.getByTestId("ai-insights-error")).getByTestId(
1350
+ "refresh-button",
1351
+ ),
1352
+ );
1353
+
1354
+ expect(await findText("Investigations")).toBeInTheDocument();
1355
+ expect(postsTo(INSIGHTS_ROUTE).length).toBe(2);
1356
+ expect(
1357
+ screen.queryByText("The insights service is unavailable."),
1358
+ ).not.toBeInTheDocument();
1359
+ });
1360
+
1361
+ test("explains a body it cannot read", async () => {
1362
+ serve(ok({ unexpected: true }));
1363
+ openInsightsPage();
1364
+
1365
+ expect(
1366
+ await findText(
1367
+ "The server returned AI insights this page cannot read.",
1368
+ ),
1369
+ ).toBeInTheDocument();
1370
+ });
1371
+
1372
+ test("survives a dropped connection", async () => {
1373
+ serve(networkError());
1374
+ openInsightsPage();
1375
+
1376
+ const error: HTMLElement = await findTestId("ai-insights-error");
1377
+ expect(within(error).getByTestId("refresh-button")).toBeInTheDocument();
1378
+ expect(screen.queryByText("Investigations")).not.toBeInTheDocument();
1379
+ // The command history does not depend on the insights route.
1380
+ expect(
1381
+ await findText(KUBECTL_COMMANDS_EMPTY_MESSAGE),
1382
+ ).toBeInTheDocument();
1383
+ });
1384
+ });
1385
+
1386
+ describe("kubectl commands table", () => {
1387
+ test("asks for every column its cells read and labels rows from them", async () => {
1388
+ jobs = [
1389
+ makeJob("investigation", { aiRunId: new ObjectID(RUN_ID) }),
1390
+ makeJob("connection-test"),
1391
+ makeJob("failed", {
1392
+ aiRunId: new ObjectID(RUN_ID),
1393
+ status: RunnerJobStatus.Failed,
1394
+ exitCode: 1,
1395
+ errorMessage: "pods is forbidden: User cannot list resource",
1396
+ }),
1397
+ ];
1398
+ openInsightsPage();
1399
+
1400
+ expect(
1401
+ await findText("kubectl get pods -n web-investigation"),
1402
+ ).toBeInTheDocument();
1403
+ expect(
1404
+ screen.getByText("kubectl get pods -n web-connection-test"),
1405
+ ).toBeInTheDocument();
1406
+
1407
+ const whyCells: Array<string> = screen
1408
+ .getAllByText(/^(Investigation|Connection test) \(read-only\)$/)
1409
+ .map((cell: HTMLElement): string => {
1410
+ return cell.textContent || "";
1411
+ });
1412
+ expect(whyCells).toEqual([
1413
+ "Investigation (read-only)",
1414
+ "Connection test (read-only)",
1415
+ "Investigation (read-only)",
1416
+ ]);
1417
+
1418
+ expect(screen.getByText("Failed (exit 1)")).toBeInTheDocument();
1419
+ expect(screen.getAllByText("Succeeded (exit 0)").length).toBe(2);
1420
+ expect(
1421
+ screen.getByText("pods is forbidden: User cannot list resource"),
1422
+ ).toBeInTheDocument();
1423
+
1424
+ const request: ListRequest | undefined = jobListRequest();
1425
+ expect(request?.select).toEqual(
1426
+ expect.objectContaining({
1427
+ createdAt: true,
1428
+ payload: true,
1429
+ origin: true,
1430
+ status: true,
1431
+ aiRunId: true,
1432
+ exitCode: true,
1433
+ errorMessage: true,
1434
+ }),
1435
+ );
1436
+ // Summaries only: never the command output.
1437
+ expect(request?.select).not.toHaveProperty("output");
1438
+ expect(request?.query).toEqual(
1439
+ expect.objectContaining({ stepType: RunbookStepType.Kubectl }),
1440
+ );
1441
+ expect(String(request?.query?.["kubernetesClusterId"])).toBe(CLUSTER_ID);
1442
+ });
1443
+
1444
+ test("labels a fix command", async () => {
1445
+ jobs = [
1446
+ makeJob("fix", {
1447
+ origin: RunnerJobOrigin.AiRemediation,
1448
+ aiRunId: new ObjectID(RUN_ID),
1449
+ payload: {
1450
+ displayCommand: "kubectl rollout restart deployment/web -n web",
1451
+ },
1452
+ }),
1453
+ ];
1454
+ openInsightsPage();
1455
+
1456
+ const commandCell: HTMLElement = await findText(
1457
+ "kubectl rollout restart deployment/web -n web",
1458
+ );
1459
+ const row: HTMLElement | null = commandCell.closest("tr");
1460
+ expect(row).not.toBeNull();
1461
+ expect(within(row as HTMLElement).getByText("Fix")).toBeInTheDocument();
1462
+ });
1463
+
1464
+ test("is titled 'kubectl commands' and says when nothing ran yet", async () => {
1465
+ openInsightsPage();
1466
+
1467
+ expect(
1468
+ await findText(KUBECTL_COMMANDS_EMPTY_MESSAGE),
1469
+ ).toBeInTheDocument();
1470
+ expect(
1471
+ screen.getAllByText(KUBECTL_COMMANDS_CARD_TITLE).length,
1472
+ ).toBeGreaterThan(0);
1473
+ });
1474
+ });
1475
+
1476
+ /*
1477
+ * Settings roles and ReadKubernetesCluster may open the cluster but not
1478
+ * read RunnerJob rows. RunnerJob's ACL is not widened; they get an
1479
+ * explanation and no failing request — while the investigations and fixes,
1480
+ * which the server summarises behind the cluster's own read gate, still
1481
+ * show.
1482
+ */
1483
+ describe("command history permission", () => {
1484
+ for (const permission of [
1485
+ Permission.SettingsAdmin,
1486
+ Permission.SettingsViewer,
1487
+ Permission.ReadKubernetesCluster,
1488
+ ]) {
1489
+ test(`explains instead of failing for ${permission}`, async () => {
1490
+ grant([...BASE_PERMISSIONS, permission]);
1491
+ openInsightsPage();
1492
+
1493
+ const note: HTMLElement = await findTestId(
1494
+ "kubectl-jobs-permission-note",
1495
+ );
1496
+ expect(note).toHaveTextContent("Runbook Viewer");
1497
+ expect(note).toHaveTextContent("Viewer");
1498
+ expect(
1499
+ screen.getByText(KUBECTL_COMMANDS_CARD_TITLE),
1500
+ ).toBeInTheDocument();
1501
+ expect(jobListRequest()).toBeUndefined();
1502
+
1503
+ expect(await findText("Investigations")).toBeInTheDocument();
1504
+ expect(investigationRows().length).toBe(3);
1505
+ });
1506
+ }
1507
+
1508
+ for (const permission of [Permission.ProjectMember, Permission.Viewer]) {
1509
+ test(`shows the table for ${permission}`, async () => {
1510
+ grant([...BASE_PERMISSIONS, permission]);
1511
+ openInsightsPage();
1512
+
1513
+ await findText(KUBECTL_COMMANDS_EMPTY_MESSAGE);
1514
+ expect(
1515
+ screen.queryByTestId("kubectl-jobs-permission-note"),
1516
+ ).not.toBeInTheDocument();
1517
+ expect(jobListRequest()?.query).toEqual(
1518
+ expect.objectContaining({ stepType: RunbookStepType.Kubectl }),
1519
+ );
1520
+ });
1521
+ }
1522
+ });
1523
+ });