@oneuptime/common 14.0.7 → 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 (1064) hide show
  1. package/Models/DatabaseModels/Incident.ts +267 -1
  2. package/Models/DatabaseModels/IncidentCustomField.ts +202 -0
  3. package/Models/DatabaseModels/IncidentEpisode.ts +30 -0
  4. package/Models/DatabaseModels/IncidentTemplate.ts +126 -1
  5. package/Models/DatabaseModels/Index.ts +2 -0
  6. package/Models/DatabaseModels/KubernetesAiAgent.ts +364 -0
  7. package/Models/DatabaseModels/KubernetesCluster.ts +3 -3
  8. package/Models/DatabaseModels/Project.ts +55 -5
  9. package/Models/DatabaseModels/ProxmoxResource.ts +49 -1
  10. package/Models/DatabaseModels/RunnerJob.ts +75 -0
  11. package/Models/DatabaseModels/StatusPage.ts +55 -0
  12. package/Models/DatabaseModels/StatusPageSubscriber.ts +128 -0
  13. package/Models/DatabaseModels/TeamComplianceSetting.ts +143 -2
  14. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +17 -0
  15. package/Server/API/AIChatAPI.ts +6 -3
  16. package/Server/API/AIInvestigationAPI.ts +17 -10
  17. package/Server/API/AutoRemediationAPI.ts +207 -25
  18. package/Server/API/IncidentAPI.ts +162 -0
  19. package/Server/API/KubernetesClusterAiAccessAPI.ts +623 -21
  20. package/Server/API/MicrosoftTeamsAPI.ts +93 -14
  21. package/Server/API/ProxmoxResourceAPI.ts +244 -14
  22. package/Server/API/StatusPageAPI.ts +487 -171
  23. package/Server/API/StatusPageSubscriberAPI.ts +63 -15
  24. package/Server/Infrastructure/GlobalCache.ts +32 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.ts +49 -0
  26. package/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.ts +108 -0
  27. package/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.ts +33 -0
  28. package/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.ts +51 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.ts +33 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.ts +168 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.ts +44 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.ts +99 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.ts +137 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.ts +42 -0
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +20 -0
  36. package/Server/Infrastructure/Semaphore.ts +10 -0
  37. package/Server/Middleware/IdentityRateLimit.ts +108 -3
  38. package/Server/Middleware/SubscriberNotificationPreviewRateLimit.ts +237 -0
  39. package/Server/Middleware/SubscriberNotificationTestSendRateLimit.ts +304 -0
  40. package/Server/Middleware/TelemetryIngest.ts +41 -18
  41. package/Server/Services/AIService.ts +70 -1
  42. package/Server/Services/AlertService.ts +352 -11
  43. package/Server/Services/AlertSeverityService.ts +65 -15
  44. package/Server/Services/AutoRemediationRuleService.ts +1 -1
  45. package/Server/Services/DatabaseService.ts +93 -10
  46. package/Server/Services/IncidentCustomFieldService.ts +276 -1
  47. package/Server/Services/IncidentEpisodePublicNoteService.ts +28 -0
  48. package/Server/Services/IncidentEpisodeService.ts +15 -0
  49. package/Server/Services/IncidentEpisodeStateTimelineService.ts +27 -1
  50. package/Server/Services/IncidentPublicNoteService.ts +30 -1
  51. package/Server/Services/IncidentService.ts +1788 -142
  52. package/Server/Services/IncidentSeverityService.ts +66 -15
  53. package/Server/Services/IncidentStateTimelineService.ts +13 -0
  54. package/Server/Services/IncidentTemplateService.ts +109 -0
  55. package/Server/Services/KubernetesAiAgentJobService.ts +185 -0
  56. package/Server/Services/KubernetesAiAgentService.ts +1634 -0
  57. package/Server/Services/KubernetesClusterAiAccessService.ts +1118 -451
  58. package/Server/Services/KubernetesClusterService.ts +129 -21
  59. package/Server/Services/ProjectService.ts +32 -0
  60. package/Server/Services/ProxmoxResourceService.ts +322 -5
  61. package/Server/Services/RunnerJobService.ts +241 -104
  62. package/Server/Services/RunnerService.ts +1 -1
  63. package/Server/Services/ScheduledMaintenancePublicNoteService.ts +30 -1
  64. package/Server/Services/ScheduledMaintenanceService.ts +805 -19
  65. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +414 -56
  66. package/Server/Services/SmsService.ts +10 -0
  67. package/Server/Services/StatusPageService.ts +65 -8
  68. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +299 -37
  69. package/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.ts +129 -0
  70. package/Server/Services/StatusPageSubscriberService.ts +1161 -64
  71. package/Server/Services/TeamComplianceSettingService.ts +1125 -9
  72. package/Server/Types/Markdown.ts +153 -6
  73. package/Server/Types/Workflow/Components/Email.ts +61 -2
  74. package/Server/Utils/AI/ClusterAccess/ClusterAccessContext.ts +67 -9
  75. package/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.ts +39 -10
  76. package/Server/Utils/AI/ClusterAccess/KubectlJobRunner.ts +14 -3
  77. package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +37 -15
  78. package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +67 -24
  79. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +48 -5
  80. package/Server/Utils/AI/SRE/IncidentPostmortemRunner.ts +40 -8
  81. package/Server/Utils/AI/SRE/InvestigationEligibility.ts +13 -2
  82. package/Server/Utils/AI/SRE/InvestigationGrader.ts +1 -1
  83. package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +6 -1
  84. package/Server/Utils/CustomField/CustomFieldRename.ts +378 -0
  85. package/Server/Utils/DataSource/EgressGuard.ts +66 -21
  86. package/Server/Utils/Mail/PinnedSmtpSocket.ts +130 -0
  87. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +10 -0
  88. package/Server/Utils/SSRFProtection.ts +76 -16
  89. package/Server/Utils/StatusPage/IncidentStatusPageScope.ts +948 -0
  90. package/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.ts +656 -0
  91. package/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.ts +805 -0
  92. package/Server/Utils/StatusPage/StatusPageExclusion.ts +18 -0
  93. package/Server/Utils/StatusPage/StatusPageReadAccess.ts +125 -0
  94. package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.ts +151 -0
  95. package/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.ts +70 -0
  96. package/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.ts +421 -0
  97. package/Server/Utils/StatusPage/SubscriberNotificationClaim.ts +107 -0
  98. package/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.ts +1049 -0
  99. package/Server/Utils/StatusPage/SubscriberNotificationFanOut.ts +180 -0
  100. package/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.ts +839 -0
  101. package/Server/Utils/StatusPage/SubscriberNotificationResendAccess.ts +393 -0
  102. package/Server/Utils/StatusPage/SubscriberNotificationRunLimit.ts +246 -0
  103. package/Server/Utils/StatusPage/SubscriberNotificationTiming.ts +133 -0
  104. package/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.ts +215 -0
  105. package/Server/Utils/StatusPageResource.ts +51 -20
  106. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +133 -7
  107. package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +58 -4
  108. package/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.ts +447 -0
  109. package/Server/Utils/Telemetry/ProxmoxNativePush.ts +903 -0
  110. package/Server/Utils/Telemetry/TelemetryEntity.ts +87 -27
  111. package/Server/Utils/VM/VMAPI.ts +1 -0
  112. package/Server/Utils/VM/VMRunner.ts +10 -0
  113. package/Server/Utils/Workflow/OAuth2TokenClient.ts +10 -0
  114. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +684 -47
  115. package/Server/Views/Partials/SensitiveUrlToken.ejs +8 -5
  116. package/Tests/App/Accounts/RegisterVerifyEmail.test.tsx +748 -0
  117. package/Tests/App/Accounts/ResendVerificationEmail.test.tsx +1222 -0
  118. package/Tests/App/Accounts/VerificationEmailResendUtils.test.ts +797 -0
  119. package/Tests/App/Accounts/VerifyEmailPending.test.tsx +538 -0
  120. package/Tests/App/Accounts/VerifyEmailResend.test.tsx +779 -0
  121. package/Tests/App/AdminDashboard/RowActionPlacementOverrides.test.tsx +278 -0
  122. package/Tests/App/Dashboard/AIChatChartWidgetZoom.test.tsx +738 -0
  123. package/Tests/App/Dashboard/AIFeaturesSettingsPointers.test.tsx +147 -0
  124. package/Tests/App/Dashboard/AIInsightsKubernetesClusterPage.test.tsx +1523 -0
  125. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +463 -3
  126. package/Tests/App/Dashboard/AffectedResourcesPickerDropdownPlacement.test.tsx +958 -0
  127. package/Tests/App/Dashboard/AffectedResourcesPickerFocusLeave.test.tsx +595 -0
  128. package/Tests/App/Dashboard/AskAiAvailability.test.ts +20 -3
  129. package/Tests/App/Dashboard/AskAiDisabled.test.tsx +8 -3
  130. package/Tests/App/Dashboard/CephRateChartReload.test.tsx +1456 -0
  131. package/Tests/App/Dashboard/CephTimeRangeZoom.test.tsx +1229 -0
  132. package/Tests/App/Dashboard/ChartCardTimeRangeZoom.test.tsx +151 -0
  133. package/Tests/App/Dashboard/ChartCardZoomAffordances.test.tsx +257 -0
  134. package/Tests/App/Dashboard/ChartRefetchFrame.test.tsx +141 -0
  135. package/Tests/App/Dashboard/ChartZoomHarness.tsx +249 -0
  136. package/Tests/App/Dashboard/ChartZoomStandIn.tsx +131 -0
  137. package/Tests/App/Dashboard/CloudServerlessSlowLoads.test.tsx +628 -0
  138. package/Tests/App/Dashboard/CloudServerlessTimeRangeZoom.test.tsx +607 -0
  139. package/Tests/App/Dashboard/ClusterAccessNotice.test.tsx +91 -19
  140. package/Tests/App/Dashboard/CompanionMetricsTabZoom.test.tsx +632 -0
  141. package/Tests/App/Dashboard/ContainerChartZoomStandIn.tsx +195 -0
  142. package/Tests/App/Dashboard/ContainerHostOverviewSlowLoads.test.tsx +542 -0
  143. package/Tests/App/Dashboard/ContainerHostOverviewTimeRangeZoom.test.tsx +848 -0
  144. package/Tests/App/Dashboard/ContainerMetricsTimeRangeZoom.test.tsx +561 -0
  145. package/Tests/App/Dashboard/CustomFieldsSettingsIncidentOptions.test.tsx +357 -0
  146. package/Tests/App/Dashboard/DashboardBoardWideZoomShell.test.tsx +1147 -0
  147. package/Tests/App/Dashboard/DashboardEditPermissions.test.tsx +3 -2
  148. package/Tests/App/Dashboard/DashboardHistogramWidgetDragCursor.test.tsx +399 -0
  149. package/Tests/App/Dashboard/DashboardHistogramWidgetZoom.test.tsx +985 -0
  150. package/Tests/App/Dashboard/DashboardHistogramWidgetZoomHint.test.tsx +803 -0
  151. package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +1 -0
  152. package/Tests/App/Dashboard/DashboardSettingsPreviewZoom.test.tsx +679 -0
  153. package/Tests/App/Dashboard/DashboardSloAndDataSourceChartZoom.test.tsx +881 -0
  154. package/Tests/App/Dashboard/DashboardTraceStatusWidgets.test.tsx +878 -0
  155. package/Tests/App/Dashboard/DatabaseMetricChartModalTimeRangeZoom.test.tsx +442 -0
  156. package/Tests/App/Dashboard/DatabaseOverviewTimeRangeZoom.test.tsx +831 -0
  157. package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +43 -0
  158. package/Tests/App/Dashboard/DockerSwarmInsightsTimeRangeZoom.test.tsx +524 -0
  159. package/Tests/App/Dashboard/DockerSwarmRateChartRefresh.test.tsx +259 -0
  160. package/Tests/App/Dashboard/EdgeDetailPanelHistory.test.tsx +763 -0
  161. package/Tests/App/Dashboard/EdgeDetailPanelTimeRangeZoom.test.tsx +547 -0
  162. package/Tests/App/Dashboard/EdgeHistoryBucketsCases.tsx +565 -0
  163. package/Tests/App/Dashboard/EdgeHistoryBucketsKathmandu.test.tsx +73 -0
  164. package/Tests/App/Dashboard/EdgeHistoryBucketsKolkata.test.tsx +74 -0
  165. package/Tests/App/Dashboard/EdgeHistoryBucketsNewYork.test.tsx +72 -0
  166. package/Tests/App/Dashboard/EdgeHistoryBucketsUtc.test.tsx +72 -0
  167. package/Tests/App/Dashboard/EmbeddedMetricCardRefresh.test.tsx +310 -0
  168. package/Tests/App/Dashboard/EmbeddedMetricCardTimeRangeZoom.test.tsx +536 -0
  169. package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +3 -1
  170. package/Tests/App/Dashboard/EscalationRuleRowActions.test.tsx +660 -0
  171. package/Tests/App/Dashboard/EventNotes.test.tsx +778 -2
  172. package/Tests/App/Dashboard/EventNotesPages.test.tsx +119 -0
  173. package/Tests/App/Dashboard/EventNotesUtil.test.ts +116 -17
  174. package/Tests/App/Dashboard/EventOverviewPages.test.tsx +828 -17
  175. package/Tests/App/Dashboard/EventOverviewSnapshotExplorerZoom.test.tsx +1139 -0
  176. package/Tests/App/Dashboard/EventOverviewSnapshotZoomAcrossTabs.test.tsx +1057 -0
  177. package/Tests/App/Dashboard/EventOverviewTelemetrySnapshotZoom.test.tsx +757 -0
  178. package/Tests/App/Dashboard/ExceptionOccurrenceTrendZoom.test.tsx +675 -0
  179. package/Tests/App/Dashboard/ExceptionOccurrencesZoomSurvivesViewSwitch.test.tsx +405 -0
  180. package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +67 -3
  181. package/Tests/App/Dashboard/ExceptionTablesSpanStatus.test.tsx +515 -0
  182. package/Tests/App/Dashboard/ExplorerOwnZoomRegression.test.tsx +739 -0
  183. package/Tests/App/Dashboard/FlowBandwidthAxisCases.tsx +461 -0
  184. package/Tests/App/Dashboard/FlowBandwidthAxisKathmandu.test.tsx +25 -0
  185. package/Tests/App/Dashboard/FlowBandwidthAxisKolkata.test.tsx +25 -0
  186. package/Tests/App/Dashboard/FlowBandwidthAxisNewYork.test.tsx +25 -0
  187. package/Tests/App/Dashboard/FlowBandwidthAxisUtc.test.tsx +24 -0
  188. package/Tests/App/Dashboard/FlowTopTalkersZoomRefetch.test.tsx +722 -0
  189. package/Tests/App/Dashboard/FlowViewportGuard.test.tsx +2526 -0
  190. package/Tests/App/Dashboard/HostPagesSlowLoads.test.tsx +872 -0
  191. package/Tests/App/Dashboard/HostPagesTimeRangeZoom.test.tsx +1208 -0
  192. package/Tests/App/Dashboard/IncidentChangeStateNotifyDefault.test.tsx +91 -1
  193. package/Tests/App/Dashboard/IncidentCreateDetailsStep.test.tsx +643 -0
  194. package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +8 -1
  195. package/Tests/App/Dashboard/IncidentCreateStatusPageScope.test.tsx +601 -0
  196. package/Tests/App/Dashboard/IncidentCustomFieldTemplateVariables.test.tsx +386 -0
  197. package/Tests/App/Dashboard/IncidentNoteTemplatePlaceholders.test.tsx +124 -0
  198. package/Tests/App/Dashboard/IncidentNoteTemplateVariablesLoader.test.ts +305 -0
  199. package/Tests/App/Dashboard/IncidentOverviewNotifyDefault.test.tsx +112 -0
  200. package/Tests/App/Dashboard/IncidentRootCauseTimeRangeZoom.test.tsx +358 -0
  201. package/Tests/App/Dashboard/IncidentSettingsRenotifyOnPublish.test.tsx +674 -0
  202. package/Tests/App/Dashboard/IncidentSettingsStatusPageScope.test.tsx +1051 -0
  203. package/Tests/App/Dashboard/IncidentTemplateCustomFields.test.tsx +384 -0
  204. package/Tests/App/Dashboard/IncidentTemplateStatusPageScopeCard.test.tsx +215 -0
  205. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +100 -9
  206. package/Tests/App/Dashboard/InfrastructureMetricsTabTimeRangeZoom.test.tsx +485 -0
  207. package/Tests/App/Dashboard/InvestigationDrawerCompanionZoom.test.tsx +655 -0
  208. package/Tests/App/Dashboard/InvestigationDrawerZoom.test.tsx +830 -0
  209. package/Tests/App/Dashboard/InvestigationNotStarted.test.tsx +338 -2
  210. package/Tests/App/Dashboard/InvoicesPayInvoice.test.tsx +454 -16
  211. package/Tests/App/Dashboard/KubernetesClusterAiAccessSettings.test.ts +799 -819
  212. package/Tests/App/Dashboard/KubernetesClusterAiAgentOverviewCard.test.tsx +367 -0
  213. package/Tests/App/Dashboard/KubernetesClusterAiAgentStatus.test.ts +1202 -0
  214. package/Tests/App/Dashboard/KubernetesClusterAiDocumentationMarkdown.test.ts +106 -0
  215. package/Tests/App/Dashboard/KubernetesClusterAiNavigation.test.tsx +354 -0
  216. package/Tests/App/Dashboard/KubernetesClusterAiPage.test.tsx +1884 -2054
  217. package/Tests/App/Dashboard/KubernetesClusterAiPageServerParity.test.ts +478 -179
  218. package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +50 -0
  219. package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +2 -1
  220. package/Tests/App/Dashboard/KubernetesClusterOverviewZoom.test.tsx +1217 -0
  221. package/Tests/App/Dashboard/KubernetesClusterPagesZoom.test.tsx +731 -0
  222. package/Tests/App/Dashboard/KubernetesCostsLoadRecovery.test.tsx +869 -0
  223. package/Tests/App/Dashboard/KubernetesCostsZoom.test.tsx +1214 -0
  224. package/Tests/App/Dashboard/KubernetesCostsZoomExactHours.test.tsx +692 -0
  225. package/Tests/App/Dashboard/KubernetesNetworkThroughputReload.test.tsx +902 -0
  226. package/Tests/App/Dashboard/KubernetesNetworkZoomBucketEdges.test.tsx +571 -0
  227. package/Tests/App/Dashboard/KubernetesOverviewZoomBucketEdges.test.tsx +697 -0
  228. package/Tests/App/Dashboard/KubernetesResourceMetricsTabZoom.test.tsx +633 -0
  229. package/Tests/App/Dashboard/LogsExplorerTimeRangeZoom.test.tsx +470 -0
  230. package/Tests/App/Dashboard/LogsInsightsTimeRangeZoom.test.tsx +948 -0
  231. package/Tests/App/Dashboard/LogsViewerPinnedWindowZoom.test.tsx +486 -0
  232. package/Tests/App/Dashboard/MetricChartsBucketInspectorDoubleClick.test.tsx +1001 -0
  233. package/Tests/App/Dashboard/MetricExplorerInvestigationAutoRefresh.test.tsx +514 -0
  234. package/Tests/App/Dashboard/MetricExplorerSavedViewZoomRoundTrip.test.tsx +581 -0
  235. package/Tests/App/Dashboard/MetricExplorerTimeRangeZoom.test.tsx +1162 -0
  236. package/Tests/App/Dashboard/MetricMonitorPreviewZoomLabel.test.ts +72 -0
  237. package/Tests/App/Dashboard/MetricRowSparklineTimeRangeZoom.test.tsx +204 -0
  238. package/Tests/App/Dashboard/MetricViewFirstResultLands.test.tsx +408 -0
  239. package/Tests/App/Dashboard/MetricViewOwnZoomResetPlacement.test.tsx +952 -0
  240. package/Tests/App/Dashboard/MetricViewTimeRange.test.ts +139 -0
  241. package/Tests/App/Dashboard/MetricViewTimeRangeZoom.test.tsx +556 -0
  242. package/Tests/App/Dashboard/MonitorMetricsChartWrappersZoom.test.tsx +552 -0
  243. package/Tests/App/Dashboard/MonitorMetricsTabsTimeRangeZoom.test.tsx +740 -0
  244. package/Tests/App/Dashboard/MonitorOverviewChartsTimeRangeZoom.test.tsx +735 -0
  245. package/Tests/App/Dashboard/MonitorStepPreviewsTimeRangeZoom.test.tsx +835 -0
  246. package/Tests/App/Dashboard/NetworkDeviceTimeRangeZoom.test.tsx +1115 -0
  247. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +186 -17
  248. package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +155 -46
  249. package/Tests/App/Dashboard/PasskeySettings.test.tsx +87 -2
  250. package/Tests/App/Dashboard/ProjectAISettingsPages.test.tsx +884 -0
  251. package/Tests/App/Dashboard/ProxmoxNodeRemove.test.tsx +621 -0
  252. package/Tests/App/Dashboard/ProxmoxOverviewSlowLoads.test.tsx +584 -0
  253. package/Tests/App/Dashboard/ProxmoxPagesTimeRangeZoom.test.tsx +823 -0
  254. package/Tests/App/Dashboard/ProxmoxRateChartReload.test.tsx +1253 -0
  255. package/Tests/App/Dashboard/PublicDashboardBoardWideZoom.test.tsx +605 -0
  256. package/Tests/App/Dashboard/PublicNotePagesNotifyDefault.test.tsx +113 -1
  257. package/Tests/App/Dashboard/RealRowsLineChart.tsx +250 -0
  258. package/Tests/App/Dashboard/RechartsHostsPlotCursor.test.tsx +513 -0
  259. package/Tests/App/Dashboard/RowActionPlacementOverrides.test.tsx +865 -0
  260. package/Tests/App/Dashboard/RumOverviewTimeRangeZoom.test.tsx +877 -0
  261. package/Tests/App/Dashboard/RunnerKubernetesAgentForm.test.tsx +20 -4
  262. package/Tests/App/Dashboard/RunnerSupersededByAiAgentNote.test.tsx +296 -0
  263. package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +146 -36
  264. package/Tests/App/Dashboard/ServiceDatabaseSlowLoads.test.tsx +1166 -0
  265. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +1243 -104
  266. package/Tests/App/Dashboard/ServiceOverviewTimeRangeZoom.test.tsx +792 -0
  267. package/Tests/App/Dashboard/SloBudgetBurnDownZoom.test.tsx +771 -0
  268. package/Tests/App/Dashboard/SloBudgetBurnDownZoomAffordances.test.tsx +380 -0
  269. package/Tests/App/Dashboard/SloEventMetricsBarChartZoom.test.tsx +369 -0
  270. package/Tests/App/Dashboard/SloHistoryChartsTimeRangeZoom.test.tsx +694 -0
  271. package/Tests/App/Dashboard/SloHistoryChartsZoomAffordances.test.tsx +497 -0
  272. package/Tests/App/Dashboard/SloMetricsTimeRangeZoom.test.tsx +541 -0
  273. package/Tests/App/Dashboard/SlowLoadHarness.ts +123 -0
  274. package/Tests/App/Dashboard/SnapshotExplorerPrimaryZoom.test.tsx +1194 -0
  275. package/Tests/App/Dashboard/SnapshotViewerPinnedWindow.test.tsx +620 -0
  276. package/Tests/App/Dashboard/SubscriberAudienceSummary.test.tsx +512 -0
  277. package/Tests/App/Dashboard/SubscriberNotificationPreviewModal.test.tsx +837 -0
  278. package/Tests/App/Dashboard/SubscriberNotificationStatusLabel.test.tsx +88 -0
  279. package/Tests/App/Dashboard/SubscriberNotificationStatusResend.test.tsx +473 -0
  280. package/Tests/App/Dashboard/SubscriberTemplateLivePreview.test.tsx +196 -0
  281. package/Tests/App/Dashboard/TeamAddedSubscribersUnsubscribedNotice.test.tsx +248 -0
  282. package/Tests/App/Dashboard/TelemetrySnapshotPanelTimeRangeZoom.test.tsx +765 -0
  283. package/Tests/App/Dashboard/TelemetrySnapshotZoom.test.tsx +516 -0
  284. package/Tests/App/Dashboard/TelemetrySnapshotZoomAcrossTabs.test.tsx +827 -0
  285. package/Tests/App/Dashboard/TimeRangeZoomPageHarness.tsx +463 -0
  286. package/Tests/App/Dashboard/TraceChartEditorStatusSplit.test.tsx +543 -0
  287. package/Tests/App/Dashboard/TraceStatusPresentation.test.tsx +967 -0
  288. package/Tests/App/Dashboard/TraceTimelineMinimapZoom.test.tsx +315 -0
  289. package/Tests/App/Dashboard/TracesAnalyticsStatusSplit.test.tsx +778 -0
  290. package/Tests/App/Dashboard/TracesAnalyticsViewZoom.test.tsx +349 -0
  291. package/Tests/App/Dashboard/TracesExplorerTimeRangeZoom.test.tsx +551 -0
  292. package/Tests/App/Dashboard/TracesStatusChartAndFacet.test.tsx +687 -0
  293. package/Tests/App/Dashboard/TracesViewerStaleResponses.test.tsx +458 -0
  294. package/Tests/App/Dashboard/UptimeStripsExcludedFromZoom.test.tsx +406 -0
  295. package/Tests/App/Dashboard/VMwareOverviewSlowLoads.test.tsx +535 -0
  296. package/Tests/App/Dashboard/VMwarePagesTimeRangeZoom.test.tsx +606 -0
  297. package/Tests/App/Dashboard/ValueWidgetSparklineStaleHover.test.tsx +1292 -0
  298. package/Tests/App/Dashboard/ValueWidgetSparklineZoom.test.tsx +765 -0
  299. package/Tests/App/SensitiveUrlTokenBootstrap.test.ts +64 -3
  300. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +98 -0
  301. package/Tests/App/StatusPage/StatusPageUnsubscribePage.test.tsx +412 -0
  302. package/Tests/Models/DatabaseModels/KubernetesAiAgentModel.test.ts +295 -0
  303. package/Tests/Models/DatabaseModels/TeamComplianceSettingModel.test.ts +347 -0
  304. package/Tests/Models/IncidentCustomFieldApiSchemaContract.test.ts +217 -0
  305. package/Tests/Models/IncidentCustomFieldCreateSettingsColumns.test.ts +252 -0
  306. package/Tests/Models/StatusPageSubscriberUnsubscribeColumns.test.ts +218 -0
  307. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +58 -0
  308. package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +1 -1
  309. package/Tests/Server/API/AIInvestigationAPI.test.ts +123 -45
  310. package/Tests/Server/API/AutoRemediationApproveClusterRound.test.ts +1329 -0
  311. package/Tests/Server/API/AutoRemediationApproveKubectlRecheck.test.ts +25 -2
  312. package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +7 -0
  313. package/Tests/Server/API/IncidentSubscriberAudienceAPI.test.ts +1408 -0
  314. package/Tests/Server/API/KubernetesClusterAiAccessAPI.test.ts +2 -0
  315. package/Tests/Server/API/KubernetesClusterAiAccessAPIAgent.test.ts +903 -0
  316. package/Tests/Server/API/MicrosoftTeamsChatsRefreshRoute.test.ts +667 -0
  317. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +294 -6
  318. package/Tests/Server/API/ProxmoxResourceAPIRemoveNode.test.ts +1932 -0
  319. package/Tests/Server/API/StatusPageIncidentScope.test.ts +1868 -0
  320. package/Tests/Server/API/StatusPageIncidentScopePostgres.test.ts +790 -0
  321. package/Tests/Server/API/StatusPageManageSubscriptionRecipient.test.ts +10 -1
  322. package/Tests/Server/API/StatusPageManageSubscriptionTemplateVariables.test.ts +206 -18
  323. package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +14 -2
  324. package/Tests/Server/API/StatusPageSubscriberCreateResponse.test.ts +217 -0
  325. package/Tests/Server/API/StatusPageSubscriberOwnership.test.ts +103 -2
  326. package/Tests/Server/API/StatusPageUnsubscribeAPI.test.ts +396 -0
  327. package/Tests/Server/Infrastructure/GlobalCache.test.ts +195 -0
  328. package/Tests/Server/Infrastructure/Postgres/AddIncidentStatusPageScopeMigration.test.ts +395 -0
  329. package/Tests/Server/Infrastructure/Postgres/AddIncidentTemplateStatusPageScopeFlagMigration.test.ts +150 -0
  330. package/Tests/Server/Infrastructure/Postgres/AddKubernetesAiAgentAndAiDefaultsMigration.test.ts +623 -0
  331. package/Tests/Server/Infrastructure/Postgres/AddKubernetesClusterAiAccessMigration.test.ts +87 -10
  332. package/Tests/Server/Infrastructure/Postgres/AddSubscriberNotificationClaimedAtMigration.test.ts +156 -0
  333. package/Tests/Server/Infrastructure/Postgres/AddTeamComplianceRuleScopeMigration.test.ts +622 -0
  334. package/Tests/Server/Infrastructure/Postgres/SkipHiddenIncidentCreatedNotificationsMigration.test.ts +410 -0
  335. package/Tests/Server/Middleware/IdentityRateLimit.test.ts +551 -1
  336. package/Tests/Server/Middleware/SubscriberNotificationPreviewRateLimit.test.ts +238 -0
  337. package/Tests/Server/Middleware/SubscriberNotificationTestSendRateLimit.test.ts +245 -0
  338. package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +61 -4
  339. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerPinnedKey.test.ts +98 -2
  340. package/Tests/Server/Middleware/TelemetryIngestKubernetesAgentRunnerRateLimit.test.ts +105 -2
  341. package/Tests/Server/Services/AIServiceBalanceBlocker.test.ts +282 -0
  342. package/Tests/Server/Services/AddStatusPageSubscriberUnsubscribeTokenMigration.test.ts +152 -0
  343. package/Tests/Server/Services/AlertMonitorClearWritesNull.test.ts +409 -0
  344. package/Tests/Server/Services/AlertMonitorEditFeed.test.ts +474 -0
  345. package/Tests/Server/Services/AlertMonitorEditGuard.test.ts +610 -0
  346. package/Tests/Server/Services/AutoRemediationClusterRuleEngine.test.ts +2 -0
  347. package/Tests/Server/Services/AutoRemediationRuleCommandRunners.test.ts +2 -1
  348. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +221 -36
  349. package/Tests/Server/Services/DatabaseServiceCompareAndSetColumns.test.ts +191 -0
  350. package/Tests/Server/Services/IncidentAffectedMonitorsEdit.test.ts +1360 -0
  351. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +3 -1
  352. package/Tests/Server/Services/IncidentCreatedRenotifyOnPublish.test.ts +681 -0
  353. package/Tests/Server/Services/IncidentCreatedResend.test.ts +775 -0
  354. package/Tests/Server/Services/IncidentCustomFieldRenamePostgres.test.ts +713 -0
  355. package/Tests/Server/Services/IncidentCustomFieldService.test.ts +751 -0
  356. package/Tests/Server/Services/IncidentCustomFieldValueValidation.test.ts +556 -0
  357. package/Tests/Server/Services/IncidentSeverityDeletePausesComplianceRules.test.ts +488 -0
  358. package/Tests/Server/Services/IncidentStatusPageScope.test.ts +2971 -0
  359. package/Tests/Server/Services/IncidentStatusPageScopeFeed.test.ts +131 -0
  360. package/Tests/Server/Services/IncidentStatusPageScopePostgres.test.ts +625 -0
  361. package/Tests/Server/Services/IncidentTemplateCustomFieldsCopy.test.ts +311 -0
  362. package/Tests/Server/Services/IncidentTemplateStatusPageScope.test.ts +363 -0
  363. package/Tests/Server/Services/KubernetesAiAgentJobService.test.ts +329 -0
  364. package/Tests/Server/Services/KubernetesAiAgentServiceHelpers.test.ts +618 -0
  365. package/Tests/Server/Services/KubernetesAiAgentServiceLifecycle.test.ts +1444 -0
  366. package/Tests/Server/Services/KubernetesAiAgentServiceRegister.test.ts +1666 -0
  367. package/Tests/Server/Services/KubernetesClusterAiAccessDeletedRunnerRemedy.test.ts +140 -111
  368. package/Tests/Server/Services/KubernetesClusterAiAccessGapNextSteps.test.ts +267 -105
  369. package/Tests/Server/Services/KubernetesClusterAiAccessLegacySuperseded.test.ts +440 -0
  370. package/Tests/Server/Services/KubernetesClusterAiAccessService.test.ts +567 -411
  371. package/Tests/Server/Services/KubernetesClusterAiAccessTarget.test.ts +1168 -0
  372. package/Tests/Server/Services/KubernetesClusterAiSettingsPermission.test.ts +410 -12
  373. package/Tests/Server/Services/KubernetesClusterServiceAiAccessFeed.test.ts +55 -0
  374. package/Tests/Server/Services/ProjectServiceNewProjectAiDefaults.test.ts +288 -0
  375. package/Tests/Server/Services/ProxmoxResourcePostgres.test.ts +5633 -0
  376. package/Tests/Server/Services/ProxmoxResourceService.test.ts +6458 -5
  377. package/Tests/Server/Services/PublicNoteEditFeedEventType.test.ts +213 -0
  378. package/Tests/Server/Services/PublicNoteResendAccess.test.ts +994 -0
  379. package/Tests/Server/Services/RunnerJobEnqueueKubectl.test.ts +48 -15
  380. package/Tests/Server/Services/RunnerJobEnqueueKubectlAiAgent.test.ts +620 -0
  381. package/Tests/Server/Services/RunnerJobKubectlWriteScope.test.ts +170 -3
  382. package/Tests/Server/Services/RunnerJobTimeoutReason.test.ts +72 -0
  383. package/Tests/Server/Services/RunnerServiceAgentName.test.ts +6 -4
  384. package/Tests/Server/Services/ScheduledMaintenanceAttachmentEdit.test.ts +2113 -0
  385. package/Tests/Server/Services/ScheduledMaintenanceMonitorRelease.test.ts +972 -0
  386. package/Tests/Server/Services/ScheduledMaintenanceSubscriberTemplateFormats.test.ts +289 -9
  387. package/Tests/Server/Services/SmsServiceFailIfNotSent.test.ts +67 -0
  388. package/Tests/Server/Services/StatusPageReportIncidentCounts.test.ts +610 -0
  389. package/Tests/Server/Services/StatusPageSubscriberAudienceCounts.test.ts +201 -0
  390. package/Tests/Server/Services/StatusPageSubscriberAudienceCountsPostgres.test.ts +271 -0
  391. package/Tests/Server/Services/StatusPageSubscriberConfirmationTemplateEscaping.test.ts +200 -0
  392. package/Tests/Server/Services/StatusPageSubscriberNotificationLogRedaction.test.ts +54 -0
  393. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateCompile.test.ts +37 -0
  394. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateIncidentRecordAccess.test.ts +772 -0
  395. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookup.test.ts +506 -0
  396. package/Tests/Server/Services/StatusPageSubscriberNotificationTemplateLookupPostgres.test.ts +261 -0
  397. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +21 -5
  398. package/Tests/Server/Services/StatusPageSubscriberUnsubscribe.test.ts +1618 -0
  399. package/Tests/Server/Services/StatusPageSubscriberUnsubscribeLinkSenders.test.ts +373 -0
  400. package/Tests/Server/Services/StatusPageSubscriberUnsubscribePostgres.test.ts +388 -0
  401. package/Tests/Server/Services/SubscriberNotificationInFlightRequeue.test.ts +500 -0
  402. package/Tests/Server/Services/TeamComplianceSettingPostgres.test.ts +2237 -0
  403. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +3988 -138
  404. package/Tests/Server/Types/MarkdownEmailLinks.test.ts +320 -0
  405. package/Tests/Server/Types/Workflow/Components/Email.test.ts +491 -1
  406. package/Tests/Server/Types/Workflow/Components/EmailSmtpPinning.test.ts +398 -0
  407. package/Tests/Server/Utils/AI/ClusterAccessContext.test.ts +107 -1
  408. package/Tests/Server/Utils/AI/IncidentPostmortemRunner.test.ts +351 -0
  409. package/Tests/Server/Utils/AI/InvestigationClusterAccessWiring.test.ts +2 -0
  410. package/Tests/Server/Utils/AI/InvestigationEligibility.test.ts +310 -0
  411. package/Tests/Server/Utils/AI/KubectlInvestigationToolkit.test.ts +70 -0
  412. package/Tests/Server/Utils/AI/KubectlJobRunnerFinishedJob.test.ts +58 -0
  413. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectl.test.ts +2 -0
  414. package/Tests/Server/Utils/AI/RemediationCommandToolsKubectlOutcome.test.ts +2 -0
  415. package/Tests/Server/Utils/AI/RemediationCommandToolsLiveCluster.test.ts +2 -0
  416. package/Tests/Server/Utils/AI/RemediationCommandToolsRunnerScope.test.ts +125 -1
  417. package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +252 -3
  418. package/Tests/Server/Utils/AI/RemediationExecutionRunnerClusterMode.test.ts +2 -0
  419. package/Tests/Server/Utils/AI/RemediationExecutionRunnerProjectGates.test.ts +240 -0
  420. package/Tests/Server/Utils/AI/RemediationKubectlCopyParity.test.ts +2 -0
  421. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +2 -0
  422. package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutorUnknownRun.test.ts +2 -0
  423. package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +2 -0
  424. package/Tests/Server/Utils/CustomField/CustomFieldRename.test.ts +586 -0
  425. package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +246 -10
  426. package/Tests/Server/Utils/DataSource/EgressGuardDeploymentConfig.test.ts +226 -0
  427. package/Tests/Server/Utils/DataSource/EgressGuardPinning.test.ts +131 -0
  428. package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +4 -0
  429. package/Tests/Server/Utils/EgressPolicyEnvironment.ts +45 -0
  430. package/Tests/Server/Utils/Mail/PinnedSmtpSocket.test.ts +409 -0
  431. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +23 -0
  432. package/Tests/Server/Utils/SSRFProtection.test.ts +4 -0
  433. package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +4 -0
  434. package/Tests/Server/Utils/SSRFProtectionPrivateNetwork.test.ts +4 -0
  435. package/Tests/Server/Utils/SSRFProtectionResolutionDetail.test.ts +314 -0
  436. package/Tests/Server/Utils/StatusPage/IncidentStatusPageScope.test.ts +1699 -0
  437. package/Tests/Server/Utils/StatusPage/IncidentStatusPageScopeQueriesPostgres.test.ts +508 -0
  438. package/Tests/Server/Utils/StatusPage/IncidentSubscriberAudienceCallerPermission.test.ts +246 -0
  439. package/Tests/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.test.ts +1348 -0
  440. package/Tests/Server/Utils/StatusPage/StatusPageReadAccess.test.ts +221 -0
  441. package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.test.ts +132 -0
  442. package/Tests/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.test.ts +155 -0
  443. package/Tests/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.test.ts +708 -0
  444. package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaim.test.ts +255 -0
  445. package/Tests/Server/Utils/StatusPage/SubscriberNotificationClaimPostgres.test.ts +528 -0
  446. package/Tests/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.test.ts +1238 -0
  447. package/Tests/Server/Utils/StatusPage/SubscriberNotificationFanOut.test.ts +472 -0
  448. package/Tests/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.test.ts +1168 -0
  449. package/Tests/Server/Utils/StatusPage/SubscriberNotificationRunLimit.test.ts +275 -0
  450. package/Tests/Server/Utils/StatusPage/SubscriberNotificationTiming.test.ts +131 -0
  451. package/Tests/Server/Utils/StatusPageResource.test.ts +98 -0
  452. package/Tests/Server/Utils/Telemetry/HostAssetAttributesFromOtlp.test.ts +68 -0
  453. package/Tests/Server/Utils/Telemetry/InventoryMirrorPostgres.test.ts +370 -0
  454. package/Tests/Server/Utils/Telemetry/NetworkDeviceInventoryAttributes.test.ts +522 -0
  455. package/Tests/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.test.ts +155 -0
  456. package/Tests/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.test.ts +8631 -0
  457. package/Tests/Server/Utils/Telemetry/ProxmoxNativePush.test.ts +2852 -0
  458. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +367 -1
  459. package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +45 -3
  460. package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +17 -0
  461. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +75 -0
  462. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +25 -0
  463. package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNames.test.ts +3119 -0
  464. package/Tests/Server/Utils/Workspace/MicrosoftTeamsGroupChatNamesPostgres.test.ts +1721 -0
  465. package/Tests/Types/CustomField/CustomFieldSavedViews.test.ts +247 -0
  466. package/Tests/Types/CustomField/CustomFieldTemplateValues.test.ts +186 -0
  467. package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +48 -0
  468. package/Tests/Types/CustomField/CustomFieldValueFormat.test.ts +215 -0
  469. package/Tests/Types/CustomField/CustomFieldValueValidator.test.ts +343 -0
  470. package/Tests/Types/CustomField/CustomFieldVariableKey.test.ts +364 -0
  471. package/Tests/Types/Kubernetes/KubernetesAgentChartAiAccess.test.ts +581 -113
  472. package/Tests/Types/Kubernetes/KubernetesAiAgentAccessContract.test.ts +510 -0
  473. package/Tests/Types/SafeHtml.test.ts +102 -0
  474. package/Tests/Types/StatusPage/IncidentCreatedRenotify.test.ts +419 -0
  475. package/Tests/Types/StatusPage/IncidentCreatedResend.test.ts +118 -0
  476. package/Tests/Types/StatusPage/IncidentScopeAddedPagesNotification.test.ts +558 -0
  477. package/Tests/Types/StatusPage/IncidentSubscriberAudience.test.ts +284 -0
  478. package/Tests/Types/StatusPage/StatusPageSubscriberUnsubscribe.test.ts +341 -0
  479. package/Tests/Types/StatusPage/SubscriberNotificationPreview.test.ts +196 -0
  480. package/Tests/Types/StatusPage/SubscriberNotificationResend.test.ts +201 -0
  481. package/Tests/Types/StatusPage/SubscriberNotificationTemplateCompiler.test.ts +277 -0
  482. package/Tests/Types/StatusPage/SubscriberNotificationTemplatePreview.test.ts +179 -0
  483. package/Tests/Types/StatusPage/SubscriberNotificationTemplateVariables.test.ts +324 -0
  484. package/Tests/Types/StatusPage/SubscriberUpdateNotification.test.ts +60 -13
  485. package/Tests/Types/Team/ComplianceRule.test.ts +1029 -0
  486. package/Tests/Types/Team/TeamComplianceStatus.test.ts +176 -0
  487. package/Tests/UI/Components/ActionButton/RowActions.test.tsx +641 -0
  488. package/Tests/UI/Components/ActionButton/RowActionsAccessibility.test.tsx +414 -0
  489. package/Tests/UI/Components/ActionButton/SplitActionButtons.test.ts +565 -0
  490. package/Tests/UI/Components/Charts/BarChartDragToSelect.test.tsx +386 -0
  491. package/Tests/UI/Components/Charts/ChartDoubleClickResetWithoutDblclick.test.tsx +1025 -0
  492. package/Tests/UI/Components/Charts/ChartGroupPageZoomHint.test.tsx +196 -0
  493. package/Tests/UI/Components/Charts/ChartPlaceholdersTakeDoubleClick.test.tsx +1861 -0
  494. package/Tests/UI/Components/Charts/ChartPlotClicksWhileResetArmed.test.tsx +382 -0
  495. package/Tests/UI/Components/Charts/ChartRangeSelection.test.tsx +704 -0
  496. package/Tests/UI/Components/Charts/ChartRangeSelectionDoubleClickReset.test.tsx +1481 -0
  497. package/Tests/UI/Components/Charts/ChartRangeSelectionRealRecharts.test.tsx +918 -0
  498. package/Tests/UI/Components/Charts/ChartRowBucketStart.test.ts +350 -0
  499. package/Tests/UI/Components/Charts/DoubleClickReset.test.tsx +996 -0
  500. package/Tests/UI/Components/Charts/HistogramDoubleClickResetRealRecharts.test.tsx +965 -0
  501. package/Tests/UI/Components/Charts/HistogramDriftThenRefetch.test.tsx +277 -0
  502. package/Tests/UI/Components/Charts/HistogramRangeSelectionDataChange.test.tsx +1757 -0
  503. package/Tests/UI/Components/Charts/HistogramRangeSelectionRealRecharts.test.tsx +560 -0
  504. package/Tests/UI/Components/Charts/TimeRangeZoom/ChartPageTimeRangeZoom.test.tsx +513 -0
  505. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.test.tsx +845 -0
  506. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.test.tsx +119 -0
  507. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHintTailwind.test.tsx +274 -0
  508. package/Tests/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.test.ts +433 -0
  509. package/Tests/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.test.tsx +863 -0
  510. package/Tests/UI/Components/Charts/TimeRangeZoom/ZoomDocsPromises.test.tsx +1147 -0
  511. package/Tests/UI/Components/CustomFields/CustomFieldFormFields.test.ts +432 -0
  512. package/Tests/UI/Components/CustomFields/CustomFieldModelFormFields.test.tsx +685 -0
  513. package/Tests/UI/Components/CustomFields/CustomFieldsDetailOrder.test.tsx +469 -0
  514. package/Tests/UI/Components/DisabledButtonTooltip.test.tsx +14 -3
  515. package/Tests/UI/Components/EntityDropdownEscape.test.tsx +454 -0
  516. package/Tests/UI/Components/EntityDropdownFocusLeave.test.tsx +420 -0
  517. package/Tests/UI/Components/Forms/FormFieldDropdownDisabled.test.tsx +233 -0
  518. package/Tests/UI/Components/Forms/ModelFormCustomFieldInputs.test.tsx +325 -0
  519. package/Tests/UI/Components/Forms/ModelFormDropdownOptionsRefused.test.tsx +358 -0
  520. package/Tests/UI/Components/Input.test.tsx +80 -1
  521. package/Tests/UI/Components/LogsAnalyticsViewZoom.test.tsx +947 -0
  522. package/Tests/UI/Components/LogsHistogramDragTooltip.test.tsx +17 -5
  523. package/Tests/UI/Components/LogsViewerAnalyticsZoom.test.tsx +501 -0
  524. package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +1 -1
  525. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +6 -4
  526. package/Tests/UI/Components/ModelTable/BaseModelTablePermissionGating.test.tsx +60 -8
  527. package/Tests/UI/Components/ModelTable/BaseModelTableRowActions.test.tsx +476 -0
  528. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +22 -0
  529. package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +7 -5
  530. package/Tests/UI/Components/MoreMenuPortal.test.tsx +623 -0
  531. package/Tests/UI/Components/StatusPage/ResourceGridActions.test.tsx +538 -0
  532. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +14 -4
  533. package/Tests/UI/Components/TableCellWrapping.test.tsx +3 -3
  534. package/Tests/UI/Components/TableRowActions.test.tsx +472 -0
  535. package/Tests/UI/Components/TelemetryHistogramDragTooltip.test.tsx +1 -1
  536. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerHistogramClickZoom.test.tsx +1 -1
  537. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerZoomScope.test.tsx +470 -0
  538. package/Tests/UI/Components/TelemetryViewer/UseViewerTimeRangeZoom.test.tsx +571 -0
  539. package/Tests/UI/Components/TelemetryViewer/ViewerDoubleClickResetUnderDataChange.test.tsx +928 -0
  540. package/Tests/UI/Components/TelemetryViewer/ViewerFollowsEnclosingZoom.test.tsx +1569 -0
  541. package/Tests/UI/Components/Toggle.test.tsx +66 -0
  542. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardRefreshNames.test.tsx +1023 -0
  543. package/Tests/UI/Monitor/NetworkPathView.test.tsx +289 -0
  544. package/Tests/UI/Rum/ChunkLoader.test.ts +201 -0
  545. package/Tests/UI/Rum/FidelityNotices.test.ts +54 -0
  546. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +72 -0
  547. package/Tests/UI/Rum/ReplayFrameCapture.test.ts +1009 -9
  548. package/Tests/UI/Rum/ReplayRecordedAssets.test.ts +1069 -0
  549. package/Tests/UI/Rum/ReplayStage.test.tsx +913 -2
  550. package/Tests/UI/Rum/SessionReplayTable.test.tsx +443 -8
  551. package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +439 -9
  552. package/Tests/UI/Rum/UserFlowTablesRowActions.test.tsx +501 -0
  553. package/Tests/Utils/AiRemediation/KubectlPolicyChartRbacParity.test.ts +47 -19
  554. package/Tests/Utils/AiRemediation/KubectlWriteScopeCallerParity.test.ts +2 -0
  555. package/Tests/Utils/AiRemediation/KubernetesAiAgentPolicyCopyParity.test.ts +396 -0
  556. package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +104 -1
  557. package/Tests/Utils/Incident/IncidentNoteTemplateVariables.test.ts +623 -0
  558. package/Tests/Utils/Markdown/MarkdownEscape.test.ts +89 -0
  559. package/Tests/Utils/ProbeNetworkFailureUtil.test.ts +992 -0
  560. package/Tests/Utils/RequestFailedDetails.test.ts +575 -0
  561. package/Types/AI/AIRunType.ts +9 -4
  562. package/Types/AI/InvestigationNotStartedReason.ts +6 -0
  563. package/Types/AutoRemediation/AiRemediationCommandPlan.ts +14 -16
  564. package/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.ts +28 -0
  565. package/Types/CustomField/CustomFieldOrder.ts +55 -0
  566. package/Types/CustomField/CustomFieldSavedViews.ts +225 -0
  567. package/Types/CustomField/CustomFieldTemplateMerge.ts +71 -0
  568. package/Types/CustomField/CustomFieldType.ts +14 -0
  569. package/Types/CustomField/CustomFieldValueFormat.ts +161 -0
  570. package/Types/CustomField/CustomFieldValueValidator.ts +488 -0
  571. package/Types/CustomField/CustomFieldVariableKey.ts +211 -0
  572. package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +4 -2
  573. package/Types/Kubernetes/KubernetesClusterAiAccess.ts +263 -32
  574. package/Types/Kubernetes/KubernetesClusterAiAccessPermissions.ts +20 -16
  575. package/Types/Kubernetes/KubernetesClusterAiInsights.ts +81 -0
  576. package/Types/Monitor/ProxmoxAlertTemplates.ts +4 -4
  577. package/Types/SafeHtml.ts +93 -0
  578. package/Types/StatusPage/IncidentCreatedRenotify.ts +224 -0
  579. package/Types/StatusPage/IncidentCreatedResend.ts +108 -0
  580. package/Types/StatusPage/IncidentScopeAddedPagesNotification.ts +343 -0
  581. package/Types/StatusPage/IncidentSubscriberAudience.ts +332 -0
  582. package/Types/StatusPage/StatusPageSubscriberUnsubscribe.ts +405 -0
  583. package/Types/StatusPage/SubscriberNotificationInterruption.ts +32 -0
  584. package/Types/StatusPage/SubscriberNotificationPreview.ts +322 -0
  585. package/Types/StatusPage/SubscriberNotificationResend.ts +132 -0
  586. package/Types/StatusPage/SubscriberNotificationTemplateCompiler.ts +180 -0
  587. package/Types/StatusPage/SubscriberNotificationTemplatePreview.ts +301 -0
  588. package/Types/StatusPage/SubscriberNotificationTemplateVariables.ts +217 -4
  589. package/Types/StatusPage/SubscriberUpdateNotification.ts +32 -11
  590. package/Types/Team/ComplianceNotificationChannel.ts +22 -0
  591. package/Types/Team/ComplianceRule.ts +434 -0
  592. package/Types/Team/ComplianceRuleType.ts +21 -0
  593. package/Types/Team/TeamComplianceStatus.ts +81 -0
  594. package/Types/Telemetry/TelemetryIngestSurface.ts +28 -0
  595. package/UI/Components/ActionButton/ActionButtonSchema.ts +21 -0
  596. package/UI/Components/ActionButton/RowActions.tsx +238 -0
  597. package/UI/Components/ActionButton/SplitActionButtons.ts +173 -0
  598. package/UI/Components/Charts/Area/AreaChart.tsx +32 -2
  599. package/UI/Components/Charts/Bar/BarChart.tsx +40 -3
  600. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +37 -11
  601. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +80 -243
  602. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +122 -31
  603. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +95 -244
  604. package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +214 -0
  605. package/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.ts +32 -0
  606. package/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.ts +599 -0
  607. package/UI/Components/Charts/Line/LineChart.tsx +32 -2
  608. package/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.tsx +70 -0
  609. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.tsx +226 -0
  610. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.tsx +73 -0
  611. package/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.ts +163 -0
  612. package/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.ts +215 -0
  613. package/UI/Components/Charts/Utils/DataPoint.ts +20 -8
  614. package/UI/Components/Charts/Utils/XAxis.ts +75 -0
  615. package/UI/Components/Charts/Utils/useHistogramRangeSelection.ts +493 -59
  616. package/UI/Components/CustomFields/CustomFieldFormFields.ts +317 -0
  617. package/UI/Components/CustomFields/CustomFieldModelFormFields.tsx +395 -0
  618. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +95 -55
  619. package/UI/Components/EntityDropdown/EntityDropdown.tsx +107 -11
  620. package/UI/Components/Forms/Fields/FormField.tsx +10 -0
  621. package/UI/Components/Forms/ModelForm.tsx +70 -8
  622. package/UI/Components/Forms/Validation.ts +1 -1
  623. package/UI/Components/Input/Input.tsx +90 -57
  624. package/UI/Components/List/ListRow.tsx +8 -74
  625. package/UI/Components/LogsViewer/LogsViewer.tsx +40 -11
  626. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +22 -6
  627. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +209 -6
  628. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +127 -84
  629. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +3 -0
  630. package/UI/Components/ModelFormModal/ModelFormModal.tsx +2 -4
  631. package/UI/Components/ModelTable/AnalyticsModelTable.tsx +2 -6
  632. package/UI/Components/ModelTable/BaseModelTable.tsx +21 -9
  633. package/UI/Components/ModelTable/CustomFieldColumns.tsx +14 -1
  634. package/UI/Components/ModelTable/ModelTable.tsx +6 -7
  635. package/UI/Components/MoreMenu/MoreMenu.tsx +365 -30
  636. package/UI/Components/MoreMenu/MoreMenuItem.tsx +70 -17
  637. package/UI/Components/OrderedStatesList/Item.tsx +7 -72
  638. package/UI/Components/StatusPage/ResourceGrid.tsx +126 -77
  639. package/UI/Components/Table/TableBody.tsx +8 -2
  640. package/UI/Components/Table/TableRow.tsx +20 -140
  641. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +41 -10
  642. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +47 -9
  643. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +25 -6
  644. package/UI/Components/TelemetryViewer/types.ts +2 -0
  645. package/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.ts +448 -0
  646. package/UI/Components/Toggle/Toggle.tsx +26 -0
  647. package/UI/Components/Tooltip/Tooltip.tsx +8 -1
  648. package/UI/Styles/Theme.css +14 -2
  649. package/UI/Utils/SensitiveUrlToken.ts +10 -5
  650. package/Utils/API.ts +247 -2
  651. package/Utils/AiRemediation/CommandPolicy.ts +1 -1
  652. package/Utils/AiRemediation/KubectlPolicy.ts +1 -1
  653. package/Utils/Dashboard/DashboardTimeRangeZoom.ts +46 -2
  654. package/Utils/Incident/IncidentNoteTemplateVariables.ts +304 -0
  655. package/Utils/Markdown/MarkdownEscape.ts +65 -0
  656. package/Utils/ProbeNetworkFailureUtil.ts +673 -0
  657. package/build/dist/Models/DatabaseModels/Incident.js +267 -1
  658. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  659. package/build/dist/Models/DatabaseModels/IncidentCustomField.js +207 -1
  660. package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
  661. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +31 -0
  662. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  663. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +124 -1
  664. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  665. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  666. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  667. package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js +385 -0
  668. package/build/dist/Models/DatabaseModels/KubernetesAiAgent.js.map +1 -0
  669. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +3 -3
  670. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  671. package/build/dist/Models/DatabaseModels/Project.js +56 -5
  672. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  673. package/build/dist/Models/DatabaseModels/ProxmoxResource.js +51 -1
  674. package/build/dist/Models/DatabaseModels/ProxmoxResource.js.map +1 -1
  675. package/build/dist/Models/DatabaseModels/RunnerJob.js +74 -0
  676. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  677. package/build/dist/Models/DatabaseModels/StatusPage.js +56 -0
  678. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  679. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +131 -0
  680. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  681. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js +134 -3
  682. package/build/dist/Models/DatabaseModels/TeamComplianceSetting.js.map +1 -1
  683. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  684. package/build/dist/Server/API/AIChatAPI.js +6 -1
  685. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  686. package/build/dist/Server/API/AIInvestigationAPI.js +17 -10
  687. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  688. package/build/dist/Server/API/AutoRemediationAPI.js +144 -24
  689. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  690. package/build/dist/Server/API/IncidentAPI.js +100 -1
  691. package/build/dist/Server/API/IncidentAPI.js.map +1 -1
  692. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js +436 -22
  693. package/build/dist/Server/API/KubernetesClusterAiAccessAPI.js.map +1 -1
  694. package/build/dist/Server/API/MicrosoftTeamsAPI.js +69 -13
  695. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  696. package/build/dist/Server/API/ProxmoxResourceAPI.js +187 -14
  697. package/build/dist/Server/API/ProxmoxResourceAPI.js.map +1 -1
  698. package/build/dist/Server/API/StatusPageAPI.js +417 -176
  699. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  700. package/build/dist/Server/API/StatusPageSubscriberAPI.js +52 -13
  701. package/build/dist/Server/API/StatusPageSubscriberAPI.js.map +1 -1
  702. package/build/dist/Server/Infrastructure/GlobalCache.js +27 -0
  703. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  704. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js +41 -0
  705. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795300000000-SkipHiddenIncidentCreatedNotifications.js.map +1 -0
  706. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js +57 -0
  707. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795400000000-AddIncidentStatusPageScope.js.map +1 -0
  708. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js +23 -0
  709. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795500000000-AddIncidentTemplateStatusPageScopeFlag.js.map +1 -0
  710. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js +40 -0
  711. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795600000000-AddStatusPageSubscriberUnsubscribeToken.js.map +1 -0
  712. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js +26 -0
  713. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795700000000-AddStatusPageSubscriberIsAddedByTeam.js.map +1 -0
  714. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js +89 -0
  715. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795800000000-AddIncidentCustomFieldCreateAndNotificationSettings.js.map +1 -0
  716. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js +29 -0
  717. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795900000000-AddSubscriberNotificationClaimedAt.js.map +1 -0
  718. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js +50 -0
  719. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796000000000-AddTeamComplianceRuleScope.js.map +1 -0
  720. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js +84 -0
  721. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796100000000-AddKubernetesAiAgentAndAiDefaults.js.map +1 -0
  722. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js +31 -0
  723. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1796200000000-AddProxmoxResourceNativePushColumns.js.map +1 -0
  724. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +20 -0
  725. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  726. package/build/dist/Server/Infrastructure/Semaphore.js +3 -0
  727. package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
  728. package/build/dist/Server/Middleware/IdentityRateLimit.js +89 -3
  729. package/build/dist/Server/Middleware/IdentityRateLimit.js.map +1 -1
  730. package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js +149 -0
  731. package/build/dist/Server/Middleware/SubscriberNotificationPreviewRateLimit.js.map +1 -0
  732. package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js +190 -0
  733. package/build/dist/Server/Middleware/SubscriberNotificationTestSendRateLimit.js.map +1 -0
  734. package/build/dist/Server/Middleware/TelemetryIngest.js +34 -19
  735. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  736. package/build/dist/Server/Services/AIService.js +59 -1
  737. package/build/dist/Server/Services/AIService.js.map +1 -1
  738. package/build/dist/Server/Services/AlertService.js +248 -13
  739. package/build/dist/Server/Services/AlertService.js.map +1 -1
  740. package/build/dist/Server/Services/AlertSeverityService.js +51 -15
  741. package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
  742. package/build/dist/Server/Services/AutoRemediationRuleService.js +1 -1
  743. package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -1
  744. package/build/dist/Server/Services/DatabaseService.js +56 -7
  745. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  746. package/build/dist/Server/Services/IncidentCustomFieldService.js +204 -8
  747. package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
  748. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +24 -0
  749. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -1
  750. package/build/dist/Server/Services/IncidentEpisodeService.js +14 -0
  751. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  752. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +26 -0
  753. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
  754. package/build/dist/Server/Services/IncidentPublicNoteService.js +26 -1
  755. package/build/dist/Server/Services/IncidentPublicNoteService.js.map +1 -1
  756. package/build/dist/Server/Services/IncidentService.js +1251 -72
  757. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  758. package/build/dist/Server/Services/IncidentSeverityService.js +51 -15
  759. package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
  760. package/build/dist/Server/Services/IncidentStateTimelineService.js +12 -0
  761. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  762. package/build/dist/Server/Services/IncidentTemplateService.js +83 -0
  763. package/build/dist/Server/Services/IncidentTemplateService.js.map +1 -1
  764. package/build/dist/Server/Services/KubernetesAiAgentJobService.js +158 -0
  765. package/build/dist/Server/Services/KubernetesAiAgentJobService.js.map +1 -0
  766. package/build/dist/Server/Services/KubernetesAiAgentService.js +1145 -0
  767. package/build/dist/Server/Services/KubernetesAiAgentService.js.map +1 -0
  768. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js +842 -371
  769. package/build/dist/Server/Services/KubernetesClusterAiAccessService.js.map +1 -1
  770. package/build/dist/Server/Services/KubernetesClusterService.js +86 -17
  771. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  772. package/build/dist/Server/Services/ProjectService.js +22 -0
  773. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  774. package/build/dist/Server/Services/ProxmoxResourceService.js +258 -3
  775. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  776. package/build/dist/Server/Services/RunnerJobService.js +178 -92
  777. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  778. package/build/dist/Server/Services/RunnerService.js +1 -1
  779. package/build/dist/Server/Services/RunnerService.js.map +1 -1
  780. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js +26 -1
  781. package/build/dist/Server/Services/ScheduledMaintenancePublicNoteService.js.map +1 -1
  782. package/build/dist/Server/Services/ScheduledMaintenanceService.js +582 -20
  783. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  784. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +316 -46
  785. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  786. package/build/dist/Server/Services/SmsService.js +2 -22
  787. package/build/dist/Server/Services/SmsService.js.map +1 -1
  788. package/build/dist/Server/Services/StatusPageService.js +47 -9
  789. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  790. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +237 -32
  791. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  792. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js +109 -0
  793. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateStatusPageService.js.map +1 -1
  794. package/build/dist/Server/Services/StatusPageSubscriberService.js +925 -99
  795. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  796. package/build/dist/Server/Services/TeamComplianceSettingService.js +833 -7
  797. package/build/dist/Server/Services/TeamComplianceSettingService.js.map +1 -1
  798. package/build/dist/Server/Types/Markdown.js +110 -6
  799. package/build/dist/Server/Types/Markdown.js.map +1 -1
  800. package/build/dist/Server/Types/Workflow/Components/Email.js +55 -6
  801. package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
  802. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js +52 -9
  803. package/build/dist/Server/Utils/AI/ClusterAccess/ClusterAccessContext.js.map +1 -1
  804. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js +13 -9
  805. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlInvestigationToolkit.js.map +1 -1
  806. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js +7 -3
  807. package/build/dist/Server/Utils/AI/ClusterAccess/KubectlJobRunner.js.map +1 -1
  808. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +29 -15
  809. package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -1
  810. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +56 -20
  811. package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -1
  812. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +39 -5
  813. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  814. package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js +30 -3
  815. package/build/dist/Server/Utils/AI/SRE/IncidentPostmortemRunner.js.map +1 -1
  816. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js +13 -2
  817. package/build/dist/Server/Utils/AI/SRE/InvestigationEligibility.js.map +1 -1
  818. package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js +1 -1
  819. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +4 -2
  820. package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -1
  821. package/build/dist/Server/Utils/CustomField/CustomFieldRename.js +211 -0
  822. package/build/dist/Server/Utils/CustomField/CustomFieldRename.js.map +1 -0
  823. package/build/dist/Server/Utils/DataSource/EgressGuard.js +41 -12
  824. package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
  825. package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js +101 -0
  826. package/build/dist/Server/Utils/Mail/PinnedSmtpSocket.js.map +1 -0
  827. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -0
  828. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  829. package/build/dist/Server/Utils/SSRFProtection.js +48 -6
  830. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  831. package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js +631 -0
  832. package/build/dist/Server/Utils/StatusPage/IncidentStatusPageScope.js.map +1 -0
  833. package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js +403 -0
  834. package/build/dist/Server/Utils/StatusPage/IncidentSubscriberAudienceBuilder.js.map +1 -0
  835. package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js +451 -0
  836. package/build/dist/Server/Utils/StatusPage/IncidentTemplateVariableBuilder.js.map +1 -0
  837. package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js +13 -0
  838. package/build/dist/Server/Utils/StatusPage/StatusPageExclusion.js.map +1 -0
  839. package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js +91 -0
  840. package/build/dist/Server/Utils/StatusPage/StatusPageReadAccess.js.map +1 -0
  841. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js +101 -0
  842. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeNotice.js.map +1 -0
  843. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js +47 -0
  844. package/build/dist/Server/Utils/StatusPage/StatusPageSubscriberUnsubscribeToken.js.map +1 -0
  845. package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js +273 -0
  846. package/build/dist/Server/Utils/StatusPage/SubscriberIncidentEmailBuilder.js.map +1 -0
  847. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js +73 -0
  848. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationClaim.js.map +1 -0
  849. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js +722 -0
  850. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationDeliveryRecord.js.map +1 -0
  851. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js +102 -0
  852. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationFanOut.js.map +1 -0
  853. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js +527 -0
  854. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationPreviewBuilder.js.map +1 -0
  855. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js +278 -0
  856. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationResendAccess.js.map +1 -0
  857. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js +137 -0
  858. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationRunLimit.js.map +1 -0
  859. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js +103 -0
  860. package/build/dist/Server/Utils/StatusPage/SubscriberNotificationTiming.js.map +1 -0
  861. package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js +114 -0
  862. package/build/dist/Server/Utils/StatusPage/SubscriberTemplateIncidentRecordAccess.js.map +1 -0
  863. package/build/dist/Server/Utils/StatusPageResource.js +39 -7
  864. package/build/dist/Server/Utils/StatusPageResource.js.map +1 -1
  865. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +111 -6
  866. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  867. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +40 -4
  868. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
  869. package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js +296 -0
  870. package/build/dist/Server/Utils/Telemetry/ProxmoxNativeNodeLiveness.js.map +1 -0
  871. package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js +699 -0
  872. package/build/dist/Server/Utils/Telemetry/ProxmoxNativePush.js.map +1 -0
  873. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +77 -19
  874. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  875. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  876. package/build/dist/Server/Utils/VM/VMRunner.js +1 -0
  877. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  878. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js +3 -0
  879. package/build/dist/Server/Utils/Workflow/OAuth2TokenClient.js.map +1 -1
  880. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +505 -42
  881. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  882. package/build/dist/Types/AI/AIRunType.js +9 -4
  883. package/build/dist/Types/AI/AIRunType.js.map +1 -1
  884. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +10 -16
  885. package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -1
  886. package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js +28 -0
  887. package/build/dist/Types/AutoRemediation/AiRemediationCommandPolicyVerdict.js.map +1 -0
  888. package/build/dist/Types/CustomField/CustomFieldOrder.js +36 -0
  889. package/build/dist/Types/CustomField/CustomFieldOrder.js.map +1 -0
  890. package/build/dist/Types/CustomField/CustomFieldSavedViews.js +135 -0
  891. package/build/dist/Types/CustomField/CustomFieldSavedViews.js.map +1 -0
  892. package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js +27 -0
  893. package/build/dist/Types/CustomField/CustomFieldTemplateMerge.js.map +1 -0
  894. package/build/dist/Types/CustomField/CustomFieldType.js +12 -0
  895. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  896. package/build/dist/Types/CustomField/CustomFieldValueFormat.js +126 -0
  897. package/build/dist/Types/CustomField/CustomFieldValueFormat.js.map +1 -0
  898. package/build/dist/Types/CustomField/CustomFieldValueValidator.js +280 -0
  899. package/build/dist/Types/CustomField/CustomFieldValueValidator.js.map +1 -0
  900. package/build/dist/Types/CustomField/CustomFieldVariableKey.js +150 -0
  901. package/build/dist/Types/CustomField/CustomFieldVariableKey.js.map +1 -0
  902. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js +72 -16
  903. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccess.js.map +1 -1
  904. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js +20 -16
  905. package/build/dist/Types/Kubernetes/KubernetesClusterAiAccessPermissions.js.map +1 -1
  906. package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js +17 -0
  907. package/build/dist/Types/Kubernetes/KubernetesClusterAiInsights.js.map +1 -0
  908. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +4 -4
  909. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
  910. package/build/dist/Types/SafeHtml.js +84 -0
  911. package/build/dist/Types/SafeHtml.js.map +1 -0
  912. package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js +117 -0
  913. package/build/dist/Types/StatusPage/IncidentCreatedRenotify.js.map +1 -0
  914. package/build/dist/Types/StatusPage/IncidentCreatedResend.js +86 -0
  915. package/build/dist/Types/StatusPage/IncidentCreatedResend.js.map +1 -0
  916. package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js +246 -0
  917. package/build/dist/Types/StatusPage/IncidentScopeAddedPagesNotification.js.map +1 -0
  918. package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js +183 -0
  919. package/build/dist/Types/StatusPage/IncidentSubscriberAudience.js.map +1 -0
  920. package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js +277 -0
  921. package/build/dist/Types/StatusPage/StatusPageSubscriberUnsubscribe.js.map +1 -0
  922. package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js +27 -0
  923. package/build/dist/Types/StatusPage/SubscriberNotificationInterruption.js.map +1 -0
  924. package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js +190 -0
  925. package/build/dist/Types/StatusPage/SubscriberNotificationPreview.js.map +1 -0
  926. package/build/dist/Types/StatusPage/SubscriberNotificationResend.js +102 -0
  927. package/build/dist/Types/StatusPage/SubscriberNotificationResend.js.map +1 -0
  928. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js +97 -0
  929. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateCompiler.js.map +1 -0
  930. package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js +175 -0
  931. package/build/dist/Types/StatusPage/SubscriberNotificationTemplatePreview.js.map +1 -0
  932. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js +144 -4
  933. package/build/dist/Types/StatusPage/SubscriberNotificationTemplateVariables.js.map +1 -1
  934. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js +29 -9
  935. package/build/dist/Types/StatusPage/SubscriberUpdateNotification.js.map +1 -1
  936. package/build/dist/Types/Team/ComplianceNotificationChannel.js +23 -0
  937. package/build/dist/Types/Team/ComplianceNotificationChannel.js.map +1 -0
  938. package/build/dist/Types/Team/ComplianceRule.js +304 -0
  939. package/build/dist/Types/Team/ComplianceRule.js.map +1 -0
  940. package/build/dist/Types/Team/ComplianceRuleType.js +21 -0
  941. package/build/dist/Types/Team/ComplianceRuleType.js.map +1 -1
  942. package/build/dist/Types/Team/TeamComplianceStatus.js +2 -0
  943. package/build/dist/Types/Team/TeamComplianceStatus.js.map +1 -0
  944. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +25 -0
  945. package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -1
  946. package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js +20 -1
  947. package/build/dist/UI/Components/ActionButton/ActionButtonSchema.js.map +1 -1
  948. package/build/dist/UI/Components/ActionButton/RowActions.js +113 -0
  949. package/build/dist/UI/Components/ActionButton/RowActions.js.map +1 -0
  950. package/build/dist/UI/Components/ActionButton/SplitActionButtons.js +71 -0
  951. package/build/dist/UI/Components/ActionButton/SplitActionButtons.js.map +1 -0
  952. package/build/dist/UI/Components/Charts/Area/AreaChart.js +16 -1
  953. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  954. package/build/dist/UI/Components/Charts/Bar/BarChart.js +16 -1
  955. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  956. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +27 -5
  957. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  958. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +61 -194
  959. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  960. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +84 -26
  961. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  962. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +75 -196
  963. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  964. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +133 -0
  965. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -1
  966. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js +26 -0
  967. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartBucketsChange.js.map +1 -0
  968. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js +371 -0
  969. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseChartRangeSelection.js.map +1 -0
  970. package/build/dist/UI/Components/Charts/Line/LineChart.js +16 -1
  971. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  972. package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js +35 -0
  973. package/build/dist/UI/Components/Charts/TimeRangeZoom/ResetTimeRangeZoomButton.js.map +1 -0
  974. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js +107 -0
  975. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomContext.js.map +1 -0
  976. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js +41 -0
  977. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomHint.js.map +1 -0
  978. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js +117 -0
  979. package/build/dist/UI/Components/Charts/TimeRangeZoom/TimeRangeZoomUtil.js.map +1 -0
  980. package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js +121 -0
  981. package/build/dist/UI/Components/Charts/TimeRangeZoom/UseTimeRangeZoom.js.map +1 -0
  982. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -7
  983. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  984. package/build/dist/UI/Components/Charts/Utils/XAxis.js +70 -0
  985. package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
  986. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js +305 -38
  987. package/build/dist/UI/Components/Charts/Utils/useHistogramRangeSelection.js.map +1 -1
  988. package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js +169 -0
  989. package/build/dist/UI/Components/CustomFields/CustomFieldFormFields.js.map +1 -0
  990. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js +212 -0
  991. package/build/dist/UI/Components/CustomFields/CustomFieldModelFormFields.js.map +1 -0
  992. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +64 -49
  993. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  994. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +89 -14
  995. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  996. package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -2
  997. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  998. package/build/dist/UI/Components/Forms/ModelForm.js +39 -5
  999. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  1000. package/build/dist/UI/Components/Forms/Validation.js +1 -1
  1001. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  1002. package/build/dist/UI/Components/Input/Input.js +69 -54
  1003. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  1004. package/build/dist/UI/Components/List/ListRow.js +4 -39
  1005. package/build/dist/UI/Components/List/ListRow.js.map +1 -1
  1006. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +31 -8
  1007. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  1008. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +10 -1
  1009. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
  1010. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +139 -21
  1011. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  1012. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +30 -8
  1013. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  1014. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
  1015. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  1016. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  1017. package/build/dist/UI/Components/ModelTable/AnalyticsModelTable.js.map +1 -1
  1018. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +12 -2
  1019. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  1020. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +13 -1
  1021. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  1022. package/build/dist/UI/Components/ModelTable/ModelTable.js +1 -1
  1023. package/build/dist/UI/Components/ModelTable/ModelTable.js.map +1 -1
  1024. package/build/dist/UI/Components/MoreMenu/MoreMenu.js +253 -14
  1025. package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
  1026. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js +43 -13
  1027. package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js.map +1 -1
  1028. package/build/dist/UI/Components/OrderedStatesList/Item.js +2 -38
  1029. package/build/dist/UI/Components/OrderedStatesList/Item.js.map +1 -1
  1030. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +53 -15
  1031. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  1032. package/build/dist/UI/Components/Table/TableBody.js +6 -2
  1033. package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
  1034. package/build/dist/UI/Components/Table/TableRow.js +12 -75
  1035. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  1036. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +32 -7
  1037. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  1038. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +27 -9
  1039. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  1040. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +11 -1
  1041. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
  1042. package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js +272 -0
  1043. package/build/dist/UI/Components/TelemetryViewer/useViewerTimeRangeZoom.js.map +1 -0
  1044. package/build/dist/UI/Components/Toggle/Toggle.js +14 -1
  1045. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  1046. package/build/dist/UI/Components/Tooltip/Tooltip.js +1 -1
  1047. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  1048. package/build/dist/UI/Utils/SensitiveUrlToken.js +10 -5
  1049. package/build/dist/UI/Utils/SensitiveUrlToken.js.map +1 -1
  1050. package/build/dist/Utils/API.js +177 -1
  1051. package/build/dist/Utils/API.js.map +1 -1
  1052. package/build/dist/Utils/AiRemediation/CommandPolicy.js +1 -1
  1053. package/build/dist/Utils/AiRemediation/CommandPolicy.js.map +1 -1
  1054. package/build/dist/Utils/AiRemediation/KubectlPolicy.js +1 -1
  1055. package/build/dist/Utils/AiRemediation/KubectlPolicy.js.map +1 -1
  1056. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +27 -1
  1057. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -1
  1058. package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js +155 -0
  1059. package/build/dist/Utils/Incident/IncidentNoteTemplateVariables.js.map +1 -0
  1060. package/build/dist/Utils/Markdown/MarkdownEscape.js +48 -0
  1061. package/build/dist/Utils/Markdown/MarkdownEscape.js.map +1 -1
  1062. package/build/dist/Utils/ProbeNetworkFailureUtil.js +460 -0
  1063. package/build/dist/Utils/ProbeNetworkFailureUtil.js.map +1 -0
  1064. package/package.json +1 -1
@@ -7,10 +7,17 @@ import {
7
7
  test,
8
8
  } from "@jest/globals";
9
9
  import {
10
- AI_ACCESS_EXAMPLE_WRITE_NAMESPACES,
11
- AiAccessHelmCommands,
12
- KUBERNETES_AGENT_HELM_NAMESPACE,
13
- KUBERNETES_AGENT_HELM_RELEASE,
10
+ AI_AGENT_CLUSTER_WIDE_NAMESPACES_FLAG,
11
+ AI_AGENT_EXAMPLE_WRITE_NAMESPACES,
12
+ AiAgentHelmCommands,
13
+ formatNameList,
14
+ getAiAgentClusterWideCommandNote,
15
+ getAiAgentHelmCommands,
16
+ getAiAgentLogsCommand,
17
+ getAiAgentScopedCommandNote,
18
+ getAiAgentWriteDisclosure,
19
+ } from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesAiAccessSetup";
20
+ import {
14
21
  KubernetesAiAccessConfirmation,
15
22
  KubernetesAiAccessOfferedFields,
16
23
  KubernetesAiAccessSavedSettings,
@@ -18,47 +25,62 @@ import {
18
25
  KubernetesAiRunnerDirectoryEntry,
19
26
  REMEDIATION_MODES_BY_AUTONOMY,
20
27
  REMEDIATION_MODE_LABELS,
28
+ REMEDIATION_MODE_SHORT_NAMES,
29
+ REMEDIATION_MODE_SUMMARIES,
21
30
  SavedKubectlAllowlist,
22
31
  buildKubernetesAiCredentialDirectory,
23
32
  buildKubernetesAiCredentialOptions,
24
33
  buildKubernetesAiRunnerDirectory,
25
34
  buildKubernetesAiRunnerOptions,
26
- canConfigureUnattendedKubernetesAiAccess,
27
- canPickKubernetesRunner,
28
- canReadKubectlJobs,
29
- describeRunnerWriteAccess,
30
- getAccessTestPermissionGate,
31
- getAccessTestPermissionMessage,
32
- getAiAccessClusterWideCommandNote,
33
- getAiAccessConnectCardMode,
34
- getAiAccessHelmCommands,
35
- getAiAccessScopedCommandNote,
36
- getAiAccessWriteDisclosure,
35
+ capitalizeFirst,
36
+ getAllowlistInEffect,
37
37
  getEveryModeProtectionsSentence,
38
38
  getKubectlAllowlistRemovalOnlyError,
39
- getKubectlJobsPermissionTitles,
40
39
  getKubernetesAiAccessAdminPermissionTitles,
41
40
  getKubernetesAiAccessBindingError,
42
41
  getKubernetesAiAccessChosenRunnerId,
43
42
  getKubernetesAiAccessConfirmation,
44
- getKubernetesAiAccessEditCapabilities,
45
43
  getKubernetesAiAccessLooseningChanges,
46
44
  getKubernetesAiAccessOfferedFields,
47
45
  getKubernetesAiAccessSettingsChanges,
48
46
  getKubernetesAiAccessSettingsInitialValues,
47
+ getKubernetesAiAccessSubmittedFields,
49
48
  getKubernetesAiCredentialAssignmentError,
50
49
  getKubernetesAiCredentialFieldDescription,
51
- getKubernetesRunnerPermissionTitles,
50
+ getPermissionTitles,
52
51
  getRemediationModeFieldDescription,
52
+ isAllowlistFieldShown,
53
53
  isRemediationModeOpenToEveryEditor,
54
- isThisClustersAgentRunner,
55
54
  normalizeSavedKubectlAllowlist,
56
55
  parseKubectlAllowlistText,
56
+ readDropdownId,
57
57
  readKubernetesAiAccessSavedSettings,
58
+ readRemediationMode,
58
59
  readStoredKubectlAllowlist,
59
60
  validateKubectlAllowlistText,
60
- } from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/AI";
61
- import { getKubernetesInstallationMarkdown } from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/DocumentationMarkdown";
61
+ } from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesAiAccessSettings";
62
+ import {
63
+ PROJECT_AI_SETTINGS_PERMISSIONS,
64
+ canChangeProjectAiSettings,
65
+ canConfigureUnattendedKubernetesAiAccess,
66
+ canPickKubernetesCredential,
67
+ canPickKubernetesRunner,
68
+ canReadKubectlJobs,
69
+ canResetKubernetesAiAgent,
70
+ getAccessTestPermissionGate,
71
+ getAccessTestPermissionMessage,
72
+ getAccessTestPermissionRequirement,
73
+ getKubectlJobsPermissionTitles,
74
+ getKubernetesAiAccessEditCapabilities,
75
+ getKubernetesCredentialPermissionTitles,
76
+ getKubernetesRunnerPermissionTitles,
77
+ holdsKubernetesAiAccessPermission,
78
+ } from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesAiAccessPermissions";
79
+ import {
80
+ KUBERNETES_AGENT_HELM_NAMESPACE,
81
+ KUBERNETES_AGENT_HELM_RELEASE,
82
+ getKubernetesInstallationMarkdown,
83
+ } from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/DocumentationMarkdown";
62
84
  import { isKubernetesAgentRunnerRow } from "../../../../App/FeatureSet/Dashboard/src/Pages/Kubernetes/Utils/KubernetesAgentRunner";
63
85
  import KubernetesCluster from "../../../Models/DatabaseModels/KubernetesCluster";
64
86
  import RunbookCredential from "../../../Models/DatabaseModels/RunbookCredential";
@@ -66,18 +88,21 @@ import Runner from "../../../Models/DatabaseModels/Runner";
66
88
  import { AiRemediationCommandPolicyVerdict } from "../../../Types/AutoRemediation/AiRemediationCommandPlan";
67
89
  import { JSONObject } from "../../../Types/JSON";
68
90
  import {
69
- KubernetesAiAccessGap,
70
91
  KubernetesAiRemediationMode,
71
- KubernetesClusterAiAccessStatus,
72
92
  PROTECTED_KUBERNETES_NAMESPACES,
73
93
  } from "../../../Types/Kubernetes/KubernetesClusterAiAccess";
74
- import { KUBERNETES_AI_ACCESS_ADMIN_PERMISSIONS } from "../../../Types/Kubernetes/KubernetesClusterAiAccessPermissions";
94
+ import {
95
+ KUBERNETES_AI_ACCESS_ADMIN_PERMISSIONS,
96
+ KUBERNETES_AI_ACCESS_CREDENTIAL_PERMISSIONS,
97
+ } from "../../../Types/Kubernetes/KubernetesClusterAiAccessPermissions";
75
98
  import ObjectID from "../../../Types/ObjectID";
76
99
  import Permission from "../../../Types/Permission";
77
100
  import RunbookCredentialType from "../../../Types/Runbook/RunbookCredentialType";
78
101
  import { DropdownOption } from "../../../UI/Components/Dropdown/Dropdown";
79
102
  import FormValues from "../../../UI/Components/Forms/Types/FormValues";
80
- import { PermissionGateResult } from "../../../UI/Utils/PermissionGate";
103
+ import PermissionGate, {
104
+ PermissionGateResult,
105
+ } from "../../../UI/Utils/PermissionGate";
81
106
  import PermissionUtil from "../../../UI/Utils/Permission";
82
107
  import User from "../../../UI/Utils/User";
83
108
  import KubectlPolicy, {
@@ -88,13 +113,16 @@ import fs from "fs";
88
113
  import path from "path";
89
114
 
90
115
  /*
91
- * The pure pieces behind the cluster AI page's settings: who may loosen
92
- * what AI does (relative to the saved settings, as the server decides it),
93
- * what an edit actually sends, when a save is confirmed first, how the
94
- * allowlist is read and checked (by KubectlPolicy, the matcher's own
95
- * rules), which Runners and credentials the pickers offer, which card helps
96
- * connect a Runner, and the helm upgrades. The rendered page is covered by
97
- * KubernetesClusterAiPage.test.tsx; the server-side parity checks by
116
+ * The pure pieces behind the cluster's AI agent page (AI → Agent): the
117
+ * helm commands it prints (KubernetesAiAccessSetup), who may loosen what AI
118
+ * does — relative to the saved settings, as the server decides it — what a
119
+ * Change actually sends, when a save is confirmed first, how the allowlist
120
+ * is read and checked (by KubectlPolicy, the matcher's own rules), which
121
+ * Runners and credentials an advanced binding's pickers offer
122
+ * (KubernetesAiAccessSettings), and the permission reads
123
+ * (KubernetesAiAccessPermissions). The status helpers are covered by
124
+ * KubernetesClusterAiAgentStatus.test.ts, the rendered page by
125
+ * KubernetesClusterAiPage.test.tsx, and the server-side parity checks by
98
126
  * KubernetesClusterAiPageServerParity.test.ts.
99
127
  */
100
128
 
@@ -112,10 +140,9 @@ const BASE_PERMISSIONS: Array<Permission> = [
112
140
  const SET_IMAGE_PATTERN: string = "kubectl set image deployment/web * -n web";
113
141
  const PATCH_PATTERN: string = "kubectl patch deployment/web -n web -p *";
114
142
  /*
115
- * A broad but valid entry: deleting any Deployment in any namespace. The
116
- * fixtures used "kubectl * * * -n *" until KubectlPolicy made a wildcard
117
- * verb invalid (round two): such an entry is refused where it is typed and
118
- * skipped by the matcher, so it is never broad and never confirmed.
143
+ * A broad but valid entry: deleting any Deployment in any namespace. A
144
+ * wildcard verb ("kubectl * * * -n *") is invalid since KubectlPolicy made
145
+ * it so: refused where it is typed and skipped by the matcher.
119
146
  */
120
147
  const BROAD_PATTERN: string = "kubectl delete deployment * -n *";
121
148
  // Wildcard verbs (and a lone "*"): invalid, refused by the form.
@@ -129,22 +156,9 @@ const VERB_WILDCARD_PATTERNS: Array<string> = [
129
156
  // A line continuation left dangling at the end of a command.
130
157
  const TRAILING_CONTINUATION_REGEX: RegExp = /\\\s*$/;
131
158
  const CHART_VERSION_FLAG_REGEX: RegExp = /--version/;
132
- const OLD_CHART_VERSION_REGEX: RegExp = /0\.7\.0/;
133
159
  const MATCHES_NEARLY_EVERYTHING_REGEX: RegExp = /nearly/;
134
160
  const ANY_IMAGE_ANY_SERVICE_ACCOUNT_REGEX: RegExp =
135
- /any image as any ServiceAccount/;
136
-
137
- /*
138
- * The reset of aiAccess.remediation.namespaces that works under
139
- * `helm upgrade --reuse-values`, as the page prints it. `--set ...=null`
140
- * does not: Helm drops a null override whose key the release's stored
141
- * values hold (the stored list survives), and on a release from a chart
142
- * without aiAccess the null reaches the values schema and fails the
143
- * upgrade. Reproduced with the real helm binary against a stored release;
144
- * helm-unittest renders from values files and cannot show it.
145
- */
146
- const EMPTY_LIST_RESET_FLAG: string =
147
- "--set-json 'aiAccess.remediation.namespaces=[]'";
161
+ /equivalent to running any image as any ServiceAccount in that namespace and reading its Secrets/;
148
162
  // A backslash-newline line continuation, which the shell reads as a space.
149
163
  const LINE_CONTINUATION_REGEX: RegExp = /\\\n/g;
150
164
  // One shell word: a run of unquoted, single-quoted or double-quoted parts.
@@ -153,7 +167,30 @@ const SHELL_WORD_REGEX: RegExp = /(?:[^\s'"]+|'[^']*'|"[^"]*")+/g;
153
167
  const SHELL_QUOTED_PART_REGEX: RegExp = /'([^']*)'|"([^"]*)"/g;
154
168
  // A --set value that sets the namespace list to null.
155
169
  const NULL_NAMESPACES_VALUE_REGEX: RegExp =
156
- /^aiAccess\.remediation\.namespaces=null$/;
170
+ /^aiAgent\.remediation\.namespaces=null$/;
171
+
172
+ /*
173
+ * The shared exact strings every slice prints (phase-b-slices.md): the
174
+ * dashboard, the server's ai_agent_not_connected gap and the docs all show
175
+ * these, so they are pinned character for character.
176
+ */
177
+ const EXACT_INSTALL_COMMAND: string = `helm repo update
178
+ helm upgrade kubernetes-agent oneuptime/kubernetes-agent \\
179
+ --namespace oneuptime-agent --reuse-values \\
180
+ --set aiAgent.enabled=true`;
181
+ const EXACT_SCOPED_COMMAND: string = `helm repo update
182
+ helm upgrade kubernetes-agent oneuptime/kubernetes-agent \\
183
+ --namespace oneuptime-agent --reuse-values \\
184
+ --set aiAgent.enabled=true \\
185
+ --set aiAgent.remediation.enabled=true \\
186
+ --set "aiAgent.remediation.namespaces={web,api}" \\
187
+ --set aiAgent.remediation.nodeOperations=false`;
188
+ const EXACT_CLUSTER_WIDE_COMMAND: string = `helm repo update
189
+ helm upgrade kubernetes-agent oneuptime/kubernetes-agent \\
190
+ --namespace oneuptime-agent --reuse-values \\
191
+ --set aiAgent.enabled=true \\
192
+ --set aiAgent.remediation.enabled=true \\
193
+ --set-json 'aiAgent.remediation.namespaces=[]'`;
157
194
 
158
195
  /*
159
196
  * The argv of the `helm upgrade` line in a copy-paste command, the way a
@@ -232,6 +269,7 @@ function grantNothingYet(): void {
232
269
  jest.spyOn(PermissionUtil, "getProjectPermissions").mockReturnValue(null);
233
270
  }
234
271
 
272
+ // Saved settings of a cluster bound to an advanced (outside-the-chart) Runner.
235
273
  function makeSaved(
236
274
  overrides: Partial<KubernetesAiAccessSavedSettings> = {},
237
275
  ): KubernetesAiAccessSavedSettings {
@@ -240,13 +278,24 @@ function makeSaved(
240
278
  aiRemediationMode: KubernetesAiRemediationMode.Automatic,
241
279
  aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN],
242
280
  aiAccessRunnerId: RUNNER_ID,
243
- aiAccessRunnerName: "kubernetes-agent/prod-east",
281
+ aiAccessRunnerName: "bash-runner",
244
282
  aiAccessCredentialId: null,
245
283
  aiAccessCredentialName: null,
246
284
  ...overrides,
247
285
  };
248
286
  }
249
287
 
288
+ // Saved settings of a cluster reached through its Kubernetes AI agent.
289
+ function makeAgentSaved(
290
+ overrides: Partial<KubernetesAiAccessSavedSettings> = {},
291
+ ): KubernetesAiAccessSavedSettings {
292
+ return makeSaved({
293
+ aiAccessRunnerId: null,
294
+ aiAccessRunnerName: null,
295
+ ...overrides,
296
+ });
297
+ }
298
+
250
299
  const NOTHING_OFFERED: KubernetesAiAccessOfferedFields = {
251
300
  allowlist: false,
252
301
  allowlistRemoveOnly: false,
@@ -364,15 +413,7 @@ afterEach(() => {
364
413
  jest.restoreAllMocks();
365
414
  });
366
415
 
367
- describe("helm upgrades", () => {
368
- /*
369
- * The finding: the page printed `helm upgrade oneuptime-agent ...
370
- * --namespace oneuptime-kubernetes-agent`, while the Dashboard's own
371
- * install instructions install `kubernetes-agent` into `oneuptime-agent`.
372
- * helm answers a missing release with "has no deployed releases", so a
373
- * cluster installed the way the product said could not connect. The pair
374
- * is now one exported constant pair in DocumentationMarkdown.ts.
375
- */
416
+ describe("the AI agent helm commands", () => {
376
417
  const installMarkdown: string = getKubernetesInstallationMarkdown({
377
418
  clusterName: "prod-east",
378
419
  oneuptimeUrl: "https://oneuptime.example.com",
@@ -380,7 +421,7 @@ describe("helm upgrades", () => {
380
421
  });
381
422
 
382
423
  function allCommands(): Array<string> {
383
- const commands: AiAccessHelmCommands = getAiAccessHelmCommands();
424
+ const commands: AiAgentHelmCommands = getAiAgentHelmCommands();
384
425
  return Object.values(commands);
385
426
  }
386
427
 
@@ -396,7 +437,24 @@ describe("helm upgrades", () => {
396
437
  return pairs;
397
438
  }
398
439
 
399
- test("uses the release and namespace of the Dashboard's install instructions", () => {
440
+ test("are exactly the shared strings every slice prints", () => {
441
+ const commands: AiAgentHelmCommands = getAiAgentHelmCommands();
442
+ expect(commands.install).toBe(EXACT_INSTALL_COMMAND);
443
+ expect(commands.enableRemediationScoped).toBe(EXACT_SCOPED_COMMAND);
444
+ expect(commands.enableRemediation).toBe(EXACT_CLUSTER_WIDE_COMMAND);
445
+ expect(Object.keys(commands).sort()).toEqual([
446
+ "enableRemediation",
447
+ "enableRemediationScoped",
448
+ "install",
449
+ ]);
450
+ });
451
+
452
+ /*
453
+ * `helm upgrade` of a release that does not exist fails with "has no
454
+ * deployed releases", so the page's release and namespace must be the
455
+ * install instructions' — one exported pair in DocumentationMarkdown.ts.
456
+ */
457
+ test("use the release and namespace of the Dashboard's install instructions", () => {
400
458
  const pairs: Array<{ release: string; namespace: string }> = installPairs();
401
459
 
402
460
  // Harness guard: the parse found the install commands to compare with.
@@ -414,104 +472,71 @@ describe("helm upgrades", () => {
414
472
  `helm upgrade ${pairs[0]!.release} oneuptime/kubernetes-agent`,
415
473
  );
416
474
  expect(command).toContain(`--namespace ${pairs[0]!.namespace}`);
417
- expect(command).not.toContain("oneuptime-kubernetes-agent");
418
475
  expect(command).toContain("--reuse-values");
419
476
  }
420
477
  });
421
478
 
422
- test("the install markdown names the namespace and release only through the shared pair", () => {
423
- // Every -n / --namespace / uninstall in the instructions uses the pair.
424
- const namespaces: Array<string> = Array.from(
425
- installMarkdown.matchAll(/(?:--namespace|-n) ([\w.-]+)/g),
426
- ).map((match: RegExpMatchArray): string => {
427
- return match[1]!;
428
- });
429
- expect(namespaces.length).toBeGreaterThan(5);
430
- for (const namespace of namespaces) {
431
- expect(namespace).toBe(KUBERNETES_AGENT_HELM_NAMESPACE);
432
- }
433
- expect(installMarkdown).toContain(
434
- `helm uninstall ${KUBERNETES_AGENT_HELM_RELEASE} --namespace ${KUBERNETES_AGENT_HELM_NAMESPACE}`,
435
- );
436
- expect(installMarkdown).toContain(
437
- `deployment/${KUBERNETES_AGENT_HELM_RELEASE}`,
438
- );
439
- });
440
-
441
- /*
442
- * Every command, not only the read-only one: the write-access commands
443
- * are offered on their own, so an operator who runs one directly on an
444
- * old chart index would otherwise hit "Additional property aiAccess is
445
- * not allowed".
446
- */
447
- test("every command updates the chart index first", () => {
479
+ test("every command updates the chart index first and turns the agent on", () => {
448
480
  expect(allCommands()).toHaveLength(3);
449
481
  for (const command of allCommands()) {
450
482
  expect(command.startsWith("helm repo update\n")).toBe(true);
451
- expect(command.indexOf("helm repo update")).toBeLessThan(
452
- command.indexOf("helm upgrade"),
483
+ /*
484
+ * Always aiAgent.enabled=true, harmless when the agent is on: nobody
485
+ * has to decide whether theirs was installed with it turned off.
486
+ */
487
+ expect(getFlagValues(getHelmUpgradeArgv(command), "--set")).toContain(
488
+ "aiAgent.enabled=true",
453
489
  );
454
490
  }
455
491
  });
456
492
 
457
- test("the default command grants read-only access", () => {
458
- const readOnly: string = getAiAccessHelmCommands().readOnly;
459
- expect(readOnly).toContain("--set aiAccess.enabled=true");
460
- expect(readOnly).not.toContain("remediation");
493
+ test("the install command is read-only and names no deprecated aiAccess value", () => {
494
+ const install: string = getAiAgentHelmCommands().install;
495
+ expect(install).not.toContain("remediation");
496
+ for (const command of allCommands()) {
497
+ expect(command).not.toContain("aiAccess");
498
+ }
461
499
  });
462
500
 
463
- test("the recommended write-access command binds the write role only in the listed namespaces and keeps fixes off nodes", () => {
464
- const scoped: string = getAiAccessHelmCommands().enableRemediationScoped;
465
- expect(scoped).toContain("--set aiAccess.enabled=true");
466
- expect(scoped).toContain("--set aiAccess.remediation.enabled=true");
467
- expect(scoped).toContain(
468
- `--set "aiAccess.remediation.namespaces=${AI_ACCESS_EXAMPLE_WRITE_NAMESPACES}"`,
501
+ test("the recommended write command binds only the listed namespaces and keeps fixes off nodes", () => {
502
+ const argv: Array<string> = getHelmUpgradeArgv(
503
+ getAiAgentHelmCommands().enableRemediationScoped,
469
504
  );
470
- expect(scoped).toContain("--set aiAccess.remediation.nodeOperations=false");
471
-
472
- const clusterWide: string = getAiAccessHelmCommands().enableRemediation;
473
- expect(clusterWide).toContain("--set aiAccess.remediation.enabled=true");
474
- /*
475
- * Replaces the pin that the cluster-wide command names no namespaces:
476
- * under --reuse-values that left a list stored by the scoped command in
477
- * force, so "write access across the cluster" bound the role only
478
- * where the old list said. It now resets the list — with an empty JSON
479
- * list, not `=null` (round four: under --reuse-values Helm dropped the
480
- * null and kept a stored list, and on an install from a chart without
481
- * aiAccess the null failed the values schema, so the command failed
482
- * for every existing install).
483
- */
484
- expect(clusterWide).toContain(EMPTY_LIST_RESET_FLAG);
485
- expect(clusterWide).not.toContain("namespaces=null");
486
- expect(clusterWide).not.toContain(AI_ACCESS_EXAMPLE_WRITE_NAMESPACES);
487
- expect(clusterWide).not.toContain("nodeOperations");
505
+ expect(getFlagValues(argv, "--set")).toEqual([
506
+ "aiAgent.enabled=true",
507
+ "aiAgent.remediation.enabled=true",
508
+ `aiAgent.remediation.namespaces=${AI_AGENT_EXAMPLE_WRITE_NAMESPACES}`,
509
+ "aiAgent.remediation.nodeOperations=false",
510
+ ]);
511
+ expect(getFlagValues(argv, "--set-json")).toEqual([]);
488
512
  });
489
513
 
490
514
  /*
491
- * What helm receives, not only what the page prints: the shell hands
492
- * `--set-json` one word, `aiAccess.remediation.namespaces=[]` (the single
493
- * quotes keep a shell such as zsh from reading `[]` as a glob), whose
494
- * value is an empty JSON list — a value the chart's schema (namespaces is
495
- * an array) accepts and that replaces a stored list under --reuse-values.
515
+ * What helm receives: the shell hands `--set-json` one word whose value
516
+ * is an empty JSON list — a value the schema accepts and that replaces a
517
+ * stored list under --reuse-values (a `=null` override would be dropped).
496
518
  */
497
519
  test("the cluster-wide command hands helm an empty JSON list for the namespaces", () => {
498
520
  const argv: Array<string> = getHelmUpgradeArgv(
499
- getAiAccessHelmCommands().enableRemediation,
521
+ getAiAgentHelmCommands().enableRemediation,
500
522
  );
501
523
  expect(argv).toContain("--reuse-values");
502
524
 
503
525
  const setJson: Array<string> = getFlagValues(argv, "--set-json");
504
- expect(setJson).toEqual(["aiAccess.remediation.namespaces=[]"]);
526
+ expect(setJson).toEqual(["aiAgent.remediation.namespaces=[]"]);
505
527
  const assignment: string = setJson[0]!;
506
528
  const separator: number = assignment.indexOf("=");
507
529
  expect(assignment.slice(0, separator)).toBe(
508
- "aiAccess.remediation.namespaces",
530
+ "aiAgent.remediation.namespaces",
509
531
  );
510
532
  expect(JSON.parse(assignment.slice(separator + 1))).toEqual([]);
533
+ expect(AI_AGENT_CLUSTER_WIDE_NAMESPACES_FLAG).toBe(
534
+ "--set-json 'aiAgent.remediation.namespaces=[]'",
535
+ );
511
536
 
512
537
  // Negative control: the helper does read a null reset where one is.
513
538
  const withNull: Array<string> = getHelmUpgradeArgv(
514
- "helm repo update\nhelm upgrade r c --reuse-values \\\n --set aiAccess.remediation.namespaces=null",
539
+ "helm repo update\nhelm upgrade r c --reuse-values \\\n --set aiAgent.remediation.namespaces=null",
515
540
  );
516
541
  expect(
517
542
  getFlagValues(withNull, "--set").some((setting: string): boolean => {
@@ -520,14 +545,7 @@ describe("helm upgrades", () => {
520
545
  ).toBe(true);
521
546
  });
522
547
 
523
- /*
524
- * No command the page prints pairs --reuse-values (which every one of
525
- * them uses) with a null namespace list: that pairing is the one that
526
- * keeps a stored list, or fails the upgrade on an install from a chart
527
- * without aiAccess.
528
- */
529
548
  test("no command pairs --reuse-values with a null namespace list", () => {
530
- expect(allCommands()).toHaveLength(3);
531
549
  for (const command of allCommands()) {
532
550
  const argv: Array<string> = getHelmUpgradeArgv(command);
533
551
  expect({
@@ -548,65 +566,9 @@ describe("helm upgrades", () => {
548
566
  }
549
567
  });
550
568
 
551
- /*
552
- * What the page says under the two write-access commands: every listed
553
- * namespace must already exist (the chart never creates one, and a
554
- * missing one fails the whole upgrade), the empty-list `--set-json`
555
- * resets a stored list (and `=null` does not, under --reuse-values),
556
- * turning node operations back on takes `=true` under --reuse-values,
557
- * and a write outside the list is refused before it reaches the Runner.
558
- */
559
- test("the scoped command's note says what the chart and the server do with the list", () => {
560
- const note: string = getAiAccessScopedCommandNote();
561
- expect(note).toContain(AI_ACCESS_EXAMPLE_WRITE_NAMESPACES);
562
- expect(note).toContain("Every namespace you list must already exist");
563
- expect(note).toContain("never creates a namespace");
564
- expect(note).toContain('namespaces "<name>" not found');
565
- expect(note).toContain(
566
- `${EMPTY_LIST_RESET_FLAG} resets it to cluster-wide, as the next command does.`,
567
- );
568
- // The round-three claim, which Helm does not honour under --reuse-values.
569
- expect(note).not.toContain("=null resets it to cluster-wide");
570
- expect(note).toContain(
571
- "--set aiAccess.remediation.namespaces=null does not reset a stored list under --reuse-values",
572
- );
573
- expect(note).toContain(
574
- "it only works with --reset-then-reuse-values (Helm 3.14+) in place of --reuse-values",
575
- );
576
- expect(note).toContain(
577
- "a fix anywhere else is refused when it is proposed or approved, and the Runner refuses it again",
578
- );
579
- expect(note).toContain("set it to true to let AI cordon");
580
- /*
581
- * Round four: a patch of a node joins the drain and the taint (a taint
582
- * written as a node patch is the same change), and it still waits for
583
- * a human when node operations are turned on.
584
- */
585
- expect(note).toContain(
586
- "a drain, a taint or a patch of a node still waits for a human",
587
- );
588
- // The old advice: dropping the line keeps a stored false under --reuse-values.
589
- expect(note).not.toContain("leave that line out");
590
- });
591
-
592
- test("the cluster-wide command's note says the list is reset and node operations are kept", () => {
593
- const note: string = getAiAccessClusterWideCommandNote();
594
- expect(note.startsWith(`${EMPTY_LIST_RESET_FLAG} resets`)).toBe(true);
595
- expect(note).toContain("bound cluster-wide");
596
- expect(note).toContain("--set-json needs Helm 3.10 or later");
597
- expect(note).toContain(
598
- "--set aiAccess.remediation.namespaces=null does not reset a stored list under --reuse-values",
599
- );
600
- expect(note).toContain("--reset-then-reuse-values (Helm 3.14+)");
601
- expect(note).toContain("Node operations keep the release's setting");
602
- });
603
-
604
569
  test("every command is complete, never a line to append", () => {
605
570
  for (const command of allCommands()) {
606
- // No trailing line continuation: the shell would wait for more input.
607
571
  expect(TRAILING_CONTINUATION_REGEX.test(command)).toBe(false);
608
-
609
- // Every continued line is followed by a real one.
610
572
  const lines: Array<string> = command.split("\n");
611
573
  lines.forEach((line: string, index: number) => {
612
574
  if (line.trimEnd().endsWith("\\")) {
@@ -619,151 +581,71 @@ describe("helm upgrades", () => {
619
581
  test("names no chart version (published charts carry the OneUptime version)", () => {
620
582
  for (const command of allCommands()) {
621
583
  expect(command).not.toMatch(CHART_VERSION_FLAG_REGEX);
622
- expect(command).not.toMatch(OLD_CHART_VERSION_REGEX);
623
584
  }
624
585
  });
625
586
 
587
+ test("the scoped note says a listed namespace must exist and how to go back to cluster-wide", () => {
588
+ const note: string = getAiAgentScopedCommandNote();
589
+ expect(note).toContain(AI_AGENT_EXAMPLE_WRITE_NAMESPACES);
590
+ expect(note).toContain("must already exist");
591
+ expect(note).toContain("never creates a namespace");
592
+ expect(note).toContain("fails the whole upgrade");
593
+ expect(note).toContain(AI_AGENT_CLUSTER_WIDE_NAMESPACES_FLAG);
594
+ expect(note).toContain("not =null");
595
+ expect(note).toContain("set it to true to allow cordon");
596
+ // A drain, a taint and a node patch still ask, even with node operations on.
597
+ expect(note).toContain(
598
+ "a drain, a taint or a node patch still waits for a person",
599
+ );
600
+ });
601
+
602
+ test("the cluster-wide note says the list is cleared and node operations are kept", () => {
603
+ const note: string = getAiAgentClusterWideCommandNote();
604
+ expect(
605
+ note.startsWith(`${AI_AGENT_CLUSTER_WIDE_NAMESPACES_FLAG} clears`),
606
+ ).toBe(true);
607
+ expect(note).toContain("Helm 3.10 or later");
608
+ expect(note).toContain("Node operations keep the release's setting");
609
+ });
610
+
626
611
  /*
627
- * The chart docs must say what write RBAC amounts to wherever write
628
- * access is offered; this page is where customers copy the command.
612
+ * The docs suite holds this disclosure to the chart docs' rules: what
613
+ * write RBAC amounts to, every protected namespace, the agent's own
614
+ * namespace, and the value that scopes it.
629
615
  */
630
- test("the write-access disclosure says what patch access amounts to and where the policy holds the line", () => {
631
- const disclosure: string = getAiAccessWriteDisclosure();
616
+ test("the write-access disclosure says what write access amounts to and where OneUptime holds the line", () => {
617
+ const disclosure: string = getAiAgentWriteDisclosure();
632
618
  expect(disclosure).toMatch(ANY_IMAGE_ANY_SERVICE_ACCOUNT_REGEX);
633
- expect(disclosure).toContain("reading its Secrets");
634
- expect(disclosure).toContain("aiAccess.remediation.namespaces");
635
- expect(disclosure).toContain("aiAccess.remediation.nodeOperations=false");
636
- expect(disclosure).toContain("own namespace");
619
+ expect(disclosure).toContain("aiAgent.remediation.namespaces");
620
+ expect(disclosure).toContain("aiAgent.remediation.nodeOperations=false");
621
+ expect(disclosure).toContain("the agent's own namespace");
637
622
  for (const namespace of PROTECTED_KUBERNETES_NAMESPACES) {
638
623
  expect(disclosure).toContain(namespace);
639
624
  }
640
- // Refused up front (the toolkit, the approval route, the enqueue), not only on the Runner.
641
- expect(disclosure).toContain(
642
- "a write anywhere else is refused when it is proposed or approved, and again by the Runner",
643
- );
644
- expect(disclosure).not.toContain(
645
- "and the Runner refuses a write anywhere else.",
646
- );
647
- });
648
- });
649
-
650
- describe("the bound Runner's write access", () => {
651
- test("says where an in-cluster Runner may write", () => {
652
- expect(describeRunnerWriteAccess(undefined)).toBeNull();
653
- // A Runner outside the cluster: its credential's RBAC decides.
654
- expect(describeRunnerWriteAccess({ inCluster: false })).toBeNull();
655
- expect(
656
- describeRunnerWriteAccess({ inCluster: true, allowWrites: false }),
657
- ).toBe("read-only RBAC");
658
- // Absent: an older Runner that did not say.
659
- expect(
660
- describeRunnerWriteAccess({ inCluster: true, allowWrites: true }),
661
- ).toBe("writes allowed");
662
- expect(
663
- describeRunnerWriteAccess({
664
- inCluster: true,
665
- allowWrites: true,
666
- writeNamespaces: [],
667
- }),
668
- ).toBe("writes allowed cluster-wide");
669
- expect(
670
- describeRunnerWriteAccess({
671
- inCluster: true,
672
- allowWrites: true,
673
- writeNamespaces: ["web", "api"],
674
- allowNodeOperations: false,
675
- }),
676
- ).toBe("writes allowed in web, api, no node operations");
625
+ expect(disclosure).toContain("always needs a person");
626
+ expect(disclosure).not.toContain("aiAccess");
627
+ expect(disclosure).not.toContain("Runner");
677
628
  });
678
- });
679
-
680
- describe("the connect card", () => {
681
- const baseStatus: KubernetesClusterAiAccessStatus = {
682
- clusterId: "c1",
683
- clusterName: "prod-east",
684
- clusterIdentifier: "prod-east",
685
- runner: {
686
- id: RUNNER_ID,
687
- name: "kubernetes-agent/prod-east",
688
- isOnline: true,
689
- canRunAiCommands: true,
690
- posture: { inCluster: true, allowWrites: false },
691
- },
692
- accessMethod: "in_cluster",
693
- kubectlAllowlist: [],
694
- isInvestigationEnabled: true,
695
- isInvestigationReady: true,
696
- remediationMode: KubernetesAiRemediationMode.RequireApproval,
697
- isRemediationReady: false,
698
- gaps: [],
699
- evaluatedAt: "2026-09-22T10:00:00.000Z",
700
- };
701
-
702
- function gap(
703
- code: KubernetesAiAccessGap["code"],
704
- nextStep: string,
705
- ): KubernetesAiAccessGap {
706
- return { code, title: code, description: code, nextStep, blocks: "both" };
707
- }
708
629
 
709
- test("offers the helm upgrade while no Runner is bound and none is installed", () => {
710
- expect(
711
- getAiAccessConnectCardMode({
712
- ...baseStatus,
713
- runner: null,
714
- gaps: [
715
- gap(
716
- "no_runner_bound",
717
- "Upgrade the Kubernetes agent with --set aiAccess.enabled=true to install an in-cluster Runner (one command), or bind an existing Runner and a Kubernetes credential on this cluster's AI page.",
718
- ),
719
- ],
720
- }),
721
- ).toBe("connect");
722
- expect(
723
- getAiAccessConnectCardMode({
724
- ...baseStatus,
725
- gaps: [gap("runner_missing", "Reload this page.")],
726
- }),
727
- ).toBe("connect");
630
+ test("the logs command uses the namespace the agent reported, else the install namespace", () => {
631
+ expect(getAiAgentLogsCommand("monitoring")).toBe(
632
+ "kubectl logs -n monitoring -l component=ai-agent --tail=100",
633
+ );
634
+ expect(getAiAgentLogsCommand(" monitoring ")).toBe(
635
+ "kubectl logs -n monitoring -l component=ai-agent --tail=100",
636
+ );
637
+ for (const missing of [undefined, "", " "]) {
638
+ expect(getAiAgentLogsCommand(missing)).toBe(
639
+ `kubectl logs -n ${KUBERNETES_AGENT_HELM_NAMESPACE} -l component=ai-agent --tail=100`,
640
+ );
641
+ }
728
642
  });
729
643
 
730
- /*
731
- * Known follow-up 7: the in-cluster Runner is registered but not
732
- * selected. Re-running helm would change nothing, so the helm card is
733
- * replaced by "select it".
734
- */
735
- test("asks to select the Runner when this cluster's in-cluster Runner is installed but not selected", () => {
736
- expect(
737
- getAiAccessConnectCardMode({
738
- ...baseStatus,
739
- runner: null,
740
- gaps: [
741
- gap(
742
- "no_runner_bound",
743
- 'Select the kubernetes-agent Runner "kubernetes-agent/prod-east" as this cluster\'s Runner on this page (leave the credential empty). No helm change is needed.',
744
- ),
745
- ],
746
- }),
747
- ).toBe("select_agent_runner");
748
- });
749
-
750
- test("shows no connect card once a Runner is bound, whatever else is missing", () => {
751
- expect(getAiAccessConnectCardMode(baseStatus)).toBe("none");
752
- expect(
753
- getAiAccessConnectCardMode({
754
- ...baseStatus,
755
- gaps: [
756
- gap(
757
- "credential_on_agent_runner",
758
- "Create a Runner under Project Settings → Runners, assign the Kubernetes credential to it and select both on this page — or install the in-cluster Runner on THIS cluster with --set aiAccess.enabled=true, which needs no credential.",
759
- ),
760
- gap(
761
- "remediation_write_access_missing",
762
- "Upgrade the agent with --set aiAccess.remediation.enabled=true.",
763
- ),
764
- ],
765
- }),
766
- ).toBe("none");
644
+ test("formatNameList joins names the way the copy reads them", () => {
645
+ expect(formatNameList([], "and")).toBe("");
646
+ expect(formatNameList(["a"], "and")).toBe("a");
647
+ expect(formatNameList(["a", "b"], "or")).toBe("a or b");
648
+ expect(formatNameList(["a", "b", "c"], "and")).toBe("a, b and c");
767
649
  });
768
650
  });
769
651
 
@@ -795,12 +677,6 @@ describe("kubectl allowlist text", () => {
795
677
  ).toBeNull();
796
678
  });
797
679
 
798
- /*
799
- * KubectlPolicy made a wildcard verb invalid: it would pre-approve every
800
- * KIND of change, and the matcher skips it. The form refuses it with the
801
- * policy's words; this replaces the pin that accepted "*" and
802
- * "kubectl * * * -n *" as broad but well-formed.
803
- */
804
680
  test("refuses a wildcard verb, which the matcher never reads", () => {
805
681
  for (const pattern of VERB_WILDCARD_PATTERNS) {
806
682
  expect({
@@ -814,11 +690,6 @@ describe("kubectl allowlist text", () => {
814
690
  }
815
691
  });
816
692
 
817
- /*
818
- * The matcher reads a pattern with or without the leading "kubectl" (it
819
- * tokenizes patterns like commands), so the form accepts both; the old
820
- * form refused "set image …" although the matcher would have used it.
821
- */
822
693
  test("accepts a pattern without the leading kubectl, as the matcher does", () => {
823
694
  expect(
824
695
  validateKubectlAllowlistText("set image deployment/web * -n web"),
@@ -827,11 +698,6 @@ describe("kubectl allowlist text", () => {
827
698
 
828
699
  test("refuses a bare kubectl, which names no command", () => {
829
700
  expect(validateKubectlAllowlistText("kubectl")).toMatch(/^Pattern 1: /);
830
- // The old message taught the whole-string glob reading.
831
- expect(validateKubectlAllowlistText("kubectl")).not.toMatch(
832
- /matched against the whole command/,
833
- );
834
- // Negative control: a real command right next to it passes.
835
701
  expect(validateKubectlAllowlistText("kubectl get pods")).toBeNull();
836
702
  expect(
837
703
  validateKubectlAllowlistText(`${SET_IMAGE_PATTERN}\nkubectl`),
@@ -864,10 +730,6 @@ describe("kubectl allowlist text", () => {
864
730
  expect(validateKubectlAllowlistText(tooLong)).toMatch(/at most 500/);
865
731
  });
866
732
 
867
- /*
868
- * One definition of validity: the form refuses a line exactly when
869
- * KubectlPolicy says the matcher cannot use it.
870
- */
871
733
  test("refuses a line exactly when KubectlPolicy.describeAllowlistPatternProblem does", () => {
872
734
  for (const pattern of [
873
735
  "kubectl",
@@ -906,7 +768,6 @@ describe("the stored allowlist", () => {
906
768
  value: JSON.stringify([SET_IMAGE_PATTERN]),
907
769
  expected: { patterns: [SET_IMAGE_PATTERN], isClean: true },
908
770
  },
909
- // Unusable: the server uses these as an empty allowlist.
910
771
  {
911
772
  value: { patterns: [SET_IMAGE_PATTERN] },
912
773
  expected: { patterns: [], isClean: false },
@@ -916,11 +777,7 @@ describe("the stored allowlist", () => {
916
777
  value: [1, "", SET_IMAGE_PATTERN],
917
778
  expected: { patterns: [SET_IMAGE_PATTERN], isClean: false },
918
779
  },
919
- /*
920
- * A string that is not JSON is ONE pattern to the server (its
921
- * readStoredKubectlAllowlist and the status's normalizeAllowlist),
922
- * so the policy uses it; it used to read here as no pattern at all.
923
- */
780
+ // A string that is not JSON is ONE pattern to the server.
924
781
  {
925
782
  value: "kubectl *",
926
783
  expected: { patterns: ["kubectl *"], isClean: false },
@@ -943,6 +800,14 @@ describe("the stored allowlist", () => {
943
800
  .patterns,
944
801
  ).toEqual(["kubectl scale deployment/web"]);
945
802
  });
803
+
804
+ test("the allowlist in effect is the status's non-blank strings only", () => {
805
+ expect(getAllowlistInEffect([SET_IMAGE_PATTERN, "", " ", 3])).toEqual([
806
+ SET_IMAGE_PATTERN,
807
+ ]);
808
+ expect(getAllowlistInEffect(undefined)).toEqual([]);
809
+ expect(getAllowlistInEffect("kubectl *")).toEqual([]);
810
+ });
946
811
  });
947
812
 
948
813
  describe("reading the saved settings", () => {
@@ -954,12 +819,12 @@ describe("reading the saved settings", () => {
954
819
  aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
955
820
  aiAccessRunner: Object.assign(new Runner(), {
956
821
  _id: RUNNER_ID,
957
- name: "kubernetes-agent/prod-east",
822
+ name: "bash-runner",
958
823
  }),
959
824
  }),
960
825
  );
961
826
  expect(fromRelation.aiAccessRunnerId).toBe(RUNNER_ID);
962
- expect(fromRelation.aiAccessRunnerName).toBe("kubernetes-agent/prod-east");
827
+ expect(fromRelation.aiAccessRunnerName).toBe("bash-runner");
963
828
  expect(fromRelation.aiAccessCredentialId).toBeNull();
964
829
 
965
830
  const fromColumn: KubernetesAiAccessSavedSettings =
@@ -971,13 +836,51 @@ describe("reading the saved settings", () => {
971
836
  );
972
837
  expect(fromColumn.aiAccessRunnerId).toBe(RUNNER_ID);
973
838
  expect(fromColumn.aiAccessCredentialId).toBe(CREDENTIAL_ID);
974
- // Missing switches read as the column defaults.
975
- expect(fromColumn.isAiInvestigationEnabled).toBe(false);
976
839
  expect(fromColumn.aiRemediationMode).toBe(
977
840
  KubernetesAiRemediationMode.Disabled,
978
841
  );
979
842
  });
980
843
 
844
+ /*
845
+ * Regression: the column now defaults to true (AI on out of the box).
846
+ * The page read a missing value as false, so an untouched form showed
847
+ * investigation off and a save that changed nothing else turned it on
848
+ * "again" — or, worse, the page told the operator it was off.
849
+ */
850
+ test("reads a missing investigation switch as on, the column's default", () => {
851
+ expect(
852
+ readKubernetesAiAccessSavedSettings(new KubernetesCluster())
853
+ .isAiInvestigationEnabled,
854
+ ).toBe(true);
855
+ expect(
856
+ readKubernetesAiAccessSavedSettings(
857
+ Object.assign(new KubernetesCluster(), {
858
+ isAiInvestigationEnabled: null,
859
+ }),
860
+ ).isAiInvestigationEnabled,
861
+ ).toBe(true);
862
+ // Negative control: an explicit false stays false.
863
+ expect(
864
+ readKubernetesAiAccessSavedSettings(
865
+ Object.assign(new KubernetesCluster(), {
866
+ isAiInvestigationEnabled: false,
867
+ }),
868
+ ).isAiInvestigationEnabled,
869
+ ).toBe(false);
870
+ });
871
+
872
+ test("an unknown stored mode reads as Off, the server's reading", () => {
873
+ expect(readRemediationMode("automatic")).toBe(
874
+ KubernetesAiRemediationMode.Disabled,
875
+ );
876
+ expect(readRemediationMode(undefined)).toBe(
877
+ KubernetesAiRemediationMode.Disabled,
878
+ );
879
+ for (const mode of Object.values(KubernetesAiRemediationMode)) {
880
+ expect(readRemediationMode(mode)).toBe(mode);
881
+ }
882
+ });
883
+
981
884
  test("seeds the form with the allowlist as lines and both clear switches off", () => {
982
885
  const initial: SettingsFormValues =
983
886
  getKubernetesAiAccessSettingsInitialValues(
@@ -990,48 +893,127 @@ describe("reading the saved settings", () => {
990
893
  );
991
894
  expect(initial.clearAiAccessRunner).toBe(false);
992
895
  expect(initial.clearAiAccessCredential).toBe(false);
896
+ expect(initial.aiAccessRunnerId).toBe(RUNNER_ID);
897
+ expect(initial.aiAccessCredentialId).toBe("");
993
898
  });
994
899
  });
995
900
 
996
- describe("what the edit modal offers", () => {
997
- test("an admin with both pickers gets the pickers and a free allowlist", () => {
901
+ describe("what the Change modal offers", () => {
902
+ test("an admin on an advanced binding with both pickers gets the pickers and a free allowlist", () => {
998
903
  expect(
999
904
  getKubernetesAiAccessOfferedFields({
1000
905
  saved: makeSaved({ aiKubectlCommandAllowlist: [] }),
1001
906
  canConfigureUnattended: true,
1002
907
  isRunnerPickerAvailable: true,
1003
908
  isCredentialPickerAvailable: true,
909
+ isAdvancedBinding: true,
910
+ hasAiAgent: true,
1004
911
  }),
1005
912
  ).toEqual(EVERYTHING_OFFERED);
1006
913
  });
1007
914
 
1008
915
  /*
1009
- * The finding (SIA-4, XP-7): the server, the docs and the gap copy
1010
- * promise every cluster editor they may clear the Runner, credential and
1011
- * allowlist, and the page offered them none of it.
916
+ * The pickers are removed for every cluster without an advanced binding:
917
+ * it is reached through its Kubernetes AI agent, and there is nothing to
918
+ * pick. Even an admin who could list Runners gets none.
1012
919
  */
1013
- test("a cluster editor without the admin set may remove patterns and unbind what is bound", () => {
920
+ test("a cluster without an advanced binding gets no Runner or credential field at all", () => {
921
+ for (const saved of [
922
+ makeAgentSaved(),
923
+ // Still bound to the previous in-cluster Runner.
924
+ makeSaved({ aiAccessRunnerName: "kubernetes-agent/prod-east" }),
925
+ ]) {
926
+ for (const canConfigureUnattended of [true, false]) {
927
+ const offered: KubernetesAiAccessOfferedFields =
928
+ getKubernetesAiAccessOfferedFields({
929
+ saved,
930
+ canConfigureUnattended,
931
+ isRunnerPickerAvailable: true,
932
+ isCredentialPickerAvailable: true,
933
+ isAdvancedBinding: false,
934
+ hasAiAgent: true,
935
+ });
936
+ expect({
937
+ runner: offered.runner,
938
+ credential: offered.credential,
939
+ runnerClear: offered.runnerClear,
940
+ credentialClear: offered.credentialClear,
941
+ }).toEqual({
942
+ runner: false,
943
+ credential: false,
944
+ runnerClear: false,
945
+ credentialClear: false,
946
+ });
947
+ }
948
+ }
949
+ });
950
+
951
+ /*
952
+ * Unbinding an advanced Runner used to be a tightening every editor could
953
+ * make. With an AI agent row it moves the cluster to the agent — which
954
+ * may hold broader write RBAC — so the switch is no longer offered to a
955
+ * cluster editor without the admin set (critique finding 2).
956
+ */
957
+ test("a cluster editor may unbind an advanced binding only while the cluster has no AI agent", () => {
958
+ const saved: KubernetesAiAccessSavedSettings = makeSaved({
959
+ aiAccessCredentialId: CREDENTIAL_ID,
960
+ });
961
+
1014
962
  expect(
1015
963
  getKubernetesAiAccessOfferedFields({
1016
- saved: makeSaved({ aiAccessCredentialId: CREDENTIAL_ID }),
964
+ saved,
1017
965
  canConfigureUnattended: false,
1018
966
  isRunnerPickerAvailable: false,
1019
967
  isCredentialPickerAvailable: false,
968
+ isAdvancedBinding: true,
969
+ hasAiAgent: false,
1020
970
  }),
1021
971
  ).toEqual(TIGHTEN_ONLY_OFFERED);
972
+
973
+ expect(
974
+ getKubernetesAiAccessOfferedFields({
975
+ saved,
976
+ canConfigureUnattended: false,
977
+ isRunnerPickerAvailable: false,
978
+ isCredentialPickerAvailable: false,
979
+ isAdvancedBinding: true,
980
+ hasAiAgent: true,
981
+ }),
982
+ ).toEqual({
983
+ ...TIGHTEN_ONLY_OFFERED,
984
+ runnerClear: false,
985
+ credentialClear: false,
986
+ });
1022
987
  });
1023
988
 
1024
- test("nothing to remove or unbind means no allowlist field and no clear switch", () => {
989
+ test("an admin who may not list Runners may still unbind, with or without an agent", () => {
990
+ for (const hasAiAgent of [true, false]) {
991
+ const offered: KubernetesAiAccessOfferedFields =
992
+ getKubernetesAiAccessOfferedFields({
993
+ saved: makeSaved(),
994
+ canConfigureUnattended: true,
995
+ isRunnerPickerAvailable: false,
996
+ isCredentialPickerAvailable: true,
997
+ isAdvancedBinding: true,
998
+ hasAiAgent,
999
+ });
1000
+ expect(offered.runner).toBe(false);
1001
+ expect(offered.runnerClear).toBe(true);
1002
+ expect(offered.credential).toBe(true);
1003
+ // Nothing bound: nothing to unbind.
1004
+ expect(offered.credentialClear).toBe(false);
1005
+ }
1006
+ });
1007
+
1008
+ test("nothing to remove means no allowlist field for a cluster editor", () => {
1025
1009
  expect(
1026
1010
  getKubernetesAiAccessOfferedFields({
1027
- saved: makeSaved({
1028
- aiKubectlCommandAllowlist: [],
1029
- aiAccessRunnerId: null,
1030
- aiAccessRunnerName: null,
1031
- }),
1011
+ saved: makeAgentSaved({ aiKubectlCommandAllowlist: [] }),
1032
1012
  canConfigureUnattended: false,
1033
1013
  isRunnerPickerAvailable: false,
1034
1014
  isCredentialPickerAvailable: false,
1015
+ isAdvancedBinding: false,
1016
+ hasAiAgent: true,
1035
1017
  }),
1036
1018
  ).toEqual({
1037
1019
  ...NOTHING_OFFERED,
@@ -1041,40 +1023,74 @@ describe("what the edit modal offers", () => {
1041
1023
  // An unclean stored allowlist is offered, so it can be cleared.
1042
1024
  expect(
1043
1025
  getKubernetesAiAccessOfferedFields({
1044
- saved: makeSaved({ aiKubectlCommandAllowlist: { a: 1 } }),
1026
+ saved: makeAgentSaved({ aiKubectlCommandAllowlist: { a: 1 } }),
1045
1027
  canConfigureUnattended: false,
1046
1028
  isRunnerPickerAvailable: false,
1047
1029
  isCredentialPickerAvailable: false,
1030
+ isAdvancedBinding: false,
1031
+ hasAiAgent: true,
1048
1032
  }).allowlist,
1049
1033
  ).toBe(true);
1050
1034
  });
1051
1035
 
1052
- test("an admin who may not list Runners may still unbind the bound one", () => {
1053
- const offered: KubernetesAiAccessOfferedFields =
1054
- getKubernetesAiAccessOfferedFields({
1055
- saved: makeSaved(),
1056
- canConfigureUnattended: true,
1057
- isRunnerPickerAvailable: false,
1058
- isCredentialPickerAvailable: true,
1036
+ test("the allowlist field shows, and is sent, only while Automatic is chosen", () => {
1037
+ const saved: KubernetesAiAccessSavedSettings = makeAgentSaved();
1038
+
1039
+ expect(isAllowlistFieldShown(formValues(saved))).toBe(true);
1040
+ expect(
1041
+ isAllowlistFieldShown(
1042
+ formValues(saved, {
1043
+ aiRemediationMode: {
1044
+ value: KubernetesAiRemediationMode.Automatic,
1045
+ label: "Automatic",
1046
+ },
1047
+ }),
1048
+ ),
1049
+ ).toBe(true);
1050
+ for (const mode of [
1051
+ KubernetesAiRemediationMode.Disabled,
1052
+ KubernetesAiRemediationMode.RequireApproval,
1053
+ KubernetesAiRemediationMode.BypassApproval,
1054
+ ]) {
1055
+ expect(
1056
+ isAllowlistFieldShown(formValues(saved, { aiRemediationMode: mode })),
1057
+ ).toBe(false);
1058
+ }
1059
+
1060
+ // Text typed while Automatic, then the mode changed: nothing is sent for it.
1061
+ const values: SettingsFormValues = formValues(saved, {
1062
+ aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
1063
+ kubectlAllowlistText: "kubectl",
1064
+ });
1065
+ const submitted: KubernetesAiAccessOfferedFields =
1066
+ getKubernetesAiAccessSubmittedFields({
1067
+ offered: EVERYTHING_OFFERED,
1068
+ values,
1059
1069
  });
1060
- expect(offered.runner).toBe(false);
1061
- expect(offered.runnerClear).toBe(true);
1062
- expect(offered.credential).toBe(true);
1063
- expect(offered.credentialClear).toBe(false);
1070
+ expect(submitted).toEqual({ ...EVERYTHING_OFFERED, allowlist: false });
1071
+ expect(
1072
+ getKubernetesAiAccessSettingsChanges({
1073
+ saved,
1074
+ values,
1075
+ offered: submitted,
1076
+ }),
1077
+ ).toEqual({
1078
+ aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
1079
+ });
1064
1080
  });
1065
1081
  });
1066
1082
 
1067
1083
  describe("which modes a cluster editor without the admin set may choose", () => {
1068
- test("every mode at or below the saved one", () => {
1069
- function openModes(saved: KubernetesAiRemediationMode): Array<string> {
1070
- return REMEDIATION_MODES_BY_AUTONOMY.filter(
1071
- (mode: KubernetesAiRemediationMode): boolean => {
1072
- return isRemediationModeOpenToEveryEditor(mode, saved);
1073
- },
1074
- );
1075
- }
1084
+ function openModes(saved: KubernetesAiRemediationMode): Array<string> {
1085
+ return REMEDIATION_MODES_BY_AUTONOMY.filter(
1086
+ (mode: KubernetesAiRemediationMode): boolean => {
1087
+ return isRemediationModeOpenToEveryEditor(mode, saved);
1088
+ },
1089
+ );
1090
+ }
1076
1091
 
1077
- expect(openModes(KubernetesAiRemediationMode.Disabled)).toEqual([
1092
+ test("every mode at or below the saved one", () => {
1093
+ expect(openModes(KubernetesAiRemediationMode.RequireApproval)).toEqual([
1078
1094
  KubernetesAiRemediationMode.Disabled,
1079
1095
  KubernetesAiRemediationMode.RequireApproval,
1080
1096
  ]);
@@ -1088,15 +1104,26 @@ describe("which modes a cluster editor without the admin set may choose", () =>
1088
1104
  REMEDIATION_MODES_BY_AUTONOMY,
1089
1105
  );
1090
1106
  });
1091
- });
1092
1107
 
1093
- describe("what an edit sends", () => {
1094
1108
  /*
1095
- * The finding: ModelForm submits every field, so an editor who only
1096
- * switched investigation off re-sent the unchanged Automatic mode, the
1097
- * allowlist and the Runner binding — writes the server refuses without
1098
- * the admin permissions. Only the change may be sent.
1109
+ * Regression (§11 "Disabled→enabled needs admin"): Off -> Ask for
1110
+ * approval was open to every editor. Turning fixes on at all now needs
1111
+ * the admin set, so an Off cluster offers a cluster editor only Off.
1099
1112
  */
1113
+ test("turning fixes on from Off is not open to every editor", () => {
1114
+ expect(openModes(KubernetesAiRemediationMode.Disabled)).toEqual([
1115
+ KubernetesAiRemediationMode.Disabled,
1116
+ ]);
1117
+ expect(
1118
+ isRemediationModeOpenToEveryEditor(
1119
+ KubernetesAiRemediationMode.RequireApproval,
1120
+ KubernetesAiRemediationMode.Disabled,
1121
+ ),
1122
+ ).toBe(false);
1123
+ });
1124
+ });
1125
+
1126
+ describe("what an edit sends", () => {
1100
1127
  test("an editor who only toggles investigation sends only that", () => {
1101
1128
  const saved: KubernetesAiAccessSavedSettings = makeSaved();
1102
1129
  for (const offered of [
@@ -1118,20 +1145,15 @@ describe("what an edit sends", () => {
1118
1145
  const saved: KubernetesAiAccessSavedSettings = makeSaved({
1119
1146
  aiAccessCredentialId: CREDENTIAL_ID,
1120
1147
  });
1121
- expect(
1122
- getKubernetesAiAccessSettingsChanges({
1123
- saved,
1124
- values: formValues(saved),
1125
- offered: EVERYTHING_OFFERED,
1126
- }),
1127
- ).toEqual({});
1128
- expect(
1129
- getKubernetesAiAccessSettingsChanges({
1130
- saved,
1131
- values: formValues(saved),
1132
- offered: TIGHTEN_ONLY_OFFERED,
1133
- }),
1134
- ).toEqual({});
1148
+ for (const offered of [EVERYTHING_OFFERED, TIGHTEN_ONLY_OFFERED]) {
1149
+ expect(
1150
+ getKubernetesAiAccessSettingsChanges({
1151
+ saved,
1152
+ values: formValues(saved),
1153
+ offered,
1154
+ }),
1155
+ ).toEqual({});
1156
+ }
1135
1157
  });
1136
1158
 
1137
1159
  test("tightening the mode sends the mode", () => {
@@ -1163,6 +1185,9 @@ describe("what an edit sends", () => {
1163
1185
  ).toEqual({
1164
1186
  aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
1165
1187
  });
1188
+ expect(readDropdownId({ value: { value: "x" } })).toBe("x");
1189
+ expect(readDropdownId("")).toBeNull();
1190
+ expect(readDropdownId(null)).toBeNull();
1166
1191
  });
1167
1192
 
1168
1193
  test("ignores a mode that is not a mode", () => {
@@ -1212,11 +1237,6 @@ describe("what an edit sends", () => {
1212
1237
  ).toEqual({ aiKubectlCommandAllowlist: [] });
1213
1238
  });
1214
1239
 
1215
- /*
1216
- * The server decides "does this write add a pattern?" on the stored
1217
- * strings. A line the form shows collapsed is sent in its stored
1218
- * spelling, or removing one line would read as adding another.
1219
- */
1220
1240
  test("sends every kept line in its stored spelling", () => {
1221
1241
  const oddlySpaced: string =
1222
1242
  "kubectl scale deployment/web --replicas=* -n web";
@@ -1318,7 +1338,6 @@ describe("what an edit sends", () => {
1318
1338
  aiKubectlCommandAllowlist: [],
1319
1339
  });
1320
1340
 
1321
- // Switches off: nothing is sent.
1322
1341
  expect(
1323
1342
  getKubernetesAiAccessSettingsChanges({
1324
1343
  saved,
@@ -1380,13 +1399,11 @@ describe("what an edit sends", () => {
1380
1399
 
1381
1400
  /*
1382
1401
  * The server's rule (KubernetesClusterService.getAiAccessLoosening) is
1383
- * RELATIVE to the saved settings; the page used an absolute one — any
1384
- * unattended mode, any non-empty allowlist — and so hid tightening the
1385
- * server accepts. KubernetesClusterAiPageServerParity.test.ts runs the same
1386
- * cases through the server.
1402
+ * RELATIVE to the saved settings. KubernetesClusterAiPageServerParity.test.ts
1403
+ * runs the same cases through the server.
1387
1404
  */
1388
1405
  describe("loosening changes", () => {
1389
- test("moving the mode up to an unattended mode loosens; moving it down never does", () => {
1406
+ test("any move of the mode up loosens, Off -> Ask for approval included; moving it down never does", () => {
1390
1407
  const fromAutomatic: KubernetesAiAccessSavedSettings = makeSaved();
1391
1408
  const fromBypass: KubernetesAiAccessSavedSettings = makeSaved({
1392
1409
  aiRemediationMode: KubernetesAiRemediationMode.BypassApproval,
@@ -1401,58 +1418,72 @@ describe("loosening changes", () => {
1401
1418
  changes: {
1402
1419
  aiRemediationMode: KubernetesAiRemediationMode.BypassApproval,
1403
1420
  },
1421
+ hasAiAgent: false,
1404
1422
  }),
1405
- ).toEqual(["switching AI remediation to Bypass approval"]);
1423
+ ).toEqual(["switching fixes to Bypass approval"]);
1406
1424
  expect(
1407
1425
  getKubernetesAiAccessLooseningChanges({
1408
1426
  saved: fromOff,
1409
1427
  changes: { aiRemediationMode: KubernetesAiRemediationMode.Automatic },
1428
+ hasAiAgent: false,
1429
+ }),
1430
+ ).toEqual(["switching fixes to Automatic"]);
1431
+ // Regression (§11): turning fixes on at all needs the admin set now.
1432
+ expect(
1433
+ getKubernetesAiAccessLooseningChanges({
1434
+ saved: fromOff,
1435
+ changes: {
1436
+ aiRemediationMode: KubernetesAiRemediationMode.RequireApproval,
1437
+ },
1438
+ hasAiAgent: false,
1410
1439
  }),
1411
- ).toEqual(["switching AI remediation to Automatic"]);
1440
+ ).toEqual(["switching fixes to Ask for approval"]);
1412
1441
 
1413
1442
  // Bypass approval -> Automatic is a tightening.
1414
1443
  expect(
1415
1444
  getKubernetesAiAccessLooseningChanges({
1416
1445
  saved: fromBypass,
1417
1446
  changes: { aiRemediationMode: KubernetesAiRemediationMode.Automatic },
1447
+ hasAiAgent: false,
1418
1448
  }),
1419
1449
  ).toEqual([]);
1420
1450
  for (const mode of [
1421
1451
  KubernetesAiRemediationMode.Disabled,
1422
1452
  KubernetesAiRemediationMode.RequireApproval,
1453
+ KubernetesAiRemediationMode.Automatic,
1423
1454
  ]) {
1424
1455
  expect(
1425
1456
  getKubernetesAiAccessLooseningChanges({
1426
- saved: fromOff,
1457
+ saved: fromAutomatic,
1427
1458
  changes: { aiRemediationMode: mode },
1428
- }),
1429
- ).toEqual([]);
1430
- }
1431
- });
1432
-
1433
- test("adding a pattern loosens; removing patterns or clearing the list never does", () => {
1434
- const saved: KubernetesAiAccessSavedSettings = makeSaved({
1435
- aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN, PATCH_PATTERN],
1436
- });
1437
-
1438
- expect(
1439
- getKubernetesAiAccessLooseningChanges({
1440
- saved,
1441
- changes: { aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN] },
1442
- }),
1443
- ).toEqual([]);
1444
- expect(
1445
- getKubernetesAiAccessLooseningChanges({
1446
- saved,
1447
- changes: { aiKubectlCommandAllowlist: [] },
1448
- }),
1449
- ).toEqual([]);
1459
+ hasAiAgent: true,
1460
+ }),
1461
+ ).toEqual([]);
1462
+ }
1463
+ // Not a mode: nothing to judge.
1450
1464
  expect(
1451
1465
  getKubernetesAiAccessLooseningChanges({
1452
- saved,
1453
- changes: { aiKubectlCommandAllowlist: null },
1466
+ saved: fromOff,
1467
+ changes: { aiRemediationMode: "automatic" },
1468
+ hasAiAgent: false,
1454
1469
  }),
1455
1470
  ).toEqual([]);
1471
+ });
1472
+
1473
+ test("adding a pattern loosens; removing patterns or clearing the list never does", () => {
1474
+ const saved: KubernetesAiAccessSavedSettings = makeSaved({
1475
+ aiKubectlCommandAllowlist: [SET_IMAGE_PATTERN, PATCH_PATTERN],
1476
+ });
1477
+
1478
+ for (const allowlist of [[SET_IMAGE_PATTERN], [], null]) {
1479
+ expect(
1480
+ getKubernetesAiAccessLooseningChanges({
1481
+ saved,
1482
+ changes: { aiKubectlCommandAllowlist: allowlist },
1483
+ hasAiAgent: false,
1484
+ }),
1485
+ ).toEqual([]);
1486
+ }
1456
1487
 
1457
1488
  expect(
1458
1489
  getKubernetesAiAccessLooseningChanges({
@@ -1463,25 +1494,69 @@ describe("loosening changes", () => {
1463
1494
  "kubectl delete pod * -n web",
1464
1495
  ],
1465
1496
  },
1497
+ hasAiAgent: false,
1466
1498
  }),
1467
1499
  ).toEqual([
1468
1500
  'adding the kubectl allowlist pattern "kubectl delete pod * -n web"',
1469
1501
  ]);
1470
1502
 
1471
- // Editing a pattern into another string is adding one.
1472
1503
  expect(
1473
1504
  getKubernetesAiAccessLooseningChanges({
1474
1505
  saved,
1475
1506
  changes: {
1476
1507
  aiKubectlCommandAllowlist: [
1477
1508
  "kubectl set image deployment/web * -n prod",
1509
+ "kubectl set image deployment/api * -n prod",
1478
1510
  ],
1479
1511
  },
1512
+ hasAiAgent: false,
1480
1513
  }),
1481
- ).toHaveLength(1);
1514
+ ).toEqual([
1515
+ 'adding the kubectl allowlist patterns "kubectl set image deployment/web * -n prod", "kubectl set image deployment/api * -n prod"',
1516
+ ]);
1517
+ });
1518
+
1519
+ test("binding another Runner or a credential loosens; re-sending the bound one never does", () => {
1520
+ const saved: KubernetesAiAccessSavedSettings = makeSaved({
1521
+ aiAccessCredentialId: CREDENTIAL_ID,
1522
+ });
1523
+
1524
+ for (const hasAiAgent of [true, false]) {
1525
+ expect(
1526
+ getKubernetesAiAccessLooseningChanges({
1527
+ saved,
1528
+ changes: { aiAccessRunnerId: OTHER_RUNNER_ID },
1529
+ hasAiAgent,
1530
+ }),
1531
+ ).toEqual(["binding a different Runner"]);
1532
+ expect(
1533
+ getKubernetesAiAccessLooseningChanges({
1534
+ saved: makeSaved(),
1535
+ changes: { aiAccessCredentialId: CREDENTIAL_ID },
1536
+ hasAiAgent,
1537
+ }),
1538
+ ).toEqual(["binding a Kubernetes credential"]);
1539
+
1540
+ for (const changes of [
1541
+ { aiAccessRunnerId: RUNNER_ID },
1542
+ { aiAccessCredentialId: CREDENTIAL_ID },
1543
+ { isAiInvestigationEnabled: true },
1544
+ { isAiInvestigationEnabled: false },
1545
+ ] as Array<JSONObject>) {
1546
+ expect(
1547
+ getKubernetesAiAccessLooseningChanges({ saved, changes, hasAiAgent }),
1548
+ ).toEqual([]);
1549
+ }
1550
+ }
1482
1551
  });
1483
1552
 
1484
- test("binding another Runner or a credential loosens; unbinding or re-sending the bound one never does", () => {
1553
+ /*
1554
+ * Regression (§11 "clearing a Runner binding with an agent row needs
1555
+ * admin"): clearing the binding used to be a tightening every editor
1556
+ * could make. With an agent row it changes the resolved access target to
1557
+ * the agent, so it loosens; without one it only takes access away.
1558
+ */
1559
+ test("clearing the Runner or credential loosens exactly when the cluster has an AI agent", () => {
1485
1560
  const saved: KubernetesAiAccessSavedSettings = makeSaved({
1486
1561
  aiAccessCredentialId: CREDENTIAL_ID,
1487
1562
  });
@@ -1489,28 +1564,36 @@ describe("loosening changes", () => {
1489
1564
  expect(
1490
1565
  getKubernetesAiAccessLooseningChanges({
1491
1566
  saved,
1492
- changes: { aiAccessRunnerId: OTHER_RUNNER_ID },
1567
+ changes: { aiAccessRunnerId: null, aiAccessCredentialId: null },
1568
+ hasAiAgent: true,
1493
1569
  }),
1494
- ).toEqual(["binding a different Runner"]);
1570
+ ).toEqual([
1571
+ "switching this cluster to its Kubernetes AI agent",
1572
+ "removing the Kubernetes credential",
1573
+ ]);
1495
1574
  expect(
1496
1575
  getKubernetesAiAccessLooseningChanges({
1497
- saved: makeSaved(),
1498
- changes: { aiAccessCredentialId: CREDENTIAL_ID },
1576
+ saved,
1577
+ changes: { aiAccessRunnerId: null, aiAccessCredentialId: null },
1578
+ hasAiAgent: false,
1499
1579
  }),
1500
- ).toEqual(["binding a Kubernetes credential"]);
1501
-
1502
- for (const changes of [
1503
- { aiAccessRunnerId: null },
1504
- { aiAccessCredentialId: null },
1505
- { aiAccessRunnerId: RUNNER_ID },
1506
- { aiAccessCredentialId: CREDENTIAL_ID },
1507
- { isAiInvestigationEnabled: true },
1508
- { isAiInvestigationEnabled: false },
1509
- ] as Array<JSONObject>) {
1510
- expect(getKubernetesAiAccessLooseningChanges({ saved, changes })).toEqual(
1511
- [],
1512
- );
1513
- }
1580
+ ).toEqual([]);
1581
+
1582
+ // Clearing what is not bound changes nothing.
1583
+ expect(
1584
+ getKubernetesAiAccessLooseningChanges({
1585
+ saved: makeAgentSaved(),
1586
+ changes: { aiAccessRunnerId: null, aiAccessCredentialId: null },
1587
+ hasAiAgent: true,
1588
+ }),
1589
+ ).toEqual([]);
1590
+ });
1591
+
1592
+ test("capitalizes the first loosening for the refusal", () => {
1593
+ expect(capitalizeFirst("switching fixes to Automatic")).toBe(
1594
+ "Switching fixes to Automatic",
1595
+ );
1596
+ expect(capitalizeFirst("")).toBe("");
1514
1597
  });
1515
1598
  });
1516
1599
 
@@ -1546,15 +1629,6 @@ describe("the remove-only allowlist", () => {
1546
1629
  });
1547
1630
  });
1548
1631
 
1549
- /*
1550
- * The confirmation is the UI's safety net for an allowlist that behaves
1551
- * like Bypass approval. It used a whole-string glob reading ("*",
1552
- * "kubectl *" were broad; anything with a literal "-n" was not), while the
1553
- * matcher is word by word: "kubectl * * * -n *" auto-approves deleting any
1554
- * Deployment in any non-protected namespace and was never confirmed. Broad
1555
- * is now KubectlPolicy.isBroadAllowlistPattern, decided on the matcher's
1556
- * own tokens, and the parity check below runs the real matcher.
1557
- */
1558
1632
  describe("confirming a save that lets riskier changes run unattended", () => {
1559
1633
  function expectNamesWhatItUnlocks(
1560
1634
  confirmation: KubernetesAiAccessConfirmation | null,
@@ -1564,10 +1638,8 @@ describe("confirming a save that lets riskier changes run unattended", () => {
1564
1638
  for (const word of ["set image", "patch", "drain", "deleting workloads"]) {
1565
1639
  expect(text).toContain(word);
1566
1640
  }
1567
- // What still never runs unattended is said too.
1568
1641
  expect(text).toMatch(/never run/);
1569
1642
  expect(text).toContain("kube-system");
1570
- // The old copy taught the whole-string glob reading.
1571
1643
  expect(text).not.toMatch(MATCHES_NEARLY_EVERYTHING_REGEX);
1572
1644
  }
1573
1645
 
@@ -1594,7 +1666,6 @@ describe("confirming a save that lets riskier changes run unattended", () => {
1594
1666
  "kubectl set image deployment/* * --namespace=*",
1595
1667
  "kubectl patch deployment * -p * -n *",
1596
1668
  "kubectl rollout restart deployment -n *",
1597
- // Without the leading "kubectl", as the matcher reads it too.
1598
1669
  "set image deployment/web * -n *",
1599
1670
  ]) {
1600
1671
  const confirmation: KubernetesAiAccessConfirmation | null =
@@ -1610,8 +1681,6 @@ describe("confirming a save that lets riskier changes run unattended", () => {
1610
1681
  expect(confirmation!.description).toContain(
1611
1682
  "a wildcard for an object, the namespace, a selector or a --from source",
1612
1683
  );
1613
- // A wildcard verb is invalid, so the copy no longer names it.
1614
- expect(confirmation!.description).not.toContain("for the verb");
1615
1684
  }
1616
1685
  });
1617
1686
 
@@ -1657,7 +1726,6 @@ describe("confirming a save that lets riskier changes run unattended", () => {
1657
1726
  saved: KubernetesAiAccessSavedSettings;
1658
1727
  changes: JSONObject;
1659
1728
  }> = [
1660
- // Narrow allowlist, any mode change short of Bypass.
1661
1729
  {
1662
1730
  saved: makeSaved(),
1663
1731
  changes: { aiRemediationMode: KubernetesAiRemediationMode.Disabled },
@@ -1668,7 +1736,6 @@ describe("confirming a save that lets riskier changes run unattended", () => {
1668
1736
  }),
1669
1737
  changes: { aiRemediationMode: KubernetesAiRemediationMode.Automatic },
1670
1738
  },
1671
- // Narrow patterns: one object in one namespace, a wildcard only in a value.
1672
1739
  {
1673
1740
  saved: makeSaved(),
1674
1741
  changes: {
@@ -1680,7 +1747,6 @@ describe("confirming a save that lets riskier changes run unattended", () => {
1680
1747
  ],
1681
1748
  },
1682
1749
  },
1683
- // An already-saved broad pattern is not re-confirmed on unrelated edits.
1684
1750
  {
1685
1751
  saved: makeSaved({ aiKubectlCommandAllowlist: [BROAD_PATTERN] }),
1686
1752
  changes: { isAiInvestigationEnabled: false },
@@ -1691,7 +1757,6 @@ describe("confirming a save that lets riskier changes run unattended", () => {
1691
1757
  aiKubectlCommandAllowlist: [BROAD_PATTERN, SET_IMAGE_PATTERN],
1692
1758
  },
1693
1759
  },
1694
- // Re-sent in its stored spelling: still the saved pattern.
1695
1760
  {
1696
1761
  saved: makeSaved({
1697
1762
  aiKubectlCommandAllowlist: ["kubectl delete deployment * -n *"],
@@ -1700,7 +1765,6 @@ describe("confirming a save that lets riskier changes run unattended", () => {
1700
1765
  aiKubectlCommandAllowlist: ["kubectl delete deployment * -n *"],
1701
1766
  },
1702
1767
  },
1703
- // Leaving Bypass approval — to Automatic too — or clearing the allowlist.
1704
1768
  {
1705
1769
  saved: makeSaved({
1706
1770
  aiRemediationMode: KubernetesAiRemediationMode.BypassApproval,
@@ -1731,11 +1795,11 @@ describe("confirming a save that lets riskier changes run unattended", () => {
1731
1795
  });
1732
1796
 
1733
1797
  /*
1734
- * The parity check the finding asked for, run on the real matcher. Every
1735
- * probe names an object and namespaces nobody would type into a pattern,
1736
- * so a pattern that auto-approves a probe reaches objects and namespaces
1737
- * it does not name. Each such pattern must be confirmed; each narrow
1738
- * pattern must neither be confirmed nor reach the probes.
1798
+ * The parity check on the real matcher. Every probe names an object and
1799
+ * namespaces nobody would type into a pattern, so a pattern that
1800
+ * auto-approves a probe reaches objects and namespaces it does not name.
1801
+ * Each such pattern must be confirmed; each narrow one must neither be
1802
+ * confirmed nor reach the probes.
1739
1803
  */
1740
1804
  describe("agrees with the matcher that actually runs", () => {
1741
1805
  const PROBES: Array<string> = [];
@@ -1762,7 +1826,6 @@ describe("confirming a save that lets riskier changes run unattended", () => {
1762
1826
  "kubectl taint nodes zz-probe-obj k=v:NoSchedule",
1763
1827
  );
1764
1828
 
1765
- // Probes the empty allowlist leaves for a human: what a pattern could promote.
1766
1829
  const promotable: Array<string> = PROBES.filter(
1767
1830
  (command: string): boolean => {
1768
1831
  return (
@@ -1798,15 +1861,8 @@ describe("confirming a save that lets riskier changes run unattended", () => {
1798
1861
  "*",
1799
1862
  "kubectl *",
1800
1863
  "kubectl * *",
1801
- "kubectl * * *",
1802
- "kubectl * * * *",
1803
1864
  "kubectl * * -n *",
1804
1865
  "kubectl * * * -n *",
1805
- "kubectl * * * * -n *",
1806
- "kubectl * * * -p * -n *",
1807
- "kubectl * * * --namespace *",
1808
- "kubectl * * * --namespace=*",
1809
- "* * * -n *",
1810
1866
  "kubectl delete * * -n *",
1811
1867
  "kubectl delete deployment * -n *",
1812
1868
  "kubectl delete deployment *",
@@ -1833,17 +1889,8 @@ describe("confirming a save that lets riskier changes run unattended", () => {
1833
1889
 
1834
1890
  test("harness guard: the probes are riskier changes a pattern can promote", () => {
1835
1891
  expect(promotable.length).toBeGreaterThanOrEqual(15);
1836
- /*
1837
- * Broad entries that really do promote probes (the wildcard-verb
1838
- * entries this guard used promote nothing since KubectlPolicy made
1839
- * them invalid).
1840
- */
1841
1892
  expect(promotedBy(BROAD_PATTERN).length).toBeGreaterThan(0);
1842
1893
  expect(promotedBy("kubectl delete * * -n *").length).toBeGreaterThan(0);
1843
- expect(
1844
- promotedBy("kubectl patch deployment * -p * -n *").length,
1845
- ).toBeGreaterThan(0);
1846
- // An invalid entry promotes nothing: the matcher skips it.
1847
1894
  for (const pattern of VERB_WILDCARD_PATTERNS) {
1848
1895
  expect({ pattern, promoted: promotedBy(pattern) }).toEqual({
1849
1896
  pattern,
@@ -1878,183 +1925,72 @@ describe("confirming a save that lets riskier changes run unattended", () => {
1878
1925
  });
1879
1926
  });
1880
1927
 
1881
- describe("Runner picker options", () => {
1928
+ /*
1929
+ * The Runner picker exists only for a cluster already bound to a Runner
1930
+ * outside the chart. The Kubernetes AI agent replaces the chart's previous
1931
+ * in-cluster Runner, so no kubernetes-agent row is ever offered — this
1932
+ * cluster's included — and switching back to the agent is "leave the
1933
+ * Runner empty".
1934
+ */
1935
+ describe("Runner picker options (advanced bindings)", () => {
1882
1936
  const runners: Array<Runner> = [
1883
1937
  makeRunner("r-this", "kubernetes-agent/prod-east", true, "prod-east"),
1884
1938
  makeRunner("r-other-cluster", "kubernetes-agent/prod-eu", true, "prod-eu"),
1939
+ makeRunner("r-renamed", "prod-eu-kubectl", true, "prod-eu"),
1885
1940
  makeRunner("r-host", "bash-runner"),
1886
1941
  makeRunner("r-off", "ops-runner", false),
1887
1942
  ];
1888
1943
 
1889
- test("offer only Runners that can serve this cluster, its own agent first", () => {
1944
+ test("offer only Runners outside the chart that run AI commands", () => {
1890
1945
  const options: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
1891
1946
  runners,
1892
- clusterIdentifier: "prod-east",
1893
1947
  boundRunnerId: null,
1894
1948
  boundRunnerName: null,
1895
1949
  });
1896
1950
 
1897
- expect(optionValues(options)).toEqual(["r-this", "r-host"]);
1898
- expect(options[0]!.label).toMatch(/in-cluster Runner for this cluster/);
1951
+ expect(options).toEqual([{ value: "r-host", label: "bash-runner" }]);
1899
1952
  });
1900
1953
 
1901
- test("match this cluster's agent Runner whatever the identifier's casing", () => {
1954
+ test("keep the bound Runner, saying why it cannot serve the cluster", () => {
1902
1955
  expect(
1903
- optionValues(
1956
+ optionLabel(
1904
1957
  buildKubernetesAiRunnerOptions({
1905
1958
  runners,
1906
- clusterIdentifier: "Prod-East",
1907
- boundRunnerId: null,
1908
- boundRunnerName: null,
1959
+ boundRunnerId: "r-this",
1960
+ boundRunnerName: "kubernetes-agent/prod-east",
1909
1961
  }),
1962
+ "r-this",
1910
1963
  ),
1911
- ).toEqual(["r-this", "r-host"]);
1912
- });
1964
+ ).toBe(
1965
+ "kubernetes-agent/prod-east (currently bound — installed by the Kubernetes agent chart, which now uses the Kubernetes AI agent)",
1966
+ );
1913
1967
 
1914
- test("offer no agent Runner when the cluster has no identifier", () => {
1968
+ // A posture-only agent row is an agent row too (the server's rule).
1915
1969
  expect(
1916
- optionValues(
1970
+ optionLabel(
1917
1971
  buildKubernetesAiRunnerOptions({
1918
1972
  runners,
1919
- clusterIdentifier: undefined,
1920
- boundRunnerId: null,
1921
- boundRunnerName: null,
1973
+ boundRunnerId: "r-renamed",
1974
+ boundRunnerName: "prod-eu-kubectl",
1922
1975
  }),
1976
+ "r-renamed",
1923
1977
  ),
1924
- ).toEqual(["r-host"]);
1925
- });
1926
-
1927
- /*
1928
- * The finding (dashboard-ai-page-2): the server names an agent Runner
1929
- * "kubernetes-agent/<id>" only while that fits 100 characters, and
1930
- * "kubernetes-agent/<head>-<hash>" beyond. The picker rebuilt the name
1931
- * from the identifier, so for an identifier of 84+ characters it hid
1932
- * this cluster's own Runner, or called it another cluster's. It now
1933
- * matches the Runner's posture, as the server does.
1934
- */
1935
- describe("a cluster identifier long enough for the server to shorten the Runner name", () => {
1936
- const longIdentifier: string =
1937
- "arn:aws:eks:ap-southeast-2:123456789012:cluster/payments-platform-production-blue-green";
1938
- const otherLongIdentifier: string =
1939
- "arn:aws:eks:ap-southeast-2:123456789012:cluster/payments-platform-producti-other-region";
1940
- const head: string = `kubernetes-agent/${longIdentifier.slice(0, 74)}`;
1941
- const thisRunner: Runner = makeRunner(
1942
- "r-long",
1943
- `${head}-fe7fc1b3`,
1944
- true,
1945
- longIdentifier,
1946
- );
1947
- // Same head, another hash: another long-named cluster's Runner.
1948
- const otherRunner: Runner = makeRunner(
1949
- "r-long-other",
1950
- `${head}-0a1b2c3d`,
1951
- true,
1952
- otherLongIdentifier,
1953
- );
1954
-
1955
- test("harness guard: the identifier is past the length the name keeps whole", () => {
1956
- expect(longIdentifier.length).toBe(87);
1957
- expect(`kubernetes-agent/${longIdentifier}`.length).toBeGreaterThan(100);
1958
- expect(thisRunner.name!.endsWith(longIdentifier)).toBe(false);
1959
- });
1960
-
1961
- test("lists this cluster's Runner first, as its in-cluster Runner, bound or not", () => {
1962
- for (const boundRunnerId of [null, "r-long"]) {
1963
- const options: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
1964
- runners: [
1965
- otherRunner,
1966
- thisRunner,
1967
- makeRunner("r-host", "bash-runner"),
1968
- ],
1969
- clusterIdentifier: longIdentifier,
1970
- boundRunnerId,
1971
- boundRunnerName: boundRunnerId ? thisRunner.name! : null,
1972
- });
1973
- expect(optionValues(options)).toEqual(["r-long", "r-host"]);
1974
- expect(options[0]!.label).toBe(
1975
- `${thisRunner.name} (in-cluster Runner for this cluster)`,
1976
- );
1977
- }
1978
- });
1979
-
1980
- test("still calls another long-named cluster's Runner another cluster's", () => {
1981
- const options: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
1982
- runners: [otherRunner, thisRunner],
1983
- clusterIdentifier: longIdentifier,
1984
- boundRunnerId: "r-long-other",
1985
- boundRunnerName: otherRunner.name!,
1986
- });
1987
- expect(optionLabel(options, "r-long-other")).toMatch(
1988
- /currently bound — runs inside another cluster/,
1989
- );
1990
- });
1991
- });
1978
+ ).toMatch(/currently bound — installed by the Kubernetes agent chart/);
1992
1979
 
1993
- test("an agent-named Runner whose posture names another cluster, or none, is not this cluster's", () => {
1994
- expect(
1995
- isThisClustersAgentRunner(
1996
- makeRunner("a", "kubernetes-agent/prod-east", true, "prod-eu"),
1997
- "prod-east",
1998
- ),
1999
- ).toBe(false);
2000
- expect(
2001
- isThisClustersAgentRunner(
2002
- makeRunner("a", "kubernetes-agent/prod-east"),
2003
- "prod-east",
2004
- ),
2005
- ).toBe(false);
2006
- /*
2007
- * A row without the name marker that reports an agent posture for this
2008
- * cluster IS this cluster's agent: only the kubernetes-agent Runner
2009
- * binary reports such a posture, and RunnerService.isKubernetesAgent
2010
- * RunnerRow / isKubernetesAgentRunnerOfCluster read it so. This replaces
2011
- * the pin that called it "not an agent row" (the page read the name
2012
- * alone); a Runner in a pod that names no cluster is still not one.
2013
- */
2014
- expect(
2015
- isThisClustersAgentRunner(
2016
- makeRunner("a", "office-runner", true, "prod-east"),
2017
- "prod-east",
2018
- ),
2019
- ).toBe(true);
2020
1980
  expect(
2021
- isThisClustersAgentRunner(
2022
- Object.assign(new Runner(), {
2023
- _id: "a",
2024
- name: "office-runner",
2025
- hostInfo: { kubernetes: { inCluster: true } },
1981
+ optionLabel(
1982
+ buildKubernetesAiRunnerOptions({
1983
+ runners,
1984
+ boundRunnerId: "r-off",
1985
+ boundRunnerName: "ops-runner",
2026
1986
  }),
2027
- "prod-east",
1987
+ "r-off",
2028
1988
  ),
2029
- ).toBe(false);
2030
- });
2031
-
2032
- test("keep the bound Runner, saying why it cannot serve the cluster", () => {
2033
- const boundToOther: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
2034
- runners,
2035
- clusterIdentifier: "prod-east",
2036
- boundRunnerId: "r-other-cluster",
2037
- boundRunnerName: "kubernetes-agent/prod-eu",
2038
- });
2039
- expect(optionValues(boundToOther)).toContain("r-other-cluster");
2040
- expect(optionLabel(boundToOther, "r-other-cluster")).toMatch(
2041
- /currently bound — runs inside another cluster/,
2042
- );
2043
-
2044
- const boundToOff: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
2045
- runners,
2046
- clusterIdentifier: "prod-east",
2047
- boundRunnerId: "r-off",
2048
- boundRunnerName: "ops-runner",
2049
- });
2050
- expect(optionLabel(boundToOff, "r-off")).toMatch(
2051
- /Runs AI Remediation Commands/,
2052
- );
1989
+ ).toMatch(/Runs AI Remediation Commands/);
2053
1990
 
2054
1991
  const boundToUnlisted: Array<DropdownOption> =
2055
1992
  buildKubernetesAiRunnerOptions({
2056
1993
  runners,
2057
- clusterIdentifier: "prod-east",
2058
1994
  boundRunnerId: "r-gone",
2059
1995
  boundRunnerName: "old-runner",
2060
1996
  });
@@ -2062,6 +1998,25 @@ describe("Runner picker options", () => {
2062
1998
  value: "r-gone",
2063
1999
  label: "old-runner (currently bound)",
2064
2000
  });
2001
+ expect(
2002
+ buildKubernetesAiRunnerOptions({
2003
+ runners: [],
2004
+ boundRunnerId: "r-gone",
2005
+ boundRunnerName: null,
2006
+ }),
2007
+ ).toEqual([
2008
+ { value: "r-gone", label: "The bound Runner (currently bound)" },
2009
+ ]);
2010
+ });
2011
+
2012
+ test("skip a row without an id", () => {
2013
+ expect(
2014
+ buildKubernetesAiRunnerOptions({
2015
+ runners: [Object.assign(new Runner(), { name: "no-id" })],
2016
+ boundRunnerId: null,
2017
+ boundRunnerName: null,
2018
+ }),
2019
+ ).toEqual([]);
2065
2020
  });
2066
2021
 
2067
2022
  test("the directory names each Runner and says which are agent rows", () => {
@@ -2071,6 +2026,7 @@ describe("Runner picker options", () => {
2071
2026
  name: "kubernetes-agent/prod-east",
2072
2027
  isAgent: true,
2073
2028
  });
2029
+ expect(directory["r-renamed"]!.isAgent).toBe(true);
2074
2030
  expect(directory["r-host"]).toEqual({
2075
2031
  name: "bash-runner",
2076
2032
  isAgent: false,
@@ -2078,13 +2034,6 @@ describe("Runner picker options", () => {
2078
2034
  });
2079
2035
  });
2080
2036
 
2081
- /*
2082
- * The finding (dashboard-ai-page-7): the picker offered every Kubernetes
2083
- * credential, so an admin could save a Runner with a credential that is
2084
- * not assigned to it (the status then blocks everything with a
2085
- * credential_missing gap), or a credential for a kubernetes-agent Runner,
2086
- * which is never given one.
2087
- */
2088
2037
  describe("credential picker options", () => {
2089
2038
  const credentials: Array<RunbookCredential> = [
2090
2039
  makeCredential("c-ssh", "ssh key", ["r-host"], RunbookCredentialType.SSH),
@@ -2110,7 +2059,6 @@ describe("credential picker options", () => {
2110
2059
  ),
2111
2060
  ).toEqual(["c-host", "c-both"]);
2112
2061
 
2113
- // Negative control: another Runner gets its own credentials.
2114
2062
  expect(
2115
2063
  optionValues(
2116
2064
  buildKubernetesAiCredentialOptions({
@@ -2144,7 +2092,7 @@ describe("credential picker options", () => {
2144
2092
  name: "kubernetes-agent/prod-east",
2145
2093
  }),
2146
2094
  ).toMatch(
2147
- /No credential can be chosen: "kubernetes-agent\/prod-east" is an in-cluster Runner/,
2095
+ /^No credential can be chosen: "kubernetes-agent\/prod-east" is an in-cluster Runner/,
2148
2096
  );
2149
2097
  expect(
2150
2098
  getKubernetesAiCredentialFieldDescription({ id: null, name: null }),
@@ -2214,45 +2162,64 @@ describe("credential picker options", () => {
2214
2162
  ),
2215
2163
  ).toEqual(["c-unknown"]);
2216
2164
  });
2165
+
2166
+ test("the directory names each credential and its Runners", () => {
2167
+ const directory: Record<string, KubernetesAiCredentialDirectoryEntry> =
2168
+ buildKubernetesAiCredentialDirectory([
2169
+ ...credentials,
2170
+ makeCredential("c-unknown", "", undefined),
2171
+ ]);
2172
+ expect(directory["c-both"]).toEqual({
2173
+ name: "shared token",
2174
+ runnerIds: ["r-host", "r-other-host"],
2175
+ });
2176
+ expect(directory["c-unknown"]).toEqual({
2177
+ name: "c-unknown",
2178
+ runnerIds: undefined,
2179
+ });
2180
+ });
2217
2181
  });
2218
2182
 
2219
2183
  describe("a Runner and credential that cannot work together", () => {
2220
2184
  test("a credential not assigned to the Runner is refused, naming both", () => {
2221
- const error: string | null = getKubernetesAiCredentialAssignmentError({
2222
- runner: { id: "r-host", name: "bash-runner" },
2223
- credentialId: "c-other",
2224
- credentialName: "staging token",
2225
- credentialRunnerIds: ["r-other-host"],
2226
- });
2227
- expect(error).toMatch(
2228
- /"staging token" is not assigned to Runner "bash-runner"/,
2229
- );
2230
-
2231
- // Negative controls: assigned, unknown assignments, or no credential.
2232
2185
  expect(
2233
2186
  getKubernetesAiCredentialAssignmentError({
2234
2187
  runner: { id: "r-host", name: "bash-runner" },
2235
- credentialId: "c-host",
2236
- credentialName: "prod-east token",
2237
- credentialRunnerIds: ["r-host"],
2188
+ credentialId: "c-other",
2189
+ credentialName: "staging token",
2190
+ credentialRunnerIds: ["r-other-host"],
2238
2191
  }),
2239
- ).toBeNull();
2192
+ ).toMatch(/"staging token" is not assigned to Runner "bash-runner"/);
2193
+
2194
+ for (const credentialRunnerIds of [["r-host"], undefined]) {
2195
+ expect(
2196
+ getKubernetesAiCredentialAssignmentError({
2197
+ runner: { id: "r-host", name: "bash-runner" },
2198
+ credentialId: "c-host",
2199
+ credentialName: "prod-east token",
2200
+ credentialRunnerIds,
2201
+ }),
2202
+ ).toBeNull();
2203
+ }
2240
2204
  expect(
2241
2205
  getKubernetesAiCredentialAssignmentError({
2242
2206
  runner: { id: "r-host", name: "bash-runner" },
2243
- credentialId: "c-host",
2244
- credentialName: "prod-east token",
2207
+ credentialId: null,
2208
+ credentialName: null,
2245
2209
  credentialRunnerIds: undefined,
2246
2210
  }),
2247
2211
  ).toBeNull();
2212
+ });
2213
+
2214
+ test("a credential without a Runner asks for one", () => {
2248
2215
  expect(
2249
2216
  getKubernetesAiCredentialAssignmentError({
2250
- runner: { id: "r-host", name: "bash-runner" },
2251
- credentialId: null,
2217
+ runner: { id: null, name: null },
2218
+ credentialId: "c-host",
2252
2219
  credentialName: null,
2253
- credentialRunnerIds: undefined,
2220
+ credentialRunnerIds: ["r-host"],
2254
2221
  }),
2255
- ).toBeNull();
2222
+ ).toMatch(/^The Kubernetes credential is only used through a Runner/);
2256
2223
  });
2257
2224
 
2258
2225
  test("a kubernetes-agent Runner with a credential asks for the credential to be cleared", () => {
@@ -2260,7 +2227,6 @@ describe("a Runner and credential that cannot work together", () => {
2260
2227
  runner: { id: "r-this", name: "kubernetes-agent/prod-east" },
2261
2228
  credentialId: "c-host",
2262
2229
  credentialName: "prod-east token",
2263
- // Even assigned: an agent Runner is never given a credential.
2264
2230
  credentialRunnerIds: ["r-this"],
2265
2231
  });
2266
2232
  expect(error).toMatch(/is an in-cluster Runner/);
@@ -2352,6 +2318,8 @@ describe("who may loosen a cluster's AI access", () => {
2352
2318
  for (const permission of KUBERNETES_AI_ACCESS_ADMIN_PERMISSIONS) {
2353
2319
  grant([...BASE_PERMISSIONS, permission]);
2354
2320
  expect(canConfigureUnattendedKubernetesAiAccess()).toBe(true);
2321
+ // Resetting the agent takes the same set (the server's route gate).
2322
+ expect(canResetKubernetesAiAgent()).toBe(true);
2355
2323
  }
2356
2324
 
2357
2325
  expect(getKubernetesAiAccessAdminPermissionTitles()).toEqual(
@@ -2373,6 +2341,7 @@ describe("who may loosen a cluster's AI access", () => {
2373
2341
  ]) {
2374
2342
  grant([...BASE_PERMISSIONS, permission]);
2375
2343
  expect(canConfigureUnattendedKubernetesAiAccess()).toBe(false);
2344
+ expect(canResetKubernetesAiAgent()).toBe(false);
2376
2345
  }
2377
2346
  });
2378
2347
 
@@ -2382,6 +2351,9 @@ describe("who may loosen a cluster's AI access", () => {
2382
2351
  [Permission.ProjectAdmin, Permission.EditAutoRemediationRule],
2383
2352
  );
2384
2353
  expect(canConfigureUnattendedKubernetesAiAccess()).toBe(false);
2354
+ expect(holdsKubernetesAiAccessPermission([Permission.ProjectMember])).toBe(
2355
+ true,
2356
+ );
2385
2357
  });
2386
2358
 
2387
2359
  test("is allowed to a master admin and withheld before the snapshot lands", () => {
@@ -2392,7 +2364,7 @@ describe("who may loosen a cluster's AI access", () => {
2392
2364
  expect(canConfigureUnattendedKubernetesAiAccess()).toBe(true);
2393
2365
  });
2394
2366
 
2395
- test("decides what the edit modal offers", () => {
2367
+ test("decides what the Change modal offers", () => {
2396
2368
  grant([...BASE_PERMISSIONS, Permission.ProjectAdmin]);
2397
2369
  expect(getKubernetesAiAccessEditCapabilities()).toEqual({
2398
2370
  canConfigureUnattended: true,
@@ -2400,7 +2372,6 @@ describe("who may loosen a cluster's AI access", () => {
2400
2372
  canPickCredential: true,
2401
2373
  });
2402
2374
 
2403
- // May loosen and list Runners, may not read credentials.
2404
2375
  grant([
2405
2376
  ...BASE_PERMISSIONS,
2406
2377
  Permission.ProjectMember,
@@ -2412,7 +2383,6 @@ describe("who may loosen a cluster's AI access", () => {
2412
2383
  canPickCredential: false,
2413
2384
  });
2414
2385
 
2415
- // May loosen, but may not list Runners.
2416
2386
  grant([
2417
2387
  ...BASE_PERMISSIONS,
2418
2388
  Permission.EditAutoRemediationRule,
@@ -2425,7 +2395,6 @@ describe("who may loosen a cluster's AI access", () => {
2425
2395
  canPickCredential: false,
2426
2396
  });
2427
2397
 
2428
- // Reading credentials alone does not let a member bind one.
2429
2398
  grant([
2430
2399
  ...BASE_PERMISSIONS,
2431
2400
  Permission.ProjectMember,
@@ -2436,11 +2405,64 @@ describe("who may loosen a cluster's AI access", () => {
2436
2405
  canPickRunner: false,
2437
2406
  canPickCredential: false,
2438
2407
  });
2408
+ expect(KUBERNETES_AI_ACCESS_CREDENTIAL_PERMISSIONS.length).toBeGreaterThan(
2409
+ 0,
2410
+ );
2411
+ });
2412
+
2413
+ // The page's titles and PermissionGate's must never disagree.
2414
+ test("names permissions exactly as PermissionGate does, without a browser", () => {
2415
+ for (const permissions of [
2416
+ KUBERNETES_AI_ACCESS_ADMIN_PERMISSIONS,
2417
+ KUBERNETES_AI_ACCESS_CREDENTIAL_PERMISSIONS,
2418
+ PROJECT_AI_SETTINGS_PERMISSIONS,
2419
+ [Permission.ProjectOwner, Permission.ProjectOwner],
2420
+ ]) {
2421
+ expect(getPermissionTitles(permissions)).toEqual(
2422
+ PermissionGate.getPermissionTitles(permissions),
2423
+ );
2424
+ }
2425
+ });
2426
+ });
2427
+
2428
+ /*
2429
+ * The project's AI switches (AI Features, LLM providers, AI credits,
2430
+ * automatic investigation) are changed by project owners and admins — the
2431
+ * update ACL of Project.enableAutomaticIncidentInvestigation. Everyone else
2432
+ * is told who to ask.
2433
+ */
2434
+ describe("who may change the project's AI settings from the cluster page", () => {
2435
+ test("project owners, admins and a master admin", () => {
2436
+ expect(PROJECT_AI_SETTINGS_PERMISSIONS).toEqual([
2437
+ Permission.ProjectOwner,
2438
+ Permission.ProjectAdmin,
2439
+ ]);
2440
+ for (const permission of PROJECT_AI_SETTINGS_PERMISSIONS) {
2441
+ grant([...BASE_PERMISSIONS, permission]);
2442
+ expect(canChangeProjectAiSettings()).toBe(true);
2443
+ }
2444
+
2445
+ grantNothingYet();
2446
+ expect(canChangeProjectAiSettings()).toBe(false);
2447
+ jest.spyOn(User, "isMasterAdmin").mockReturnValue(true);
2448
+ expect(canChangeProjectAiSettings()).toBe(true);
2449
+ });
2450
+
2451
+ test("not members, cluster editors or the auto-remediation admin set", () => {
2452
+ for (const permission of [
2453
+ Permission.ProjectMember,
2454
+ Permission.EditKubernetesCluster,
2455
+ Permission.EditAutoRemediationRule,
2456
+ Permission.SettingsAdmin,
2457
+ ]) {
2458
+ grant([...BASE_PERMISSIONS, permission]);
2459
+ expect(canChangeProjectAiSettings()).toBe(false);
2460
+ }
2439
2461
  });
2440
2462
  });
2441
2463
 
2442
- describe("Runner picker permission", () => {
2443
- test("is withheld from cluster editors who may not read Runners", () => {
2464
+ describe("Runner and credential picker permissions", () => {
2465
+ test("the Runner picker is withheld from cluster editors who may not read Runners", () => {
2444
2466
  for (const permissions of [
2445
2467
  [Permission.SettingsAdmin],
2446
2468
  [Permission.SettingsMember],
@@ -2451,7 +2473,7 @@ describe("Runner picker permission", () => {
2451
2473
  }
2452
2474
  });
2453
2475
 
2454
- test("is offered to Runner readers and a master admin", () => {
2476
+ test("the Runner picker is offered to Runner readers and a master admin", () => {
2455
2477
  for (const permission of [
2456
2478
  Permission.ProjectMember,
2457
2479
  Permission.ReadRunner,
@@ -2468,13 +2490,26 @@ describe("Runner picker permission", () => {
2468
2490
  expect(canPickKubernetesRunner()).toBe(true);
2469
2491
  });
2470
2492
 
2471
- test("names the permissions that would unlock it", () => {
2493
+ test("the credential picker needs permission to read credentials", () => {
2494
+ expect(canPickKubernetesCredential()).toBe(false);
2495
+ grant([...BASE_PERMISSIONS, Permission.ReadRunbookCredential]);
2496
+ expect(canPickKubernetesCredential()).toBe(true);
2497
+ });
2498
+
2499
+ test("name the permissions that would unlock them", () => {
2472
2500
  expect(getKubernetesRunnerPermissionTitles()).toEqual(
2473
2501
  expect.arrayContaining(["Read Runbook Agent", "Project Member"]),
2474
2502
  );
2503
+ expect(getKubernetesCredentialPermissionTitles().join(", ")).toMatch(
2504
+ /Runbook Credential/,
2505
+ );
2475
2506
  });
2476
2507
  });
2477
2508
 
2509
+ /*
2510
+ * The kubectl command history (now on the AI Insights page) is RunnerJob
2511
+ * rows, which roles that may open the cluster's pages may not read.
2512
+ */
2478
2513
  describe("command history permission", () => {
2479
2514
  test("is withheld from roles that may open the page but not read Runner jobs", () => {
2480
2515
  for (const permission of [
@@ -2511,7 +2546,7 @@ describe("command history permission", () => {
2511
2546
  });
2512
2547
  });
2513
2548
 
2514
- describe("access test permission", () => {
2549
+ describe("connection test permission", () => {
2515
2550
  test("requires edit access to the cluster, as the /test route does", () => {
2516
2551
  grant([...BASE_PERMISSIONS, Permission.ReadKubernetesCluster]);
2517
2552
  const refused: PermissionGateResult = getAccessTestPermissionGate();
@@ -2532,9 +2567,11 @@ describe("access test permission", () => {
2532
2567
  expect(getAccessTestPermissionGate()).toEqual({ isAllowed: false });
2533
2568
  });
2534
2569
 
2535
- test("a refusal talks about running the test, not changing settings", () => {
2570
+ test("a refusal talks about testing the connection, not changing settings", () => {
2571
+ expect(getAccessTestPermissionRequirement()).toMatch(
2572
+ /^Testing the connection needs permission to edit this cluster/,
2573
+ );
2536
2574
  const message: string = getAccessTestPermissionMessage();
2537
- expect(message).toMatch(/Running the access test needs permission/);
2538
2575
  expect(message).toMatch(/Nothing .* was changed/);
2539
2576
  expect(message).not.toMatch(/permission to change/);
2540
2577
  });
@@ -2542,8 +2579,7 @@ describe("access test permission", () => {
2542
2579
 
2543
2580
  /*
2544
2581
  * The page's mode copy against the canonical description on
2545
- * KubernetesAiRemediationMode (Common/Types/Kubernetes/
2546
- * KubernetesClusterAiAccess.ts). The shared phrases are checked in the
2582
+ * KubernetesAiRemediationMode. The shared phrases are checked in the
2547
2583
  * canonical comment too, so rewording it flags this copy for review.
2548
2584
  */
2549
2585
  describe("the mode copy follows the canonical description", () => {
@@ -2566,20 +2602,11 @@ describe("the mode copy follows the canonical description", () => {
2566
2602
  .replace(COMMENT_LINE_BREAK_REGEX, " ");
2567
2603
  }
2568
2604
 
2569
- // Clauses of the canonical "In EVERY mode" paragraph the page repeats verbatim.
2570
2605
  const EVERY_MODE_CLAUSES: Array<string> = [
2571
2606
  "never changes its own namespace or anything outside the namespaces its chart may write",
2572
2607
  "the hourly per-cluster circuit breaker trips or another unattended round already holds the cluster",
2573
2608
  ];
2574
2609
 
2575
- /*
2576
- * The node clause. The canonical comment names a node drain and a node
2577
- * taint ("a node drain and a node taint always need a human"); the page
2578
- * adds a patch of a node, which the policy holds to the same rule since
2579
- * round four (a NoExecute taint written with `kubectl patch node` is the
2580
- * same change as `kubectl taint`). Matched as a shape, so the canonical
2581
- * comment may name the node patch too without flagging this copy.
2582
- */
2583
2610
  const NODE_CLAUSE_SHAPE_REGEX: RegExp =
2584
2611
  /a node drain(,| and) a node taint[^;]{0,120} always need a human/;
2585
2612
  const PAGE_NODE_CLAUSE: string =
@@ -2601,21 +2628,14 @@ describe("the mode copy follows the canonical description", () => {
2601
2628
  expect(canonical).toMatch(NODE_CLAUSE_SHAPE_REGEX);
2602
2629
  });
2603
2630
 
2604
- // Negative control: the shape does not accept a clause that drops the taint.
2605
2631
  test("the node clause shape needs both the drain and the taint", () => {
2606
2632
  expect(PAGE_NODE_CLAUSE).toMatch(NODE_CLAUSE_SHAPE_REGEX);
2607
- expect("a node drain and a node taint always need a human").toMatch(
2608
- NODE_CLAUSE_SHAPE_REGEX,
2609
- );
2610
2633
  expect("a node drain always needs a human").not.toMatch(
2611
2634
  NODE_CLAUSE_SHAPE_REGEX,
2612
2635
  );
2613
- expect(
2614
- "a node drain and a patch of a node always need a human",
2615
- ).not.toMatch(NODE_CLAUSE_SHAPE_REGEX);
2616
2636
  });
2617
2637
 
2618
- test("every mode's protections name the drain, the taint, the Runner's scope and both proposal cases", () => {
2638
+ test("every mode's protections name the drain, the taint, the agent's scope and both proposal cases", () => {
2619
2639
  const sentence: string = getEveryModeProtectionsSentence();
2620
2640
  for (const clause of EVERY_MODE_CLAUSES) {
2621
2641
  expect({ clause, said: sentence.includes(clause) }).toEqual({
@@ -2623,39 +2643,78 @@ describe("the mode copy follows the canonical description", () => {
2623
2643
  said: true,
2624
2644
  });
2625
2645
  }
2626
- /*
2627
- * Replaces the pin on "a node drain and a node taint always need a
2628
- * human": round four adds a patch of a node, which a human must
2629
- * approve in every mode too.
2630
- */
2631
2646
  expect(sentence).toContain(PAGE_NODE_CLAUSE);
2632
- expect(sentence).toMatch(NODE_CLAUSE_SHAPE_REGEX);
2647
+ expect(sentence).toContain("the in-cluster agent never changes");
2633
2648
  for (const namespace of PROTECTED_KUBERNETES_NAMESPACES) {
2634
2649
  expect(sentence).toContain(namespace);
2635
2650
  }
2636
- // The earlier copy: only the breaker, and "a taint" without "node".
2637
- expect(sentence).not.toContain(
2638
- "the hourly circuit breaker turns an unattended run into a proposal",
2639
- );
2640
2651
  });
2641
2652
 
2642
- test("the mode field names the taint among the riskier changes and Bypass's exceptions", () => {
2653
+ test("the mode field names the riskier changes, the one-click proposal and Bypass's exceptions", () => {
2643
2654
  const description: string = getRemediationModeFieldDescription();
2644
2655
  expect(description).toContain(
2645
2656
  "A riskier change (patch, set image, drain, taint, scale to zero,",
2646
2657
  );
2658
+ expect(description).toMatch(
2659
+ /when the round could only find riskier fixes, it ends by proposing exactly those for one-click approval/,
2660
+ );
2661
+ expect(description).toContain("delete a named pod");
2662
+ expect(description).not.toMatch(/delete a named pod or job/);
2647
2663
  expect(description).toContain(
2648
2664
  "follow-up rounds included, except for what always asks. In every mode, Bypass approval included:",
2649
2665
  );
2650
2666
  expect(description).toContain(getEveryModeProtectionsSentence());
2651
2667
  });
2652
2668
 
2669
+ test("every mode has its own label, short name and one-line summary", () => {
2670
+ for (const record of [
2671
+ REMEDIATION_MODE_LABELS,
2672
+ REMEDIATION_MODE_SHORT_NAMES,
2673
+ REMEDIATION_MODE_SUMMARIES,
2674
+ ]) {
2675
+ const values: Array<string> = Object.values(
2676
+ KubernetesAiRemediationMode,
2677
+ ).map((mode: KubernetesAiRemediationMode): string => {
2678
+ return record[mode];
2679
+ });
2680
+ expect(
2681
+ values.every((value: string): boolean => {
2682
+ return value.trim().length > 0;
2683
+ }),
2684
+ ).toBe(true);
2685
+ expect(new Set(values).size).toBe(values.length);
2686
+ }
2687
+ expect(REMEDIATION_MODE_SHORT_NAMES).toEqual({
2688
+ [KubernetesAiRemediationMode.Disabled]: "Off",
2689
+ [KubernetesAiRemediationMode.RequireApproval]: "Ask for approval",
2690
+ [KubernetesAiRemediationMode.Automatic]: "Automatic",
2691
+ [KubernetesAiRemediationMode.BypassApproval]: "Bypass approval",
2692
+ });
2693
+ // Each label starts with its short name, so the dropdown and card agree.
2694
+ for (const mode of Object.values(KubernetesAiRemediationMode)) {
2695
+ expect(
2696
+ REMEDIATION_MODE_LABELS[mode].startsWith(
2697
+ REMEDIATION_MODE_SHORT_NAMES[mode],
2698
+ ),
2699
+ ).toBe(true);
2700
+ }
2701
+ });
2702
+
2703
+ test("Automatic says a riskier fix waits for one-click approval", () => {
2704
+ for (const text of [
2705
+ REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.Automatic],
2706
+ REMEDIATION_MODE_SUMMARIES[KubernetesAiRemediationMode.Automatic],
2707
+ ]) {
2708
+ expect(text).toMatch(/one-click approval/);
2709
+ expect(text).not.toMatch(/left for you/);
2710
+ }
2711
+ });
2712
+
2653
2713
  test("the Bypass approval label names every exception, another unattended round included", () => {
2654
2714
  const label: string =
2655
2715
  REMEDIATION_MODE_LABELS[KubernetesAiRemediationMode.BypassApproval];
2656
2716
  for (const exception of [
2657
2717
  "protected namespaces",
2658
- // Round four: a patch of a node joins the drain and the taint.
2659
2718
  "node drains, taints and patches",
2660
2719
  "circuit breaker",
2661
2720
  "another unattended round",
@@ -2666,6 +2725,10 @@ describe("the mode copy follows the canonical description", () => {
2666
2725
  });
2667
2726
  }
2668
2727
  expect(label).not.toMatch(/\bonly\b/);
2728
+ expect(label).not.toMatch(/nobody is asked/);
2729
+ expect(
2730
+ REMEDIATION_MODE_SUMMARIES[KubernetesAiRemediationMode.BypassApproval],
2731
+ ).toContain("still ask a person");
2669
2732
  });
2670
2733
 
2671
2734
  test("the Bypass approval confirmation says AI does not ask, then what still asks", () => {
@@ -2681,7 +2744,6 @@ describe("the mode copy follows the canonical description", () => {
2681
2744
  expect(confirmation!.description).toContain(
2682
2745
  "With Bypass approval OneUptime AI does not ask",
2683
2746
  );
2684
- expect(confirmation!.description).not.toContain("without asking anyone");
2685
2747
  expect(confirmation!.description).toContain(
2686
2748
  getEveryModeProtectionsSentence(),
2687
2749
  );
@@ -2691,32 +2753,20 @@ describe("the mode copy follows the canonical description", () => {
2691
2753
  /*
2692
2754
  * Which Runner rows are kubernetes-agent Runners: the server's one rule
2693
2755
  * (RunnerService.isKubernetesAgentRunnerRow) — the name marker, compared
2694
- * case-insensitively, OR an agent posture. The page read the name alone,
2695
- * so a row whose posture says it is an agent (a legacy rename) was
2696
- * offered like a host Runner and given credentials the server refuses.
2697
- * KubernetesClusterAiPageServerParity.test.ts runs the same table against
2698
- * the real RunnerService.
2756
+ * case-insensitively, OR an agent posture.
2699
2757
  */
2700
2758
  describe("a kubernetes-agent Runner, by the server's rule", () => {
2701
- // An agent posture without the name marker: in a cluster, naming it.
2702
2759
  const postureOnly: Runner = makeRunner(
2703
2760
  "r-renamed",
2704
2761
  "prod-eu-kubectl",
2705
2762
  true,
2706
2763
  "prod-eu",
2707
2764
  );
2708
- const postureOnlyHere: Runner = makeRunner(
2709
- "r-renamed-here",
2710
- "east-kubectl",
2711
- true,
2712
- "prod-east",
2713
- );
2714
2765
  const upperCaseName: Runner = makeRunner(
2715
2766
  "r-upper",
2716
2767
  "KUBERNETES-AGENT/prod-eu",
2717
2768
  true,
2718
2769
  );
2719
- // In a pod, but naming no cluster: an ordinary Runner, not an agent.
2720
2770
  const podNoCluster: Runner = Object.assign(new Runner(), {
2721
2771
  _id: "r-pod",
2722
2772
  name: "pod-runner",
@@ -2731,7 +2781,6 @@ describe("a kubernetes-agent Runner, by the server's rule", () => {
2731
2781
  expect(
2732
2782
  isKubernetesAgentRunnerRow({ name: " Kubernetes-Agent/prod-eu " }),
2733
2783
  ).toBe(true);
2734
- // Negative controls: an ordinary Runner, in a pod or not.
2735
2784
  expect(isKubernetesAgentRunnerRow(podNoCluster)).toBe(false);
2736
2785
  expect(isKubernetesAgentRunnerRow(hostRunner)).toBe(false);
2737
2786
  expect(isKubernetesAgentRunnerRow({ name: "kubernetes-agent" })).toBe(
@@ -2741,68 +2790,23 @@ describe("a kubernetes-agent Runner, by the server's rule", () => {
2741
2790
  expect(isKubernetesAgentRunnerRow({})).toBe(false);
2742
2791
  });
2743
2792
 
2744
- test("the directory marks a posture-only or upper-case agent row", () => {
2745
- const directory: Record<string, KubernetesAiRunnerDirectoryEntry> =
2746
- buildKubernetesAiRunnerDirectory([
2747
- postureOnly,
2748
- upperCaseName,
2749
- podNoCluster,
2750
- hostRunner,
2751
- ]);
2752
- expect(directory["r-renamed"]!.isAgent).toBe(true);
2753
- expect(directory["r-upper"]!.isAgent).toBe(true);
2754
- expect(directory["r-pod"]!.isAgent).toBe(false);
2755
- expect(directory["r-host"]!.isAgent).toBe(false);
2756
- });
2757
-
2758
- test("the Runner picker never offers another cluster's agent, whatever it is named", () => {
2793
+ test("the Runner picker never offers an agent row, whatever it is named", () => {
2759
2794
  const options: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
2760
2795
  runners: [postureOnly, upperCaseName, podNoCluster, hostRunner],
2761
- clusterIdentifier: "prod-east",
2762
2796
  boundRunnerId: null,
2763
2797
  boundRunnerName: null,
2764
2798
  });
2765
- // Before: the posture-only row was offered like a host Runner.
2799
+ // A Runner in a pod that names no cluster is an ordinary Runner.
2766
2800
  expect(optionValues(options)).toEqual(["r-pod", "r-host"]);
2767
2801
  });
2768
2802
 
2769
- test("the Runner picker lists this cluster's agent first even without the name marker", () => {
2770
- expect(isThisClustersAgentRunner(postureOnlyHere, "prod-east")).toBe(true);
2771
- expect(isThisClustersAgentRunner(postureOnly, "prod-east")).toBe(false);
2772
- expect(isThisClustersAgentRunner(podNoCluster, "prod-east")).toBe(false);
2773
-
2774
- const options: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
2775
- runners: [hostRunner, postureOnlyHere],
2776
- clusterIdentifier: "prod-east",
2777
- boundRunnerId: null,
2778
- boundRunnerName: null,
2779
- });
2780
- expect(optionValues(options)).toEqual(["r-renamed-here", "r-host"]);
2781
- expect(options[0]!.label).toBe(
2782
- "east-kubectl (in-cluster Runner for this cluster)",
2783
- );
2784
- });
2785
-
2786
- test("a bound agent of another cluster stays listed, labelled why it cannot serve", () => {
2787
- const options: Array<DropdownOption> = buildKubernetesAiRunnerOptions({
2788
- runners: [postureOnly, hostRunner],
2789
- clusterIdentifier: "prod-east",
2790
- boundRunnerId: "r-renamed",
2791
- boundRunnerName: "prod-eu-kubectl",
2792
- });
2793
- expect(optionValues(options)).toEqual(["r-renamed", "r-host"]);
2794
- expect(options[0]!.label).toMatch(
2795
- /currently bound — runs inside another cluster/,
2796
- );
2797
- });
2798
-
2799
2803
  test("the credential picker offers nothing for an agent row the name does not mark", () => {
2800
2804
  const credentials: Array<RunbookCredential> = [
2801
- makeCredential(CREDENTIAL_ID, "prod token", ["r-renamed-here"]),
2805
+ makeCredential(CREDENTIAL_ID, "prod token", ["r-renamed"]),
2802
2806
  ];
2803
2807
  const runner: { id: string; name: string; isAgent: boolean } = {
2804
- id: "r-renamed-here",
2805
- name: "east-kubectl",
2808
+ id: "r-renamed",
2809
+ name: "prod-eu-kubectl",
2806
2810
  isAgent: true,
2807
2811
  };
2808
2812
 
@@ -2814,19 +2818,15 @@ describe("a kubernetes-agent Runner, by the server's rule", () => {
2814
2818
  runner,
2815
2819
  }),
2816
2820
  ).toEqual([]);
2817
- expect(getKubernetesAiCredentialFieldDescription(runner)).toMatch(
2818
- /^No credential can be chosen: "east-kubectl" is an in-cluster Runner/,
2819
- );
2820
2821
  expect(
2821
2822
  getKubernetesAiCredentialAssignmentError({
2822
2823
  runner,
2823
2824
  credentialId: CREDENTIAL_ID,
2824
2825
  credentialName: "prod token",
2825
- credentialRunnerIds: ["r-renamed-here"],
2826
+ credentialRunnerIds: ["r-renamed"],
2826
2827
  }),
2827
2828
  ).toMatch(/is an in-cluster Runner/);
2828
2829
 
2829
- // Negative control: the same row read as an ordinary Runner.
2830
2830
  const ordinary: { id: string; name: string; isAgent: boolean } = {
2831
2831
  ...runner,
2832
2832
  isAgent: false,
@@ -2841,46 +2841,26 @@ describe("a kubernetes-agent Runner, by the server's rule", () => {
2841
2841
  }),
2842
2842
  ),
2843
2843
  ).toEqual([CREDENTIAL_ID]);
2844
- expect(
2845
- getKubernetesAiCredentialAssignmentError({
2846
- runner: ordinary,
2847
- credentialId: CREDENTIAL_ID,
2848
- credentialName: "prod token",
2849
- credentialRunnerIds: ["r-renamed-here"],
2850
- }),
2851
- ).toBeNull();
2852
- });
2853
-
2854
- test("an upper-case agent name is an agent even when the row was not read", () => {
2855
- expect(
2856
- getKubernetesAiCredentialFieldDescription({
2857
- id: "r-upper",
2858
- name: "KUBERNETES-AGENT/prod-eu",
2859
- }),
2860
- ).toMatch(/^No credential can be chosen/);
2861
2844
  });
2862
2845
 
2863
2846
  test("saving a credential with an agent row the name does not mark is refused", () => {
2864
2847
  const runners: Record<string, KubernetesAiRunnerDirectoryEntry> =
2865
- buildKubernetesAiRunnerDirectory([postureOnlyHere]);
2848
+ buildKubernetesAiRunnerDirectory([postureOnly]);
2866
2849
  const credentials: Record<string, KubernetesAiCredentialDirectoryEntry> =
2867
2850
  buildKubernetesAiCredentialDirectory([
2868
- makeCredential(CREDENTIAL_ID, "prod token", ["r-renamed-here"]),
2851
+ makeCredential(CREDENTIAL_ID, "prod token", ["r-renamed"]),
2869
2852
  ]);
2870
2853
 
2871
2854
  expect(
2872
2855
  getKubernetesAiAccessBindingError({
2873
- saved: makeSaved({
2874
- aiAccessRunnerId: null,
2875
- aiAccessCredentialId: null,
2876
- }),
2856
+ saved: makeAgentSaved(),
2877
2857
  changes: {
2878
- aiAccessRunnerId: "r-renamed-here",
2858
+ aiAccessRunnerId: "r-renamed",
2879
2859
  aiAccessCredentialId: CREDENTIAL_ID,
2880
2860
  },
2881
2861
  runners,
2882
2862
  credentials,
2883
2863
  }),
2884
- ).toMatch(/"east-kubectl" is an in-cluster Runner/);
2864
+ ).toMatch(/"prod-eu-kubectl" is an in-cluster Runner/);
2885
2865
  });
2886
2866
  });